Creating a game functionally with Stride
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.
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 aneventReceiver
, aeventMap
function, and alistMap
function.The
eventReceiver
will be the EventListener for your component. In this case, it is thePlayerEventListener
created inside theEvents
class of theMyGame
project.The
eventMap
is themap
function inside thePlayer
component class. This is so we can convert the string messages from thePlayerEvent
into a type checkedMsg
for thePlayer
component.Lastly, the
listMap
is a basicList.map
function since we need a list ofGame Msg
but we only have a list ofPlayer Msg
so we need to convert them. All this is so thatmapEvent
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, []