CI / build-test (push) Failing after 1m5s
Pathfinding (depends on Core only): GridPathfinder with A* (octile/
Manhattan heuristic), Dijkstra and BFS over a game-implemented
IPathGrid; 4/8 connectivity, diagonals never cut corners. FlowField +
FlowFieldBuilder (multi-source Dijkstra) give crowds O(1) steering per
agent per frame. All buffers are grid-sized once and invalidated by a
generation stamp - repeated queries allocate nothing and clear nothing.
Collisions (Graphics exception: Transform2D, RectF): Collider component
(circle/AABB, offset, two-way layer masks), CollisionWorld - a uniform
spatial hash on flat arrays rebuilt from scratch each tick (O(n) for
movers, zero alloc after warm-up, deterministic pair order), pair
collection, QueryAabb and closest-hit Raycast. scene.UseCollisions()
registers CollisionSystem after movement systems.
30 new tests (string-map mazes, cost weighting, corner cutting, flow
descent; pair/mask/query/raycast). Sample gains a PathfindingScene
('path' console command): click to set the goal, 250 agents follow the
flow field, the A* path is highlighted, colliding agents flash red.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
102 lines
3.2 KiB
C#
102 lines
3.2 KiB
C#
using Microsoft.Xna.Framework;
|
|
using Xunit;
|
|
|
|
namespace MrGameEng.Pathfinding.Tests;
|
|
|
|
public class FlowFieldTests
|
|
{
|
|
[Fact]
|
|
public void Build_DistancesGrowFromGoal_DirectionsDescend()
|
|
{
|
|
var grid = new TestGrid(
|
|
".....",
|
|
".###.",
|
|
".....");
|
|
var builder = new FlowFieldBuilder(grid);
|
|
var field = new FlowField();
|
|
|
|
builder.Build([new Point(0, 0)], field);
|
|
|
|
Assert.Equal(0f, field.DistanceAt(0, 0));
|
|
Assert.True(field.DistanceAt(4, 2) > field.DistanceAt(1, 0));
|
|
|
|
// Из любой достижимой не-целевой клетки направление ведёт к клетке с меньшей дистанцией.
|
|
for (var y = 0; y < grid.Height; y++)
|
|
{
|
|
for (var x = 0; x < grid.Width; x++)
|
|
{
|
|
if (!grid.IsPassable(x, y) || !field.IsReachable(x, y) || field.DistanceAt(x, y) == 0f)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var direction = field.DirectionAt(x, y);
|
|
Assert.NotEqual(Vector2.Zero, direction);
|
|
var nx = x + Math.Sign(MathF.Round(direction.X * 10f));
|
|
var ny = y + Math.Sign(MathF.Round(direction.Y * 10f));
|
|
Assert.True(field.DistanceAt(nx, ny) < field.DistanceAt(x, y),
|
|
$"direction at ({x},{y}) does not descend");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Build_UnreachablePocket_IsFlagged()
|
|
{
|
|
var grid = new TestGrid(
|
|
"..#..",
|
|
"..#..",
|
|
"..#..");
|
|
var builder = new FlowFieldBuilder(grid);
|
|
var field = new FlowField();
|
|
|
|
builder.Build([new Point(0, 1)], field);
|
|
|
|
Assert.False(field.IsReachable(4, 1));
|
|
Assert.Equal(Vector2.Zero, field.DirectionAt(4, 1));
|
|
Assert.True(field.IsReachable(1, 2));
|
|
}
|
|
|
|
[Fact]
|
|
public void Build_MultipleGoals_EachCellFlowsToNearest()
|
|
{
|
|
var grid = new TestGrid("..........");
|
|
var builder = new FlowFieldBuilder(grid);
|
|
var field = new FlowField();
|
|
|
|
builder.Build([new Point(0, 0), new Point(9, 0)], field);
|
|
|
|
Assert.True(field.DirectionAt(2, 0).X < 0f); // ближе к левой цели
|
|
Assert.True(field.DirectionAt(7, 0).X > 0f); // ближе к правой
|
|
Assert.Equal(0f, field.DistanceAt(9, 0));
|
|
}
|
|
|
|
[Fact]
|
|
public void Build_Rebuild_OverwritesPreviousField()
|
|
{
|
|
var grid = new TestGrid(".....");
|
|
var builder = new FlowFieldBuilder(grid);
|
|
var field = new FlowField();
|
|
|
|
builder.Build([new Point(0, 0)], field);
|
|
Assert.True(field.DirectionAt(4, 0).X < 0f);
|
|
|
|
builder.Build([new Point(4, 0)], field);
|
|
Assert.True(field.DirectionAt(0, 0).X > 0f);
|
|
Assert.Equal(0f, field.DistanceAt(4, 0));
|
|
}
|
|
|
|
[Fact]
|
|
public void Build_NoValidGoals_EverythingUnreachable()
|
|
{
|
|
var grid = new TestGrid(".#.");
|
|
var builder = new FlowFieldBuilder(grid);
|
|
var field = new FlowField();
|
|
|
|
builder.Build([new Point(1, 0)], field); // цель — стена
|
|
|
|
Assert.False(field.IsReachable(0, 0));
|
|
Assert.False(field.IsReachable(2, 0));
|
|
}
|
|
}
|