From bc95227e8c37206e28059059d670349dd678ef50 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Thu, 11 Jun 2026 15:17:22 +0300 Subject: [PATCH] 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 --- src/LittleSim/LittleSim.csproj | 2 + src/LittleSim/Program.cs | 2 + src/LittleSim/Scenes/TerrainScene.cs | 103 +++++++++++++++++---------- src/LittleSim/Sim/SimSystems.cs | 50 +++++++++++++ 4 files changed, 119 insertions(+), 38 deletions(-) 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