From fa7f27642ce9d2e2b0a10ae34982f485a2854ed7 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Thu, 11 Jun 2026 10:36:27 +0300 Subject: [PATCH] Add TerrainScene: code-built tilemap terrain with pawns and props Small seed-generated terrain rendered as a single Tilemap entity: water, sand and mountains are tinted tiles, grass and forest use atlas textures. Three wandering pawns and a few props (snowman, lamp, stool, trees) live on the shared Y-sort layer (now in GameLayers, used by both scenes). Console: 'terrain [seed]' from the world scene, 'regen'/'world' inside. Co-Authored-By: Claude Fable 5 --- src/LittleSim/LittleSim.csproj | 1 + src/LittleSim/Scenes/GameLayers.cs | 27 +++++ src/LittleSim/Scenes/TerrainScene.cs | 173 +++++++++++++++++++++++++++ src/LittleSim/Scenes/WorldScene.cs | 28 +++-- src/LittleSim/Sim/SimSystems.cs | 2 +- 5 files changed, 219 insertions(+), 12 deletions(-) create mode 100644 src/LittleSim/Scenes/GameLayers.cs create mode 100644 src/LittleSim/Scenes/TerrainScene.cs diff --git a/src/LittleSim/LittleSim.csproj b/src/LittleSim/LittleSim.csproj index d9c7994..5861788 100644 --- a/src/LittleSim/LittleSim.csproj +++ b/src/LittleSim/LittleSim.csproj @@ -12,6 +12,7 @@ + +/// Слои рендера игры. Renderer2D и его реестр слоёв общие для всех сцен, +/// поэтому регистрация выполняется один раз на процесс. +/// +public static class GameLayers +{ + private static bool _registered; + + /// Y-sort слой существ и объектов мира: кто ниже на экране — тот ближе. + public static LayerId Beings { get; private set; } + + /// Регистрирует слои при первом вызове. + public static void EnsureRegistered(Renderer2D renderer) + { + if (_registered) + { + return; + } + + Beings = renderer.Layers.Register("Beings", LayerSpace.World, LayerSortMode.YSort); + _registered = true; + } +} diff --git a/src/LittleSim/Scenes/TerrainScene.cs b/src/LittleSim/Scenes/TerrainScene.cs new file mode 100644 index 0000000..5cf4efe --- /dev/null +++ b/src/LittleSim/Scenes/TerrainScene.cs @@ -0,0 +1,173 @@ +using Friflo.Engine.ECS; +using LittleSim.Sim; +using Microsoft.Xna.Framework; +using MrGameEng.Assets; +using MrGameEng.Atlases; +using MrGameEng.Core; +using MrGameEng.DevConsole; +using MrGameEng.Graphics; +using MrGameEng.Input; +using MrGameEng.Tilemaps; +using MrGameEng.UI; +using Myra.Graphics2D.UI; + +namespace LittleSim.Scenes; + +/// +/// Демо-сцена тайлового террейна: небольшая карта-Tilemap, сгенерированная по сиду +/// (вода и горы — тонированные тайлы, трава и лес — текстуры из атласа), пара бродящих +/// персонажей и несколько объектов. Команды консоли: "regen [seed]", "world". +/// +public sealed class TerrainScene : Scene +{ + private const int TerrainWidth = 44; + private const int TerrainHeight = 28; + private const int CellSize = 24; + private const int Population = 3; + + private static readonly RectF TerrainBounds = new(0f, 0f, TerrainWidth * CellSize, TerrainHeight * CellSize); + + private static readonly (string Key, float Cells)[] Objects = + [ + ("Things/Building/Art/Snowman/Snowman_A", 1.8f), + ("Things/Building/BuildingStyles/Rustic/RusticLampStanding", 1.4f), + ("Things/Building/BuildingStyles/Totemic/Stool/TotemicStool_east", 1.1f), + ]; + + private readonly int _seed; + + public TerrainScene(int seed) => _seed = seed; + + protected override void OnLoad() + { + var assets = Context.Services.GetOrDefault() ?? Context.UseAssets(); + Context.UseTextureAtlases(); + var input = this.UseInput(); + var renderer = this.UseRenderer2D(new Renderer2DOptions { VirtualResolution = new Point(1280, 720) }); + GameLayers.EnsureRegistered(renderer); + this.UseTilemaps(); + + var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White)); + var terrainAtlas = assets.Load(GameAssets.Atlases.TerrainSurfaces); + var pawnAtlas = assets.Load(GameAssets.Atlases.ThingsPawn); + var plantAtlas = assets.Load(GameAssets.Atlases.ThingsPlant); + var buildingAtlas = assets.Load(GameAssets.Atlases.ThingsBuilding); + + // Тайлсет: вода/песок/горы — тонированная белая текстура, трава и лес — атлас + // (две текстуры террейна → весь пол рисуется парой draw call'ов). + var tiles = new TileSet(); + var tileByKind = new Dictionary + { + [TerrainKind.DeepWater] = tiles.Add(white, new Color(23, 63, 95)), + [TerrainKind.Water] = tiles.Add(white, new Color(32, 99, 155)), + [TerrainKind.Sand] = tiles.Add(white, new Color(237, 201, 120)), + [TerrainKind.Grass] = tiles.Add(terrainAtlas.GetRegion("Terrain/Surfaces/Marsh_bright")), + [TerrainKind.Forest] = tiles.Add(terrainAtlas.GetRegion("Terrain/Surfaces/Marsh")), + [TerrainKind.Mountain] = tiles.Add(white, new Color(136, 132, 128)), + }; + + // Террейн строится кодом: высоты по сиду → вид клетки → id тайла в гриде. + var random = new Random(_seed); + var heights = WorldGenerator.GenerateHeights(TerrainWidth, TerrainHeight, _seed); + var grid = new TileGrid(TerrainWidth, TerrainHeight); + var landCells = new List(); + for (var x = 0; x < TerrainWidth; x++) + { + for (var y = 0; y < TerrainHeight; y++) + { + var kind = WorldGenerator.Classify(heights[x, y]); + grid[x, y] = tileByKind[kind]; + if (WorldGenerator.IsLand(kind)) + { + landCells.Add(new Point(x, y)); + } + + if (kind == TerrainKind.Forest && random.NextSingle() < 0.3f) + { + SpawnOnCell( + plantAtlas.GetRegion( + "Things/Plant/" + WorldScene.ForestTrees[random.Next(WorldScene.ForestTrees.Length)]), + new Point(x, y), random, sizeCells: 2.2f); + } + } + } + + Store.CreateEntity(new Tilemap(grid, tiles, CellSize)); + + // Несколько объектов на суше: снеговик, лампа, табурет. + foreach (var (key, sizeCells) in Objects) + { + if (landCells.Count == 0) + { + break; + } + + SpawnOnCell(buildingAtlas.GetRegion(key), landCells[random.Next(landCells.Count)], random, sizeCells); + } + + // Пара персонажей, бродящих по террейну. + for (var i = 0; i < Population && landCells.Count > 0; i++) + { + var cell = landCells[random.Next(landCells.Count)]; + var body = pawnAtlas.GetRegion( + "Things/Pawn/Humanlike/Bodies/" + WorldScene.BeingBodies[random.Next(WorldScene.BeingBodies.Length)] + "_south"); + var sprite = new Sprite(body, GameLayers.Beings); + sprite.CenterOrigin(); + Store.CreateEntity( + new Transform2D( + new Vector2((cell.X + 0.5f) * CellSize, (cell.Y + 0.5f) * CellSize), + scale: new Vector2(CellSize * 1.6f / body.Width)), + sprite, + new Wander()); + } + + var camera = Store.CreateEntity(new Camera(TerrainBounds.Center, zoom: 1.6f, bounds: TerrainBounds)); + + var desktop = this.UseUI(); + var hudLabel = new Label { Left = 10, Top = 8 }; + desktop.Root = hudLabel; + hudLabel.Text = $"LittleSim | террейн | seed {_seed}\nWASD — камера, колесо — зум, ` — консоль ('regen', 'world')"; + + var console = this.UseDevConsole(); + console.Register("regen", "regen [seed] — regenerate the terrain", (c, args) => + { + if (Context.Scenes.IsTransitioning) + { + return; + } + + var seed = args.Length > 0 ? int.Parse(args[0]) : Random.Shared.Next(); + c.WriteLine($"regenerating terrain, seed {seed}"); + Context.Scenes.Switch(new TerrainScene(seed), Transition.Fade(0.6f)); + }); + console.Register("world", "world [seed] — switch to the world scene", (c, args) => + { + if (Context.Scenes.IsTransitioning) + { + return; + } + + var seed = args.Length > 0 ? int.Parse(args[0]) : _seed; + Context.Scenes.Switch(new WorldScene(seed), Transition.Fade(0.6f)); + }); + + UpdateSystems.Add(new WanderSystem(_seed, TerrainBounds)); + UpdateSystems.Add(new GodCameraSystem(camera, input, console)); + + Log.Info($"Terrain generated: seed {_seed}, {TerrainWidth}x{TerrainHeight} cells"); + } + + /// Ставит объект на клетку с лёгким смещением; масштаб задаётся в клетках. + private void SpawnOnCell(Texture2DRegion region, Point cell, Random random, float sizeCells) + { + var sprite = new Sprite(region, GameLayers.Beings); + sprite.CenterOrigin(); + Store.CreateEntity( + new Transform2D( + new Vector2( + (cell.X + 0.3f + random.NextSingle() * 0.4f) * CellSize, + (cell.Y + 0.3f + random.NextSingle() * 0.4f) * CellSize), + scale: new Vector2(CellSize * sizeCells / region.Width)), + sprite); + } +} diff --git a/src/LittleSim/Scenes/WorldScene.cs b/src/LittleSim/Scenes/WorldScene.cs index bee51ff..65885f6 100644 --- a/src/LittleSim/Scenes/WorldScene.cs +++ b/src/LittleSim/Scenes/WorldScene.cs @@ -24,17 +24,15 @@ public sealed class WorldScene : Scene private const int Population = 80; private static readonly RectF WorldBounds = new(0f, 0f, WorldWidth * CellSize, WorldHeight * CellSize); - private static LayerId _beingsLayer; - private static bool _layersRegistered; private readonly int _seed; public WorldScene(int seed) => _seed = seed; - private static readonly string[] BeingBodies = + internal static readonly string[] BeingBodies = ["FurCovered_Male", "FurCovered_Female", "FurCovered_Thin", "FurCovered_Fat", "FurCovered_Hulk"]; - private static readonly string[] ForestTrees = + internal static readonly string[] ForestTrees = ["TreeOak/TreeOakA", "TreeOak/TreeOakB", "TreeBirch/TreeBirchA", "TreeGrayPine/TreeGrayPineA"]; protected override void OnLoad() @@ -43,11 +41,7 @@ public sealed class WorldScene : Scene Context.UseTextureAtlases(); var input = this.UseInput(); var renderer = this.UseRenderer2D(new Renderer2DOptions { VirtualResolution = new Point(1280, 720) }); - if (!_layersRegistered) - { - _beingsLayer = renderer.Layers.Register("Beings", LayerSpace.World, LayerSortMode.YSort); - _layersRegistered = true; - } + GameLayers.EnsureRegistered(renderer); var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White)); @@ -76,7 +70,7 @@ public sealed class WorldScene : Scene if (kind == TerrainKind.Forest && random.NextSingle() < 0.35f) { var tree = plantAtlas.GetRegion("Things/Plant/" + ForestTrees[random.Next(ForestTrees.Length)]); - var treeSprite = new Sprite(tree, _beingsLayer); + var treeSprite = new Sprite(tree, GameLayers.Beings); treeSprite.CenterOrigin(); Store.CreateEntity( new Transform2D( @@ -96,7 +90,7 @@ public sealed class WorldScene : Scene var cell = landCells[random.Next(landCells.Count)]; var body = pawnAtlas.GetRegion( "Things/Pawn/Humanlike/Bodies/" + BeingBodies[random.Next(BeingBodies.Length)] + "_south"); - var sprite = new Sprite(body, _beingsLayer); + var sprite = new Sprite(body, GameLayers.Beings); sprite.CenterOrigin(); Store.CreateEntity( new Transform2D( @@ -162,6 +156,18 @@ public sealed class WorldScene : Scene c.WriteLine($"{keys.Count} region(s){(keys.Count > 25 ? ", showing first 25" : "")}"); }); + console.Register("terrain", "terrain [seed] — switch to the tile terrain scene", (c, args) => + { + if (Context.Scenes.IsTransitioning) + { + return; + } + + var seed = args.Length > 0 ? int.Parse(args[0]) : _seed; + c.WriteLine($"switching to terrain scene, seed {seed}"); + Context.Scenes.Switch(new TerrainScene(seed), Transition.Fade(0.6f)); + }); + UpdateSystems.Add(new WanderSystem(_seed, WorldBounds)); UpdateSystems.Add(new GodCameraSystem(camera, input, console)); UpdateSystems.Add(new HudSystem(Context, hudLabel, _seed, Population)); diff --git a/src/LittleSim/Sim/SimSystems.cs b/src/LittleSim/Sim/SimSystems.cs index 31db8c7..87d31d9 100644 --- a/src/LittleSim/Sim/SimSystems.cs +++ b/src/LittleSim/Sim/SimSystems.cs @@ -100,6 +100,6 @@ public sealed class HudSystem( var fps = _frames / _accumulated; _accumulated = 0f; _frames = 0; - label.Text = $"LittleSim | seed {seed} | население {population} | {fps:F0} FPS\nWASD — камера, колесо — зум, ` — консоль ('regen', 'timescale', 'atlas')"; + label.Text = $"LittleSim | seed {seed} | население {population} | {fps:F0} FPS\nWASD — камера, колесо — зум, ` — консоль ('regen', 'terrain', 'timescale', 'atlas')"; } }