Scaling the complexity of games

Now that I could finally watch the videos by Joël Quenneville, Jordy Moos and Roman Potasovs at Elm Europe 2019, I want to talk a bit about the methods I used while developing the two games Little World Puzzler and Asteroid Miner .

Time and Randomness

Most games need both time and randomness. Thus, I typically model my game in the following way.

import Page.Game as Game

type Model =
    Loading --The title screen
    | InGame
        { game : Game.Model
        , seed : Seed
        , time : Posix
        }

type Msg =
    LoadingSpecific LoadingMsg
    | GameSpecific Game.Msg

type LoadingMsg =
    GotTime Posix
    | GotSeed Seed

I use time mostly for synchronizing with some backend. As for the seed, I do not want to handle the seed all the time. Therefore, my update function for the game will return a Generator.

updateGame : Game.Msg -> Game.Model -> Generator (GameModel,Cmd GameMsg)

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case (msg, model) of
        (GameSpecific gameMsg, InGame inGameModel) ->
            let
                ((game,cmd),seed) = model.seed
                    |> Random.step
                        ( updateGame gameMsg ingGameModel.game)
            in
            ( InGameSpecific
                { inGameModel
                | game = game
                , seed = seed
                }
            , cmd
            )
     ..

You can find the discussion behind this concept over here:


The tutorial vs. the normal game

Title screens, Game modes and Tutorials

Once the game is done, I typically need to add a title screen, for more complicated games maybe also a tutorial or different game modes. This was very tricky to get right. The trick is, to think of the game as a website like any other. Therefore, a title screen or different modes are just different pages. You can find more on splitting pages in the talk “Moving to the Actor Model in Elm” by Albert Dhalin. Note that you should not start wrapping everything into a separate component.

In my games it looks something like this:

type Model =
    HomeScreen Home.Model
    | NormalMode Normal.Model
    | ChallengeMode Challenge.Model
    | Tutorial Tutorial.Model
    | EndScreen End.Model

type Msg =
    | HomeSpecific Home.Msg
    | NormalSpecific Normal.Msg
    | ChallengeSpecific Challenge.Msg
    | TutorialSpecific Tutorial.Msg
    | EndSpecific Endscreen.Msg

update : Msg -> Model -> (Model,Cmd)
update msg model =
    case (msg,model) of
        (HomeScreen homeModel,HomeSpecific homeMsg) ->
            Homescreen.update homeModel homeMsg
            |> Tuple.mapBoth HomeScreen HomeSpecific
            --For simplicity im cutting page transitions
        ..

  • Note 1: The Highscore in the picture is taken from a remote server.
  • Note 2: Pressing “Replay Highscore” will fetch the game from the server and start a spectator mode.

and Gameover screens

Sadly it’s not as easy. Typically, a game is won at some point. After that you might want to display a Leaderboard or return to the main screen. Maybe a Tutorial would reset after a game over and special game modes (like spectator) might want to restart. So in order to keep everything organized I created orasund/elm-action, a package that lets you model page-transitions as a state machine.

As the complexity of the things around the game increased, I wanted to separate the actual game, so I modelled the game like a “reusable view” and updating it might respond in a transition. The page will then decide how to continue.

import Action exposing (Action) --Orasund/elm-action

type GameAction =
    Action
        Game.Model
        Game.Msg
        Int --The transition data aka. the final score
        Never --The Game can't trigger exiting the page

update : Game.Msg -> Game.Model -> Action
update  msg model =
    case msg of
        Won score ->
            Action.transition score
            --The page can either display the score or start again
        Tick ->
            let
                newModel : Game.Model
                newModel = model
                    |> movePlayer
                    |> moveEnemies
           in
           Action.updating (newModel,Cmd.none)

Further Reading

I feel like this post is long enough, but there are still a lot of interesting topics to talk about when it comes to structuring a game in Elm. Here are some notable links and packages:

  • Entity Component System
    You might have seen that currently I let the player move first, and then I move the enemies. For larger games this will lead to inconsistencies where the player might be able to do things, that enemies can’t. To avoid this problem completely you would combine both Players and Enemies into a single type: Entity and then use components like “moveable” or “deadly” to describe a player or an enemy. Checkout justgook/elm-game-logic for a package that implements an ECS for you.
    Justgook has also given a talk on Elm Europe about game performance.

  • Multiplayer
    I’m currently trying to wrap my head around Multiplayer and to release a package in the future that will let you write multiplayer games out of the box using jsonstore as a backend. This will probably be finished at the beginning of next year.

  • Pathfinding
    An honourable mention goes to krisajenkins/astar for an A* algorithm implemented in Elm. I haven’t used it yet but its always on my mind.

I hope you’ve found this post useful, cheers :wink:

7 Likes

I like that use of Generator as the function return type, a bit neater than explicitly passing the seed through every time.

I notice the definition of Generator expresses that idea anyway, which is not really a surprise:

type Generator a =
    Generator (Seed -> (a, Seed))
1 Like

If your multiplayer is suitable for peer-to-peer then something like https://peerjs.com/ or https://github.com/chr15m/bugout work well. I’ve used the former to build a PoC shared redux like state manager, and the latter to build a simple tic-tac-toe example. Both have the limitation of requiring a browser that supports the WebRTC data channel.

1 Like

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