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:
co-authored by
Claude Fable 5
parent
56dd760a2c
commit
fa7f27642c
@@ -12,6 +12,7 @@
|
|||||||
<ProjectReference Include="..\..\engine\src\MrGameEng.Audio\MrGameEng.Audio.csproj" />
|
<ProjectReference Include="..\..\engine\src\MrGameEng.Audio\MrGameEng.Audio.csproj" />
|
||||||
<ProjectReference Include="..\..\engine\src\MrGameEng.Assets\MrGameEng.Assets.csproj" />
|
<ProjectReference Include="..\..\engine\src\MrGameEng.Assets\MrGameEng.Assets.csproj" />
|
||||||
<ProjectReference Include="..\..\engine\src\MrGameEng.Atlases\MrGameEng.Atlases.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.UI\MrGameEng.UI.csproj" />
|
||||||
<ProjectReference Include="..\..\engine\src\MrGameEng.DevConsole\MrGameEng.DevConsole.csproj" />
|
<ProjectReference Include="..\..\engine\src\MrGameEng.DevConsole\MrGameEng.DevConsole.csproj" />
|
||||||
<ProjectReference Include="..\..\engine\src\MrGameEng.Assets.Generator\MrGameEng.Assets.Generator.csproj"
|
<ProjectReference Include="..\..\engine\src\MrGameEng.Assets.Generator\MrGameEng.Assets.Generator.csproj"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,17 +24,15 @@ public sealed class WorldScene : Scene
|
|||||||
private const int Population = 80;
|
private const int Population = 80;
|
||||||
|
|
||||||
private static readonly RectF WorldBounds = new(0f, 0f, WorldWidth * CellSize, WorldHeight * CellSize);
|
private static readonly RectF WorldBounds = new(0f, 0f, WorldWidth * CellSize, WorldHeight * CellSize);
|
||||||
private static LayerId _beingsLayer;
|
|
||||||
private static bool _layersRegistered;
|
|
||||||
|
|
||||||
private readonly int _seed;
|
private readonly int _seed;
|
||||||
|
|
||||||
public WorldScene(int seed) => _seed = 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"];
|
["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"];
|
["TreeOak/TreeOakA", "TreeOak/TreeOakB", "TreeBirch/TreeBirchA", "TreeGrayPine/TreeGrayPineA"];
|
||||||
|
|
||||||
protected override void OnLoad()
|
protected override void OnLoad()
|
||||||
@@ -43,11 +41,7 @@ public sealed class WorldScene : Scene
|
|||||||
Context.UseTextureAtlases();
|
Context.UseTextureAtlases();
|
||||||
var input = this.UseInput();
|
var input = this.UseInput();
|
||||||
var renderer = this.UseRenderer2D(new Renderer2DOptions { VirtualResolution = new Point(1280, 720) });
|
var renderer = this.UseRenderer2D(new Renderer2DOptions { VirtualResolution = new Point(1280, 720) });
|
||||||
if (!_layersRegistered)
|
GameLayers.EnsureRegistered(renderer);
|
||||||
{
|
|
||||||
_beingsLayer = renderer.Layers.Register("Beings", LayerSpace.World, LayerSortMode.YSort);
|
|
||||||
_layersRegistered = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var white = new Texture2DRegion(assets.Load(GameAssets.Textures.White));
|
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)
|
if (kind == TerrainKind.Forest && random.NextSingle() < 0.35f)
|
||||||
{
|
{
|
||||||
var tree = plantAtlas.GetRegion("Things/Plant/" + ForestTrees[random.Next(ForestTrees.Length)]);
|
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();
|
treeSprite.CenterOrigin();
|
||||||
Store.CreateEntity(
|
Store.CreateEntity(
|
||||||
new Transform2D(
|
new Transform2D(
|
||||||
@@ -96,7 +90,7 @@ public sealed class WorldScene : Scene
|
|||||||
var cell = landCells[random.Next(landCells.Count)];
|
var cell = landCells[random.Next(landCells.Count)];
|
||||||
var body = pawnAtlas.GetRegion(
|
var body = pawnAtlas.GetRegion(
|
||||||
"Things/Pawn/Humanlike/Bodies/" + BeingBodies[random.Next(BeingBodies.Length)] + "_south");
|
"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();
|
sprite.CenterOrigin();
|
||||||
Store.CreateEntity(
|
Store.CreateEntity(
|
||||||
new Transform2D(
|
new Transform2D(
|
||||||
@@ -162,6 +156,18 @@ public sealed class WorldScene : Scene
|
|||||||
c.WriteLine($"{keys.Count} region(s){(keys.Count > 25 ? ", showing first 25" : "")}");
|
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 WanderSystem(_seed, WorldBounds));
|
||||||
UpdateSystems.Add(new GodCameraSystem(camera, input, console));
|
UpdateSystems.Add(new GodCameraSystem(camera, input, console));
|
||||||
UpdateSystems.Add(new HudSystem(Context, hudLabel, _seed, Population));
|
UpdateSystems.Add(new HudSystem(Context, hudLabel, _seed, Population));
|
||||||
|
|||||||
@@ -100,6 +100,6 @@ public sealed class HudSystem(
|
|||||||
var fps = _frames / _accumulated;
|
var fps = _frames / _accumulated;
|
||||||
_accumulated = 0f;
|
_accumulated = 0f;
|
||||||
_frames = 0;
|
_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')";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user