1

Using Haskell’s Gloss library, I’m trying to simulate a starfield. The visual aspect (drawing ‘stars’ with various speeds and sizes to the screen) is working. However, for some reason the stars aren’t being randomly distributed, resulting in a simulation which has a pattern. I also have this problem with an explosion simulation, but for simplicity’s sake, I’ll leave that one out for now. This is a simplified version of my code so far:

type Position       = (Float, Float)
type Velocity       = (Float, Float)
type Size           = Float
type Speed          = Float
type Drag           = Float
type Life           = Int

type Particle       = (Position, Velocity, Speed, Drag, Life, Size)

-- timeHandler is called every frame by the gloss ‘Play’ function. It's being passed
-- the delta time and the world it needs to update.
timeHandler dt world = world {rndGen = mkStdGen (timer world),
                              timer  = (timer world) + 1,
                              stars  = spawnParticle world (rndGen world) : updateParticles (stars world) dt world}

randomFloat :: StdGen -> Float -> Float -> Float
randomFloat rand min max = fst $ randomR (min, max) rand

spawnParticle :: World -> StdGen -> Particle
spawnParticle world gen = ((pos, (1 * speed, 0), speed, 1, 0, size), snd (split gen))
where pos   = (px', py')
      px'   = randomFloat gen (-600) (-500)
      py'   = randomFloat gen (-250) 250
      speed = size * (randomFloat gen 100 300) -- the smaller a particle, the slower 
      size  = randomFloat gen 0.1 1.3

updateParticles :: [Particle] -> Float -> World -> [Particle]
updateParticles []     _  _     = []
updateParticles (x:xs) dt world | fst(posPart x) > 500 = updateParticles xs dt world
                                | otherwise            = updatedPart : updateParticles xs dt world
    where pos'        = updateParticlePosition dt x world
          updatedPart = (pos', velPart x, speedPart x, 1, 0, sizePart x)

Note: velPart, speedPart etc. are functions to get a property out of a given particle. Again, drawing works fine, so I’ll leave that code out. updateParticlePosition simply adds the velocity to the current position of a star.

I think the problem has something to do with the fact that my random generators are not passed properly, but I’m too confused to come up with a solution… Any help is much appreciated!

Felix
  • 147
  • 2
  • 10
  • Remember that all Haskell functions are *pure*. With this in mind, how can `randomFloat :: StdGen -> Float -> Float -> Float` be expected to give different results from call to call? – gspr Oct 28 '16 at 15:21
  • I thought it should give different results if it's being passed a unique generator every call. At least, that's what Im trying to do.. – Felix Oct 28 '16 at 15:27
  • 1
    You discard the new generator state by discarding the second half of the value returned from `randomR`. – gspr Oct 28 '16 at 15:39

1 Answers1

2

This is nothing to do with gloss, just with the semantics of pure random number generation. Re-initializing a new generator with a new seed every time does not give you pseudo-random numbers, because the randomness only comes from the fact that the new generator created by e.g. randomR or split will have seeds very different from the original. You simply have map mkStdGen [0..], as you are simply adding 1 to timer at each step.

Consider the difference in the following distributions:

map (fst . randomR (1,1000) . mkStdGen) [0..1000]
take 1000 $ randomRs (1,1000) (mkStdGen 123)

The first is what you are doing, and the second is proper random numbers.

The solution is simple, just use split inside your time update function (you already have it inside of spawnParticle) :

timeHandler dt world = 
  let (newRndGen, g) = split (rndGen world) in 
   world { rndGen = newRndGen 
         , timer  = (timer world) + 1 , 
         , stars  = spawnParticle world g : updateParticles (stars world) dt world
         }

Note that now timer isn't used, and it probably makes more sense to have timer = timer world + dt anyways (the time deltas may not be exactly equal).

Also keep in mind you shouldn't reuse generators, so if you have many local functions which take a generator as a parameter, you may want something like:

timeHandler dt world = 
  let (newRndGen:g0:g1:g2:_) = unfoldr (Just . split) (rndGen world) in 
   world { rndGen = newRndGen 
         , timer  = (timer world) + 1 , 
         , stars  = spawnParticle world g0 : updateParticles (stars world) dt world
         , stuff0 = randomStuff0 g1 , stuff1 = randomStuff1 g2 }
user2407038
  • 14,400
  • 3
  • 29
  • 42
  • Thanks for your reply! It makes sense, however, the starfield is still not random. Remarkably, there is a decrease in particle size / speed, from the top of the screen to the bottom... Is this because I'm reusing `gen` in my `spawnParticle` function? – Felix Oct 28 '16 at 16:15
  • I should've tried it first before asking: I applied the same technique in `spawnParticle` as you used in `timeHandler` and it's working now! – Felix Oct 28 '16 at 16:24