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 #6: Adding time and characters
I’ve been working off and on making a video game in F#. I thought I’d drop an update saying my progress.
Progress
Enemy AI
A placeholder for enemy AI has been wired up. Each ‘M’ character on the screen represents a monster going about its business. The entirety of the “AI” is shown below. The more important concept is that the code has been placed appropriately.
let direction =
(RNG.RandomVector world.RNG (Vector2i.create (2, 2)))
- Vector2i.create (1, 1)
if direction = Vector2i.Zero then
ActionRequest.Pause context.TimeTable.Next.ID
else
ActionRequest.Move
{ ActionRequestMove.CharacterID = context.TimeTable.Next.ID
Direction = direction }
Time and Entity Component System (ECS)
Time should pass in a world. Time in this game is represented by Int64s with a F# unit of measure called “TimeTick”. Each TimeTick represents one millisecond because I didn’t want to deal with floating point numbers.
A Character, whether a player or a monster, has a value of “NextTick” that shows when this entity may act again.
NextTick: int64<TimeTick>
Each of these Characters is kept in a table called “CharacterTable” which is optimized for insert/removal of Characters by their primary key and not by their NextTick.
Further complicating the concept of storing Characters is that not only Characters can act on this world. Soon, projectiles, dungeon effects, or other entities will be added and none of those will not be stored as a “Character”.
[<Record>]
type Character =
{ [<RecordId>]
ID: CharacterID
Race: Race
RaceModifier: RaceModifier option
Position: Vector2i
NextTick: int64<TimeTick>
Floor: int64<Floor>
NextAction: ActionArchetype
TickActions: ActionArchetype list
PlayerID: PlayerID option }
interface Relational.IRow with
[<JsonIgnore>]
member this.Key = this.ID.Key
A Character could be considered an “Entity”, the numeric identifier, with a number of components, each field attached. Each future projectile or a dungeon effect could also be considered an Entity with a number of components and some of those components could overlap with a Character, such as NextTick.
For each important combination of overlapping components between Entities, I’m trying out the concept of adding secondary indexes optimized for different kinds of retrieval.
Pulling out the constructor of the CharacterTable below, a “TimeTable” is added as an Index. These secondary indexes are called to add, update, and remove rows to keep in sync with the parent tables.
type CharacterTable(timeTable: TimeTable) as this =
inherit Table<Character, CharacterID>(CharacterID, (fun (key) -> key.Key))
do this.AddIndex timeTable
The “TimeTable” then looks like this below. It’s merely a sorted set that returns the next Character to take an action and classifies the next action into either “Waiting for Engine”, “Waiting for AI”, or “Waiting for Player Input”. Those first two types can be handled by the game engine, the third type requires player interaction and is how the engine requests the player to make a move.
type TimeTable() =
let items = SortedSet<Character>([], TimeComparer())
member this.Next = items.Min
member this.NextAction = items.Min.NextAction
member this.WaitingType: GameStateWaitingType =
match items.Min.NextAction with
| ActionArchetype.CharacterAfterInput
| ActionArchetype.CharacterBeforeInput -> GameStateWaitingType.WaitingForEngine
| ActionArchetype.CharacterEngineInput -> GameStateWaitingType.WaitingForAI
| ActionArchetype.CharacterPlayerInput -> GameStateWaitingType.WaitingForInput
interface IIndex<Character> with
member this.Add next = next |> items.Add |> ignore
member this.Update old next =
old |> items.Remove |> ignore
next |> items.Add |> ignore
member this.Remove old = old |> items.Remove |> ignore
The above TimeTable still only includes Characters as things that are “acting” on the engine. The plan when adding projectiles and dungeon effects is to implement “IIndex
type TimeTableContent =
| Character of Character
| Projectile of Projectile
member this.NextTick =
match this with
| Character character -> character.NextTick
| Projectile projectile -> projectile.NextTick
Anything that “acts” on the game engine stores the next time it may act, and then everytime the entity “acts” then the “NextTick” is increased. The TimeTable returns the entity with the lowest “NextTick”.
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