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.Atlases\MrGameEng.Atlases.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.DevConsole\MrGameEng.DevConsole.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));
|
||||
|
||||
host.Run();
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using LittleSim.Sim;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.Assets;
|
||||
using MrGameEng.Atlases;
|
||||
using MrGameEng.Collisions;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.DevConsole;
|
||||
using MrGameEng.Graphics;
|
||||
@@ -23,17 +24,34 @@ 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 const int Population = 14;
|
||||
|
||||
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/building/buildingstyles/rustic/RusticLampStanding", 1.4f),
|
||||
("things/building/buildingstyles/totemic/stool/TotemicStool_east", 1.1f),
|
||||
("things/pawn/animal/bear/Bear_east", 1.7f),
|
||||
("things/pawn/animal/deer/DeerFemale_east", 1.4f),
|
||||
("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;
|
||||
|
||||
public TerrainScene(int seed) => _seed = seed;
|
||||
@@ -51,74 +69,81 @@ public sealed class TerrainScene : Scene
|
||||
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)),
|
||||
[TerrainKind.Sand] = tiles.Add(terrainAtlas.GetRegion("terrain/surfaces/sand")),
|
||||
[TerrainKind.Grass] = tiles.Add(terrainAtlas.GetRegion("terrain/surfaces/mossy")),
|
||||
[TerrainKind.Forest] = tiles.Add(terrainAtlas.GetRegion("terrain/surfaces/soil")),
|
||||
[TerrainKind.Mountain] = tiles.Add(terrainAtlas.GetRegion("terrain/surfaces/roughhewnrock")),
|
||||
};
|
||||
|
||||
// Террейн строится кодом: высоты по сиду → вид клетки → id тайла в гриде.
|
||||
// Поверх травы — пучки травы и кусты, поверх леса — деревья (с коллайдером ствола).
|
||||
var random = new Random(_seed);
|
||||
var heights = WorldGenerator.GenerateHeights(TerrainWidth, TerrainHeight, _seed);
|
||||
var grid = new TileGrid(TerrainWidth, TerrainHeight);
|
||||
var landCells = new List<Point>();
|
||||
var grassRegion = plantAtlas.GetRegion("things/plant/grass/grassa");
|
||||
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];
|
||||
var cell = new Point(x, y);
|
||||
if (WorldGenerator.IsLand(kind))
|
||||
{
|
||||
landCells.Add(new Point(x, y));
|
||||
landCells.Add(cell);
|
||||
}
|
||||
|
||||
if (kind == TerrainKind.Forest && random.NextSingle() < 0.3f)
|
||||
switch (kind)
|
||||
{
|
||||
SpawnOnCell(
|
||||
plantAtlas.GetRegion(
|
||||
"things/plant/" + WorldScene.ForestTrees[random.Next(WorldScene.ForestTrees.Length)]),
|
||||
new Point(x, y), random, sizeCells: 2.2f);
|
||||
case TerrainKind.Forest when random.NextSingle() < 0.3f:
|
||||
var tree = SpawnOnCell(
|
||||
plantAtlas.GetRegion(
|
||||
"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));
|
||||
|
||||
// Несколько объектов на суше: снеговик, лампа, табурет.
|
||||
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 (key, sizeCells) = Animals[random.Next(Animals.Length)];
|
||||
var body = pawnAtlas.GetRegion(key);
|
||||
var sprite = new Sprite(body, GameLayers.Beings);
|
||||
sprite.CenterOrigin();
|
||||
var collider = MrGameEng.Collisions.Collider.Circle(sizeCells * CellSize * 0.32f);
|
||||
collider.Layer = AnimalLayer;
|
||||
collider.CollidesWith = AnimalLayer | ObstacleLayer;
|
||||
Store.CreateEntity(
|
||||
new Transform2D(
|
||||
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,
|
||||
new Wander());
|
||||
new Wander(),
|
||||
collider);
|
||||
}
|
||||
|
||||
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 hudLabel = new Label { Left = 10, Top = 8 };
|
||||
desktop.Root = hudLabel;
|
||||
hudLabel.Text = $"LittleSim | террейн | seed {_seed}\nWASD — камера, колесо — зум, ` — консоль ('regen', 'world')";
|
||||
hudLabel.Text = $"LittleSim | террейн | seed {_seed} | животных: {Population}\nWASD — камера, колесо — зум, ` — консоль ('regen', 'world')";
|
||||
|
||||
var console = this.UseDevConsole();
|
||||
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));
|
||||
var collisions = this.UseCollisions(cellSize: CellSize * 2f); // после движения
|
||||
UpdateSystems.Add(new SeparationSystem(collisions));
|
||||
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>
|
||||
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);
|
||||
sprite.CenterOrigin();
|
||||
Store.CreateEntity(
|
||||
return Store.CreateEntity(
|
||||
new Transform2D(
|
||||
new Vector2(
|
||||
(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>
|
||||
public sealed class GodCameraSystem(
|
||||
Entity cameraEntity, InputManager input, MrGameEng.DevConsole.DevConsole console) : BaseSystem
|
||||
|
||||
Reference in New Issue
Block a user