2D Fizzix

BlitzMax Forums/BlitzMax Programming/2D Fizzix

NM...use Brucey's Chipmunk module instead.

Updated: Version 0.05, Nov 2, 2007.

Put in realistic velocity transfer on ball to ball collision. It's acting a lot like billiards now. Try it out.

*** *** *** *** ***

Here's something I've been messing around with. I think the order that I'm doing the collision check might be off though. It's got the basics of ball collision and it's somewhat cleanly coded.

If anyone can add to this or improve it that'd be great. Feel free to use it.

main.bmx
'Advanced 2D Ball Fizzix v0.005
'Nov 2, 2007

SuperStrict

Import "Vec2D.bmx"

SeedRnd MilliSecs()

Graphics 800,600',32,75

Global BodyList:TList = New TList

'Create some balls
For Local i:Int = 1 To 30

	Local ball:TBody = CreateBody()
	ball.fRadius = Rand(10,40)
	ball.fMass = ball.fRadius / 10.0

	Local distok%
	Repeat
		distok = True
		ball.current.vPos.Set(Rand(ball.fRadius,GraphicsWidth()-ball.fRadius), Rand(ball.fRadius,GraphicsHeight()-ball.fRadius))
		Local a:TBody
		For a = EachIn BodyList
			If a <> ball
				If BodyDist(ball,a) < ball.fRadius + a.fRadius
					distok = False
					Exit
				EndIf
			EndIf
		Next
	Until distok = True

	'ball.current.vVel.Set(Rand(-250,250), Rand(-250,250))
	ball.previous = ball.current.Copy()
	ball.fGravity = 9.81

	Select Rand(4)
		Case 1
			SetBodyColor ball,255,0,0		
		Case 2
			SetBodyColor ball,255,255,0
		Case 3
			SetBodyColor ball,0,255,0
		Case 4
			SetBodyColor ball,0,0,255
	End Select
	
Next


SetBlend ALPHABLEND

Global GravityOn% = True
Local body:TBody

Global pixelScale# = 100

Local newTime:Int = MilliSecs()
Local oldtime:Int = newTime
Local LogicFPS% = 100
Local dt# = 1.0/LogicFPS
Local deltaTime#, accumulator#, alpha#, t#

While KeyDown(KEY_ESCAPE)=False And AppTerminate()=False
Cls

If KeyHit(KEY_H)
	Local temp:TBody = CreateBody(1000)
EndIf


	If KeyHit(KEY_SPACE) Then GravityOn = 1 - GravityOn

	newTime = MilliSecs()
	deltaTime# = (newTime - oldTime) * 0.001
	oldTime = newTime
	
	accumulator :+ deltaTime

	While accumulator >= dt
	
		'User input
		If KeyHit(KEY_ENTER)
			For body = EachIn bodyList
				body.current.vVel.Set(Rand(-250,250),Rand(-250,250))			
			Next
		EndIf
				
		'Update Logic
		For body = EachIn bodyList
			body.Logic(dt, t)
		Next
		
		accumulator :- dt
		t :+ dt
	Wend

	'Calculate tween
	alpha = accumulator / dt
	
	'Render with tween(alpha)
	For body = EachIn bodyList
		body.Render(alpha)
	Next

	DrawText "Press the Space Bar to toggle gravity.",5,5
	DrawText "Press the Enter Key to send em' flying.",5,25

Flip 1'False
Wend
End


Type TBody
	Field image:TImage
	Field fMass:Float = 10.0
	Field fInertia:Float = 1.0
	Field fInertiaInverse:Float
	Field fGravity:Float
	Field fRadius:Float = 16
	Field fBounce:Float = .8
	Field fFriction_roll:Float = 1.0
	Field r%, g%, b%
	Field current:TState = New TState
	Field previous:TState = New TState
	
	Method Logic(dt#, t#)
	
		'DrawText Int(self.current.vVel.Magnitude()),self.current.vPos.x+16,self.current.vPos.y-16
		
		'Store the current state of the object
		self.previous = self.current.Copy()
		
		'Reset Forces
		self.current.vForces.Zero()
		self.current.fMoment = 0

		'Gravity
		If GravityOn
			If self.fGravity Then ApplyLinearForce Self,0,self.fMass * self.fGravity * pixelScale,0
		EndIf
		
		'Linear
		self.current.vAcc = self.current.vForces.DividedByScalar(self.fMass)
		self.current.vVel = self.current.vVel.Plus(self.current.vAcc.TimesScalar(dt))
		self.current.vPos = self.current.vPos.Plus(self.current.vVel.TimesScalar(dt))

		'Rotational
		self.current.fAngAcc = self.current.fMoment * self.fInertiaInverse
		self.current.fAngVel :+  self.current.fAngAcc * dt

		'Advanced Ball Collision
		For Local body2:TBody = EachIn bodyList
		
			Local dx#, dy#, ax#, ay#, vx1#, vy1#, vx2#, vy2#, va1#, vb1#, va2#, vb2#, ed#, vaP1#, vaP2#
'Rem
			If Self <> body2
				Local dist# = BodyDist(Self, body2)
				
				If dist < self.fRadius + body2.fRadius
					
					'set body to position prior to collision
					self.current.vPos = self.previous.vPos.Copy()
					body2.current.vPos = body2.previous.vPos.Copy()
					
					'find x and y distances
					dx# = body2.current.vPos.x - self.current.vPos.x 
					dy# = body2.current.vPos.y - self.current.vPos.y  
					
					'find body to body distance
					dist = Sqr(dx*dx + dy*dy)
					
					'find normal
					ax# = dx / dist
					ay# = dy / dist
					
					'store velocities for computation convenience
					vx1# = self.current.vVel.x
					vy1# = self.current.vVel.y
					
					vx2# = body2.current.vVel.x
					vy2# = body2.current.vVel.y
					
					'calc new x and y velocities for each body
					va1# = (vx1*ax+vy1*ay)
					vb1# = (-vx1*ay+vy1*ax)
					
					va2# = (vx2*ax+vy2*ay)
					vb2# = (-vx2*ay+vy2*ax)
					
					'Elasticity Factor
					ed# = 0.8
					
					vaP1# = va1 + (1+ed)*(va2-va1)/(1+self.fMass/body2.fMass)
					vaP2# = va2 + (1+ed)*(va1-va2)/(1+body2.fMass/self.fMass)

					'set body1 new velocity
					self.current.vVel.x = vaP1*ax-vb1*ay
					self.current.vVel.y = vaP1*ay+vb1*ax
					
					'set body2 new velocity
					body2.current.vVel.x = vaP2*ax-vb2*ay
					body2.current.vVel.y = vaP2*ay+vb2*ax
					'Exit
				EndIf
				
			EndIf

		Next

		'Find out which way the ball is traveling
		Local col% = False
		Local dirx# = self.current.vPos.x - self.previous.vPos.x
		Local diry# = self.current.vPos.y - self.previous.vPos.y
		
		'Basic Collision (32 is the diameter of the ball)
		If self.current.vPos.y <= self.fRadius
			col = True
			self.current.vVel.y = -self.current.vVel.y * self.fBounce	'bounce factor
			self.current.vPos.y = self.previous.vPos.y		'set ball to previous y position
			If dirx < 0 Then self.current.fAngVel = 360.0 * Sqr(self.current.vVel.x^2) / (Pi * 32)
			If dirx > 0 Then self.current.fAngVel = 360.0 * -Sqr(self.current.vVel.x^2) / (Pi * 32)
		EndIf
		
		If self.current.vPos.y >= GraphicsHeight() - self.fRadius
			col = True
			self.current.vVel.y = -self.current.vVel.y * self.fBounce	'bounce factor
			self.current.vPos.y = self.previous.vPos.y		'set ball to previous y position
			If dirx < 0 Then self.current.fAngVel = 360.0 * -Sqr(self.current.vVel.x^2) / (Pi * 32)
			If dirx > 0 Then self.current.fAngVel = 360.0 * Sqr(self.current.vVel.x^2) / (Pi * 32)
		EndIf
		
		If self.current.vPos.x <= self.fRadius
			col = True
			self.current.vVel.x = -self.current.vVel.x * self.fBounce	'bounce factor
			self.current.vPos.x = self.previous.vPos.x		'set ball to previous x position
			If diry < 0 Then self.current.fAngVel = 360.0 * -Sqr(self.current.vVel.y^2) / (Pi * 32)
			If diry > 0 Then self.current.fAngVel = 360.0 * Sqr(self.current.vVel.y^2) / (Pi * 32)
		EndIf
		
		If self.current.vPos.x >= GraphicsWidth() - self.fRadius
			col = True
			self.current.vVel.x = -self.current.vVel.x * self.fBounce	'bounce factor
			self.current.vPos.x = self.previous.vPos.x		'set ball to previous x position
			If diry < 0 Then self.current.fAngVel = 360.0 * Sqr(self.current.vVel.y^2) / (Pi * 32)
			If diry > 0 Then self.current.fAngVel = 360.0 * -Sqr(self.current.vVel.y^2) / (Pi * 32)
		EndIf
				
		If col = True Then self.current.vVel = self.current.vVel.TimesScalar(self.fFriction_roll)
		

		'Update the object angle
		self.current.fAngle :+ self.current.fAngVel * dt

		'Reset all forces and moments
		'self.current.vForces.Zero()
		'self.current.fMoment = 0
		
	End Method
	
	Method Render(alpha#)

		Local x#, y#, s#
		x = self.current.vPos.x * alpha + self.previous.vPos.x * (1.0 - alpha)
		y = self.current.vPos.y * alpha + self.previous.vPos.y * (1.0 - alpha)
		s = self.current.fAngle * alpha + self.previous.fAngle * (1.0 - alpha)

		'SetRotation s
		SetColor self.r, self.g, self.b
		DrawOval x-self.fRadius,y-self.fRadius,self.fRadius*2,self.fRadius*2
		'DrawImage self.image, x, y
		SetRotation 0
		SetColor 255, 255, 255
		'DrawLine self.current.vPos.x,self.current.vPos.y,self.current.vPos.x + self.current.vVel.x/2,self.current.vPos.y + self.current.vVel.y/2

	End Method
End Type

Function CreateBody:TBody(mass#=1, x#=400, y# = 100, radius#=10)
	Local b:TBody = New TBody
	b.current.vPos.x = x
	b.current.vPos.y = y
	b.r = 100
	b.g = 100
	b.b = 100
	b.fGravity = 9.81
	b.fRadius = radius
	b.fMass = Mass
	b.fInertiaInverse = 1.0/b.fInertia
	BodyList.AddLast(b)
	Return b
End Function

Type TState
	Field vPos:TVec2 = Vec2()
	Field vVel:TVec2 = Vec2()
	Field vAcc:TVec2 = Vec2()
	Field vForces:TVec2 = Vec2()

	Field fAngle:Float
	Field fAngVel:Float
	Field fAngAcc:Float
	Field fMoment:Float

	Method Copy:TState()
		Local t:TState = New TState
		t.vPos = self.vPos.Copy()
		t.vVel = self.vVel.Copy()
		t.vAcc = self.vAcc.Copy()
		t.fAngle = self.fAngle
		Return t
	End Method
End Type

Function ApplyLinearForce(body:TBody, xf#, yf#, zf#)
	Local vF:TVec2 = Vec2(xf, yf)
	body.current.vForces = body.current.vForces.Plus( vF )
End Function

Function BodyDist:Float(body1:TBody, body2:TBody)
	Local x# = body1.current.vPos.x - body2.current.vPos.x
	Local y# = body2.current.vPos.y - body1.current.vPos.y
	Return Sqr(x*x + y*y)
End Function

Function BodyDist2:Float(body1:TBody, body2:TBody)
	Local x# = body1.current.vPos.x - body2.current.vPos.x
	Local y# = body1.current.vPos.y - body2.current.vPos.y
	Return Sqr(x*x + y*y)
End Function

Function SetBodyColor(body:TBody, r%, g%, b%)
	body.r = r
	body.g = g
	body.b = b
End Function


Vec2D.bmx

Rem
bbdoc:
EndRem
'2D Vector Lib

Type TVec2
	Field x:Float, y:Float
	
	Method Copy:TVec2()
		Return Vec2(self.x, self.y)
	End Method
	
	Method Plus:TVec2(v:TVec2)
		Local r:TVec2 = New TVec2
		r.x = self.x + v.x
		r.y = self.y + v.y
		Return r
	End Method
	
	Method PlusNumber:TVec2(n:Float)
		Local r:TVec2 = New TVec2
		r.x = self.x + n
		r.y = self.y + n
		Return r
	End Method
	
	Method Minus:TVec2(v:TVec2)
		Local r:TVec2 = New TVec2
		r.x = self.x - v.x
		r.y = self.y - v.y
		Return r
	End Method

	Method MinusNumber:TVec2(n:Float)
		Local r:TVec2 = New TVec2
		r.x = self.x - n
		r.y = self.y - n
		Return r
	End Method
	
	Method Times:TVec2(v:TVec2)
		Local r:TVec2 = New TVec2
		r.x = self.x * v.x
		r.y = self.y * v.y
		Return r
	End Method
	
	Method TimesScalar:TVec2(s:Float)
		Local r:TVec2 = New TVec2
		r.x = self.x * s
		r.y = self.y * s
		Return r
	End Method
	
	Method DividedBy:TVec2(v:TVec2)
		Local r:TVec2 = New TVec2
		r.x = self.x / v.x
		r.y = self.y / v.y
		Return r
	End Method
	
	Method DividedByScalar:TVec2(s:Float)
		Local r:TVec2 = New TVec2
		r.x = self.x / s
		r.y = self.y / s
		Return r
	End Method

	Method Magnitude:Float()
		Return Sqr(self.x * self.x + self.y * self.y)
	End Method

	Method Set(x#, y#)
		self.x = x
		self.y = y
	End Method
	
	Method Zero()
		self.x = 0
		self.y = 0
	End Method
	
End Type

Rem
bbdoc:
EndRem
Function Vec2:TVec2(x#=0, y#=0)
	Local v:TVec2 = New TVec2
	v.x = x
	v.y = y
	Return v
End Function

Rem
bbdoc:
EndRem
Function GetAngle2D:Float(v1:TVec2, v2:TVec2)
	Return ATan2(v1.y - v2.y, v1.x - v2.x)
End Function


can it handle other shapes yet ?

Not yet. And there's a few slight problem with the resulting velocity that I haven't figured out yet.

Just updated this to fix collisions. Now the balls don't pass into one another at all.

My only problems now are calculating the new body velocities after a collision. Also figure out the new rotation of the balls after a 2 ball collision.

have you seen this?

http://www.harveycartel.org/metanet/tutorials/tutorialA.html


This guy has some great physics tutorials.

His game "N" is really cool too.

Updated with advanced velocity transfer with ball to ball collision. Still WIP but it's acting somewhat realistic now.

Hmm...I'm getting unwanted random balls flying up. It's like they can't settle down and come to a rest.

Can anyone see anything blatantly wrong with this code or maybe why the balls won't settle to a complete rest?

probably due to floating point inaccuracies. I'd change the whole lot to doubles for a start as I'm told they are just as fast. Then try clamping the numbers to 0 when they get really small instead of letting them be just above zero but not quite. Also don't let them go negative if they started off positive etc. These are just suggestions based on problems I encoutered when coding a very similar minigame recently.

Ok I tried that and i'm still getting the weird hopping when 3 balls are touching. It's probably something really simple.

Updated again. Now each ball has it's own radius and mass. A small ball hit by a large one reacts according to the mass properties etc.

There's still something up with the collisions getting hung up..kinda like lag. Not sure what it is. If anyone has any ideas...

I had the same issue with a billiards game I was messing with. What I ended up doing is limiting the collisions to one per flip. This is obviously a hack but I couldn't be bothered figuring out how to fix it properly. I'll post the code somewhere later so as not to hijack your thread. Not sure it will help though because my approach was a lot different that yours. It's interesting to see how many solutions to one problem there are.

Well we're talking about ball physics so I don't consider it a hijacking etc or a derail. Feel free to post what you like nino.

This is very rough stuff using only graphics primitives and with some mysterious collision problems. Occasionally one of the balls will get stuck inside of a bumper or another ball but it was mainly to test physle.lib. Also not sure about the constants - they may be a little off. There is no english yet either (backspin not the language) but all in all a good start towards a billiards game.


physle.lib.bmx
Strict

Function to360:Float(a:Float) '' limits angles to 0-360
	If a > 360.00 Return a Mod 360.00
	If a < 0 Return 360.00+(a Mod 360.00)
	Return a
EndFunction

Type Tpoint
Field x:Float
Field y:Float
Method setXY(_x:Float,_y:Float)
	x=_x
	y=_y
EndMethod
EndType

Type Tvector Extends Tpoint
Field a:Float
Method setA(_a:Float)
	a=To360(_a)
EndMethod
Method ato(p:Tpoint)
	Local n:Float=p.x-x
	If n=0 n=.0001 ''dont divide by 0
	Local a2:Float=ATan((p.y-y)/n)
	If (p.x-x)<0 a2:+180
	Return a2+90
EndMethod 
Method dto:Float(p:Tpoint)
	Local h:Float = Abs(x-p.x)
	Local w:Float = Abs(y-p.y)
	Return Sqr((w*w)+(h*h))
EndMethod
EndType

Type Trect Extends Tvector
Field w:Float
Field h:Float
Method setSize(_w:Float,_h:Float)
	w=_w
	h=_h
EndMethod
Method over(r:Trect)
	If r.x-(r.w/2) > x+w/2 Return False
	If r.x+(r.w/2) < x-w/2 Return False
	If r.y-(r.h/2) > y+h/2 Return False
	If r.y+(r.h/2) < y-h/2 Return False
	Return True
EndMethod
EndType

Type Tforce
Field a:Float,s:Float,maxs:Float,acc:Float
Method accel()
	If acc > 0
		If s < maxs s:+acc
	ElseIf acc < 0
		If s > 0 s:+acc
	EndIf
EndMethod
Method apply(p:Tpoint)
	p.x:+s*Sin(a)
	p.y:-s*Cos(a)
EndMethod
Method revert(p:Tpoint)
	p.x:-s*Sin(a)
	p.y:+s*Cos(a)
EndMethod
EndType





billiards.bmx
Strict
Import "physle.lib.bmx"

Const FRICTION:Float = -.04
Const COLLISION:Float = .4
Const MAXSPEED:Float = 11

Global game:Tgame=New Tgame



'''------- classes


Type Tbumper Extends Trect
Method draw()
	DrawRect(x-(w/2),y-(h/2),w,h)
EndMethod
EndType



Type Thole Extends Trect
Method draw()
	DrawOval(x-(w/2),y-(w/2),w,w)
EndMethod
EndType



Type Tball Extends Trect
Field number
Field force:Tforce
Field previous:Tpoint
Field hit
Method New()
	w=20
	h=20
	previous=New Tpoint
	force=New Tforce
EndMethod
Method update()
	If force.s < .1 force.s=0
	If game.placing And game.cue=Self Return 0 '' bail out if placing the cue ball
	
	hit=False ''handling 2 hits at once makes this game sad :(	
	For Local b:Tball=EachIn game.balls
		If b <> Self And dto(b) < w
			x=previous.x
			y=previous.y
			If Not hit force=newForce(to360(ato(b)-180),b.force.s,MAXSPEED,FRICTION)
			If Not b.hit b.force.s=b.force.s*COLLISION
			hit=True
			b.hit=True
		EndIf
	Next
	
	For Local br:Tbumper=EachIn game.bumpers
		If over(br)
			x=previous.x
			y=previous.y
			force.a=To360(force.a+180-(force.a*2)+br.a)
		EndIf		
	Next
	
	For Local h:Thole=EachIn game.holes
		If dto(h) < h.w/2
			If game.cue=Self
				game.placing=True	
				FlushMouse()	
			Else
				game.balls.remove(Self)
			EndIf
		EndIf		
	Next
	
	If force
		force.apply(Self)
		force.accel()
	EndIf
	
	previous=Self
EndMethod
Method color()
	Select number
		Case 1 SetColor(230,200,0)
		Case 2 SetColor(0,0,190)
		Case 3 SetColor(200,0,0)
		Case 4 SetColor(100,0,100)
		Case 5 SetColor(0,200,0)
		Case 6 SetColor(200,100,0)
		Case 7 SetColor(100,0,0)
		Case 8 SetColor(0,0,0)
		Case 9 SetColor(230,200,0)
		Case 10 SetColor(0,0,190)
		Case 11 SetColor(200,0,0)
		Case 12 SetColor(100,0,100)
		Case 13 SetColor(0,200,0)
		Case 14 SetColor(200,100,0)
		Case 15 SetColor(100,0,0)
	EndSelect
EndMethod
Method draw()
	If number > 8
		DrawOval(x-(w/2),y-(w/2),w,w)
		color()
		DrawRect(x-(w/2)+2,y-6,w-4,12)
		SetColor(255,255,255)
		DrawOval(x-(w/4),y-(w/4),w/2,w/2)
		SetColor(0,0,0)
		If number=9
			DrawText(number,x-(w/4),y-(w/4)-1)
		Else
			DrawText(number,x-(w/4)-3,y-(w/4)-1)
		EndIf
		SetColor(255,255,255)
	ElseIf number
		color()
		DrawOval(x-(w/2),y-(w/2),w,w)
		SetColor(255,255,255)
		DrawOval(x-(w/4),y-(w/4),w/2,w/2)
		SetColor(0,0,0)
		DrawText(number,x-(w/4),y-(w/4)-1)
		SetColor(255,255,255)
	Else
		DrawOval(x-(w/2),y-(w/2),w,w)
	EndIf
EndMethod
EndType




Type Tgame
Field balls:TList
Field bumpers:TList
Field holes:TList
Field mouse:Tpoint
Field maxCuePower:Float=MAXSPEED
Field cuePower:Float
Field cue:Tball
Field placing
Field texta:Float
Method New()
	balls=New TList
	bumpers=New TList
	holes=New TList
	mouse=New Tpoint
	
	bumpers.AddLast(newBumper(400,15,800,30,0))
	bumpers.AddLast(newBumper(15,250,30,500,180))
	bumpers.AddLast(newBumper(785,250,30,500,180))
	bumpers.AddLast(newBumper(400,485,800,30,0))
	
	holes.AddLast(newHole(30,30))
	holes.AddLast(newHole(30,470))
	holes.AddLast(newHole(770,30))
	holes.AddLast(newHole(770,470))
	holes.AddLast(newHole(400,30))
	holes.AddLast(newHole(400,470))
	
	balls.AddFirst(newBall(0,600,250))
	balls.AddLast(newBall(1,300,250))
	balls.AddLast(newBall(2,279,240))
	balls.AddLast(newBall(9,279,261))
	balls.AddLast(newBall(10,258,230))
	balls.AddLast(newBall(8,258,251))
	balls.AddLast(newBall(4,258,272))
	balls.AddLast(newBall(7,237,220))
	balls.AddLast(newBall(13,237,241))
	balls.AddLast(newBall(3,237,262))
	balls.AddLast(newBall(11,237,283))
	balls.AddLast(newBall(15,216,210))
	balls.AddLast(newBall(12,216,231))
	balls.AddLast(newBall(5,216,252))
	balls.AddLast(newBall(14,216,273))
	balls.AddLast(newBall(6,216,294))
	
	cue=Tball(balls.First())
	placing=True
	texta=2.0
EndMethod
Method drawUI()
	SetAlpha(texta)
	DrawText("[SPACE] to see angle",12,2)
	DrawText("[ESC] to rack balls",12,14)
	DrawText("[Q] to quit game",12,26)
	If texta > .01
		texta:-.005
	Else
		texta=0
	EndIf
	SetAlpha(1)


	If MouseDown(1)
		Local n:Int=Min(Int(cuePower*10),100)
		SetColor(Min(500.0*Float(n)/100.0,255.0)-30.0,Min(500.0-(500.0*Float(n)/100.0),255.0)-30.0,0)
		DrawText(n+"%",mouse.x+12,mouse.y+18)
	ElseIf KeyDown(KEY_SPACE) And cue.force.s=0
		DrawLine(mouse.x,mouse.y,cue.x,cue.y)
	EndIf
	
	SetColor(255,255,255)
EndMethod
Method run()
	mouse.x=MouseX()
	mouse.y=MouseY()
	
	SetColor(0,45,0)
	For Local br:Tbumper=EachIn bumpers
		br.draw()
	Next
	
	SetColor(0,0,0)
	For Local h:Thole=EachIn holes
		h.draw()
	Next
	
	SetColor(255,255,255)
	For Local b:Tball=EachIn balls
		b.update()
		b.draw()
	Next

	SetColor(255,255,255)
	If placing
		cue.x=mouse.x
		cue.y=mouse.y
		cue.force=New Tforce
		If MouseHit(1)
			Local ok=True
			For Local b:Tball=EachIn balls
				If cue.dto(b)<b.w And cue<>b  ok=False
			Next
			For Local br:Tbumper=EachIn game.bumpers
				If cue.over(br)  ok=False
			Next
	
			If ok placing=False
		EndIf
	Else
		If MouseDown(1)
			If cuePower<maxCuePower cuePower:+.2
		ElseIf cuePower>0
			cue.force=newForce(to360(cue.ato(mouse)-180),cuePower,cuePower,FRICTION)
			cuePower=0
		EndIf
	EndIf
	
	drawUI()
EndMethod
EndType



'''------- helper functions



Function newBall:Tball(n,x,y)
	Local b:Tball=New Tball
	b.x=x
	b.y=y
	b.number=n
	Return b
EndFunction

Function newForce:Tforce(a:Float,speed:Float,maxspeed:Float=0.0,acc:Float=0.0)
	Local f:Tforce=New Tforce
	f.a=a
	f.s=speed
	If maxspeed
		f.maxs=maxspeed
	Else
		f.maxs=speed
	EndIf
	f.acc=acc
	Return f
EndFunction

Function newBumper:Tbumper(x,y,w,h,a)
	Local br:Tbumper=New Tbumper
	br.x=x
	br.y=y
	br.w=w
	br.h=h
	br.a=a
	Return br
EndFunction

Function newHole:Thole(x,y)
	Local h:Thole=New Thole
	h.x=x
	h.y=y
	h.w=46
	Return h
EndFunction



'''------- main procedure

Graphics(800,500)
SetClsColor(0,60,0)
SetBlend(ALPHABLEND)

While Not AppTerminate()
	Cls
	game.run()
	If KeyHit(KEY_ESCAPE) 
		game=New Tgame
		FlushMouse()
	ElseIf KeyHit(KEY_Q)
		Exit
	EndIf
	Flip
Wend