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;
}
}
}
}
}