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')";
}
}