How to implement a requestAnimationFrame game-loop with Halogen hooks?

In my previous post I asked how to render a grid-based game. I’ve decided to go with Halogen hooks. And well, here I am now.

I’d like to discuss potential ways to implement a simple game-loop that would be rendered using a HTML5 canvas managed by Halogen. My ultimate goal is to make a very simple library that my students could use to implement simple games (sokoban, GoL, snake).

My initial idea was to to have a hook-based Halogen component that manages both the main loop and the rendering. The component will receive a Config state as its input with the following fields:

type Config state =
  { initialState :: state
  , draw :: state -> Grid
  , onTick :: state -> state
  }

I planned to call the onTick function in a recursive requestAnimationFrame loop, modifying the state and requesting the renders at the same time, similarly to the following (please ignore the missing liftEffects):

gridComponent { onTick, draw, initialState } = do
  _ /\ stateId <- Hooks.useState initialState
  Hooks.useLifecycleEffect $
    -- Start the game loop
    requestAnimationFrame (ticker stateId)
  where
    ticker stateId = do
      state <- Hooks.get stateId
      let newState = onTick state
      Hooks.modify_ stateId newState
      renderGrid (draw newState)
      requestAnimationFrame (ticker stateId)

(The code is simplified, I have already implemented a functioning mouse- and key- event tracking into the component.)

However, requestAnimationFrame needs to run an Effect Unit, while the inner state of a hook-based component can only be modified in a HookM. Thus, my initial idea seems to be impossible to implement — unless it’s possible to run a MonadEffect m inside an Effect somehow.

What would be the correct course of action here?

As a sidenote, do you think a onTick :: state -> state is enough, or should I allow some effects there? I guess requiring effectful functions (like with onTick :: state -> Effect state) is an overkill and a bad practice — again, I’m open to suggestions.

Tagging @thomashoneyman for obvious reasons.

If you need to run some code in Effect in your requestAnimationFrame, then could you set up an emitter / listener pair from halogen-subscriptions during initialization, and on each tick push the data you need for your state update through?

The notify function pushes some data to a Listener:

You can subscribe to an Emitter in Halogen Hooks:

…and run some HookM code in response any time a value gets pushed through.

1 Like

Ok, that somehow didn’t occur to me, thanks! I was so deep in the hooks mindset that I forgot there are other ways to solve problems.

Do you have any opinion on the design-choice regarding how many (and which) effects should be in the onTick function? I think that mostly it should be enough to make it a pure effect-less function, but sometimes Effect or some other effects might be needed. What are the best practices for library authors in these scenarios?

In something like this, where you don’t know what someone might want to do in response to an event, you might want to be as permissive as possible (and give it an Effect or even a HookM signature).