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