Springs....

BlitzMax Forums/BlitzMax Programming/Springs....

ragtag(Posted 2006) [#1]
I've been messing with springs and having some trouble with it. I've seen some of the samples posted, and in many of them the springs don't overshoot, they just ease in towards their rest length (or so it seems). I want my springs to have both stiffness and dampening, and support for particles with different mass.

The formula for it is something like F = -kx - bv where k is stiffness, b is dampening, x is vector difference between target and current point (which I don't fully understand and v is velocity or velocity difference (which I don't fully understand either).

The method I've got for evaluating the spring now looks like this (I won't post the rest of the code now, as it's a mess).


	Method Evaluate()
		length = p1.t.Distance( p2.t )
		Local velocity:Float = length - prevLength
		Local force:Float = ( ( restLength - length ) - velocity * dampening ) * 2 / ( stiffness * stiffness )
		Local direction:TVector = p1.t.SubtractNew( p2.t )
		direction.Normalize()
		
		prevLength = p1.t.Distance( p2.t )
		
		Local combinedWeight:Float = p1.mass + p2.mass
		p1.vel.t.x:+ force * direction.x * ( p1.mass / combinedWeight )
		p1.vel.t.y:+ force * direction.y * ( p1.mass / combinedWeight )
		p2.vel.t.x:+ -1 * force * direction.x * ( p2.mass / combinedWeight )
		p2.vel.t.y:+ -1 * force * direction.y * ( p2.mass / combinedWeight )
	End Method

The .t attribute is a vector for the positoin of the particle. And vel.t is a vector for the velocity of the particle. It is set each loop elsewhere in the code.

It works, but I hardly think it's the best way to go about it.

I've written 4 or 5 different Evaluate methods, with different results, but still haven't found one that I'm fully happy with.

Anyone have any suggestions?

Cheers,

Ragnar


ragtag(Posted 2006) [#2]
Here is the whole thing so far. I've been cleaning up the code a bit, though there is probably still lots of stuff in there that are not exactly used for the test.

H - creates a dangling triangle
R - creates a dangling rope
Arrow keys and w/s adjust stiffness, damping and rest length.

First the engine code, save to a file called PrimusMotor.bmx so that it gets included in the test code

' ==========================
'  Primus Motor
'   by Ragnar Brynjulfsson
' ==========================

Rem
	Primus Motor is a simple 2D dynamics engine for BlitzMax.
	
	TODO!
	  Add Delete methods to bodies, constraints and forces.
	  Add two way connections between particles, forces and constraints, so I can delete them.
	  Lots more.... :)
End Rem


' ----------
' THE ENGINE
' ----------
Type TPrimusMotor
	Field bodyList:TList = CreateList()			' List over dynamic bodies.
	Field constraintList:TList = CreateList()			' List of constraints.
	Field forceList:TList	= CreateList()		' List of forces.
	
	Method Update()
		' Evaluate all forces and constraints.
		For Local force:TForce = EachIn forceList
			force.Evaluate()
		Next
		For Local constraint:TConstraint = EachIn constraintList
			constraint.Evaluate()
		Next
		For Local body:TBody = EachIn bodyList
			body.AddVelocity()
			body.UpdatePre()
		Next
	End Method
	
	Method Draw()
		Local i:Int = 0
		DrawText "Position of bodies", 20, 10
		For Local body:TBody = EachIn bodyList
			body.Draw()
			DrawText body.x, 20, 30+i
			DrawText body.y, 20, 50+i
			i:+ 44
		Next
		For Local force:TForce = EachIn forceList
			force.Draw()
		Next
		For Local constraint:TConstraint = EachIn constraintList
			constraint.Draw()
		Next
	End Method

	Function Create:TPrimusMotor()
		Return New TPrimusMotor
	End Function
End Type


' VECTOR STUFF
' ------------
Type TVector
	Field x:Float
	Field y:Float

	Method Length:Float()
		Return Sqr( x * x + y * y )
	End Method
	
	Method Distance:Float( v:TVector )
		Local vector:TVector = Self.SubtractNew( v )
		Return Sqr( vector.x * vector.x + vector.y * vector.y )
	End Method
	
	Method Set( v:TVector )
		x = v.x
		y = v.y
	End Method 
	
	Method Add( v:TVector )
		x:+ v.x
		y:+ v.y
	End Method
	
	Method AddFloat( f:Float )
		x:+ f
		y:+ f
	End Method
	
	Method Subtract( v:TVector )
		x:- v.x
		y:- v.y
	End Method
	
	Method SubtractFloat( f:Float )
		x:- f
		y:- f
	End Method
	
	Method SubtractNew:TVector( v:TVector )
		Return TVector.Create( ( x - v.x ), ( y - v.y ) )
	End Method 
	
	Method MultiFloat( f:Float )
		x:* f
		y:* f
	End Method
	
	Method MultiFloatNew:TVector( f:Float )
		Local vector:TVector = TVector.Create( (x*f), (y*f) )
		Return vector
	End Method
	
	Method DivideFloat( f:Float )
		x:/ f
		y:/ f
	End Method
	
	Method Normalize() 
	   Local length:Float = Sqr(x * x + y * y)
	   x:/ length
	   y:/ length
	End Method

	Method AngleBetween:Float( v:TVector )
		Local dif:TVector = TVector.Create( ( x-v.x ), ( y-v.y ) )
		Return ATan2( dif.y, dif.x )
	End Method
	
	Function Create:TVector( x:Float = 0.0, y:Float = 0.0 )
		Local vector:TVector = New TVector
		vector.x = x
		vector.y = y
		Return vector
	End Function
End Type

' -------
' TBody
' -------
Type TBody Extends TVector
	'Field pos:TVector = TVector.Create( 0.0, 0.0 )
	Field mass:Float = 1.0
	Field sprite:TSprite
	Field pre:TVector = TVector.Create( 0.0, 0.0 )
	Field vel:TVector = TVector.Create( 0.0, 0.0 )
	
	Method AddVelocity()
		Self.Add( vel )
	End Method
	
	Method Velocity:Float()
		Return Self.Distance( Self.pre )
	End Method
	
	Method UpdatePre()
		pre.Set( Self )
	End Method
	
	Method AddSprite( image:TImage )
		sprite = TSprite.Create( image )
	End Method
	
	'  Adding to engine
	Method AddToEngine( engine:TPrimusMotor )
		engine.bodyList.AddLast( Self )
	End Method
	
	' Draws the sprite associated with the body or a dot if nothing is there.
	Method Draw()
		If sprite Then 
			sprite.Draw( Self )
		Else
			DrawRect x, y, mass * 5, mass * 5
		End If
	End Method
	
	' CREATE
	Function Create:TBody( x:Float = 0.0, y:Float = 0.0 )
		Local body:TBody = New TBody
		body.x = x
		body.y = y
		body.pre.x = x
		body.pre.y = y
		body.vel.x = 0.0
		body.vel.y = 0.0
		Return body
	End Function
End Type


' ------
' FORCES
' ------
Type TForce
	Field bodyList:TList = CreateList()
	
	Method Evaluate() Abstract
	Method Draw() Abstract
	
	Method AddBody( body:TBody )
		bodyList.AddLast( body )
	End Method
	
	Method AddToEngine( engine:TPrimusMotor )
		engine.forceList.AddLast( Self )
	End Method
End Type

Type TGravity Extends TForce
	Field direction:TVector = TVector.Create()
	
	Method Evaluate()
		' Calculate the effects of gravity.
		For Local body:TBody = EachIn bodyList
			body.vel.Add( direction )
		Next
	End Method
	
	Method Draw()
	End Method
	
	Function Create:TGravity( x:Float = 0.0, y:Float = 9.8 )
		Local gravity:TGravity = New TGravity
		gravity.direction.x = x
		gravity.direction.y = y
		Return gravity
	End Function
End Type

Type TDrag Extends TForce
	Field amount:Float
	
	Method Evaluate()
		For Local body:TBody = EachIn bodyList
			body.vel.DivideFloat( amount )
		Next
	End Method

	Method Draw()
	End Method
	
	Function Create:TDrag( amount:Float = 2.0 )
		Local drag:TDrag = New TDrag
		drag.amount = amount
		Return drag
	End Function
End Type


' -----------
' CONSTRAINTS
' -----------
Type TConstraint
	Field body1:TBody
	Field body2:TBody
	
	Method Evaluate() Abstract
	Method Draw() Abstract
	
	Method AddToEngine( engine:TPrimusMotor )
		engine.constraintList.AddLast( Self )
	End Method
End Type

Type TSpring Extends TConstraint
	Field stiffness:Float = 1.0
	Field dampening:Float = 0.5
	Field restLength:Float = 10.0
	Field length:Float = 0.0
	Field prevLength:Float = 0.0
	
	Method Evaluate()
		' Difference between...not actual body1 or body2
		length = body1.Distance( body2 )
		Local velocity:Float = length - prevLength
		Local force:Float = ( ( restLength - length ) - velocity * dampening ) * 2 / ( stiffness * stiffness )
		Local direction:TVector = body1.SubtractNew( body2 )
		direction.Normalize()
		
		prevLength = body1.Distance( body2 )
		
		Local combinedWeight:Float = body1.mass + body2.mass
		body1.vel.x:+ force * direction.x * ( body1.mass / combinedWeight )
		body1.vel.y:+ force * direction.y * ( body1.mass / combinedWeight )
		body2.vel.x:+ -1 * force * direction.x * ( body2.mass / combinedWeight )
		body2.vel.y:+ -1 * force * direction.y * ( body2.mass / combinedWeight )
	End Method
	
	Method Draw()
		DrawLine body1.x, body1.y, body2.x, body2.y
	End Method
	
	Function Create:TSpring( body1:TBody, body2:TBody )
		Local spring:TSpring = New TSpring
		spring.body1 = body1
		spring.body2 = body2
		spring.restlength = body1.Distance( body2 )
		Return spring
	End Function
End Type

Type TMouseConstraint Extends TConstraint
	Field body:TBody
	
	Method Evaluate()
		body.vel.x = 0.0
		body.vel.y = 0.0
		body.x = MouseX()
		body.y = MouseY()
	End Method
	
	Method Draw()
	End Method
	
	Function Create:TMouseConstraint( body:TBody )
		Local constrain:TMouseConstraint = New TMouseConstraint
		constrain.body = body
		Return constrain
	End Function
End Type


' ---------------------
' VISUAL REPRESENTATION
' ---------------------
' Handles all visual representation of the dynamic object.
Type TSprite
	Field r:Int = 256
	Field g:Int = 256
	Field b:Int = 256
	Field a:Float = 1.0
	Field image:TImage
	Field frame:Int = 0
	
	' Draws the sprite.
	Method Draw( body:TBody )
		'SetRotation body.r
		'SetScale body.s.x, body.s.y
		SetColor r, g, b
		SetAlpha a
		DrawImage image, body.x, body.y, frame
	End Method
	
	Function Create:TSprite( image:TImage )
		Local sprite:TSprite = New TSprite
		sprite.image = image
		Return sprite
	End Function
End Type


...and then some test code to run the engine...

' ==========================
'  First Physical
'   by Ragnar Brynjulfsson
' ==========================

Rem
	Test code for PrimusMotor
End Rem


' Start
SuperStrict

' Framework
Include "PrimusMotor.bmx"


' Incbin everything
Local First:TGame = TGame.Create()
First.Run()

	
' Base object for the entire game.
Type TGame
	' Constants
	Const w:Int = 640
	Const h:Int = 480
	Const depth:Int = 0
	Const wh:Int = w/2
	Const hh:Int = h/2
	
	' Fields
	Field engine:TPrimusMotor = TPrimusMotor.Create()
	Field QuitGame:Int = 0
	
	
	Function Create:TGame() Final
		Local Game:TGame = New TGame
		' Set up graphics mode.
		Graphics w, h, depth
		AutoMidHandle( True )
		SetBlend ALPHABLEND
		Return Game
	End Function
	
	Method Update()
		engine.Update()
		engine.Draw()
	End Method
	
	Method Draw()
	End Method
	
	
	' The main loop of the game.
	Method Run()
		Local Spring:TSpring = New TSpring
		Repeat
			Cls
			If KeyHit( KEY_ESCAPE ) Then QuitGame = 1
			If KeyHit( KEY_H ) Then
				' TARGET
				Local Target:TBody = TBody.Create( MouseX(), MouseY() )
				Target.AddToEngine( engine )
				' HERO
				Local Hero:TBody = TBody.Create( MouseX(), MouseY() + 40 )
				Hero.AddToEngine( engine )
				' HERO to TARGET
				Local s0:TSpring = TSpring.Create( Hero, Target )
				s0.stiffness = 1.5
				s0.dampening = 0.5
				s0.AddToEngine( engine )
				Spring = s0
				
				Local b1:TBody = TBody.Create( MouseX() -25, MouseY() + 80 )
				b1.mass = 1.0
				b1.AddToEngine( engine )
				Local b2:TBody = TBody.Create( MouseX() + 25, MouseY() + 80 )
				b2.mass = 1.0
				b2.AddToEngine( engine )
				
				Local s1:TSpring = TSpring.Create( Hero, b1 )
				Local s2:TSpring = TSpring.Create( Hero, b2 )
				Local s3:Tspring = TSpring.Create( b1, b2 )
				s1.stiffness = 1.5
				s2.stiffness = 1.5
				s3.stiffness = 1.5
				s1.dampening = 0.5
				s2.dampening = 0.5
				s3.dampening = 0.5
				s1.AddToEngine( engine )
				s2.AddToEngine( engine )
				s3.AddToEngine( engine )
				 
				' FORCES
				Local Gravity:TGravity = TGravity.Create()
				Gravity.direction.y = 0.98
				Gravity.AddBody( Hero )
				Gravity.AddBody( b1 )
				Gravity.AddBody( b2 )
				Gravity.AddToEngine( engine )

				Local Drag:TDrag = TDrag.Create( 1.1 )
				Drag.amount = 1.04
				Drag.AddBody( Hero )
				Drag.AddBody( b1 )
				Drag.AddBody( b2 )
				Drag.AddToEngine( engine )

				
				' TARGET to MOUSE
				Local Constrain:TMouseConstraint = TMouseConstraint.Create( Target )
				Constrain.AddToEngine( engine )
			End If
			If KeyHit( KEY_R ) Then
				Local gravity:TGravity = TGravity.Create()
				gravity.direction.y = 0.98
				gravity.AddToEngine( engine )
				Local drag:TDrag = TDrag.Create( 1.04 )
				drag.AddToEngine( engine )
				Local attach:TBody = TBody.Create( MouseX(), MouseY() )
				attach.AddToEngine( engine )
				Local prevBody:TBody = attach
				Local i:Int = 1

				While i < 10 
					Local b:TBody = TBody.Create( MouseX(), MouseY() + i * 10 )
					gravity.AddBody( b )
					drag.AddBody( b:TBody )
					b.AddToEngine( engine  )
					Local s:TSpring = TSpring.Create( prevBody, b )
					s.stiffness = 1.5
					s.dampening = 0.5
					s.AddToEngine( engine )
					prevBody = b
					Spring = s
					i:+ 1
				Wend
				' TARGET to MOUSE
				Local Constrain:TMouseConstraint = TMouseConstraint.Create( attach )
				Constrain.AddToEngine( engine )
			End If
			If KeyHit( KEY_UP ) Then
				For Local s:TSpring = EachIn engine.constraintList
					s.stiffness:+ 0.10
				Next
			End If
			If KeyHit( KEY_DOWN ) Then
				For Local s:TSpring = EachIn engine.constraintList
					s.stiffness:- 0.10
				Next
			End If
			If KeyHit( KEY_RIGHT ) Then
				For Local s:TSpring = EachIn engine.constraintList
					s.dampening:+ 0.10
				Next
			End If
			If KeyHit( KEY_LEFT ) Then
				For Local s:TSpring = EachIn engine.constraintList
					s.dampening:- 0.10
				Next
			End If
			If KeyHit( KEY_W ) Then
				For Local s:TSpring = EachIn engine.constraintList
					s.restLength:+ 1.0
				Next
			End If
			If KeyHit( KEY_S ) Then
				For Local s:TSpring = EachIn engine.constraintList
					s.restLength:- 1.0
				Next
			End If
			If KeyHit( KEY_K ) Then
				engine.bodyList.clear()
				engine.constraintList.clear()
				engine.forceList.clear()
			EndIf

						
			DrawText "Stiffness    "+Spring.stiffness, 200, 20
			DrawText "Dampening    "+Spring.dampening, 200, 40
			DrawText "RestLength   "+Spring.restLength, 200, 60
			
			DrawText "Arrow Up/Down", 400, 20
			DrawText "Arrow Left/Right", 400, 40
			DrawText "W/S", 400, 60
			DrawText "H to Create dangling triangle", 400, 80
			DrawText "R to Create danglng rope", 400, 100
			DrawText "K to Kill all dynamic objects", 400, 120
			DrawText "Escape to quit", 400, 140
			'WaitMouse()
			Update()
			Draw()
			Flip
		Until QuitGame
	End Method
End Type


I'd still like some feedback on those springs (will happily accept feedback on the the code, structure etc. too).

I'm doing this to try to teach myself some more math and physics for use in future games. :-)

Cheers,

Ragnar


Haramanai(Posted 2006) [#3]
Nice.
Have you been throu the ( clasic ) tutorial Advanced Character Physics by Thomas Jakobsen?
Looks like you had... If not you must. You can find it in gamasutra.
But beware the real nightmare comes with "Collision Detection and Responce"