diff options
| -rw-r--r-- | LivingVillage.sln | 34 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/Game.fs | 149 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj | 21 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/Program.fs | 9 | ||||
| -rw-r--r-- | src/LivingVillage.Kernel/LivingVillage.Kernel.fsproj | 11 | ||||
| -rw-r--r-- | src/LivingVillage.Kernel/Sim.fs | 46 |
6 files changed, 270 insertions, 0 deletions
diff --git a/LivingVillage.sln b/LivingVillage.sln new file mode 100644 index 0000000..4e8f210 --- /dev/null +++ b/LivingVillage.sln @@ -0,0 +1,34 @@ +
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{DFF4B1C6-CF0A-45CB-912B-5BFD446A76FF}"
+EndProject
+Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "LivingVillage.Kernel", "src\LivingVillage.Kernel\LivingVillage.Kernel.fsproj", "{504BBDD4-106D-4423-A3BE-9A82848D1656}"
+EndProject
+Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "LivingVillage.Desktop", "src\LivingVillage.Desktop\LivingVillage.Desktop.fsproj", "{CC15DA38-E36A-45A2-A031-5392C8A6BF0D}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {504BBDD4-106D-4423-A3BE-9A82848D1656}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {504BBDD4-106D-4423-A3BE-9A82848D1656}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {504BBDD4-106D-4423-A3BE-9A82848D1656}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {504BBDD4-106D-4423-A3BE-9A82848D1656}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CC15DA38-E36A-45A2-A031-5392C8A6BF0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CC15DA38-E36A-45A2-A031-5392C8A6BF0D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CC15DA38-E36A-45A2-A031-5392C8A6BF0D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CC15DA38-E36A-45A2-A031-5392C8A6BF0D}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {504BBDD4-106D-4423-A3BE-9A82848D1656} = {DFF4B1C6-CF0A-45CB-912B-5BFD446A76FF}
+ {CC15DA38-E36A-45A2-A031-5392C8A6BF0D} = {DFF4B1C6-CF0A-45CB-912B-5BFD446A76FF}
+ EndGlobalSection
+EndGlobal
diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs new file mode 100644 index 0000000..960a8c6 --- /dev/null +++ b/src/LivingVillage.Desktop/Game.fs @@ -0,0 +1,149 @@ +namespace LivingVillage.Desktop + +open System +open Microsoft.Xna.Framework +open Microsoft.Xna.Framework.Graphics +open Microsoft.Xna.Framework.Input +open LivingVillage.Kernel +open LivingVillage.Kernel.Sim + +type TileKind = + | Grass = 0 + | Path = 1 + | Water = 2 + | Flower = 3 + +module MapGen = + let generate (width: int) (height: int) : TileKind[,] = + let rnd = Random(20260918) + let map = Array2D.zeroCreate<TileKind> width height + for y in 0 .. height - 1 do + for x in 0 .. width - 1 do + let border = x = 0 || y = 0 || x = width - 1 || y = height - 1 + map.[x, y] <- + if border then TileKind.Water + elif rnd.NextDouble() < 0.04 then TileKind.Flower + else TileKind.Grass + let midX = width / 2 + let midY = height / 2 + for x in 1 .. width - 2 do + map.[x, midY] <- TileKind.Path + for y in 1 .. height - 2 do + map.[midX, y] <- TileKind.Path + map + +module Atlas = + let tilePixels = 32 + + let build (device: GraphicsDevice) : Texture2D = + let rnd = Random(7) + let c (r: int) (g: int) (b: int) = Color(r, g, b) + let tex = new Texture2D(device, tilePixels * 4, tilePixels) + let colors = Array.zeroCreate<Color> (tilePixels * tilePixels * 4) + for t in 0 .. 3 do + let baseColor, variance, isFlower = + match t with + | 1 -> (c 148 118 84), 12, false + | 2 -> (c 52 92 168), 10, false + | 3 -> (c 56 118 48), 14, true + | _ -> (c 56 118 48), 14, false + for y in 0 .. tilePixels - 1 do + for x in 0 .. tilePixels - 1 do + let jitter = rnd.Next(variance + 1) - variance / 2 + colors.[t * tilePixels * tilePixels + y * tilePixels + x] <- + c (int baseColor.R + jitter) (int baseColor.G + jitter) (int baseColor.B + jitter) + if isFlower then + for _ in 1 .. 6 do + let fx = rnd.Next(tilePixels - 2) + let fy = rnd.Next(tilePixels - 2) + let fc = if rnd.NextDouble() < 0.5 then c 240 220 80 else c 235 240 245 + for dy in 0 .. 1 do + for dx in 0 .. 1 do + colors.[t * tilePixels * tilePixels + (fy + dy) * tilePixels + fx + dx] <- fc + tex.SetData(colors) + tex + +type LivingVillageGame() as this = + inherit Game() + + let graphics = new GraphicsDeviceManager(this) + let mutable spriteBatch = Unchecked.defaultof<SpriteBatch> + let mutable atlas = Unchecked.defaultof<Texture2D> + let mutable pixel = Unchecked.defaultof<Texture2D> + let mutable map = MapGen.generate Sim.mapWidthTiles Sim.mapHeightTiles + let mutable world = Sim.initialWorld () + let mutable camera = Vector2.Zero + let mutable fpsFrames = 0 + let mutable fpsSeconds = 0.0 + + do + graphics.PreferredBackBufferWidth <- 1280 + graphics.PreferredBackBufferHeight <- 720 + graphics.SynchronizeWithVerticalRetrace <- true + this.IsFixedTimeStep <- true + this.TargetElapsedTime <- TimeSpan.FromTicks(TimeSpan.TicksPerSecond / 60L) + this.Window.Title <- "Living Village M0" + + member private this.CenterCamera() = + let viewport = this.GraphicsDevice.Viewport + let vw = float32 viewport.Width + let vh = float32 viewport.Height + let worldW = float32 (Sim.mapWidthTiles * Sim.tilePixels) + let worldH = float32 (Sim.mapHeightTiles * Sim.tilePixels) + let half = float32 Sim.tilePixels / 2.0f + let x = MathHelper.Clamp(world.Avatar.Pos.X + half - vw / 2.0f, 0.0f, max 0.0f (worldW - vw)) + let y = MathHelper.Clamp(world.Avatar.Pos.Y + half - vh / 2.0f, 0.0f, max 0.0f (worldH - vh)) + camera <- Vector2(x, y) + + override _.LoadContent() = + spriteBatch <- new SpriteBatch(this.GraphicsDevice) + atlas <- Atlas.build this.GraphicsDevice + pixel <- new Texture2D(this.GraphicsDevice, 1, 1) + pixel.SetData([| Color.White |]) + + override this.Update(gameTime: GameTime) = + let kb = Keyboard.GetState() + if kb.IsKeyDown(Keys.Escape) then this.Exit() + let input = + { MoveX = + (if kb.IsKeyDown(Keys.D) then 1.0f + elif kb.IsKeyDown(Keys.A) then -1.0f + else 0.0f) + MoveY = + (if kb.IsKeyDown(Keys.S) then 1.0f + elif kb.IsKeyDown(Keys.W) then -1.0f + else 0.0f) } + let ts = + { Dt = float32 gameTime.ElapsedGameTime.TotalSeconds + Input = input } + world <- Sim.step ts world + this.CenterCamera() + fpsFrames <- fpsFrames + 1 + fpsSeconds <- fpsSeconds + gameTime.ElapsedGameTime.TotalSeconds + if fpsSeconds >= 1.0 then + let fps = float fpsFrames / fpsSeconds + fpsFrames <- 0 + fpsSeconds <- 0.0 + this.Window.Title <- $"Living Village M0 | fps {fps:F1} | tick {world.Tick}" + printfn $"fps={fps:F1} tick={world.Tick} pos=({world.Avatar.Pos.X:F0},{world.Avatar.Pos.Y:F0})" + + override this.Draw(gameTime: GameTime) = + this.GraphicsDevice.Clear(Color.CornflowerBlue) + let viewport = this.GraphicsDevice.Viewport + let camX = int camera.X + let camY = int camera.Y + let tx0 = max 0 (camX / Sim.tilePixels) + let ty0 = max 0 (camY / Sim.tilePixels) + let tx1 = min (Sim.mapWidthTiles - 1) ((camX + viewport.Width) / Sim.tilePixels + 1) + let ty1 = min (Sim.mapHeightTiles - 1) ((camY + viewport.Height) / Sim.tilePixels + 1) + spriteBatch.Begin() + for y in ty0 .. ty1 do + for x in tx0 .. tx1 do + let src = Rectangle(int map.[x, y] * Sim.tilePixels, 0, Sim.tilePixels, Sim.tilePixels) + let dst = + Rectangle(x * Sim.tilePixels - camX, y * Sim.tilePixels - camY, Sim.tilePixels, Sim.tilePixels) + spriteBatch.Draw(atlas, dst, src, Color.White) + let avatarDst = + Rectangle(int world.Avatar.Pos.X - camX, int world.Avatar.Pos.Y - camY, Sim.tilePixels, Sim.tilePixels) + spriteBatch.Draw(pixel, avatarDst, Color.IndianRed) + spriteBatch.End() diff --git a/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj b/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj new file mode 100644 index 0000000..9af59ac --- /dev/null +++ b/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj @@ -0,0 +1,21 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net8.0</TargetFramework> + </PropertyGroup> + + <ItemGroup> + <Compile Include="Game.fs" /> + <Compile Include="Program.fs" /> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.5.1" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\LivingVillage.Kernel\LivingVillage.Kernel.fsproj" /> + </ItemGroup> + +</Project> diff --git a/src/LivingVillage.Desktop/Program.fs b/src/LivingVillage.Desktop/Program.fs new file mode 100644 index 0000000..0e2adcf --- /dev/null +++ b/src/LivingVillage.Desktop/Program.fs @@ -0,0 +1,9 @@ +namespace LivingVillage.Desktop + +module Program = + + [<EntryPoint>] + let main _ = + use game = new LivingVillageGame() + game.Run() + 0 diff --git a/src/LivingVillage.Kernel/LivingVillage.Kernel.fsproj b/src/LivingVillage.Kernel/LivingVillage.Kernel.fsproj new file mode 100644 index 0000000..a8ed312 --- /dev/null +++ b/src/LivingVillage.Kernel/LivingVillage.Kernel.fsproj @@ -0,0 +1,11 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net8.0</TargetFramework> + </PropertyGroup> + + <ItemGroup> + <Compile Include="Sim.fs" /> + </ItemGroup> + +</Project> diff --git a/src/LivingVillage.Kernel/Sim.fs b/src/LivingVillage.Kernel/Sim.fs new file mode 100644 index 0000000..d1b2e57 --- /dev/null +++ b/src/LivingVillage.Kernel/Sim.fs @@ -0,0 +1,46 @@ +namespace LivingVillage.Kernel + +module Sim = + + type Vec2 = + { X: float32 + Y: float32 } + + type Avatar = + { Pos: Vec2 } + + type Input = + { MoveX: float32 + MoveY: float32 } + + type TimeStep = + { Dt: float32 + Input: Input } + + type World = + { Tick: int64 + Avatar: Avatar } + + let mapWidthTiles = 64 + let mapHeightTiles = 48 + let tilePixels = 32 + let avatarSpeed = 160.0f + + let initialWorld () : World = + let centerX = float32 (mapWidthTiles * tilePixels / 2 - tilePixels / 2) + let centerY = float32 (mapHeightTiles * tilePixels / 2 - tilePixels / 2) + { Tick = 0L + Avatar = { Pos = { X = centerX; Y = centerY } } } + + let clamp v lo hi = if v < lo then lo elif v > hi then hi else v + + let step (ts: TimeStep) (world: World) : World = + let maxX = float32 (mapWidthTiles * tilePixels - tilePixels) + let maxY = float32 (mapHeightTiles * tilePixels - tilePixels) + let dx = ts.Input.MoveX * avatarSpeed * ts.Dt + let dy = ts.Input.MoveY * avatarSpeed * ts.Dt + { Tick = world.Tick + 1L + Avatar = + { Pos = + { X = clamp (world.Avatar.Pos.X + dx) 0.0f maxX + Y = clamp (world.Avatar.Pos.Y + dy) 0.0f maxY } } } |
