Series: Game Engine
- F# Game Project - September 24, 2017
- Morgemil Game Update #1 - February 26, 2019
- Morgemil Game Update #2 - March 10, 2019
Morgemil Game Update #3 - March 24, 2019
- Morgemil Game Update #4 - April 17, 2019
- Morgemil Game Update #5 - December 23, 2021
- Morgemil Game Update #6: Adding time and characters - December 27, 2021
- Morgemil Game Update #7: Notes since 2021 - September 03, 2023
Morgemil Game Update #3
I’ve been working off and on making a video game in F#. I thought I’d drop an update saying my progress.
Github
I’ve done 79 commits since the beginning of the year according to GitHub. Although progress technically started on Mar 15, 2015.
This number doesn’t mean anything to anyone. I just use it so I can look at the last progress update I did and be motivated because I am writing code, even if not all code directly becomes a very visual output.
Progress
The player may move around the game level on screen with the arrow keys. This is a square level surrounded by walls, and in either corner are stairs up to the previous level and down to the next level. When the player reaches the stairs on the bottom right, he moves on to the next level and the stairs on the top right. (Currently the next level is a duplicate of the old level.)
An interesting thing to note above is the events and updates that are happening on the background console screen.
I’ve refactored the game engine so that the visual on-screen game and the actual game logic are separate. The game logic is behind an interface, which forces the graphical engine to communicate by passing messages.
type Step< 'tRow when 'tRow :> IRow> =
{ Event: ActionEvent
Updates: 'tRow TableEvent list
}
[<RequireQualifiedAccess>]
type GameState =
| Processing
| Results of Character Step list
| WaitingForInput
[<RequireQualifiedAccess>]
type GameStateRequest =
| Input of ActionRequest
| QueryState of AsyncReplyChannel<GameState>
| SetResults of Character Step list
| Kill
| Acknowledge
type IGameStateMachine =
/// Stops the game engine
abstract member Stop: unit -> unit
/// Gets the current state of the game loop
abstract member CurrentState: GameState with get
/// Sends input
abstract member Input: ActionRequest -> unit
/// Acknowledge results
abstract member Acknowledge: unit -> unit
Inside my graphical engine that is running at 60 fps, I can interact with the Game State above:
match gameState.CurrentState with
| GameState.WaitingForInput ->
// event comes from keyboard input, if any
if event.IsSome then
gameState.Input event.Value
| GameState.Processing ->
// Nothing to do, just waiting on game logic to process
printfn "processing"
| GameState.Results results ->
results
|> List.iter (fun event ->
printfn "%A" event
match event.Event with
| ActionEvent.MapChange mapChange ->
// Going to the next level gets special treatment as an event
// Overwrite the game map
viewOnlyTileMap <- createTileMapFromData mapChange.TileMapData
// Overwrite current characters (monsters, players, etc.) on screen
viewCharacterTable <- CharacterTable()
mapChange.Characters
|> Array.iter (Table.AddRow viewCharacterTable)
| _ ->
// any other event besides a map change, just process character changes as normal
event.Updates
|> List.iter(fun tableEvent ->
match tableEvent with
| TableEvent.Added(row) -> Table.AddRow viewCharacterTable row
| TableEvent.Updated(_, row) -> Table.AddRow viewCharacterTable row
| TableEvent.Removed(row) -> Table.RemoveRow viewCharacterTable row
)
)
// acknowledge that the results have been handled
gameState.Acknowledge()
By doing game logic in a background thread, and only communicating with the graphical engine by messages of what’s changed, I can now take this further and create a game server. Anything that implements that interface, whether a Web API or such, can now serve up game requests. Even further, to record a game I merely need to record the player’s inputs.
One difficulty has been in accurately recording changes to game characters. I can’t miss any if I want to keep the front-end and the back-end in sync. I also want to display events on screen as they happen. Not every change should be a visual event on screen, and I also want events and other changes to be consistent with each other. When an event plays on-screen, only the changes so far should be apparent. And that’s what led me to the below structure as a currently suitable representation of the timeline. Given an event, I also give the list of changes leading up to that event.
type Step< 'tRow when 'tRow :> IRow> =
{ Event: ActionEvent
Updates: 'tRow TableEvent list
}
With the Step structure above, I’ve added a history to some of my table implementations.
type ITableEventHistory<'tRow when 'tRow :> IRow> =
abstract member History: unit -> TableEvent<'tRow> list with get
abstract member ClearHistory: unit -> unit
With the history capability implemented in a few tables, my game loop has become a little wordy, but I get a lot of recording for free.
type Loop(characters: CharacterTable, tileMap: TileMap) =
member this.ProcessRequest(event: ActionRequest): Character Step list =
Table.ClearHistory characters
EventHistoryBuilder characters {
match event with
| ActionRequest.Move (characterID, direction) ->
match characterID |> Table.TryGetRowByKey characters with
| None -> ()
| Some moveCharacter ->
let oldPosition = moveCharacter.Position
let newPosition = oldPosition + direction
let blocksMovement = tileMap.Item(newPosition) |> TileMap.blocksMovement
if blocksMovement then
yield
{
CharacterID = moveCharacter.ID
OldPosition = oldPosition
RequestedPosition = newPosition
}
|> ActionEvent.RefusedMove
else
Table.AddRow characters {
moveCharacter with
Position = newPosition
}
yield
{
CharacterID = moveCharacter.ID
OldPosition = oldPosition
NewPosition = newPosition
}
|> ActionEvent.AfterMove
| ActionRequest.GoToNextLevel (characterID) ->
match characterID |> Table.TryGetRowByKey characters with
| None -> ()
| Some moveCharacter ->
if tileMap.[moveCharacter.Position] |> TileMap.isExitPoint then
let items = characters
|> Table.Items
|> Seq.toArray
items
|> Seq.map(fun t -> {
t with
Position = tileMap.EntryPoints |> Seq.head
})
|> Seq.iter (Table.AddRow characters)
yield
{
Characters =
characters
|> Table.Items
|> Seq.map(fun t -> {
t with
Position = tileMap.EntryPoints |> Seq.head
})
|> Array.ofSeq
TileMapData = tileMap.TileMapData
}
|> ActionEvent.MapChange
if Table.HasHistory characters then
yield ActionEvent.Empty
}
Now if you notice, I’m doing a lot of “yield” inside a custom computation expression. Anytime I yield an ActionEvent, such as a character moved, I get a snapshot of the history on the table, and then clear the table history.
type EventHistoryBuilder<'T, 'U when 'T :> ITableEventHistory<'U> and 'U :> IRow>(table: 'T) =
member this.Bind(m, f) =
f m
member this.Return(x: ActionEvent): 'U Step list =
let step =
{ Step.Event = x
Updates = Table.History table
}
Table.ClearHistory table
[ step ]
member this.Return(x: 'U Step list ): 'U Step list =
x
member this.Zero(): 'U Step list =
[]
member this.Yield(x: ActionEvent): 'U Step list =
let step =
{ Step.Event = x
Updates = Table.History table
}
Table.ClearHistory table
[ step ]
member this.Yield(x: 'U Step list): 'U Step list =
x
member this.Combine (a: 'U Step list, b: 'U Step list): 'U Step list =
List.concat [ b; a ]
member this.Delay(f) =
f()
Given all the history snapshots, I display it to the debugging console as show in the gif above.
Summary
My code is here. I may not be doing a lot of things in a functional way of programming, but my code is always in a workable state and I’m constantly refactoring to make it better.
Series: Game Engine
- F# Game Project - September 24, 2017
- Morgemil Game Update #1 - February 26, 2019
- Morgemil Game Update #2 - March 10, 2019
Morgemil Game Update #3 - March 24, 2019
- Morgemil Game Update #4 - April 17, 2019
- Morgemil Game Update #5 - December 23, 2021
- Morgemil Game Update #6: Adding time and characters - December 27, 2021
- Morgemil Game Update #7: Notes since 2021 - September 03, 2023