Terrain scene: atlas animals, surface tiles, grass and collisions

Creatures are now animals only (bear, deer, fox, hare, boar, timber
wolf, muffalo, squirrel) from things/pawn/animal, replacing humanlike
pawns and man-made props. Land tiles use proper terrain/surfaces
textures (sand, mossy for grass, soil under forest, rough-hewn rock
for mountains); grass cells get grassa tufts and occasional bushes.

Collisions via the new engine module: animals carry circle colliders
(layer masks: animal vs animal+obstacle), trees get static trunk
colliders, and SeparationSystem resolves overlaps by pushing animals
apart and out of trees after CollisionSystem each tick.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-11 15:17:22 +03:00
co-authored by Claude Fable 5
parent 35781a21ca
commit bc95227e8c
4 changed files with 119 additions and 38 deletions
+2
View File
@@ -13,6 +13,8 @@
<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.Tilemaps\MrGameEng.Tilemaps.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.Pathfinding\MrGameEng.Pathfinding.csproj" />
<ProjectReference Include="..\..\engine\src\MrGameEng.Collisions\MrGameEng.Collisions.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"
+2
View File
@@ -13,3 +13,5 @@ using var host = new GameHost(
new WorldScene(seed: 1337)); new WorldScene(seed: 1337));
host.Run(); host.Run();
+65 -38
View File
@@ -3,6 +3,7 @@ using LittleSim.Sim;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using MrGameEng.Assets; using MrGameEng.Assets;
using MrGameEng.Atlases; using MrGameEng.Atlases;
using MrGameEng.Collisions;
using MrGameEng.Core; using MrGameEng.Core;
using MrGameEng.DevConsole; using MrGameEng.DevConsole;
using MrGameEng.Graphics; using MrGameEng.Graphics;
@@ -23,17 +24,34 @@ public sealed class TerrainScene : Scene
private const int TerrainWidth = 44; private const int TerrainWidth = 44;
private const int TerrainHeight = 28; private const int TerrainHeight = 28;
private const int CellSize = 24; private const int CellSize = 24;
private const int Population = 3; private const int Population = 14;
private static readonly RectF TerrainBounds = new(0f, 0f, TerrainWidth * CellSize, TerrainHeight * CellSize); private static readonly RectF TerrainBounds = new(0f, 0f, TerrainWidth * CellSize, TerrainHeight * CellSize);
private static readonly (string Key, float Cells)[] Objects = /// <summary>Животные из атласа pawn: ключ региона (вид сбоку) + размер в клетках.</summary>
private static readonly (string Key, float Cells)[] Animals =
[ [
("things/building/art/snowman/Snowman_A", 1.8f), ("things/pawn/animal/bear/Bear_east", 1.7f),
("things/building/buildingstyles/rustic/RusticLampStanding", 1.4f), ("things/pawn/animal/deer/DeerFemale_east", 1.4f),
("things/building/buildingstyles/totemic/stool/TotemicStool_east", 1.1f), ("things/pawn/animal/deer/DeerMale_east", 1.6f),
("things/pawn/animal/fox_red/Fox_Red_east", 1.0f),
("things/pawn/animal/hare/Hare_east", 0.7f),
("things/pawn/animal/wildboar/WildBoar_east", 1.2f),
("things/pawn/animal/wolf_timber/Wolf_Timber_east", 1.4f),
("things/pawn/animal/muffalo/Muffalo_east", 1.8f),
("things/pawn/animal/squirrel/Squirrel_east", 0.6f),
]; ];
private static readonly string[] Bushes =
[
"things/plant/bush/BushA",
"things/plant/bush/BushB",
"things/plant/bush/BushC",
];
private const uint AnimalLayer = 0b01;
private const uint ObstacleLayer = 0b10;
private readonly int _seed; private readonly int _seed;
public TerrainScene(int seed) => _seed = seed; public TerrainScene(int seed) => _seed = seed;
@@ -51,74 +69,81 @@ public sealed class TerrainScene : Scene
var terrainAtlas = assets.Load(GameAssets.Atlases.TerrainSurfaces); var terrainAtlas = assets.Load(GameAssets.Atlases.TerrainSurfaces);
var pawnAtlas = assets.Load(GameAssets.Atlases.ThingsPawn); var pawnAtlas = assets.Load(GameAssets.Atlases.ThingsPawn);
var plantAtlas = assets.Load(GameAssets.Atlases.ThingsPlant); var plantAtlas = assets.Load(GameAssets.Atlases.ThingsPlant);
var buildingAtlas = assets.Load(GameAssets.Atlases.ThingsBuilding);
// Тайлсет: вода/песок/горы — тонированная белая текстура, трава и лес — атлас // Тайлсет: вода — тонированная белая текстура, суша — поверхности из атласа
// (две текстуры террейна → весь пол рисуется парой draw call'ов). // (песок, мох под траву, почва под лес, скала под горы).
var tiles = new TileSet(); var tiles = new TileSet();
var tileByKind = new Dictionary<TerrainKind, ushort> var tileByKind = new Dictionary<TerrainKind, ushort>
{ {
[TerrainKind.DeepWater] = tiles.Add(white, new Color(23, 63, 95)), [TerrainKind.DeepWater] = tiles.Add(white, new Color(23, 63, 95)),
[TerrainKind.Water] = tiles.Add(white, new Color(32, 99, 155)), [TerrainKind.Water] = tiles.Add(white, new Color(32, 99, 155)),
[TerrainKind.Sand] = tiles.Add(white, new Color(237, 201, 120)), [TerrainKind.Sand] = tiles.Add(terrainAtlas.GetRegion("terrain/surfaces/sand")),
[TerrainKind.Grass] = tiles.Add(terrainAtlas.GetRegion("terrain/surfaces/Marsh_bright")), [TerrainKind.Grass] = tiles.Add(terrainAtlas.GetRegion("terrain/surfaces/mossy")),
[TerrainKind.Forest] = tiles.Add(terrainAtlas.GetRegion("terrain/surfaces/Marsh")), [TerrainKind.Forest] = tiles.Add(terrainAtlas.GetRegion("terrain/surfaces/soil")),
[TerrainKind.Mountain] = tiles.Add(white, new Color(136, 132, 128)), [TerrainKind.Mountain] = tiles.Add(terrainAtlas.GetRegion("terrain/surfaces/roughhewnrock")),
}; };
// Террейн строится кодом: высоты по сиду → вид клетки → id тайла в гриде. // Террейн строится кодом: высоты по сиду → вид клетки → id тайла в гриде.
// Поверх травы — пучки травы и кусты, поверх леса — деревья (с коллайдером ствола).
var random = new Random(_seed); var random = new Random(_seed);
var heights = WorldGenerator.GenerateHeights(TerrainWidth, TerrainHeight, _seed); var heights = WorldGenerator.GenerateHeights(TerrainWidth, TerrainHeight, _seed);
var grid = new TileGrid(TerrainWidth, TerrainHeight); var grid = new TileGrid(TerrainWidth, TerrainHeight);
var landCells = new List<Point>(); var landCells = new List<Point>();
var grassRegion = plantAtlas.GetRegion("things/plant/grass/grassa");
for (var x = 0; x < TerrainWidth; x++) for (var x = 0; x < TerrainWidth; x++)
{ {
for (var y = 0; y < TerrainHeight; y++) for (var y = 0; y < TerrainHeight; y++)
{ {
var kind = WorldGenerator.Classify(heights[x, y]); var kind = WorldGenerator.Classify(heights[x, y]);
grid[x, y] = tileByKind[kind]; grid[x, y] = tileByKind[kind];
var cell = new Point(x, y);
if (WorldGenerator.IsLand(kind)) if (WorldGenerator.IsLand(kind))
{ {
landCells.Add(new Point(x, y)); landCells.Add(cell);
} }
if (kind == TerrainKind.Forest && random.NextSingle() < 0.3f) switch (kind)
{ {
SpawnOnCell( case TerrainKind.Forest when random.NextSingle() < 0.3f:
plantAtlas.GetRegion( var tree = SpawnOnCell(
"things/plant/" + WorldScene.ForestTrees[random.Next(WorldScene.ForestTrees.Length)]), plantAtlas.GetRegion(
new Point(x, y), random, sizeCells: 2.2f); "things/plant/" + WorldScene.ForestTrees[random.Next(WorldScene.ForestTrees.Length)]),
cell, random, sizeCells: 2.2f);
var trunk = MrGameEng.Collisions.Collider.Circle(CellSize * 0.28f);
trunk.Layer = ObstacleLayer;
trunk.CollidesWith = AnimalLayer;
tree.AddComponent(trunk);
break;
case TerrainKind.Grass when random.NextSingle() < 0.35f:
SpawnOnCell(grassRegion, cell, random, sizeCells: 1.1f + random.NextSingle() * 0.4f);
break;
case TerrainKind.Grass when random.NextSingle() < 0.07f:
SpawnOnCell(plantAtlas.GetRegion(Bushes[random.Next(Bushes.Length)]), cell, random, sizeCells: 1.4f);
break;
} }
} }
} }
Store.CreateEntity(new Tilemap(grid, tiles, CellSize)); 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++) for (var i = 0; i < Population && landCells.Count > 0; i++)
{ {
var cell = landCells[random.Next(landCells.Count)]; var cell = landCells[random.Next(landCells.Count)];
var body = pawnAtlas.GetRegion( var (key, sizeCells) = Animals[random.Next(Animals.Length)];
"things/pawn/humanlike/bodies/" + WorldScene.BeingBodies[random.Next(WorldScene.BeingBodies.Length)] + "_south"); var body = pawnAtlas.GetRegion(key);
var sprite = new Sprite(body, GameLayers.Beings); var sprite = new Sprite(body, GameLayers.Beings);
sprite.CenterOrigin(); sprite.CenterOrigin();
var collider = MrGameEng.Collisions.Collider.Circle(sizeCells * CellSize * 0.32f);
collider.Layer = AnimalLayer;
collider.CollidesWith = AnimalLayer | ObstacleLayer;
Store.CreateEntity( Store.CreateEntity(
new Transform2D( new Transform2D(
new Vector2((cell.X + 0.5f) * CellSize, (cell.Y + 0.5f) * CellSize), new Vector2((cell.X + 0.5f) * CellSize, (cell.Y + 0.5f) * CellSize),
scale: new Vector2(CellSize * 1.6f / body.Width)), scale: new Vector2(CellSize * sizeCells / body.Width)),
sprite, sprite,
new Wander()); new Wander(),
collider);
} }
var camera = Store.CreateEntity(new Camera(TerrainBounds.Center, zoom: 1.6f, bounds: TerrainBounds)); var camera = Store.CreateEntity(new Camera(TerrainBounds.Center, zoom: 1.6f, bounds: TerrainBounds));
@@ -126,7 +151,7 @@ public sealed class TerrainScene : Scene
var desktop = this.UseUI(); var desktop = this.UseUI();
var hudLabel = new Label { Left = 10, Top = 8 }; var hudLabel = new Label { Left = 10, Top = 8 };
desktop.Root = hudLabel; desktop.Root = hudLabel;
hudLabel.Text = $"LittleSim | террейн | seed {_seed}\nWASD — камера, колесо — зум, ` — консоль ('regen', 'world')"; hudLabel.Text = $"LittleSim | террейн | seed {_seed} | животных: {Population}\nWASD — камера, колесо — зум, ` — консоль ('regen', 'world')";
var console = this.UseDevConsole(); var console = this.UseDevConsole();
console.Register("regen", "regen [seed] — regenerate the terrain", (c, args) => console.Register("regen", "regen [seed] — regenerate the terrain", (c, args) =>
@@ -152,17 +177,19 @@ public sealed class TerrainScene : Scene
}); });
UpdateSystems.Add(new WanderSystem(_seed, TerrainBounds)); UpdateSystems.Add(new WanderSystem(_seed, TerrainBounds));
var collisions = this.UseCollisions(cellSize: CellSize * 2f); // после движения
UpdateSystems.Add(new SeparationSystem(collisions));
UpdateSystems.Add(new GodCameraSystem(camera, input, console)); UpdateSystems.Add(new GodCameraSystem(camera, input, console));
Log.Info($"Terrain generated: seed {_seed}, {TerrainWidth}x{TerrainHeight} cells"); Log.Info($"Terrain generated: seed {_seed}, {TerrainWidth}x{TerrainHeight} cells, {Population} animals");
} }
/// <summary>Ставит объект на клетку с лёгким смещением; масштаб задаётся в клетках.</summary> /// <summary>Ставит объект на клетку с лёгким смещением; масштаб задаётся в клетках.</summary>
private void SpawnOnCell(Texture2DRegion region, Point cell, Random random, float sizeCells) private Entity SpawnOnCell(Texture2DRegion region, Point cell, Random random, float sizeCells)
{ {
var sprite = new Sprite(region, GameLayers.Beings); var sprite = new Sprite(region, GameLayers.Beings);
sprite.CenterOrigin(); sprite.CenterOrigin();
Store.CreateEntity( return Store.CreateEntity(
new Transform2D( new Transform2D(
new Vector2( new Vector2(
(cell.X + 0.3f + random.NextSingle() * 0.4f) * CellSize, (cell.X + 0.3f + random.NextSingle() * 0.4f) * CellSize,
+50
View File
@@ -51,6 +51,56 @@ public sealed class WanderSystem(int seed, RectF worldBounds) : QuerySystem<Tran
} }
} }
/// <summary>
/// Расталкивание пересёкшихся круговых коллайдеров по парам кадра: животные не
/// наслаиваются друг на друга и выталкиваются из статичных препятствий (деревьев).
/// Статичность определяется отсутствием компонента <see cref="Wander"/>.
/// Регистрировать после системы коллизий.
/// </summary>
public sealed class SeparationSystem(MrGameEng.Collisions.CollisionWorld world) : BaseSystem
{
protected override void OnUpdateGroup()
{
foreach (var pair in world.Pairs)
{
ref var ta = ref pair.A.GetComponent<Transform2D>();
ref var tb = ref pair.B.GetComponent<Transform2D>();
var sum = pair.A.GetComponent<MrGameEng.Collisions.Collider>().Radius +
pair.B.GetComponent<MrGameEng.Collisions.Collider>().Radius;
var delta = tb.Position - ta.Position;
var distance = delta.Length();
var overlap = sum - distance;
if (overlap <= 0f)
{
continue;
}
var direction = distance > 0.001f ? delta / distance : Vector2.UnitX;
var aStatic = !pair.A.HasComponent<Wander>();
var bStatic = !pair.B.HasComponent<Wander>();
if (aStatic && bStatic)
{
continue;
}
if (aStatic)
{
tb.Position += direction * overlap;
}
else if (bStatic)
{
ta.Position -= direction * overlap;
}
else
{
ta.Position -= direction * (overlap * 0.5f);
tb.Position += direction * (overlap * 0.5f);
}
}
}
}
/// <summary>Камера бога: WASD/стрелки — панорамирование, колесо — зум. Молчит, пока открыта консоль.</summary> /// <summary>Камера бога: WASD/стрелки — панорамирование, колесо — зум. Молчит, пока открыта консоль.</summary>
public sealed class GodCameraSystem( public sealed class GodCameraSystem(
Entity cameraEntity, InputManager input, MrGameEng.DevConsole.DevConsole console) : BaseSystem Entity cameraEntity, InputManager input, MrGameEng.DevConsole.DevConsole console) : BaseSystem