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 #5
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 20 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
Updating to .NET 6
All F# projects were in netcoreapp3.1 or netstandard2.1 before being updated to net6.0 which has been smooth.
No trouble was experienced with the framework version.
Updating packages had a few breaking changes related to XNA vs SadConsole types and a few namespace changes. On the whole, it was remarkably painless. I’ve kept this project fairly clear of dependencies, and most of the dependencies are about testing which is acceptable.
Builds with FAKE and on GitHub.
The F# ecosystem has a build tool called FAKE. I’ve defined the build script in FAKE and then calling it from GitHub Workflows. For reference, I recommend this blog post which supplied the starting point.
> dotnet fake build
The last restore is still up to date. Nothing left to do.
run All
Building project with version: LocalBuild
Shortened DependencyGraph for Target All:
<== All
<== Report
<== Test
<== Build
<== Clean
The running order is:
Group - 1
- Clean
Group - 2
- Build
Group - 3
- Test
Group - 4
- Report
Group - 5
- All
Starting target 'Clean'
Finished (Success) 'Clean' in 00:00:01.7544636
Starting target 'Build'
...
...
...
---------------------------------------------------------------------
Build Time Report
---------------------------------------------------------------------
Target Duration
------ --------
Clean 00:00:01.7453083
Build 00:01:55.9501941
Test 00:03:05.8054016
Report 00:00:18.8476461
All 00:00:00.0001567
Total: 00:05:22.5606801
Status: Ok
---------------------------------------------------------------------
...
...
...
Constructing scenario data
In update 2, I had mentioned data validation being an important piece. I feel that I should elaborate on that.
The game engine has two classifications of data: immutable scenario data and then also tracked entity data.
Immutable scenario data is loaded once at game initialization. This data is loaded from JSON files and represents the seed data for an experience. The overriding command is that anything should be able to reference this data with full expectations that the reference will never expire.
An example of a file is that of “floorgeneration.json” which currently only has one entry.
[
{ "id": "0",
"DefaultTile": 1,
"tiles": [ 0, 1 ],
"sizerange": {
"position": { "x": 8, "y": 8 },
"size": { "x": 10, "y": 10 }
},
"strategy": "openfloor"
}
]
This floor generation data file is a fantastic example because it references IDs from other files, specifically “tiles.json”.
[<RequireQualifiedAccess>]
type FloorGenerationStrategy =
| OpenFloor
type FloorGenerationParameter =
{ ID: int64
/// Default Tile
DefaultTile: int64
///Tiles used
Tiles: int64 list
///Size generation
SizeRange: Rectangle
///Generation Strategy
Strategy: Morgemil.Models.FloorGenerationStrategy
}
The model definition above that the JSON will be read from uses Int64 to reference identifiers. This isn’t preferred because this scenario data is immutable and anytime that this FloorGenerationParameter is used, there’s extra work to go find all Tiles with those IDs.
The final model being used by the game engine is below and includes direct references to the Tile type as well as making all Int64s be formal ID types. Not to mention the ID is surfaced as an interface implementation for use in entity lookup tables.
[<Record>]
type FloorGenerationParameter =
{ [<RecordId>] ID : FloorGenerationParameterID
/// Default Tile
DefaultTile : Tile
///Tiles used
Tiles : Tile list
///Size generation
SizeRange : Morgemil.Math.Rectangle
///Generation Strategy
Strategy : FloorGenerationStrategy
}
interface Relational.IRow with
[<JsonIgnore()>]
member this.Key = this.ID.Key
To reach the final model from the JSON files, all this data goes through several stages of transformation and validation. An intermediate step of transformation looks like this for FloorGenerationParameter.
let FloorGenerationParameterFromDto (getTileByID: TileID -> Tile) (floorGenerationParameter: DTO.FloorGenerationParameter) : FloorGenerationParameter =
{
FloorGenerationParameter.ID = FloorGenerationParameterID floorGenerationParameter.ID
DefaultTile = floorGenerationParameter.DefaultTile |> TileID |> getTileByID
Tiles = floorGenerationParameter.Tiles |> Seq.map (TileID >> getTileByID) |> Seq.toList
SizeRange = floorGenerationParameter.SizeRange |> RectangleFromDto
Strategy = floorGenerationParameter.Strategy
}
As a whole for all the scenario data, the intermediate phases look like these types below. These phases show where there is arrays of data and then also multiple layers of validation that can occur.
type RawDtoPhase0 =
{ Tiles: DtoValidResult<Tile[]>
TileFeatures: DtoValidResult<TileFeature[]>
Races: DtoValidResult<Race[]>
RaceModifiers: DtoValidResult<RaceModifier[]>
MonsterGenerationParameters: DtoValidResult<MonsterGenerationParameter[]>
Items: DtoValidResult<Item[]>
FloorGenerationParameters: DtoValidResult<FloorGenerationParameter[]>
Aspects: DtoValidResult<Aspect[]>
}
type RawDtoPhase1 =
{ Tiles: DtoValidResult<DtoValidResult<Tile>[]>
TileFeatures: DtoValidResult<DtoValidResult<TileFeature>[]>
Races: DtoValidResult<DtoValidResult<Race>[]>
RaceModifiers: DtoValidResult<DtoValidResult<RaceModifier>[]>
MonsterGenerationParameters: DtoValidResult<DtoValidResult<MonsterGenerationParameter>[]>
Items: DtoValidResult<DtoValidResult<Item>[]>
FloorGenerationParameters: DtoValidResult<DtoValidResult<FloorGenerationParameter>[]>
Aspects: DtoValidResult<DtoValidResult<Aspect>[]>
}
type RawDtoPhase2 =
{ Tiles: Morgemil.Models.Tile []
TileFeatures: Morgemil.Models.TileFeature []
Races: Morgemil.Models.Race []
RaceModifiers: Morgemil.Models.RaceModifier []
MonsterGenerationParameters: Morgemil.Models.MonsterGenerationParameter []
Items: Morgemil.Models.Item []
FloorGenerationParameters: Morgemil.Models.FloorGenerationParameter []
Aspects: Morgemil.Models.Aspect []
}
The final ScenarioData model output is this where all the scenario data is addressable by its ID in a readonly table.
type ScenarioData =
{ Races: IReadonlyTable<Race, RaceID>
Tiles: IReadonlyTable<Tile, TileID>
TileFeatures: IReadonlyTable<TileFeature, TileFeatureID>
Items: IReadonlyTable<Item, ItemID>
RaceModifiers: IReadonlyTable<RaceModifier, RaceModifierID>
MonsterGenerationParameters: IReadonlyTable<MonsterGenerationParameter, MonsterGenerationParameterID>
FloorGenerationParameters: IReadonlyTable<FloorGenerationParameter, FloorGenerationParameterID>
Aspects: IReadonlyTable<Aspect, AspectID> }
That’s a lot of steps to say that some of these tables of data are linked to other tables and that I should put together an entity-relationship-diagram (ERD) like for a database.
The game engine has two classifications of data: immutable scenario data and then also tracked entity data. The tracked entity data is a topic for another day.
Game Loop Processing Requests
The game engine operates via a mailbox receiving requests. All interaction from the player is via a request to do an action. For example, a player may request to move in a direction from their current location.
The current logic happening is to
- Sanity check the character even exists.
- Get the current position
- Calculate the new position that would be.
- If new position blocks movement, then refuse to move.
- If new position is ok, then move and also increment the next time this character is allowed to move.
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