After a month’s sojourn I have returned to my long term project with more knowledge. I’m replacing my Object-Oriented F# that looks like
namespace Morgemil.Map type TileType = | Void = 0 | Land = 1 | Water = 2 /// <summary> /// This defines a tile. Each tile instance will hold a reference to one of these as a base attribute. /// </summary> type TileDefinition(id : int, name : string, description : string, blocksMovement : bool, blocksSight : bool, tileType : TileType) = /// <summary> /// Use this in file storage. When saving a chunk or map, use this ID. /// </summary> member this.ID = id /// <summary> /// A short name. eg: "Lush Grass" /// </summary> member this.Name = name /// <summary> /// A long description. eg: "Beware the burning sand. Scorpions and asps make their home here." /// </summary> member this.Description = description /// <summary> /// If true, this tile ALWAYS blocks ALL movement by ANYTHING. /// </summary> member this.BlocksMovement = blocksMovement /// <summary> /// If true, this tile ALWAYS blocks ALL Line-Of-Sight of ANYTHING. /// </summary> member this.BlocksSight = blocksSight /// <summary> /// The tile type determines some things like if they can breath or not. /// </summary> member this.Type = tileType /// <summary> /// A default, minimal definition. Could be used as the edge of the map blackness? /// </summary> static member Default = TileDefinition(0, "Empty", "", true, true, TileType.Void) static member IsDefault(tile : TileDefinition) = (tile.ID = TileDefinition.Default.ID) |
with something that looks a bit more F#-ish.
namespace Morgemil.Map type TileType = | Void = 0 | Land = 1 | Water = 2 /// This defines a tile. Each tile instance will hold a reference to one of these as a base attribute. type TileDefinition = { ///Use this in file storage. When saving a chunk or map, use this ID. Id : int /// A short name. eg: "Lush Grass" Name : string ///A long description. eg: "Beware the burning sand. Scorpions and asps make their home here." Description : string ///If true, this tile ALWAYS blocks ALL movement by ANYTHING. BlocksMovement : bool ///If true, this tile ALWAYS blocks ALL Line-Of-Sight of ANYTHING. BlocksSight : bool ///The tile type determines some things like if they can breath or not. TileType : TileType } /// <summary> /// A default, minimal definition. Could be used as the edge of the map blackness? /// </summary> static member Default = { Id = 0 Name = "Empty" Description = "" BlocksMovement = true BlocksSight = true TileType = TileType.Void } static member IsDefault(tile : TileDefinition) = (tile.Id = TileDefinition.Default.Id) |
I think this is a lot more concise and removes unnecessary encapsulation.
Here is another example of something I’ve learned. I have a test project that I script odd things in to test out concepts with and I’ve had this “Walkabout” class that I use to test movement from a console window window. It was defined as this
type Walkabout(dungeon : Morgemil.Map.Chunk, player : Morgemil.Game.Person) = member this.Dungeon = dungeon member this.Player = player member this.Act(act : Morgemil.Game.Actions) = let offset = match act with | Morgemil.Game.MoveEast -> Vector2i(1, 0) | Morgemil.Game.MoveWest -> Vector2i(-1, 0) | Morgemil.Game.MoveNorth -> Vector2i(0, 1) | Morgemil.Game.MoveSouth -> Vector2i(0, -1) Walkabout(dungeon, { Id = player.Id //Body = player.Body Position = player.Position + offset }) |
For now, whether it is good design or not, the position is included in a “Person” definition. So the only thing I wanted to change on that player was the Position, but I was having to pass everything which was a pain. Then I found the context of the “with” keyword with Record Types.
type Walkabout(dungeon : Morgemil.Map.Chunk, player : Morgemil.Game.Person) = member this.Dungeon = dungeon member this.Player = player member this.Act(act : Morgemil.Game.Actions) = let offset = match act with | Morgemil.Game.MoveEast -> Vector2i(1, 0) | Morgemil.Game.MoveWest -> Vector2i(-1, 0) | Morgemil.Game.MoveNorth -> Vector2i(0, 1) | Morgemil.Game.MoveSouth -> Vector2i(0, -1) Walkabout(dungeon, { player with Position = player.Position + offset }) |
So the person can now be passed around effortlessly
///Wight, human, etc. type Race = { Id : int ///Proper noun Noun : string ///Proper adjective Adjective : string ///User-visible description Description : string } ///A body with base stats/characteristics. ///Any mutable data is in a higher level. type Body = { Id : int Race : Race ///Normal "bodies" fit in one tile (1,1). Bosses and the largest entities can take up multiple tiles. Size : Morgemil.Math.Vector2i } ///This is a high level view of an entity. Typically holds any mutable data (can change each game step). type Person = { Id : int Body : Body ///This plus Body.Size constructs the person's hitbox Position : Morgemil.Math.Vector2i } |
I’m testing this by console since my Monogame view is still very lacking.
let Instruct() = printfn "(E)ast (N)orth (W)est (S)outh (Q)uit" let Prompt() = let response = System.Console.ReadLine() match System.Char.ToLower(response.[0]) with | 'e' -> Some(Morgemil.Game.MoveEast) | 'n' -> Some(Morgemil.Game.MoveNorth) | 'w' -> Some(Morgemil.Game.MoveWest) | 's' -> Some(Morgemil.Game.MoveSouth) | _ -> None let rec Continue (depth : int) (walkabout : Morgemil.Test.Walkabout) = System.Console.WriteLine(walkabout.Player.Position) match Prompt() with | None -> () | Some(act) -> let filename = "map_test" + depth.ToString("0000") + ".bmp" let dungeonDraw = Morgemil.Test.DungeonVisualizer.Visualize [| walkabout.Dungeon |] Morgemil.Test.DungeonVisualizer.DrawPlayer walkabout.Player dungeonDraw dungeonDraw.Save(filename) Continue (depth + 1) (walkabout.Act act) [<EntryPoint>] let main argv = let createdBspDungeon = Morgemil.Map.DungeonGeneration.Generate 656556 let walkAbout = Morgemil.Test.Walkabout(createdBspDungeon, { Id = 5 Body = { Id = 5 Size = Morgemil.Math.Vector2i(1, 1) Race = { Id = 5 Noun = "Dwarf" Adjective = "Dwarven" Description = "I am a dwarf" } } Position = Morgemil.Math.Vector2i(5, 5) }) Instruct() Continue 0 walkAbout |