Morgemil Part 18 – Side effects

A beautiful thing about functional languages is that they are not fond of side-effects. And that’s what makes the following code so ugly, it’s full of side effects.

But first, a refresher on the entity and its components defined so far:

type EntityId = 
  | EntityId of int
 
type EntityType = 
  | Person
  | Door
  | Stairs
  | Object
  | Spell
 
type Entity = 
  { Id : EntityId
    Type : EntityType }
 
type PositionComponent = 
  { EntityId : EntityId
    Position : Vector2i
    Mobile : bool }
 
type PlayerComponent = 
  { EntityId : EntityId
    IsHumanControlled : bool }
 
type ResourceComponent = 
  { EntityId : EntityId
    ResourceAmount : double }

From there I created “component systems” which is fancy terminology for wrapping a dictionary: a way for me to access components by their EntityId efficiently. Do note that the coloring on the following code snippet is thrown off by the single quote in the generic type ‘T

open Morgemil.Core
open Morgemil.Logic.Extensions
 
type ComponentSystem<'T when 'T : comparison>(initialComponents : Set<'T>, getId : 'T -> EntityId) = 
 
  let mutable _components : Map<EntityId, 'T> = 
    [ for item in initialComponents -> (getId item), item ]
    |> Map.ofSeq
 
  let _added = new Event<'T>()
  let _removed = new Event<'T>()
  let _replaced = new Event<'T * 'T>()
  let _matches entityId (item : 'T) = (getId (item) = entityId)
  new(getId) = ComponentSystem(Set.empty, getId)
  member this.Components = _components |> Seq.map (fun t -> t.Value)
  member this.ComponentRemoved = _removed.Publish
  member this.ComponentAdded = _added.Publish
  member this.ComponentReplaced = _replaced.Publish
 
  member this.Find entityId = 
    match _components.ContainsKey(entityId) with
    | true -> Some(_components.[entityId])
    | _ -> None
 
  member this.Item 
    with get (entityId : EntityId) = _components.[entityId]
 
  member this.Add item = 
    _components <- _components.Add(getId item, item)
    _added.Trigger(item)
 
  member this.Remove item = 
    _components <- _components.Remove(getId item)
    _removed.Trigger(item)
 
  member this.Remove entityId = this.Remove _components.[entityId]
 
  member this.Replace(old_value : 'T, new_value : 'T) = 
    _components <- _components.Replace(getId old_value, new_value)
    _replaced.Trigger(old_value, new_value)
    new_value
 
  member this.Replace(entityId : EntityId, replacement) = 
    let old_value = this.[entityId]
    (old_value, this.Replace(old_value, replacement old_value))

There are multiple component systems in a world. One system for each component type.

open Morgemil.Core
 
type World(level, spatialComponents, resourceComponents, playerComponents) = 
  let _spatial = SpatialSystem(spatialComponents)
  let _resources = ComponentSystem<ResourceComponent>(resourceComponents, (fun resource -> resource.EntityId))
  let _players = ComponentSystem<PlayerComponent>(playerComponents, (fun player -> player.EntityId))
  let _level : Level = level
  member this.Spatial = _spatial
  member this.Level = _level
  member this.Resources = _resources
  member this.Players = _players
  static member Empty = World(Level.Empty, Set.empty, Set.empty, Set.empty)

Wait a second, I just said “One system for each component type” but there are only two components here…

The third component “PositionComponent” is there in a specialized system: “SpatialSystem”.

open Morgemil.Core
 
type SpatialSystem(initial) = 
  inherit ComponentSystem<PositionComponent>(initial, (fun position -> position.EntityId))
  member this.InRectangle(area : Rectangle) = this.Components |> Seq.filter (fun value -> area.Contains(value.Position))
  static member Empty = SpatialSystem(Set.empty)

This inherits from ComponentSystem, and tacks on some extra functionality. I can now easily check for entities that are positioned inside a rectangle. A good spatial system would do that efficiently except I’m more concerned with correctness right now.

Want an example? How about my basic game engine right now

open Morgemil.Core
open Morgemil.Logic.Extensions
 
type Game(level : Level, entities : seq<Entity>, positions : seq<PositionComponent>, players : seq<PlayerComponent>, resources : seq<ResourceComponent>) = 
  let _world = World(level, Set.ofSeq (positions), Set.ofSeq (resources), Set.ofSeq (players))
  //TODO: fix
  let mutable _globalTurnQueue = (entities |> Seq.toList).Head
 
  let _handleRequest request = 
    TurnBuilder () { 
      match request with
      | EventResult.EntityMovementRequested(req) -> 
        let old_position = _world.Spatial.[req.EntityId]
        //TODO: Check that this move is actually valid
        let new_position = 
          _world.Spatial.Replace(old_position, { old_position with Position = old_position.Position + req.Direction })
        yield Message.PositionChange(old_position, new_position)
        //Movement takes one resource. More for testing purposes. 
        let old_resource = _world.Resources.[req.EntityId]
        let new_resource = 
          _world.Resources.Replace
            (old_resource, { old_resource with ResourceAmount = old_resource.ResourceAmount - 1.0 })
        yield Message.ResourceChange(old_resource, new_resource)
      | _ -> ()
    }
 
  member this.Update() = 
    let nextEntity = _globalTurnQueue //TODO: Actually have more than one entity (the player)
    let player = _world.Players.[nextEntity.Id]
    match player.IsHumanControlled with
    | true -> ()
    | false -> () //TODO: process AI turn
 
  ///Humans can only move units right now
  member this.HumanRequest(request : RequestedMovement) = 
    let results = TurnQueue.HandleMessages _handleRequest (EventResult.EntityMovementRequested request)
    //TODO: Display results through gui
    printfn ""
    results |> Seq.iter (fun res -> printfn "%A" res)
    true

The world is created from passed in data, doesn’t really matter how right now.

What matters is the ease of use

      match request with
      | EventResult.EntityMovementRequested(req) -> 
        let old_position = _world.Spatial.[req.EntityId]
        //TODO: Check that this move is actually valid
        let new_position = 
          _world.Spatial.Replace(old_position, { old_position with Position = old_position.Position + req.Direction })
        yield Message.PositionChange(old_position, new_position)
        //Movement takes one resource. More for testing purposes. 
        let old_resource = _world.Resources.[req.EntityId]
        let new_resource = 
          _world.Resources.Replace
            (old_resource, { old_resource with ResourceAmount = old_resource.ResourceAmount - 1.0 })
        yield Message.ResourceChange(old_resource, new_resource)
      | _ -> ()

When an entity has movement requested, I’m taking the old position and replacing it with a new value, which is the old position + direction requested.

        let old_position = _world.Spatial.[req.EntityId]
        //TODO: Check that this move is actually valid
        let new_position = 
          _world.Spatial.Replace(old_position, { old_position with Position = old_position.Position + req.Direction })

I do the same thing with the resources.

        //Movement takes one resource. More for testing purposes. 
        let old_resource = _world.Resources.[req.EntityId]
        let new_resource = 
          _world.Resources.Replace
            (old_resource, { old_resource with ResourceAmount = old_resource.ResourceAmount - 1.0 })

This has side effects, the list of components is constantly changing with removal and insertions. But the side effects are limited in that I’m never updating the component’s value directly. the only way for it to be updated, is for that entire component associated with that Entity to be replaced.

Even with this compromise, record immutability has some fantastic possibilities. I can pass those component records in messages with wild abandon. Indeed, I’ve been doing so and that is next week’s topic.