using Friflo.Engine.ECS; using Friflo.Engine.ECS.Systems; using Microsoft.Xna.Framework; using MrGameEng.Assets; using MrGameEng.Collisions; using MrGameEng.Core; using MrGameEng.DevConsole; using MrGameEng.Graphics; using MrGameEng.Input; using MrGameEng.Pathfinding; using MrGameEng.UI; namespace MrGameEng.Sample.Scenes; /// /// Демо поиска пути и коллизий: ЛКМ ставит цель — толпа агентов стекается к ней по /// flow field, жёлтым подсвечен A*-путь из левого верхнего угла, столкнувшиеся агенты /// вспыхивают красным. Tab/'main' — назад. /// public sealed class PathfindingScene : Scene { private const int CellsX = 64; private const int CellsY = 36; private const int CellSize = 20; private const int AgentCount = 250; private static readonly Point AStarStart = new(1, 1); private readonly WallGrid _grid = new(CellsX, CellsY, seed: 99); private readonly FlowField _field = new(); private readonly List _path = []; private readonly List _pathMarkers = []; private FlowFieldBuilder _builder = null!; private GridPathfinder _pathfinder = null!; private Texture2DRegion _white = null!; private Entity _goalMarker; /// Состояние агента: время красной вспышки после столкновения. private struct AgentState : IComponent { public float Flash; } protected override void OnLoad() { var assets = Context.Services.GetOrDefault() ?? Context.UseAssets(); var input = this.UseInput(); var actions = SampleInput.CreateActions(input); var renderer = this.UseRenderer2D(new Renderer2DOptions { VirtualResolution = new Point(1280, 720) }); SampleLayers.EnsureRegistered(renderer); var shapesTexture = assets.Load(GameAssets.Textures.Shapes); _white = new Texture2DRegion(shapesTexture, new Rectangle(40, 41, 14, 14)); // жёлтый квадрат как "белый" регион _builder = new FlowFieldBuilder(_grid); _pathfinder = new GridPathfinder(_grid); // Стены. for (var y = 0; y < CellsY; y++) { for (var x = 0; x < CellsX; x++) { if (!_grid.IsPassable(x, y)) { var sprite = new Sprite(_white) { Color = new Color(70, 74, 84), Depth = 0f }; Store.CreateEntity(CellTransform(x, y), sprite); } } } // Агенты на проходимых клетках. var random = new Random(7); for (var i = 0; i < AgentCount; i++) { var cell = RandomPassableCell(random); var sprite = new Sprite(_white) { Color = new Color(120, 180, 255), Depth = 1f }; sprite.CenterOrigin(); Store.CreateEntity( new Transform2D(CellCenter(cell), scale: new Vector2(0.45f)), sprite, new AgentState(), Collider.Circle(4f)); } // Маркер цели. var goalSprite = new Sprite(_white) { Color = new Color(80, 220, 120), Depth = 0.6f }; _goalMarker = Store.CreateEntity(CellTransform(0, 0), goalSprite); var world = this.UseCollisions(cellSize: 16f); UpdateSystems.Add(new ClickTargetSystem(this, input, renderer)); UpdateSystems.Add(new AgentSteerSystem(this)); UpdateSystems.Add(new CollisionFlashSystem(world)); UpdateSystems.Add(new FlashDecaySystem()); UpdateSystems.Add(new SceneHotkeysSystem(Context, actions, () => new MainScene(), Transition.Fade(0.6f))); var desktop = this.UseUI(); var label = new Myra.Graphics2D.UI.Label { Text = "ЛКМ — цель: толпа идёт по flow field, жёлтое — путь A*.\nСтолкновения агентов подсвечиваются красным. Tab — назад.", }; desktop.Root = new Myra.Graphics2D.UI.VerticalStackPanel { Left = 12, Top = 12 }; ((Myra.Graphics2D.UI.VerticalStackPanel)desktop.Root).Widgets.Add(label); this.UseDevConsole(); SetGoal(new Point(CellsX / 2, CellsY / 2)); } private void SetGoal(Point goal) { if (!_grid.IsPassable(goal.X, goal.Y)) { return; } _builder.Build([goal], _field); _goalMarker.GetComponent().Position = CellTopLeft(goal); foreach (var marker in _pathMarkers) { marker.DeleteEntity(); } _pathMarkers.Clear(); if (_pathfinder.FindPath(AStarStart, goal, _path)) { foreach (var cell in _path) { var sprite = new Sprite(_white) { Color = new Color(240, 210, 60) * 0.55f, Depth = 0.5f }; _pathMarkers.Add(Store.CreateEntity(CellTransform(cell.X, cell.Y), sprite)); } } } private static Transform2D CellTransform(int x, int y) => new(CellTopLeft(new Point(x, y)), scale: new Vector2(CellSize / 14f)); private static Vector2 CellTopLeft(Point cell) => new(cell.X * CellSize, cell.Y * CellSize); private static Vector2 CellCenter(Point cell) => new(cell.X * CellSize + CellSize / 2f, cell.Y * CellSize + CellSize / 2f); private Point RandomPassableCell(Random random) { while (true) { var cell = new Point(random.Next(CellsX), random.Next(CellsY)); if (_grid.IsPassable(cell.X, cell.Y)) { return cell; } } } /// Случайные прямоугольные стены; старт A* и центр всегда свободны. private sealed class WallGrid : IPathGrid { private readonly bool[,] _walls; public WallGrid(int width, int height, int seed) { Width = width; Height = height; _walls = new bool[width, height]; var random = new Random(seed); for (var i = 0; i < 70; i++) { var w = random.Next(1, 7); var h = random.Next(1, 7); var x0 = random.Next(width - w); var y0 = random.Next(height - h); for (var y = y0; y < y0 + h; y++) { for (var x = x0; x < x0 + w; x++) { _walls[x, y] = true; } } } for (var y = 0; y < 3; y++) { for (var x = 0; x < 3; x++) { _walls[AStarStart.X + x, AStarStart.Y + y] = false; _walls[width / 2 + x - 1, height / 2 + y - 1] = false; } } } public int Width { get; } public int Height { get; } public bool IsPassable(int x, int y) => !_walls[x, y]; public float Cost(int x, int y) => 1f; } private sealed class ClickTargetSystem(PathfindingScene scene, InputManager input, Renderer2D renderer) : BaseSystem { protected override void OnUpdateGroup() { if (!input.IsMousePressed(MouseButton.Left)) { return; } var world = renderer.ScreenToWorld(input.MousePosition.ToVector2()); var cell = new Point((int)(world.X / CellSize), (int)(world.Y / CellSize)); if (cell.X >= 0 && cell.X < CellsX && cell.Y >= 0 && cell.Y < CellsY) { scene.SetGoal(cell); } } } private sealed class AgentSteerSystem(PathfindingScene scene) : QuerySystem { private const float Speed = 70f; protected override void OnUpdate() { var delta = Tick.deltaTime; var field = scene._field; foreach (var (transforms, _, _) in Query.Chunks) { var t = transforms.Span; for (var i = 0; i < t.Length; i++) { ref var position = ref t[i].Position; var cx = Math.Clamp((int)(position.X / CellSize), 0, CellsX - 1); var cy = Math.Clamp((int)(position.Y / CellSize), 0, CellsY - 1); if (!field.IsReachable(cx, cy) || field.DistanceAt(cx, cy) <= 0.6f) { continue; // недостижимо или уже у цели } position += field.DirectionAt(cx, cy) * (Speed * delta); } } } } private sealed class CollisionFlashSystem(CollisionWorld world) : BaseSystem { protected override void OnUpdateGroup() { foreach (var pair in world.Pairs) { Flash(pair.A); Flash(pair.B); } } private static void Flash(Entity entity) { if (entity.HasComponent()) { entity.GetComponent().Flash = 0.25f; } } } private sealed class FlashDecaySystem : QuerySystem { private static readonly Color Calm = new(120, 180, 255); private static readonly Color Hit = new(235, 70, 60); protected override void OnUpdate() { var delta = Tick.deltaTime; foreach (var (states, sprites, _) in Query.Chunks) { var a = states.Span; var s = sprites.Span; for (var i = 0; i < a.Length; i++) { a[i].Flash = Math.Max(0f, a[i].Flash - delta); s[i].Color = a[i].Flash > 0f ? Hit : Calm; } } } } }