Managing complex game state

I’m posting this everywhere because I’m super excited about it!
Also, I hope to see more people making games in Elm!

I wanted a game experience similar to using Unity, that allowed me to add and try a lot of weird stuff very quickly but immutability was in the way!

Eventually I managed to find an expressive and painless way to manage and prototype complex game state in Elm.
I’m very happy with the results, and since it took me a while to get there I thought I’d share it in a short guide:

Feedback is very welcome! =D

19 Likes

Interesting! thanks.

We use a similar system for our project, except we use these functions instead of the Delta data type:

combine : Effect -> Effect -> Effect
-- combine is an interface equivalent to (::) in your example

changeStateWithEffect : (State -> (State, Effect)) -> (State, Effect) -> (State, Effect)
changeStateWithEffect f (state, effect) = 
    let (newState, otherEffect) = f state
    in (newState, combine effect otherEffect)

changeState : (State -> State) -> (State, Effect) -> (State, Effect)
changeState f (state, effect) = (f state, effect)

addEffect : (State -> Effect) -> (State, Effect) -> (State, Effect)
addEffect f (state, effect) = (state, combine effect (f state))

an example of use close to your example would be :

myStateAndEffects
   |> changeState heal
   |> addEffect healingParticles
   |> changeStateWithEffect victoryCheck

Some benefits of this are that 1) we can not access the effects with our delta functions, 2) we are able to describe pretty easily what kind of changes the delta functions imply, and 3) for multiple effects, we can describe them using natural composition or piping style, rather than having a list handler in our system.

DeltaNone simply becomes identity.

@bChiquet If I understand correctly the main difference is that, in the system you describe deltas are applied as soon as they are computed, which means that each delta will be computed on a different state than the previous one, which is perfectly fine.

For my use case instead, I wanted deltas to be computed in one step and be applied i a different step, particularly because I might need to resolve conflicts.

I’m not sure what you mean by “accessing effects with our delta functions”, the only part that accesses the side effects list is the applyDelta function.

in the system you describe deltas are applied as soon as they are computed

Yes, indeed. We try to maintain a system where the order of operations doesn’t have a huge impact.
Note that it’s not a requirement though: this is a mechanism to apply patches, not to calculate them.

e.g.:

heal : State -> State -> State
heal calculatedWith appliedOn = { appliedOn | health = appliedOn.health + calculatedWith.healPower }


update previousTurn = 
    previousTurn
        |> changeState (heal previousTurn)
        |> changeStateWithEffects (victoryCheck previousTurn)

I’m not sure what you mean when you talk about resolving conflicts, do you mean you examine deltas and make some changes before applying them ?

I’m not sure what you mean by “accessing effects with our delta
functions”, the only part that accesses the side effects list is the applyDelta function.

I haven’t seen that problem in your code, but that’s something we’ve had in our codebase (parsing effects). This api eliminates this possibility so it’s easier for us to check that code using it doesn’t implement bad patterns.

By resolving conflicts, I mean two or more entities trying to do things in opposition to each other.
The typical example: two units trying to enter in the same cell, when a cell is limited to contain just one unit.
I assume that you can do it with the system you describe, but only by storing some temporary information within your larger State.

Anyway, your suggestion is interesting, do you use it already? Is there a real-world example I could check?

I suppose these conflicts are the consequence of a turn-based system ? If so, I don’t see another way than to have an order book in the model, which is applied when the NextTurn event comes. When this event comes, it can manage the game rules handling priority. That being said, I’ve never had to handle such things.

If the game is real time, then every event (e.g. A entering square x) should happen without conflict, since you’re monothreaded and two actions can’t happen in one update. If you batch actions, then it looks like the real time is modeled as a turn-based game with very fast turns.

We are in the process of refactoring our application to use this pattern, in order to have one standard way to effect the Elm runtime, so this is a WIP. I’ll see if I can extract a block of code that uses this pattern from the codebase.

Conflicts will happen regardless of the speed at which you update the state.
Indeed, if you update the game in real time you might get away with ignoring the problem and go with a first-comes-first-served (it’s what Herzog Drei does) but that’s not a given.

I feel our requirements are slightly different. What kind of application are you working on, exactly?

1 Like

I’m working on a scheduling application, so not really game related :slight_smile:

The idea of this code is that it lets you sequence actions the way you want. If you want to make rules to determine the sequence, you still have to do it yourself:

priorize : List Delta -> List Delta

deltaToChange : Delta -> State -> (State, Effect)

applyDeltas deltas currentState =
    priorize deltas 
        |> List.map deltaToChange 
        |> List.foldl changeStateWithEffect currentState 

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.