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:
co-authored by
Claude Fable 5
parent
35781a21ca
commit
bc95227e8c
@@ -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"
|
||||||
|
|||||||
@@ -13,3 +13,5 @@ using var host = new GameHost(
|
|||||||
new WorldScene(seed: 1337));
|
new WorldScene(seed: 1337));
|
||||||
|
|
||||||
host.Run();
|
host.Run();
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user