diff --git a/src/LittleSim/LittleSim.csproj b/src/LittleSim/LittleSim.csproj
index 5861788..5bf4459 100644
--- a/src/LittleSim/LittleSim.csproj
+++ b/src/LittleSim/LittleSim.csproj
@@ -13,6 +13,8 @@
+
+
Животные из атласа pawn: ключ региона (вид сбоку) + размер в клетках.
+ 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.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();
+ 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");
}
/// Ставит объект на клетку с лёгким смещением; масштаб задаётся в клетках.
- 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,
diff --git a/src/LittleSim/Sim/SimSystems.cs b/src/LittleSim/Sim/SimSystems.cs
index 87d31d9..0aa3650 100644
--- a/src/LittleSim/Sim/SimSystems.cs
+++ b/src/LittleSim/Sim/SimSystems.cs
@@ -51,6 +51,56 @@ public sealed class WanderSystem(int seed, RectF worldBounds) : QuerySystem
+/// Расталкивание пересёкшихся круговых коллайдеров по парам кадра: животные не
+/// наслаиваются друг на друга и выталкиваются из статичных препятствий (деревьев).
+/// Статичность определяется отсутствием компонента .
+/// Регистрировать после системы коллизий.
+///
+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();
+ ref var tb = ref pair.B.GetComponent();
+ var sum = pair.A.GetComponent().Radius +
+ pair.B.GetComponent().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();
+ var bStatic = !pair.B.HasComponent();
+ 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);
+ }
+ }
+ }
+}
+
/// Камера бога: WASD/стрелки — панорамирование, колесо — зум. Молчит, пока открыта консоль.
public sealed class GodCameraSystem(
Entity cameraEntity, InputManager input, MrGameEng.DevConsole.DevConsole console) : BaseSystem