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:
Then, in the main loop,
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.
Then a random emitter would have to be added to the main loop. It would become,
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,
and the updated draw type and emitter type look like,
Here's all the code in one place ready to be run!
Anyway, I hope someone finds this in some way useful and I'd love to hear comments/critiques on the design.
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.