Add MrGameEng.Pathfinding and MrGameEng.Collisions modules
CI / build-test (push) Failing after 1m5s
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>
This commit is contained in:
co-authored by
Claude Fable 5
parent
b3415120c3
commit
a395e58458
@@ -0,0 +1,101 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user