Use Elmish for game state management (basic sample)
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
dispatchonly collect the messages and then maketickthe actualupdatefunction, 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
updateandtickfunctions 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
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! 😄
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 😉
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 :+1:
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).
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.
@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.
Hey that's very interesting! I need to make my mind around it though so I will try to update another sample. Thanks @alfonsogarciacaro 👍
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 😉

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.
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 😁