Most active commenters

    ←back to thread

    333 points freetonik | 11 comments | | HN request time: 0.018s | source | bottom
    Show context
    fleabitdev ◴[] No.42471288[source]
    This engine uses a Redux-like architecture. You have a State type (containing data like "the position of the black kingside rook") and a stream of in-game actions (like "knight to F3"). Each action is handled by a pure function which converts the current State to a new State. You can either transmit State deltas from the server to the client, or just transmit the actions themselves (https://longwelwind.net/blog/networking-turn-based-game/).

    This design makes it easy to implement optimistic updates, rollback, replays, automated testing, and recovery after a disconnection. It's a surprisingly good fit for UI, too; you can render simple games as a React component which takes the current State as one of its props.

    However, a stream of context-free actions can be a really inconvenient representation for some games. The rules of a board game are often like the control flow of a computer program: you'll see branching, iteration, local variables, function calls, structured concurrency, and sometimes even race conditions and reentrancy. When you try to represent all of this logic as a State object, you're basically maintaining a snapshot of a "call stack" as plain data, and manually resuming that "program" whenever you handle an action. It doesn't seem ideal.

    I've been sketching a board game engine which would represent the game logic as normal code instead. It seems promising, but it really needs a couple of language features which don't exist in the mainstream yet, like serialisation of suspended async functions.

    replies(5): >>42471791 #>>42472084 #>>42472570 #>>42475998 #>>42477605 #
    1. LudwigNagasena ◴[] No.42472084[source]
    My main pain point with any sort of Flux-like state management is transitions [1]. The state of UI is not fully described by the state of the game [2]. If I play a card, the game state can be instantly updated to the next decison-making point, but in reality I want to show steps of the game through animations, some of which are concurrent and some of which are consecutive. That usually ends up in a mess; and I've never seen someone implement it nicely.

    [1] And generally dynamic stuff like drag-n-drop, which is infinitely times simpler in any other architecture than in React.

    [2] That is also true for business apps, but their animations are usually so simple you can simply use CSS.

    replies(7): >>42472171 #>>42472685 #>>42475075 #>>42475378 #>>42476566 #>>42476646 #>>42480234 #
    2. semitones ◴[] No.42472171[source]
    In my experience, the way to solve [1] and [2] is to design a game state that can also _fully_ describe the state of the UI, including animation cues.
    3. Longwelwind ◴[] No.42472685[source]
    The way I wanted to implement this in my turn-based game engine:

    If you implement the deterministic update pattern to handle state synchronisation you can add "event" inside the logic that handles updates that pause the processing allowing your animations to be played. In JS, for example:

        async function handleUpdate(update) {
            if (update.type == "sell-items") {
                this.player.inventory[update.itemId] -= 1;
    
                await emitEvent("itemSold");
    
                this.player.money += 10;
    
                await emitEvent("moneyGain");
            }
        }
    
    Server-side, "emitEvents" would be a no-op. Everything would resolve synchronously.

    Client-side, the UI can listen to those events to pause the updating of the game state to see the intermediary state of the game and play animations. When the animation is done, it can resolve the promise, resuming the game updating logic.

    If an update arrives while an update is being handled, it can be queued so it can be played after the current update finishes.

    4. sjrd ◴[] No.42475075[source]
    The way I did this was to design a more-or-less monadic container `Result<A>` for all my game logic functions. It batches a sequence of animation steps with a result. It can also model error conditions (like not having enough resources for example). I can then instantiate it any concrete result type, such as a full game state or just the result of individual computations. It was very nice to concisely write complicated game logic with animations while retaining the happy path.

    https://github.com/sjrd/barrage/blob/main/src/main/scala/be/...

    replies(1): >>42475752 #
    5. nkrisc ◴[] No.42475378[source]
    I toyed with an approach once that separated animations from game state updates.

    Every player action could cause a cascade of updates, which would all be resolved “instantly” to the point no more cascaded updates were left to be processed.

    While this is happening, any update that includes an animation pushes that to an “animation stack”, then the animations are played back one by one to show the player what happened. In this animation state most input in disabled and the game is effectively on hold until the animations complete (or are skipped by the player).

    The “animations” were basically commands that the Model used to update the View, just with the option to apply them one by one over time. So the model is always up to date as fast as possible as possible, and the view just lags behind a bit and catches up.

    replies(1): >>42478049 #
    6. LudwigNagasena ◴[] No.42475752[source]
    Interesting. Unfortunately, your repo seems to be private.
    replies(1): >>42475821 #
    7. sjrd ◴[] No.42475821{3}[source]
    Oh shoot, yes. I have imitation copies of the original game graphics in there, and so I can't make it public without violating copyright of their assets. :-(

    Here is a public gist with the `Result` data structure, as well a good portion of the file handling all the game mechanics, which should show it gets used. https://gist.github.com/sjrd/34fe234d1b6232cf42ffda5d23292d3...

    8. ginsider_oaks ◴[] No.42476566[source]
    I remember someone making a card game GALGA [1] in Haskell and compiling a high-level "rules" DSL to a low-level "animation" DSL and "primitive" DSL for handling animations and state changes respectively.

    [1] https://roganmurley.com/2021/12/11/free-monads.html

    9. gr4vityWall ◴[] No.42476646[source]
    (game dev) What I did in the past was having actions generate both a new state, and a stack of 'instructions' that update the UI, which was deliberately not a 1:1 representation of the game state for the exact reasons you described.

    Then you can 'execute' the stack within a command VM of some sorts, where instructions can move sprites around, play sounds, etc. You can have 'high level' instructions ("display the enemy's death") implemented as a combination of low level instructions ('reduce that health bar count until it goes to zero' -> 'change entity X's sprite to dead sprite' -> 'give player 200 gold as reward' -> 'play sound' -> 'change text in the text bar to something' -> ...)

    It ended up working waaay better than what I was expecting, felt very easy to reason about, wasn't hard to maintain, and scratched that itch of implementing an interesting solution. :)

    10. jay_kyburz ◴[] No.42478049[source]
    I've found in the past its good to have the model updates in the queue so that the animation controllers can be simpler. The animations simply need to observe the model and react to changes, and post events when they complete.
    11. tinkrr ◴[] No.42480234[source]
    I agree fully with [1], having recently experienced the pain of implementing a custom drag-and-drop UI in React.

    I ended up using ref's heavily to avoid stale closure and async re-render issues. Which basically amounts to circumventing React and interacting with the DOM directly.