1

How to create particle effects in Haskell using the Gloss library? (e.g. to show an explosion)

If anyone could help me out a bit on how this is done it'd be much appreciated.

Best Regards, Skyfe.

user2999349
  • 859
  • 8
  • 21
  • I'm currently Learning gloss. If I figure it out, I'll post. I haven't made an animated Picture yet, but basically how it would work is you have a picture representing a particle (just a randomly generated circle), and some kind of counter in your main environment. Every "tick" advances the counter, and the current value of the counter decides how the Picture is displayed (it's size, color, whether or not it still active...). The specifics obviously depend on the situation. – Carcigenicate Nov 04 '14 at 19:58

1 Answers1

1

The comment on the question does a good job of providing a high-level solution, but I'm writing this answer to add detail.

Let's start by modeling the real-world object we want to represent. In our case, it's a particle. A particle ought to have a position, a velocity and an acceleration, all of which we can represent using 2D vectors. A reasonable way to store 2D vectors in Haskell is to use the Linear.V2 module. Next, let's think about nice additional properties we'd like a particle should have, specifically one involved in a firework or explosion. Notice how the particles in a firework burn bright for a time and then just 'fizzle out'? Let's call said time the particle's lifespan, and represent it using a Float. We can now create an appropriate representation for a Particle and a Cluster of Particles

data Particle = Particle
  { _age          :: Float 
  , _lifespan     :: Float 
  , _position     :: V2 Float
  , _velocity     :: V2 Float
  , _acceleration :: V2 Float }
  deriving ( Show )

type Cluster = [Particle]

makeLenses ''Particle

There's an extra field called age in our datatype above. The lifespan of the particle represents the time for which the particle exists from creation to death, while its age represents the time that has passed since the Particle's creation. In other words, a Particle should disappear when its age exceeds its lifespan. Keep that in mind for later.

Next, let's write a function that helps us create a Particle. All it does is set the initial age to 0 and leave the rest up to additional arguments

makeParticle :: Float -> V2 Float -> V2 Float -> V2 Float -> Particle
makeParticle = Particle 0

Once this is done, we can write a function that helps us create a Cluster of n particles

makeCluster :: Int -> (Int -> Particle) -> Cluster
makeCluster n particleGen = map particleGen [0..(n - 1)]

After that, we create a function that will allow us to advance a Particle by dt seconds. The function advances the Particle's age, changes its position based on its velocity and finally changes its velocity based on its acceleration. In the end, if the age of the Particle is more than its lifespan, we symbolize the deletion of the Particle by evaluating to Nothing instead of Just the changed particle.

advanceParticle :: Float -> Particle -> Maybe Particle
advanceParticle dt = hasDecayed . updateVel . updatePos . updateAge
    where
  r2f = realToFrac
  hasDecayed p = if p^.age < p^.lifespan then Just p else Nothing
  updateAge  p = (age      %~ (dt                       +)) p
  updatePos  p = (position %~ (r2f dt * p^.velocity     +)) p
  updateVel  p = (velocity %~ (r2f dt * p^.acceleration +)) p

The following function advances a Cluster, and gets rid of 'dead' Particles

advanceCluster :: Float -> Cluster -> Cluster
advanceCluster dt = catMaybes . map (advanceParticle dt)

Now we can move on to the part of the code that has to do with actually drawing particles using Graphics.Gloss. We're going to use a Cluster to represent the state of the simulation, and so we start with a function that returns a Cluster representing the initial state of the program. For a simple animation we're going to simulate a firework, where all the particles start in the same position, have the same lifespan, radiate out from their central position at regular angles, and are subject to the same acceleration

initState :: Cluster
initState = makeCluster numParticles particleGen
    where
  numParticles = 10

  particleGen :: Int -> Particle
  particleGen i = 
    makeParticle initLifespan
                 initPosition
                 (initVelMagnitude * V2 (cos angle) (sin angle))  
                 initAcceleration
      where
    fI               = fromIntegral
    angle            = (fI i) * 2 * pi / (fI numParticles)
    initLifespan     = 10
    initPosition     = V2 0 0
    initVelMagnitude = 5
    initAcceleration = V2 0 (-3)

Then we write a function to draw a Cluster on to the screen

drawState :: Cluster -> Picture
drawState = pictures . map drawParticle
    where
  drawParticle :: Particle -> Picture
  drawParticle p = 
    translate (p^.position._x) (p^.position._y)    .
    color     (colorAdjust (p^.age / p^.lifespan)) .
    circleSolid $ circleRadius
      where
    circleRadius  = 3
    colorAdjust a = makeColor 1 0 0 (1 - a)

Probably the only non-standard part about this is the colorAdjust function. What I was going for here was to color a Particle red and when it's created have it not be transparent at all (i.e. alpha value of 1) and keep fading out as its age approaches its lifespan (i.e. alpha value that keeps approaching 0)

We're almost done! Add a function that updates the Cluster to reflect the passage of time

stepState :: ViewPort -> Float -> Cluster -> Cluster
stepState _ = advanceCluster

Finish up the program by writing a main function that ties everything together

main :: IO ()
main = 
  simulate (InWindow name (windowWidth, windowHeight) 
                          (windowLocX,  windowLocY))
           bgColor
           stepsPerSec
           initState
           drawState
           stepState
    where
  name             = "Fireworks!"
  windowWidth      = 300
  windowHeight     = 300
  windowLocX       = 30
  windowLocY       = 30 
  stepsPerSec      = 30
  bgColor          = white

I hope this helps!