Creating a game functionally with Stride

11 minute read

Background

Inspired by the blog post where Svetol ECS was implemented into Stride Engine, I want to create a custom implementation of the base Game class so that the game update loop would be better suited to a functional approach. I attempted to use Garnet but I don’t have enough experience with ECS system to create an implementation. I then pivot to a MVU architecture using the elmish library. However I could not get the game engine update loop and elmish loop to work together so I rolled my own. The original proof of concept can be found here but my implementation have changed since then.

New Project

Starting from the basic solution created by Stride, add a new F# class library called MyGame.Core, I renamed Library.fs to Program.fs, and included all the Stride nuget packages from the MyGame project. Then add MyGame project as a reference to MyGame.Core. Then add MyGame.Core as a project reference in MyGame.Windows. Make sure to save the solution and make sure go to the Stride editor and select File -> Reload Project. Otherwise, the Stride editor will remove your F# project from the solution whenever changes are made. Another issue is that the Stride editor will try to build with fsc.exe and fail. You can build the F# project first through Visual Studio then run the project via the Editor or run the project entirely through Visual Studio.

Setup

Events

For the MVU system to work, I need some way to create and receive messages while the game is running. For this, I use the built in Event system in Stride. So inside the MyGame C# project, I create a static Events class consisted of a Player EventKey and a Player EventReceiver.

using Stride.Engine;
using Stride.Engine.Events;
using System;

namespace MyGame
{
    public static class Events
    {
        public static readonly EventKey<string> PlayerEventKey = new();
        public static readonly EventReceiver<string> PlayerEventListener = new(PlayerEventKey, EventReceiverOptions.Buffered);
    }
}

I want to send "Up", "Down", "Left", "Right", and "Stop" messages so the event will be of type string.

It is important to set the event receiver to buffered. This will allows me to get all events that was fired instead of just the latest one.

Player Component

The current scene will have only a Sphere Entity so let’s create a MVU component for moving the ball. Create a new Player.fs above Program.fs. For my model, I want to hold the velocity of the ball and ball Enitity.

//Player.fs
namespace MyGame.Core

open Stride.Core.Mathematics    //Use Stride Vector3 instead of the built in F# version for better compatibility
open Stride.Engine;
open System.Linq

module Player =  
    type Model =
        {
            Velocity : Vector3
            Sphere : Entity
        }

For the message, I want to move the ball up, down, left, and right as well as stopping when no key is pressed so those will be my messages

type Msg =
    | Up
    | Down
    | Left
    | Right
    | Stop

Since I will receive "Up", "Down", "Left", "Right", and "Stop" messages, I need to create a map function to convert them.

let map message : Msg list = 
    match message with
    | "Left" -> [Left]
    | "Right" -> [Right]
    | "Up" -> [Up]
    | "Down" -> [Down]
    | "Stop" -> [Stop]
    | _ -> []

I also need to create an empty and init function. The empty function will be used to initialize the model at runtime. But since I need a reference to the Sphere entity that I can’t get at runtime, I have an init function to set the proper value once the data is loaded. Once the scene is loaded can I get the reference to my Sphere entity. The init function also return a list of Msg for when an component need to send a message once initialized.

let empty =
    { Velocity = Vector3.Zero; Sphere = new Entity () }
        
let init (scene : Scene) : Model * Msg list =
    let sphere = scene.Entities.FirstOrDefault(fun x -> x.Name = "Sphere")     
    { empty with Velocity = Vector3.Zero; Sphere = sphere }, []

I like to reuse the empty Record to create the init Record because it saves me from repeating my code to initialize the value for my labels. This is especially handy for long Record.

For the View, I want to make the ball move depending on the velocity. The delta time is also passed so that movement is framerate independent. Since I am modifying the objects directly, it might be inappropriate to call this MVU.

let view model (deltaTime : float32) =
    model.Sphere.Transform.Position <- model.Sphere.Transform.Position + model.Velocity * deltaTime

Lastly, for Update, I want to update the model’s Velocity based on the message.

let update msg model (deltaTime : float32) : Model * Msg list =
    match msg with        
    | Left -> 
        { model with Velocity = model.Velocity - Vector3.UnitX }, []                        
    | Right -> 
        { model with Velocity = model.Velocity + Vector3.UnitX }, []            
    | Up -> 
        { model with Velocity = model.Velocity - Vector3.UnitZ }, []            
    | Down -> 
        { model with Velocity = model.Velocity + Vector3.UnitZ }, []            
    | Stop -> 
        { model with Velocity = Vector3.Zero }, []

Copying from the Elmish library, my update function return a new model along with a list of Messages. This allows me to follow up with multiple messages. Also, I can return an empty list without having to create a special Empty message when I don’t need follow up message.

Game Component

Just like the player component, we need a game component. This is a crucial part of the implementation since it is responsible for reading all the messages that was created. It is also responsible for calling the view and update functions of other components. Create the Game.fs class right below Player.fs and add the following code:

//Game.fs
namespace MyGame.Core

open Stride.Engine
open Stride.Engine.Events
open Stride.Games
open System.Linq

module Game =  
    type Model =
        {
            PlayerModel : Player.Model
        }

    type Msg =
        | PlayerMsg of Player.Msg

    let empty =
        { PlayerModel = Player.empty }
        
    let init (scene : Scene) : Model * Msg list =
        let playerModel, playerMsgs = Player.init scene
        let gameMsgs =
            [
                yield! List.map (PlayerMsg) playerMsgs
            ]
            |> List.distinct
        { empty with PlayerModel = playerModel }, gameMsgs

    let private mapEvent (eventReceiver : EventReceiver<'a>) (eventMap : 'a -> 'b list) (listMap : 'b list -> Msg list) =   
        let eventList = (Seq.empty).ToList()
        let numEvent = eventReceiver.TryReceiveAll(eventList)
        let events = Seq.toList eventList
        let messages =
            [
                for e in events do
                    yield! listMap (eventMap e)
            ]
        messages

    let mapAllEvent () : Msg list =
        let messages =
            [
                yield! mapEvent MyGame.Events.PlayerEventListener Player.map (List.map PlayerMsg)
            ] |> List.distinct
        messages

    let view (gameModel : Model) (gameTime : GameTime) =
        let deltaTime = float32 gameTime.Elapsed.TotalSeconds

        Player.view gameModel.PlayerModel deltaTime

    let update (gameModel : Model) (cmds : Msg list) (gameTime : GameTime) =
        let deltaTime = float32 gameTime.Elapsed.TotalSeconds

        let updateFold ((gameModel, msgs) : Model * Msg list) cmd  = 
            match cmd with
            | PlayerMsg(m) ->
                let (model,msg) = Player.update m gameModel.PlayerModel deltaTime
                { gameModel with PlayerModel = model }, msgs @ (List.map PlayerMsg msg)

        let newModel, newMessages = List.fold updateFold (gameModel, []) cmds
        newModel , List.distinct newMessages

Like the Player component, there is the Model, Msg, empty, init, view and update. You can see how the view and update function of this class will call the respective functions for the Player component. The view and update function will need to be updated as more components are created.

The Model for the Game component will contain the model of the other component. In this case, there is only the Player model

The Msg will be a discriminated union of the other component msg.

The init function will call the Player.init function and also convert any Player Msg into the appropriate Game Msg.

update required a fold function and the accumulator is the state and follow up messages that can change after every update function for each message.

The mapEvent and mapAllEvent functions work together to read all event messages. mapEvent is designed to be generic so only mapAllEvent would need to be change to add new events.

Let’s take a look at the mapEvent function. This will take an eventReceiver, a eventMap function, and a listMap function.

The eventReceiver will be the EventListener for your component. In this case, it is the PlayerEventListener created inside the Events class of the MyGame project.

The eventMap is the map function inside the Player component class. This is so we can convert the string messages from the PlayerEvent into a type checked Msg for the Player component.

Lastly, the listMap is a basic List.map function since we need a list of Game Msg but we only have a list of Player Msg so we need to convert them. All this is so that mapEvent can be generic and can handle multiple events.

Thanks to the generic mapEvent function, more events can be added as needed like so:

let mapAllEvent () : Msg list =
    let messages =
        [
            yield! mapEvent MyGame.Events.PlayerEventListener Player.map (List.map PlayerMsg)
            //yield! mapEvent MyGame.Events.MusicEventListener Music.map (List.map MusicMsg)
            //yield! mapEvent MyGame.Events.SceneEventListener Scene.map (List.map SceneMsg)
        ] |> List.distinct
    messages

A MVU Game Class

Now that all the components are created, I need to modify the game update loop. Copying Svetol’s implementation, I create a new class derived from the base Stride Game class inside the Program.fs file.

//Program.cs
namespace MyGame.Core

open Stride.Engine;

open Game

type MvuGame() =
    inherit Game()

    let mutable Model, Messages = Game.empty, []
        
    override this.BeginRun () = 
        let mainScene = this.Content.Load<Scene>("MainScene")
        let model, messages = init mainScene
        Model <- model
        Messages <- messages    
        
    override this.Update gameTime =
        base.Update(gameTime);
            
        Messages <- Messages @ mapAllEvent ()

        let model, messages = update Model Messages gameTime
        Model <- model
        Messages <- messages

        view Model gameTime |> ignore

    override this.Destroy () =
        base.Destroy()

This class have two new members, Model and Messages. Because they are class members, I can’t pass in a Scene to grab the actual value.

In BeginRun, I will pass the current scene to Game.init to initialize the Model and Messages properties.

Update now call Game.mapAllEvent to get all event messages. It will then call Game.update to process all messages. In this implementation, I have it so that any follow up messages will be handled in the next frame. Another implementation will have the update continued until no follow up message is received instead. However, this is simpler and prevent infinite loop from messages following each other.

Creating a C# script

In the Stride Engine, nothing will happen without a script. We need a script to capture keyboard input and fire the appropriate events. Go in the editor and create a new Script called SphereController and put in the following.

//MyGameApp.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Stride.Input;
using Stride.Engine;

namespace MyGame
{
    public class SphereController : SyncScript
    {
        public List<Keys> KeysLeft { get; } = new List<Keys>();

        public List<Keys> KeysRight { get; } = new List<Keys>();

        public List<Keys> KeysUp { get; } = new List<Keys>();

        public List<Keys> KeysDown { get; } = new List<Keys>();

        public override void Update()
        {
            var isKeyPress = false;

            if (KeysLeft.Any(key => Input.IsKeyDown(key)))
            {
                isKeyPress = true;
                Events.PlayerEventKey.Broadcast("Left");
            }
            if (KeysRight.Any(key => Input.IsKeyDown(key)))
            {
                isKeyPress = true;
                Events.PlayerEventKey.Broadcast("Right");
            }
            if (KeysUp.Any(key => Input.IsKeyDown(key)))
            {
                isKeyPress = true;
                Events.PlayerEventKey.Broadcast("Up");
            }
            if (KeysDown.Any(key => Input.IsKeyDown(key)))
            {
                isKeyPress = true;
                Events.PlayerEventKey.Broadcast("Down");
            }
            if (isKeyPress == false)
            {
                Events.PlayerEventKey.Broadcast("Stop");
            }
        }
    }
}

This script will check on every frame if a specific key is pressed. If pressed, it will send the corresponding message.

Follow the video to assign the script to the Sphere entity. Assign the keys in the editor and move the camera up to a higher position to view the area.

Finally, go to your platform project(MyGame.Windows in this case) and replace the following in MyGameApp.cs

using (var game = new Game())
{
    game.Run();
}

with

using (var game = new MyGame.Core.MvuGame())
{
    game.Run();
}

Run it either from Visual Studio or The Stride Editor for the following result

The final project can found here and the original proof of concept here.

Closing Thoughts

One benefit of this approach is that it doesn’t interfere with using the engine as designed. You can still create C# script and use all the editor functionalities. You have a choice on whether to implement a feature in a functional or object oriented style. Another benefit is that this approach can be done with other C# engine with a central game class and an event system.

I have not run any benchmark so I don’t know how this will scale for large games but there is no multithreading. Two potential places for multithreading is mapping all the event messages and running the update & view functions for each components. If performance is a major concern, I believe the steps to modify the game class shown here can be used to set up an ECS architecture.

Lastly, it is not shown here but it is common for a component to send message to another component either in the update or view function. One example is a Coin component telling the Score component to increase the score once collected. There are many ways to accomplish this in an MVU architecture but the easiest way for me is to call the Events to send messages like so:

let update msg model (deltaTime : float32) =
    match msg with        
    | Collect -> 
        MyGame.Events.ScoreEventKey.Broadcast("Increase");
        model, []