diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-22 02:28:31 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-22 02:28:31 +0800 |
| commit | ef78374c316587fb585b72e6d427cd6e61203655 (patch) | |
| tree | 9eb176889054c07449debaf2a9d169ffe1851a0d /src/LivingVillage.Desktop | |
| parent | 5e036da9dfb1fe7f9e4f5e48414a8a1176292655 (diff) | |
| download | living-village-ef78374c316587fb585b72e6d427cd6e61203655.tar.gz | |
feat(mapgen): 参数化确定性生成器与视口裁剪(P20 第一步)
- 新增纯函数 MapGen:尺寸参数化(默认 64x48,可 256x192),splitmix64
随机游走河道 + 2-3 octave value noise + 泊松式布点;核心区整平后以 BFS
校验 5x6=30 个 spawn 全部可达,不可达确定性修整并复验。
- visibleTileRange/visibleTileCount 纯函数,drawWorld 改为调用之(行为不变);
drawMapFitted/drawMapViewport 仅证据用。
- 存档沿用 v2/v3 边界字段,补参数化边界 round-trip 测试,未改 Sim 语义。
- 回归:Desktop 109->115、Kernel 89->90;final_digest 与基线一致。
Diffstat (limited to 'src/LivingVillage.Desktop')
| -rw-r--r-- | src/LivingVillage.Desktop/Game.fs | 125 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj | 1 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/MapGen.fs | 249 | ||||
| -rw-r--r-- | src/LivingVillage.Desktop/VillageArt.fs | 67 |
4 files changed, 423 insertions, 19 deletions
diff --git a/src/LivingVillage.Desktop/Game.fs b/src/LivingVillage.Desktop/Game.fs index e48888e..64dc3e2 100644 --- a/src/LivingVillage.Desktop/Game.fs +++ b/src/LivingVillage.Desktop/Game.fs @@ -188,11 +188,42 @@ type LivingVillageGame() as this = [ (1, 1); (255, 191); (509, 381); (256, 60); (256, 300); (80, 200); (430, 300) ] let mutable tourIndex = 0 let mutable tourFrame = 0 + // P20 证据钩子:LV_MAPGEN_SHOT=1 时用参数化生成器出整图 + 视口裁剪帧,拍完即退出。 + let mapGenShotMode = Environment.GetEnvironmentVariable("LV_MAPGEN_SHOT") = "1" + let mapGenWidth = + match Environment.GetEnvironmentVariable("LV_MAPGEN_WIDTH") with + | null | "" -> 256 + | v -> match Int32.TryParse v with (true, value) when value > 0 -> value | _ -> 256 + let mapGenHeight = + match Environment.GetEnvironmentVariable("LV_MAPGEN_HEIGHT") with + | null | "" -> 192 + | v -> match Int32.TryParse v with (true, value) when value > 0 -> value | _ -> 192 + let mapGenSeed = + match Environment.GetEnvironmentVariable("LV_MAPGEN_SEED") with + | null | "" -> 4242UL + | v -> match UInt64.TryParse v with (true, value) -> value | _ -> 4242UL + let mutable mapGenResult = Unchecked.defaultof<MapGen.Result> + let mutable mapGenGenMs = 0.0 do if scale > 1 then Sim.configureBounds (64 * scale) (48 * scale) ProceduralMap.activate (uint64 4242) + if mapGenShotMode then + let stopwatch = System.Diagnostics.Stopwatch.StartNew() + mapGenResult <- MapGen.generateWithSize mapGenWidth mapGenHeight mapGenSeed + stopwatch.Stop() + mapGenGenMs <- stopwatch.Elapsed.TotalMilliseconds + printfn + "mapgen-gen size=%dx%d seed=%d gen_ms=%.2f tiles=%d reachable_ok=%b reachable_tiles=%d spawns=%d" + mapGenResult.Width + mapGenResult.Height + (int mapGenResult.Seed) + mapGenGenMs + mapGenResult.Tiles.Length + mapGenResult.ReachabilityOk + mapGenResult.ReachableTiles + mapGenResult.Spawns.Length if mapTourMode then tourWaypoints <- [ (1, 1); (Sim.mapWidthTiles / 2, Sim.mapHeightTiles / 2); (Sim.mapWidthTiles - 3, Sim.mapHeightTiles - 3) @@ -659,23 +690,87 @@ type LivingVillageGame() as this = printfn "menu-shot=done frames=%d" menuShotFrame this.Exit() + /// P20 证据:把一次 MapGen 绘制渲染到离屏 target 并落盘,返回绘制瓦片数。 + member private this.RenderMapGen (drawFitted: bool) : int * Texture2D = + let viewport = this.GraphicsDevice.Viewport + use target = new RenderTarget2D(this.GraphicsDevice, viewport.Width, viewport.Height) + this.GraphicsDevice.SetRenderTarget target + this.GraphicsDevice.Clear(Color(18, 26, 20)) + let camX = max 0 ((mapGenResult.Width * Sim.tilePixels - viewport.Width) / 2) + let camY = max 0 ((mapGenResult.Height * Sim.tilePixels - viewport.Height) / 2) + spriteBatch.Begin() + let drawn = + if drawFitted then + VillageArt.drawMapFitted spriteBatch artTextures viewport.Width viewport.Height mapGenResult Color.White + else + VillageArt.drawMapViewport spriteBatch artTextures (Vector2(float32 camX, float32 camY)) viewport.Width viewport.Height mapGenResult 0L Color.White + spriteBatch.End() + this.GraphicsDevice.SetRenderTarget null + let data = Array.zeroCreate<Color> (viewport.Width * viewport.Height) + target.GetData<Color>(data) + let texture = new Texture2D(this.GraphicsDevice, viewport.Width, viewport.Height) + texture.SetData(data) + (drawn, texture) + + /// P20 证据:整图等比渲染 + 1:1 视口裁剪帧各存一张,打印绘制计数后退出。 + member private this.DrawMapGenShot () = + System.IO.Directory.CreateDirectory recordDirectory |> ignore + let viewport = this.GraphicsDevice.Viewport + let fittedDrawn, fittedTexture = this.RenderMapGen true + use _fitted = fittedTexture + use stream = System.IO.File.Create(sprintf "%s/mapgen-%dx%d-seed%d.png" recordDirectory mapGenResult.Width mapGenResult.Height (int mapGenResult.Seed)) + fittedTexture.SaveAsPng(stream, viewport.Width, viewport.Height) + printfn "mapgen-fitted drawn=%d viewport=%dx%d" fittedDrawn viewport.Width viewport.Height + + let camX = max 0 ((mapGenResult.Width * Sim.tilePixels - viewport.Width) / 2) + let camY = max 0 ((mapGenResult.Height * Sim.tilePixels - viewport.Height) / 2) + let culledDrawn, culledTexture = this.RenderMapGen false + use _culled = culledTexture + use culledStream = System.IO.File.Create(sprintf "%s/mapgen-culled-%dx%d-seed%d.png" recordDirectory mapGenResult.Width mapGenResult.Height (int mapGenResult.Seed)) + culledTexture.SaveAsPng(culledStream, viewport.Width, viewport.Height) + let total = mapGenResult.Width * mapGenResult.Height + let visible = MapGen.visibleTileCount mapGenResult.Width mapGenResult.Height camX camY viewport.Width viewport.Height + printfn "mapgen-cull drawn=%d visible=%d total=%d culled=%.1f%%" culledDrawn visible total (100.0 * (1.0 - float culledDrawn / float total)) + + let smallWatch = System.Diagnostics.Stopwatch.StartNew() + let smallMap = MapGen.generateWithSize 64 48 (uint64 4242) + smallWatch.Stop() + printfn "mapgen-gen-legacy size=%dx%d gen_ms=%.2f tiles=%d reachable_ok=%b" smallMap.Width smallMap.Height smallWatch.Elapsed.TotalMilliseconds smallMap.Tiles.Length smallMap.ReachabilityOk + + let frameWatch = System.Diagnostics.Stopwatch.StartNew() + use frameTarget = new RenderTarget2D(this.GraphicsDevice, viewport.Width, viewport.Height) + this.GraphicsDevice.SetRenderTarget frameTarget + for _ in 1 .. 30 do + spriteBatch.Begin() + VillageArt.drawMapViewport spriteBatch artTextures (Vector2(float32 camX, float32 camY)) viewport.Width viewport.Height mapGenResult 0L Color.White |> ignore + spriteBatch.End() + this.GraphicsDevice.SetRenderTarget null + frameWatch.Stop() + let perFrameMs = frameWatch.Elapsed.TotalMilliseconds / 30.0 + printfn "mapgen-frame avg_ms=%.2f implied_fps=%.1f iterations=30" perFrameMs (1000.0 / perFrameMs) + printfn "mapgen-shot=done" + this.Exit() + override this.Draw(gameTime: GameTime) = - if splashActive then - this.DrawSplash() - elif menu.Page <> Playing then - this.DrawMenu() - elif relationView then - this.DrawRelationView() - this.DrawM5Overlay() + if mapGenShotMode then + this.DrawMapGenShot() else - this.DrawWorldView() - if recordMode && sampleMode then - recordIndex <- recordIndex + 1 - if recordIndex % recordEvery = 0 then - System.IO.Directory.CreateDirectory recordDirectory |> ignore - this.SaveBackBuffer(sprintf "%s/rec-frame-%04d.png" recordDirectory recordIndex) - if menuShotMode then - this.CaptureMenuShot() + if splashActive then + this.DrawSplash() + elif menu.Page <> Playing then + this.DrawMenu() + elif relationView then + this.DrawRelationView() + this.DrawM5Overlay() + else + this.DrawWorldView() + if recordMode && sampleMode then + recordIndex <- recordIndex + 1 + if recordIndex % recordEvery = 0 then + System.IO.Directory.CreateDirectory recordDirectory |> ignore + this.SaveBackBuffer(sprintf "%s/rec-frame-%04d.png" recordDirectory recordIndex) + if menuShotMode then + this.CaptureMenuShot() /// 启动画面:分层天色 + 屋脊/水面剪影 + 标题淡入上浮 + 跳过提示,全部为帧计数纯函数。 member private this.DrawSplash() = diff --git a/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj b/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj index d57036b..739b750 100644 --- a/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj +++ b/src/LivingVillage.Desktop/LivingVillage.Desktop.fsproj @@ -9,6 +9,7 @@ <Compile Include="VillagePresentation.fs" /> <Compile Include="WorldBootstrap.fs" /> <Compile Include="ProceduralMap.fs" /> + <Compile Include="MapGen.fs" /> <Compile Include="VillageArt.fs" /> <Compile Include="ChineseText.fs" /> <Compile Include="CjkGlyphAtlas.fs" /> diff --git a/src/LivingVillage.Desktop/MapGen.fs b/src/LivingVillage.Desktop/MapGen.fs new file mode 100644 index 0000000..4f59f84 --- /dev/null +++ b/src/LivingVillage.Desktop/MapGen.fs @@ -0,0 +1,249 @@ +namespace LivingVillage.Desktop + +open LivingVillage.Kernel + +/// P20 第一步:确定性大地图生成器的参数化纯函数片。 +/// 单一入口 `generate`:seed + 尺寸 -> 瓦片网格(0 grass / 1 water / 2 stone / 3 peat)。 +/// 组成:splitmix64 随机游走河道 + 2-3 octave value noise 地形 + 泊松式布点装饰, +/// 核心活动区先整平为草地,再用 BFS 校验 30 个 spawn 全部可达核心(不可达则确定性修整)。 +/// 全部为纯函数:同参两次生成逐字节一致,不含时钟/随机源。 +module MapGen = + + /// 地面码,与既有 ProceduralMap / 渲染映射保持一致。 + type GroundTile = + | Grass = 0 + | Water = 1 + | Stone = 2 + | Peat = 3 + + type Bounds = + { MinX: int + MinY: int + MaxX: int + MaxY: int } + + type Params = + { Width: int + Height: int + Seed: uint64 + RiverCount: int + RiverWidth: int + CoreSide: int + SpawnColumns: int + SpawnRows: int } + + type Result = + { Width: int + Height: int + Seed: uint64 + Tiles: int array + Core: Bounds + Spawns: (int * int) list + ReachableTiles: int + ReachabilityOk: bool } + + /// 默认仍是现状游戏尺寸 64x48,核心 14 格、5x6=30 个 spawn。 + let defaultParams (seed: uint64) : Params = + { Width = 64 + Height = 48 + Seed = seed + RiverCount = 2 + RiverWidth = 3 + CoreSide = 14 + SpawnColumns = 5 + SpawnRows = 6 } + + // ---- splitmix64 与 value noise(无外部依赖、无时钟) ---- + + let private splitmix (state: uint64) : uint64 * uint64 = + let nextState = state + 0x9E3779B97F4A7C15UL + let mutable z = nextState + z <- (z ^^^ (z >>> 30)) * 0xBF58476D1CE4E5B9UL + z <- z ^^^ (z >>> 27) + z <- z * 0x94D049BB133111EBUL + z <- z ^^^ (z >>> 31) + (nextState, z) + + let private hash2 (x: int) (y: int) (seed: uint64) : uint64 = + let base_ = seed ^^^ ((uint64 x * 0x4D3B9UL) + (uint64 y * 0x1D5UL)) + snd (splitmix base_) + + let private valueNoise (fx: float32) (fy: float32) (seed: uint64) (scale: float32) : float32 = + let xf = fx / scale + let yf = fy / scale + let x0 = int (floor xf) + let y0 = int (floor yf) + let tx = xf - float32 x0 + let ty = yf - float32 y0 + let smooth (t: float32) = t * t * (3.0f - 2.0f * t) + let sx = smooth tx + let sy = smooth ty + let v00 = float32 (hash2 x0 y0 seed % 1000UL) / 1000.0f + let v10 = float32 (hash2 (x0 + 1) y0 seed % 1000UL) / 1000.0f + let v01 = float32 (hash2 x0 (y0 + 1) seed % 1000UL) / 1000.0f + let v11 = float32 (hash2 (x0 + 1) (y0 + 1) seed % 1000UL) / 1000.0f + (v00 * (1.0f - sx) + v10 * sx) * (1.0f - sy) + (v01 * (1.0f - sx) + v11 * sx) * sy + + /// 2-3 octave 叠加;特征尺度随地图宽度等比缩放,保证小图/大图观感一致。 + let private multiOctave (widthScale: float32) (x: int) (y: int) (seed: uint64) : float32 = + let xf = float32 x + let yf = float32 y + let a = valueNoise xf yf seed (96.0f * widthScale) + let b = valueNoise xf yf (seed + 1UL) (38.0f * widthScale) + let c = valueNoise xf yf (seed + 2UL) (13.0f * widthScale) + 0.55f * a + 0.3f * b + 0.15f * c + + // ---- 生成 ---- + + let private index (width: int) (x: int) (y: int) : int = y * width + x + + let tileAt (result: Result) (x: int) (y: int) : GroundTile = + enum<GroundTile> result.Tiles.[index result.Width x y] + + /// 参数化生成。尺寸下限 8x8;核心区始终先整平为草地,再 BFS 校验可达性。 + let generate (p: Params) : Result = + let w = max 8 p.Width + let h = max 8 p.Height + let tiles = Array.zeroCreate<int> (w * h) // 0 = Grass + let idx x y = index w x y + + let side = max 6 (min (min w h) p.CoreSide) + let cx = w / 2 + let cy = h / 2 + let core = + { MinX = max 1 (cx - side / 2) + MinY = max 1 (cy - side / 2) + MaxX = min (w - 2) (cx + side / 2) + MaxY = min (h - 2) (cy + side / 2) } + let inCore x y = + x >= core.MinX && x <= core.MaxX && y >= core.MinY && y <= core.MaxY + + // 1) 多八度噪声:低洼草地转泥炭(跳过核心区)。 + let widthScale = float32 w / 64.0f + for y in 0 .. h - 1 do + for x in 0 .. w - 1 do + if multiOctave widthScale x y p.Seed < 0.18f && not (inCore x y) then + tiles.[idx x y] <- int GroundTile.Peat + + // 2) 河道:RiverCount 条自西向东随机游走,宽度 RiverWidth。 + let mutable rng = p.Seed ^^^ 0xBEEFUL + for _ in 1 .. max 0 p.RiverCount do + let nextState, rawStartY = splitmix rng + rng <- nextState + let mutable y = 2 + int (rawStartY % uint64 (max 1 (h - 4))) + for x in 2 .. w - 3 do + let nextStep, rawStep = splitmix rng + rng <- nextStep + let drift = int (rawStep % 3UL) - 1 + y <- max 2 (min (h - 3) (y + drift)) + for dy in 0 .. p.RiverWidth - 1 do + let yy = y + dy + if yy <= h - 2 && not (inCore x yy) then + tiles.[idx x yy] <- int GroundTile.Water + + // 3) 核心活动区整平为草地(生成器保证可通行)。 + for y in core.MinY .. core.MaxY do + for x in core.MinX .. core.MaxX do + tiles.[idx x y] <- int GroundTile.Grass + + // 4) 泊松式布点:最小间距随尺寸缩放,只在草地打石头装饰(跳过核心区)。 + let minDist = max 3 (w / 24) + let minSq = minDist * minDist + let attempts = max 100 ((w * h) / 16) + let mutable placed: (int * int) list = [] + let mutable scatterRng = rng + for _ in 1 .. attempts do + let nextState, raw = splitmix scatterRng + scatterRng <- nextState + let x = int (raw % uint64 w) + let y = int ((raw >>> 20) % uint64 h) + let far = + placed + |> List.forall (fun (px, py) -> + let dx = px - x + let dy = py - y + dx * dx + dy * dy > minSq) + if far && not (inCore x y) && tiles.[idx x y] = int GroundTile.Grass then + tiles.[idx x y] <- int GroundTile.Stone + placed <- (x, y) :: placed + + // 5) 30 个 spawn:核心区内的规则网格。 + let sc = max 2 p.SpawnColumns + let sr = max 2 p.SpawnRows + let spanX = max 1 (core.MaxX - core.MinX - 2) + let spanY = max 1 (core.MaxY - core.MinY - 2) + let spawns = + [ for r in 0 .. sr - 1 do + for c in 0 .. sc - 1 do + yield + (core.MinX + 1 + (c * spanX) / (sc - 1), + core.MinY + 1 + (r * spanY) / (sr - 1)) ] + + // 6) BFS(4 邻接、非水可走):从核心中心洪泛,校验全部 spawn 可达。 + let walkable x y = tiles.[idx x y] <> int GroundTile.Water + let flood startX startY = + let visited = Array.create (w * h) false + let queue = System.Collections.Generic.Queue<int>() + if walkable startX startY then + let start = idx startX startY + visited.[start] <- true + queue.Enqueue start + while queue.Count > 0 do + let cur = queue.Dequeue() + let x = cur % w + let y = cur / w + for dx, dy in [ (1, 0); (-1, 0); (0, 1); (0, -1) ] do + let nx = x + dx + let ny = y + dy + if nx >= 0 && nx < w && ny >= 0 && ny < h then + let ni = idx nx ny + if not visited.[ni] && walkable nx ny then + visited.[ni] <- true + queue.Enqueue ni + visited + + let carve (sx: int) (sy: int) = + let mutable x = sx + while x <> cx do + x <- x + sign (cx - x) + if tiles.[idx x sy] = int GroundTile.Water then tiles.[idx x sy] <- int GroundTile.Grass + let mutable y = sy + while y <> cy do + y <- y + sign (cy - y) + if tiles.[idx x y] = int GroundTile.Water then tiles.[idx x y] <- int GroundTile.Grass + + let mutable visited = flood cx cy + let unreachable = spawns |> List.filter (fun (x, y) -> not visited.[idx x y]) + for spawn in unreachable do + carve (fst spawn) (snd spawn) + if not unreachable.IsEmpty then + visited <- flood cx cy + + let reachableTiles = visited |> Array.filter id |> Array.length + let reachabilityOk = spawns |> List.forall (fun (x, y) -> visited.[idx x y]) + + { Width = w + Height = h + Seed = p.Seed + Tiles = tiles + Core = core + Spawns = spawns + ReachableTiles = reachableTiles + ReachabilityOk = reachabilityOk } + + let generateWithSize (width: int) (height: int) (seed: uint64) : Result = + generate { defaultParams seed with Width = width; Height = height } + + // ---- 视口裁剪(纯函数):只绘制可见瓦片,与 drawWorld 现有窗口一致 ---- + + /// 返回含端点的可见瓦片范围 (x0, y0, x1, y1);空视口返回 (0,0,-1,-1)。 + let visibleTileRange (mapWidth: int) (mapHeight: int) (cameraX: int) (cameraY: int) (viewportWidth: int) (viewportHeight: int) : int * int * int * int = + let x0 = max 0 (cameraX / Sim.tilePixels) + let y0 = max 0 (cameraY / Sim.tilePixels) + let x1 = min (mapWidth - 1) ((cameraX + viewportWidth) / Sim.tilePixels + 1) + let y1 = min (mapHeight - 1) ((cameraY + viewportHeight) / Sim.tilePixels + 1) + if mapWidth <= 0 || mapHeight <= 0 || x1 < x0 || y1 < y0 then (0, 0, -1, -1) else (x0, y0, x1, y1) + + let visibleTileCount (mapWidth: int) (mapHeight: int) (cameraX: int) (cameraY: int) (viewportWidth: int) (viewportHeight: int) : int = + let x0, y0, x1, y1 = visibleTileRange mapWidth mapHeight cameraX cameraY viewportWidth viewportHeight + if x1 < x0 || y1 < y0 then 0 else (x1 - x0 + 1) * (y1 - y0 + 1) diff --git a/src/LivingVillage.Desktop/VillageArt.fs b/src/LivingVillage.Desktop/VillageArt.fs index 11a6d71..51e17e0 100644 --- a/src/LivingVillage.Desktop/VillageArt.fs +++ b/src/LivingVillage.Desktop/VillageArt.fs @@ -643,10 +643,8 @@ module VillageArt = | Some cc0 -> drawCc0Tile spriteBatch cc0 tint (cc0GroundRect position) (position.X * Sim.tilePixels - int camera.X) (position.Y * Sim.tilePixels - int camera.Y) | None -> draw position GrassSprite - let tx0 = max 0 (int camera.X / Sim.tilePixels) - let ty0 = max 0 (int camera.Y / Sim.tilePixels) - let tx1 = min (Sim.mapWidthTiles - 1) ((int camera.X + viewportWidth) / Sim.tilePixels + 1) - let ty1 = min (Sim.mapHeightTiles - 1) ((int camera.Y + viewportHeight) / Sim.tilePixels + 1) + let tx0, ty0, tx1, ty1 = + MapGen.visibleTileRange Sim.mapWidthTiles Sim.mapHeightTiles (int camera.X) (int camera.Y) viewportWidth viewportHeight for y in ty0 .. ty1 do for x in tx0 .. tx1 do drawBaseTile (tilePosition x y) @@ -724,6 +722,67 @@ module VillageArt = | House -> draw (offsetTile (worldTile plan prop.Position) 0 -3) smokeSprite | _ -> () + /// P20 证据用:把整张 MapGen 地图等比缩放到视口内绘制,返回绘制的瓦片数。 + /// 仅用于 `LV_MAPGEN_SHOT` 整图取证,不参与正常游戏循环。 + let drawMapFitted + (spriteBatch: SpriteBatch) + (textures: ArtTextures) + (viewportWidth: int) + (viewportHeight: int) + (map: MapGen.Result) + (tint: XnaColor) : int = + let scale = + min (float32 viewportWidth / float32 (max 1 (map.Width * Sim.tilePixels))) + (float32 viewportHeight / float32 (max 1 (map.Height * Sim.tilePixels))) + let cell = max 1 (int (float32 Sim.tilePixels * scale)) + let offsetX = (viewportWidth - map.Width * cell) / 2 + let offsetY = (viewportHeight - map.Height * cell) / 2 + let spriteOf code = + match code with + | 1 -> WaterSprite + | 2 + | 3 -> StonePathSprite + | _ -> GrassSprite + for y in 0 .. map.Height - 1 do + for x in 0 .. map.Width - 1 do + let code = map.Tiles.[y * map.Width + x] + spriteBatch.Draw(textures.TileAtlas, XnaRectangle(offsetX + x * cell, offsetY + y * cell, cell, cell), tileSourceRectangle (spriteOf code), tint) + map.Width * map.Height + + /// P20 证据用:按相机做视口裁剪,只绘制可见瓦片(1:1 像素),返回实际绘制数。 + let drawMapViewport + (spriteBatch: SpriteBatch) + (textures: ArtTextures) + (camera: XnaVector2) + (viewportWidth: int) + (viewportHeight: int) + (map: MapGen.Result) + (tick: int64) + (tint: XnaColor) : int = + let x0, y0, x1, y1 = + MapGen.visibleTileRange map.Width map.Height (int camera.X) (int camera.Y) viewportWidth viewportHeight + let spriteOf (x: int) (y: int) code = + match code with + | 1 -> + match waterFrameTickAt tick x y with + | 0 -> WaterSprite + | 1 -> WaterSpriteB + | _ -> WaterSpriteC + | 2 + | 3 -> StonePathSprite + | _ -> GrassSprite + let mutable drawn = 0 + for y in y0 .. y1 do + for x in x0 .. x1 do + let code = map.Tiles.[y * map.Width + x] + spriteBatch.Draw( + textures.TileAtlas, + XnaRectangle(x * Sim.tilePixels - int camera.X, y * Sim.tilePixels - int camera.Y, Sim.tilePixels, Sim.tilePixels), + tileSourceRectangle (spriteOf x y code), + tint) + drawn <- drawn + 1 + drawn + let drawCharacter (spriteBatch: SpriteBatch) (textures: ArtTextures) |
