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.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"
|
||||
|
||||
@@ -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 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));
|
||||
|
||||
@@ -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')";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user