Particle System

BlitzMax Forums/BlitzMax Programming/Particle System

Hi everyone! (Full code at the bottom of the post)

I started work on an RTS a little while ago and decided that it was time to make a particle system for it. Now, I've never actually made a particle system so this was a first but I kind of like the design. So, I've decided to release it. I only started it an hour or so ago so don't expect much; but that's kind of the philosophy. It's meant to be as modular as possible to allow for any modifications that you want.

The way it works is through "modifiers". Particles themselves have nothing associated with them. Instead they are acted upon by modifiers that are all user defined. They can hold any kind of data that you'd like them to hold through the use of a "Data" type. Furthermore user defined emitters govern the creation of the particles themselves.

Anyway, code might make it slightly easier to see so here is the underlying structure:

Type TParticle
	Global number

	Field data:TList
	Field alive
	
	Method New()
		data = New TList
		alive = True
		number:+1
	End Method
	
	Method Delete()
		number:-1
	End Method
	
	Method AddData(dat:TData)
		For Local i:TData = EachIn(data)
			If dat.name$ = i.name$ Then  Return False
		Next
		data.addlast(dat)
		Return True
	End Method
	
	Method Set(kind$,dat:TData)
		For Local i:TData = EachIn(data)
			If kind = i.name$ 
				data.remove(i)
			EndIf
		Next
		data.addlast(dat)
		Return True
	End Method
	
	Method Get:TData(kind$)
		For Local i:TData = EachIn(data)
			If kind = i.name$ Then Return i
		Next
		Return Null
	End Method
End Type

Type TModifier
	Global list:TList = New TList
	
	Field plist:TList
	
	Method New()
		list.addlast(Self)
		plist = New TList
	End Method
	
	Method AddParticle(p:TParticle)
		plist.addlast(p)
		SetData(p)
	End Method
	
	Function UpdateAll()
		For Local i:TModifier = EachIn(list)
			i.UpdateParticles()
		Next
	End Function
	
	Method UpdateParticles()
		Lock()
		For Local i:TParticle = EachIn(plist)
			If i.alive = True
				Update(i)
			Else
				plist.remove(i)
			EndIf
		Next
		Unlock()
	End Method
	
	Method Update(p:TParticle) Abstract
	
	Method SetData(p:TParticle) Abstract
	
	Method Lock() Abstract
	
	Method Unlock() Abstract
End Type

Type TData
	Field name$
End Type

Type TEmitter
	Global list:TList=New TList
	
	Field rate
	Field x#,y#
	
	Field time
	
	Method New()
		list.addlast(Self)
		time = MilliSecs()
		rate = 1000
	End Method
	
	Function UpdateAll()
		Local time = MilliSecs()
		For Local i:TEmitter = EachIn(list)
			For Local t:Int = 0 To Int((time-i.time)/i.rate)-1
				i.Emit()
				i.time = MilliSecs()
			Next
		Next
	End Function
	
	Method Emit() Abstract
End Type


Then, in the main loop,
Graphics 800,600

While Not KeyDown(KEY_ESCAPE)
	Cls
	TModifier.UpdateAll()
	TEmitter.UpdateAll()
	Flip
Wend


Clearly though, this won't do anything at all! So, to make it do something useful. To start we need to add some modifiers and emitters. Lets start with making the particles able to draw and move. Lets also add an emitter that will emit particles randomly in every direction.

Type TRandomEmitter Extends TEmitter
	Global drawing:TDrawing = New TDrawing
	Global mover:TMove = New TMove
	Global culler:TCull = New TCull


	Method Emit()
		Local p:TParticle = New TParticle
		p.AddData(TPosition.Create(x,y))
		p.AddData(TVelocity.Create(Rnd(4)-2,Rnd(4)-2))
		p.AddData(TColor.Create(Rand(255),Rand(255),Rand(255)))
		drawing.AddParticle(p)
		mover.AddParticle(p)
		culler.AddParticle(p)
	End Method
End Type

Type TPosition Extends TData
	Field x#, y#
	
	Method New()
		name$ = "position"
	End Method
	
	Function Create:TPosition(x#,y#)
		Local p:TPosition = New TPosition
		p.x = x
		p.y = y
		Return p
	End Function
End Type

Type TVelocity Extends TData
	Field x#, y#
	
	Method New()
		name$ = "velocity"
	End Method

	Function Create:TVelocity(x#,y#)
		Local p:TVelocity= New TVelocity
		p.x = x
		p.y = y
		Return p
	End Function
End Type

Type TColor Extends TData
	Field r,g,b
	
	Method New()
		name$ = "color"
	End Method

	Function Create:TColor(r,g,b)
		Local p:TColor= New TColor
		p.r = r
		p.g = g
		p.b = b
		Return p
	End Function
End Type

Type TDrawing Extends TModifier
	Method Update(p:TParticle)
		Local pos:TPosition = TPosition(p.Get("position"))
		Local col:TColor = TColor(p.Get("color"))
		If Not col = Null
			SetColor col.r,col.g,col.b
		EndIf
		DrawOval pos.x#-2,pos.y#-2,4,4
		SetColor 255,255,255
	End Method
	
	Method SetData(p:TParticle)
		p.AddData(New TPosition)
	End Method
	
	Method Lock()
	End Method
	
	Method Unlock()
	End Method
End Type 

Type TMove Extends TModifier
	Method Update(p:TParticle)
		Local pos:TPosition = TPosition(p.Get("position"))
		Local vel:TVelocity = TVelocity(p.Get("velocity"))
		
		pos.x :+ vel.x
		pos.y :+ vel.y
		
	End Method
	
	Method SetData(p:TParticle)
		p.AddData(New TVelocity)
		p.AddData(New TPosition)
	End Method
	
	Method Lock()
	End Method
	
	Method Unlock()
	End Method
End Type

Type TCull Extends TModifier
	Method Update(p:TParticle)
		Local pos:TPosition = TPosition(p.Get("position"))
		If pos.x<0 Or pos.x>GraphicsWidth() Or pos.y<0 Or pos.y>GraphicsHeight() Then p.alive = False
	End Method
	
	Method SetData(p:TParticle)
		p.AddData(New TPosition)
	End Method
	
	Method Lock()
	End Method
	
	Method Unlock()
	End Method
End Type


Then a random emitter would have to be added to the main loop. It would become,
Local em:TRandomEmitter = New TRandomEmitter
em.x = 400
em.y = 300
em.rate = 5
While Not KeyDown(KEY_ESCAPE)
	Cls
	TModifier.UpdateAll()
	TEmitter.UpdateAll()
	Flip
Wend


To see how easy it is to add new modifiers let us make it so that the particles fade out. This requires four modifications:

1) The introduction of a data type to hold the alpha of the particles

2) The introduction of a type to adjust the alpha

3) Modification to the drawing type to draw with alpha

4) The addition of the fade type in the emitter

The base types look like,
Type TAlpha Extends TData
	Field alpha#
	
	Method New()
		name$ = "alpha"
		alpha = 1.0
	End Method
	
	Function Create:TAlpha(alpha#)
		Local p:TAlpha =New TAlpha
		p.alpha = alpha
		Return p
	End Function
End Type

Type TFade Extends TModifier
	Method Update(p:TParticle)
		Local alpha:TAlpha = TAlpha(p.Get("alpha"))
		alpha.alpha:-0.01
		If alpha.alpha<0 Then p.alive = False
	End Method
	
	Method SetData(p:TParticle)
		p.AddData(New TAlpha)
	End Method
	
	Method Lock()
	End Method
	
	Method Unlock()
	End Method
End Type


and the updated draw type and emitter type look like,
Type TRandomEmitter Extends TEmitter
	Global drawing:TDrawing = New TDrawing
	Global mover:TMove = New TMove
	Global culler:TCull = New TCull
	Global fader:TFade = New TFade


	Method Emit()
		Local p:TParticle = New TParticle
		p.AddData(TPosition.Create(x,y))
		p.AddData(TVelocity.Create(Rnd(4)-2,Rnd(4)-2))
		p.AddData(TColor.Create(Rand(255),Rand(255),Rand(255)))
		p.AddData(New TAlpha)
		fader.AddParticle(p)
		drawing.AddParticle(p)
		mover.AddParticle(p)
		culler.AddParticle(p)
	End Method
End Type

Type TDrawing Extends TModifier
	Field blendmode
	
	Method Update(p:TParticle)
		Local pos:TPosition = TPosition(p.Get("position"))
		Local col:TColor = TColor(p.Get("color"))
		Local alpha:TAlpha = TAlpha(p.Get("alpha"))
		If Not alpha = Null
			SetAlpha alpha.alpha
		EndIf
		If Not col = Null
			SetColor col.r,col.g,col.b
		EndIf
		DrawOval pos.x#-2,pos.y#-2,4,4
		SetColor 255,255,255
		SetAlpha 1
	End Method
	
	Method SetData(p:TParticle)
		p.AddData(New TPosition)
	End Method
	
	Method Lock()
		blendmode = GetBlend()
		SetBlend ALPHABLEND
	End Method
	
	Method Unlock()
		SetBlend blendmode
	End Method
End Type 


Here's all the code in one place ready to be run!
Strict

Type TParticle
	Global number

	Field data:TList
	Field alive
	
	Method New()
		data = New TList
		alive = True
		number:+1
	End Method
	
	Method Delete()
		number:-1
	End Method
	
	Method AddData(dat:TData)
		For Local i:TData = EachIn(data)
			If dat.name$ = i.name$ Then  Return False
		Next
		data.addlast(dat)
		Return True
	End Method
	
	Method Set(kind$,dat:TData)
		For Local i:TData = EachIn(data)
			If kind = i.name$ 
				data.remove(i)
			EndIf
		Next
		data.addlast(dat)
		Return True
	End Method
	
	Method Get:TData(kind$)
		For Local i:TData = EachIn(data)
			If kind = i.name$ Then Return i
		Next
		Return Null
	End Method
End Type

Type TModifier
	Global list:TList = New TList
	
	Field plist:TList
	
	Method New()
		list.addlast(Self)
		plist = New TList
	End Method
	
	Method AddParticle(p:TParticle)
		plist.addlast(p)
		SetData(p)
	End Method
	
	Function UpdateAll()
		For Local i:TModifier = EachIn(list)
			i.UpdateParticles()
		Next
	End Function
	
	Method UpdateParticles()
		Lock()
		For Local i:TParticle = EachIn(plist)
			If i.alive = True
				Update(i)
			Else
				plist.remove(i)
			EndIf
		Next
		Unlock()
	End Method
	
	Method Update(p:TParticle) Abstract
	
	Method SetData(p:TParticle) Abstract
	
	Method Lock() Abstract
	
	Method Unlock() Abstract
End Type

Type TData
	Field name$
End Type

Type TEmitter
	Global list:TList=New TList
	
	Field rate
	Field x#,y#
	
	Field time
	
	Method New()
		list.addlast(Self)
		time = MilliSecs()
		rate = 1000
	End Method
	
	Function UpdateAll()
		Local time = MilliSecs()
		For Local i:TEmitter = EachIn(list)
			For Local t:Int = 0 To Int((time-i.time)/i.rate)-1
				i.Emit()
				i.time = MilliSecs()
			Next
		Next
	End Function
	
	Method Emit() Abstract
End Type

Type TRandomEmitter Extends TEmitter
	Global drawing:TDrawing = New TDrawing
	Global mover:TMove = New TMove
	Global culler:TCull = New TCull
	Global fader:TFade = New TFade


	Method Emit()
		Local p:TParticle = New TParticle
		p.AddData(TPosition.Create(x,y))
		p.AddData(TVelocity.Create(Rnd(4)-2,Rnd(4)-2))
		p.AddData(TColor.Create(Rand(255),Rand(255),Rand(255)))
		p.AddData(New TAlpha)
		fader.AddParticle(p)
		drawing.AddParticle(p)
		mover.AddParticle(p)
		culler.AddParticle(p)
	End Method
End Type

Type TPosition Extends TData
	Field x#, y#
	
	Method New()
		name$ = "position"
	End Method
	
	Function Create:TPosition(x#,y#)
		Local p:TPosition = New TPosition
		p.x = x
		p.y = y
		Return p
	End Function
End Type

Type TVelocity Extends TData
	Field x#, y#
	
	Method New()
		name$ = "velocity"
	End Method

	Function Create:TVelocity(x#,y#)
		Local p:TVelocity= New TVelocity
		p.x = x
		p.y = y
		Return p
	End Function
End Type

Type TColor Extends TData
	Field r,g,b
	
	Method New()
		name$ = "color"
	End Method

	Function Create:TColor(r,g,b)
		Local p:TColor= New TColor
		p.r = r
		p.g = g
		p.b = b
		Return p
	End Function
End Type

Type TAlpha Extends TData
	Field alpha#
	
	Method New()
		name$ = "alpha"
		alpha = 1.0
	End Method
	
	Function Create:TAlpha(alpha#)
		Local p:TAlpha =New TAlpha
		p.alpha = alpha
		Return p
	End Function
End Type

Type TDrawing Extends TModifier
	Field blendmode
	
	Method Update(p:TParticle)
		Local pos:TPosition = TPosition(p.Get("position"))
		Local col:TColor = TColor(p.Get("color"))
		Local alpha:TAlpha = TAlpha(p.Get("alpha"))
		If Not alpha = Null
			SetAlpha alpha.alpha
		EndIf
		If Not col = Null
			SetColor col.r,col.g,col.b
		EndIf
		DrawOval pos.x#-2,pos.y#-2,4,4
		SetColor 255,255,255
		SetAlpha 1
	End Method
	
	Method SetData(p:TParticle)
		p.AddData(New TPosition)
	End Method
	
	Method Lock()
		blendmode = GetBlend()
		SetBlend ALPHABLEND
	End Method
	
	Method Unlock()
		SetBlend blendmode
	End Method
End Type 

Type TMove Extends TModifier
	Method Update(p:TParticle)
		Local pos:TPosition = TPosition(p.Get("position"))
		Local vel:TVelocity = TVelocity(p.Get("velocity"))
		
		pos.x :+ vel.x
		pos.y :+ vel.y
		
	End Method
	
	Method SetData(p:TParticle)
		p.AddData(New TVelocity)
		p.AddData(New TPosition)
	End Method
	
	Method Lock()
	End Method
	
	Method Unlock()
	End Method
End Type

Type TCull Extends TModifier
	Method Update(p:TParticle)
		Local pos:TPosition = TPosition(p.Get("position"))
		If pos.x<0 Or pos.x>GraphicsWidth() Or pos.y<0 Or pos.y>GraphicsHeight() Then p.alive = False
	End Method
	
	Method SetData(p:TParticle)
		p.AddData(New TPosition)
	End Method
	
	Method Lock()
	End Method
	
	Method Unlock()
	End Method
End Type

Type TFade Extends TModifier
	Method Update(p:TParticle)
		Local alpha:TAlpha = TAlpha(p.Get("alpha"))
		alpha.alpha:-0.01
		If alpha.alpha<0 Then p.alive = False
	End Method
	
	Method SetData(p:TParticle)
		p.AddData(New TAlpha)
	End Method
	
	Method Lock()
	End Method
	
	Method Unlock()
	End Method
End Type
		
Graphics 800,600

Local em:TRandomEmitter = New TRandomEmitter
em.x = 400
em.y = 300
em.rate = 5

Local fps = 0
Local frames = 0
Local time = MilliSecs()
While Not KeyDown(KEY_ESCAPE)
	Cls
	TModifier.UpdateAll()
	TEmitter.UpdateAll()
	frames = frames + 1
	If MilliSecs()-time>1000
		time = MilliSecs()
		fps = frames
		frames = 0
	EndIf
	DrawText TParticle.number + " Particles",10,10
	DrawText fps + " Frames Per Second",10,30
	Flip
Wend


Anyway, I hope someone finds this in some way useful and I'd love to hear comments/critiques on the design.

If anyone actually decides they might want to use this then post here and I'll write up some more formal documentation about the addition of new modifiers etc... The basic theory is that you should never have to touch any of the underlying code (that which is in the first box).