Skip to content
This repository has been archived by the owner on Nov 13, 2018. It is now read-only.

Use Elmish for game state management (basic sample) #28

Open
wants to merge 14 commits into
base: master
Choose a base branch
from

Conversation

alfonsogarciacaro
Copy link
Member

Finally I got some time to have a look at this :) First I tried to update #27 (actually this PR is branched from that) but I realized that #27, while sharing the state between the React and Pixi parts (so, for example, dragons can be added by clicking the button or the sprite), Elmish is used to manage the React part and it's complicated to share the update function with Pixi so at the end I opted for integrating Elmish with the basic sample.

Please have a look at src/basic/App.fs, it should be structured more or less as a typical Elmish application. For that I use the src/basic/Elmish.Animation.fs helper, which is an adaptation of the Elmish.Worker in the FableConf game workshop. Note that I've used an additional tick function to solve the issue mentioned here. Actually, I noticed with this the PIXI ticker is not actually necessary so I removed it.

Advantages of this approach:

  • Reduce the cognitive load: Devs can create games using the Elmish model.
  • Reuse Elmish helpers: In theory we can use other Elmish helpers. I say "in theory" because I had problems with the debugger (apparently Pixi objects don't serialize well) and HMR (it works but only the first time) so they may still need some work.

But there also disadvantages:

  • We still need the tick function: So devs have to handle two update functions. I tried to make dispatch only collect the messages and then make tick the actual update function, which would receive the collection of messages (aka events) happened in this frame together with the delta. However, this was not possible the way Elmish works. We would have to modify it for this.
  • Pixi sprites are not a good fit for an immutable model: There's mutation every where so it's difficult to keep state snapshots after every update. And restricting state mutations to only the update and tick functions can only be enforced by the programmer discipline, not the compiler.

So basically we need to decide if we want to a) adapt Elmish as is for games, b) create something similar but more fit to games, or c) leave the Elmish way definitely. What do you think? @whitetigle @MangelMaxime

@whitetigle
Copy link
Contributor

First of all thanks for taking time to make these searches on that difficult Elmish topic @alfonsogarciacaro 👍
I need to review you code. Hopefully I will post my thoughts a little bit later this week-end! 😄

@whitetigle
Copy link
Contributor

Hi!
In order to see what at stake here, I have gathered some information relative to a simple game architecture and what we have today.

State machines

Through the years I've always been using state machines.
A state is essentially a model that evolves through time until reaching its END_OF_STATE condition then triggering the change to a brand new state.

  let mutable gameState = Init
  
  // our render loop, every frame 
  app.ticker.add (fun delta -> 
    gameState<- 
      match screen with 

      | Init -> // prepare model then move to start 
         let model = ...
         NextScreen (Start model)
      
      | Start model -> // launch start anim, often some title tween
          let model, reachedEnd = Start.Update model
          if reachedEnd then 
             Start.Cleanup() // one call to remove all unused sprites
             NextScreen (Game model)
      
      | NextScreen nextScreen -> nextScreen // wait one frame to go to the next state

      | GameOver model -> 
         let model, done = GameOverState.Update model
        if done then 
             GameOver.Cleanup()
             NextScreen Init

      | Game model -> 
          let model, reachedEnd = GameState.Update model
          if reachedEnd then 
             GameState.Cleanup() // one call to remove all unused sprites
             NextScreen GameOver
 
    ) |> ignore 

This sample could easily be refactored to have central cleanup system and GoToNextState mechanism.
Then in every Update, there should be an internal local state machine.

So we would end with

  • General game model: the model all states would share, responsible to hold such things as numbers of players, configuration, high scores, etc...

  • Local state model: a model local to the current state. For instance: sprite list, current score, remaining lives, etc..

  • a way to know when to switch from one state to the other.

Events

Events plague all game developments with callbacks... that may call other callbacks to bubble up to an event management system wtht will tell what to do.

Here, the Dispatch mechanism in Elmish is just great. We can respond to events that come from the remotest sprite in a complex layer hierarchy. So that's great!

But then the information dispatched needs to be analysed thanks to a context, which would often be our local model. For instance, if sprite A collided with Sprite B, we would dispatch a CollisionBetween A B localModel. In our localModel we would have some sprite list which we would update consequently and maybe the reaching of END_OF_STATE (for instance Sprite A was our space ship and it just died)

Then, we would let the main loop or an upper management system handle this END_OF_STATE message. For instance, it would lead to the cleanup of the current state, local model and start of a new Game Over state to display a neat GameOver animation.

Elmish

Now regarding Elmish, I think that dispatching messages is the most important thing. But as you said, we are working in an always mutable land that does not need to wait for user input or model input to update its rendering. I mean, unlike your previous Canvas sample, here with pixi (and also with paperjs and maybe any game lib) I think it may be best to let the rendering happen and just control the logic and analyse the dispatched messages.

Strengthening

But like OpenFrameworks or Processing (setup, draw) I think we should enforce some mechanism to make our FSM more robust. It whould not be complicated because what we always end up using:

  • some sprite list which may be a custom list or just the container holding the sprites
  • some container list which we could also name layers since we have this notion of stacking up containers on top of each other.

Sprite licecycle

Also a sprite needs to be taken care of by removing it from its ancestor. There are two ways to do that:

  • either we have a reference to the container and do container.removeChild sprite

or

  • we do this right from the sprite like this mySprite.parent.removeChild mySprite. In case of generic sprites, this has the huge benefit of not having to use a custom list. We use the sole pixe container system.

Conclusion

if I read you well, and if I recollect my memories from previous experiences, I think using Elmish is not a good fit unless we animate everything using event based animation systems like Animatejs which allows us to have some [start-during-endfAnimation callbacks and can be easily fit into an Elmish architecture.

I do have this kind of projects. I think Secret of Monkey Island could fit well in an Elmish Architecture.

But to make a Bullet Hell shmup like like the Touhou series... well, I think we may just find try to dig further into a GloriousStateMachine 😉

I don't think I have said all. But I'm a little bit short on time so let's say here are my starters 😉

@alfonsogarciacaro
Copy link
Member Author

Thanks a lot for your feedback @whitetigle! So I guess the best option is to build our own framework, inspired by Elmish but more adapted to a 60-FPS game 👍

State Machines

I absolutely agree with this. Actually one of the drawbacks of Elmish for me is that, because everything works by composition you cannot define isolated state machines. I'm a big fan of the proposal of Tomas Petricek to model state machines with async. This would allow us to represent the top flow very nicely. For example:

type Result = Win | Lose
type MenuOption = StartGame | ShowCredits
type Screen = Menu | GamePlay of int | GameOver | GameCompleted | Credits

let rec startGame(): Async<unit> = async {
    let! option = showScreen Menu
    match option with
    | StartGame ->
        let mutable level = 1
        while level > 0 && level < MAX_LEVEL do
            let! res = showScreen (GamePlay level)
            match res with
            | Win  -> level <- level + 1
            | Lose -> level <- 0
                      do! showScreen GameOver
        if level = MAX_LEVEL then
            do! showScreen GameCompleted
    | ShowCredits ->
        do! showScreen Credits
    return! startGame()
}

Events

I also agree here that a dispatch model à la Elmish is easier than a subscriber model. My only doubt is if the events have to be dealt with immediately or one by one, or rather just collected and be dealt with later in the update that in a game is going to be run every frame anyways. I have the feeling that collecting events and running update only once per frame is both easier for devs an also more performant. What do you think @whitetigle? As a reference, in the FableConf Game workshop there was a mixed model as user input (arrows) were dealt with one by one, while collisions (as provided by the physics woker) were handled all at once every frame.

State

Besides different levels of State (global and local) I wonder if the state should be coupled with the renderer or not. Basically, whether Pixi sprites, etc, should go into the state or not. Having only simple types in the state can have advantages: it's easier to serialize and thus makes time travel debugging and exchanges with a worker also easier, separates rendering from logic. On the other hand, it can be more complicated to the dev, who will have to update the Pixi objects in the render function (unless we can create something like Virtual DOM for Pixi).

@whitetigle
Copy link
Contributor

Thanks @alfonsogarciacaro 👏

State Machines: ok for Async. Could you just prepare some sample so that I can better see what it involves?

Events: I think you're right. So how could we apply that to a Drag and drop model with pixi Interaction Events like in this sample?

State: Like I said, I think the state should be decoupled from the renderer. It should be somehow agnostic.

@alfonsogarciacaro
Copy link
Member Author

@whitetigle I've updated the dragging sample. The result is much more verbose than the original and probably many things can be improved but I hope it helps you get a better idea. It's a very simple state machine with two states: dragging and waiting for dragging to start, each one with a different update function.

The logic (State.fs) is the decoupled from the view (Renderer.fs) module. The main function is an asynchronous loop which sends a dispatch function to the Renderer to collect the events and sends the collected events to the appropriate update function according to the state. Note for example, that we don't need to add a dragging flag to all the dragons because this info is in the state machine.

@whitetigle
Copy link
Contributor

Hey that's very interesting!
I need to make my mind around it though so I will try to update another sample.
Thanks @alfonsogarciacaro 👍

@whitetigle
Copy link
Contributor

Ok. I started a very secret ㊙️ project involving your proposal. It's here

It's my little contribution to the advent calendar.

So there's no real events like your take on the drag and drop sample. There's just a render function that dispatch messages. But still it's clearly interesting to dispatch messages.

It's running at 60fps on my laptop and there are some particles 😉

@alfonsogarciacaro
Copy link
Member Author

Himitsu

@MangelMaxime
Copy link
Member

I think @alfonsogarciacaro is creating a bank of image and gif since several months and is now using them :)

Other than that, great work to both or you on this Elmish + Pixi related stuff. Don't have much time to help on this subject ATM.

@whitetigle
Copy link
Contributor

I think @alfonsogarciacaro is creating a bank of image and gif since several months and is now using them :)

😆😆😆

Other than that, great work to both or you on this Elmish + Pixi related stuff. Don't have much time to help on this subject ATM.

No problem at all. I also have zounds of things to do. Anyway thanks @MangelMaxime . In 2018 we'll Hink the world 😉 🍷 at Strasbourg 😁

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants