[Haskell-cafe] Observer pattern in haskell FRP

Heinrich Apfelmus apfelmus at quantentunnel.de
Tue Dec 18 22:52:54 CET 2012


Nathan Hüsken wrote:
> On 12/08/2012 10:32 AM, Heinrich Apfelmus wrote:
>> Fair enough, but I don't see how this can be fitted into a general
>> pattern. If the animation "state" is coupled tightly to the game logic
>> "state", then the question whether the animation is part of the game
>> logic or not does not have a clear answer anymore. Hm.
> 
> I see it like this: The "logic state" may not depend on the "rendering
> state". If for example the animation of an asteroid changes how or when
> objects collide with it, than the animation is part of the logic.
> If however the physics of the game treat the asteroid as a object whose
> shape does not change depending in the animation, than the animation is
> part of the "rendering state".
> So the coupling may only be logic->rendering.
> 
>> Maybe discussing a concrete example could be very helpful. Could you
>> give a minimal example that still contains the key difficulties?
> 
> I put a pseudo C++ example below the mail. I use the terms "model" and
> "view" for the game logic and rendering respectively.
> The example is a little different. Asteroids explode when they collide.
> The moment asteroids explode, they are removed from the model (the game
> logic) while in the view (rendering) they still exist until the
> explosion animation is over.

(Sorry for the late reply.)

I see, that's a nice example.

Indeed, if you try to model this situation with dynamic collections

   galaxyModel :: Behavior [AsteroidModel]
   galaxyView  :: Behavior [AsteroidView]

then you have to keep track of IDs in some way, because a change in the 
collection of asteroid models needs to be reflected in a corresponding 
change in the collection of asteroid views.

   identifier :: AsteroidModel -> ID

   eCollisions :: Event [ID]
   eCollisions = collisions <$> galaxyModel <@ eTick
       where
       collisions asteroids = [identifier a | a <- asteroids,
                               b <- asteroids, a `collides` b]

   galaxyModel = accumB initialAsteroidModels
               $ removeFromList <$> eCollisions

   galaxyView  = accumB initialAsteroidViews
               $ startExplosions <$> eCollisions


That said, do note that any significant use of pointers in an imperative 
program translates to the use of identifiers in the purely functional 
variant. This is very much *independent* of FRP! In other words, if you 
find that giving certain game objects an "identity" is a good way to 
structure your code, then you need to use identifiers, regardless of 
whether you use FRP or not.


Of course, as you note in another message, there are other ways to 
structure this code. For instance, the second idea would be to use a 
data type

    type Asteroid = (Maybe AsteroidModel, AsteroidView)

which represents live asteroids as  (Just positionEtc, view)  and dead 
asteroids as  (Nothing, explosionView) . Then again, one unsatisfactory 
point about this approach is that an exploding asteroid is now 
represented explicitly in the game logic as a  Nothing  value.


A third approach would be to keep an explicit list of explosions.

    data AsteroidModel =
         AsteroidModel { view :: AsteroidView, pos :: Position }
    data AsteroidView  =
         AsteroidView  { rotation :: Angle }
    data Explosion     =
         Explosion     { posExp :: Position }

    galaxyView :: Behavior ([AsteroidView], [Explosion])
    galaxyView = (,) <$> (map view <$> galaxyModel) <$> explosions

    explosions = accumB [] $ startExplosions <$> eCollisions

You do need an event to communicate which asteroids have exploded, but 
an exploding asteroid will not appear in  galaxyModel  anymore. Instead, 
it will be added as an "anonymous" explosion to the rendering logic. (In 
a sense, the asteroid views with the state variables  dead = false  and 
  dead = true  have been split into different types.)


I find the third approach to be quite satisfactory. What is your opinion?

The more I think about this example, the more I think that the 
underlying difficulty is not FRP, but the use of pointers / identities.


Best regards,
Heinrich Apfelmus

--
http://apfelmus.nfshost.com




More information about the Haskell-Cafe mailing list