Games require human input. Games that do not require human input are simulations.
At the heart of every game is a way of acquiring the player’s input. So most games have the concept of a game loop. A game loop keeps the game running by continuously checking for user input, drawing to the screen, and updating state internally.
I have a basic game loop with some mocked up data
open Morgemil.Core open Morgemil.Logic [<EntryPoint>] let main argv = let player = { Entity.Id = EntityId 3 Type = EntityType.Person } let position = { PositionComponent.EntityId = player.Id Position = Vector2i(5, 5) Mobile = true } let controller = { PlayerComponent.EntityId = player.Id IsHumanControlled = true } let resource = { ResourceComponent.EntityId = player.Id ResourceAmount = 50.0 } let level = Morgemil.Core.DungeonGeneration.Generate({ DungeonParameter.Depth = 1 RngSeed = 5 Type = DungeonGenerationType.Square }) let game = Morgemil.Logic.Game(level, [| player |], [| position |], [| controller |], [| resource |]) let rec _continue() = game.Update() let key = System.Console.ReadKey() let direction = match key.Key with | System.ConsoleKey.W -> Vector2i(-1, 0) | System.ConsoleKey.E -> Vector2i(1, 0) | System.ConsoleKey.N -> Vector2i(0, -1) | System.ConsoleKey.S -> Vector2i(0, 1) | _ -> Vector2i() game.HumanRequest({ RequestedMovement.EntityId = player.Id Direction = direction }) |> ignore _continue() _continue() 0 |
Running this from console gives this output
Those messages shown is the results of each turn as generated by this engine
namespace Morgemil.Logic open Morgemil.Core 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 |
Notice something interesting about those messages’ names? They are verbs in the past tense. The messages are saying that something happened. It’s then up to everything else to react.
The ability for the human to interact with the system is this method
///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 |
Given a human input, the game loop will process while messages are still being generated. That happens thanks to this method
let results = TurnQueue.HandleMessages _handleRequest (EventResult.EntityMovementRequested request) |
The logic in that is a continuous loop while messages are available to process
namespace Morgemil.Logic ///The callback to handle a request and results. type EventMessageHandler = EventResult -> TurnStep ///Receives and processes events that make up a turn. ///Example: The action of moving onto a tile with a trap causes a new message chain starting with the trap's activation. module TurnQueue = let HandleMessages (handler : EventMessageHandler) (initialRequest : EventResult) : TurnStep = let _processedEvents = new System.Collections.Generic.List<EventResult>() let _eventQueue = new System.Collections.Generic.Queue<EventResult>() let rec _handle() = match _eventQueue.Count with | 0 -> () | _ -> let request = _eventQueue.Dequeue() request |> _processedEvents.Add request |> handler |> Seq.iter _eventQueue.Enqueue _handle() _eventQueue.Enqueue(initialRequest) _handle() (List.ofSeq _processedEvents) |
See that fancy type definition of a callback at the top?
///The callback to handle a request and results. type EventMessageHandler = EventResult -> TurnStep |
Perhaps the definition of an EventResult will start to clear things up
namespace Morgemil.Logic open Morgemil.Core type RequestedMovement = { EntityId : EntityId Direction : Vector2i } type ResultMoved = { EntityId : EntityId MovedFrom : Vector2i MovedTo : Vector2i } type ResultResourceChanged = { EntityId : EntityId OldValue : double NewValue : double } member this.ResourceChanged = this.OldValue - this.NewValue ///This represents the results of an action type EventResult = | EntityMoved of ResultMoved | EntityMovementRequested of RequestedMovement | EntityResourceChanged of ResultResourceChanged |
But that doesn’t say what a TurnStep is
type TurnStep = List<EventResult> |
So an EventMessageHandler is a type definition for a function that given an EventResult, returns a list of EventResult.
///The callback to handle a request and results. type EventMessageHandler = EventResult -> TurnStep |
The game engine has a method which is exactly that:
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) | _ -> () } |
Now you might notice that the function in no way returns a list of event results. But… it does yield EventResult within the context of a TurnBuilder.
let _handleRequest request = TurnBuilder () { ... } |
A TurnBuilder is a computation expression which takes away the hassle of having to keep track of a return list as it does it for me.
type TurnBuilder() = member this.Bind(x, f) = f x member this.Zero() = TurnStep.Empty member this.Yield(expr : EventResult) : TurnStep = [ expr ] member this.Return(expr) = TurnStep.Empty member this.Yield(expr) = TurnStep.Empty member this.Combine(a : TurnStep, b : TurnStep) = List.concat [ a; b ] member this.Delay(f) = f() |
This post ended up being me throwing a lot of code out there with minimal explanation. That’s probably a sign for me to nail down some of this logic.