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 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-11 10:36:27 +03:00
co-authored by Claude Fable 5
parent 56dd760a2c
commit fa7f27642c
5 changed files with 219 additions and 12 deletions
+1
View File
@@ -12,6 +12,7 @@
<ProjectReference Include="..\..\engine\src\MrGameEng.Audio\MrGameEng.Audio.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.Assets\MrGameEng.Assets.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.Atlases\MrGameEng.Atlases.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.Tilemaps\MrGameEng.Tilemaps.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.UI\MrGameEng.UI.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.DevConsole\MrGameEng.DevConsole.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.Assets.Generator\MrGameEng.Assets.Generator.csproj"
+27
View File
@@ -0,0 +1,27 @@
using MrGameEng.Graphics;
namespace LittleSim.Scenes;
/// <summary>
/// Слои рендера игры. Renderer2D и его реестр слоёв общие для всех сцен,
/// поэтому регистрация выполняется один раз на процесс.
/// </summary>
public static class GameLayers
{
private static bool _registered;
/// <summary>Y-sort слой существ и объектов мира: кто ниже на экране — тот ближе.</summary>
public static LayerId Beings { get; private set; }
/// <summary>Регистрирует слои при первом вызове.</summary>
public static void EnsureRegistered(Renderer2D renderer)
{
if (_registered)
{
return;
}
Beings = renderer.Layers.Register("Beings", LayerSpace.World, LayerSortMode.YSort);
_registered = true;
}
}
+173
View File
@@ -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;
/// <summary>
/// Демо-сцена тайлового террейна: небольшая карта-Tilemap, сгенерированная по сиду
/// (вода и горы — тонированные тайлы, трава и лес — текстуры из атласа), пара бродящих
/// персонажей и несколько объектов. Команды консоли: "regen [seed]", "world".
/// </summary>
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<AssetManager>() ?? 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, ushort>
{
[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<Point>();
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");
}
/// <summary>Ставит объект на клетку с лёгким смещением; масштаб задаётся в клетках.</summary>
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);
}
}
+17 -11
View File
@@ -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));
+1 -1
View File
@@ -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')";
}
}