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

Game with monsters shown

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” and make the SortedSet’s stored type into something like this with a few convenience properties to surface common data.

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”.