Fix grid system
This commit is contained in:
parent
7a694259cf
commit
5cf70ccf4b
@ -20,11 +20,17 @@ public partial class CampusController : Node2D
|
||||
[Export] public int AssignedTaskChancePercent { get; set; } = 60;
|
||||
[Export] public float RoundDurationSeconds { get; set; } = 30.0f;
|
||||
[Export] public float AgentMoveSpeed { get; set; } = 90.0f;
|
||||
[Export] public float GridCellSize { get; set; } = 8.0f;
|
||||
[Export] public float GridWalkableTolerance { get; set; } = 2.0f;
|
||||
[Export] public bool DebugGridEnabled { get; set; }
|
||||
[Export] public Key DebugGridToggleKey { get; set; } = Key.G;
|
||||
[Export] public bool DebugLogGrid { get; set; }
|
||||
|
||||
private NavigationRegion2D _navigationRegion;
|
||||
private Node2D _studentsRoot;
|
||||
private TopBar _topBar;
|
||||
private RichTextLabel _logLabel;
|
||||
private DebugGridOverlay _debugGridOverlay;
|
||||
private readonly List<Vector2> _coveragePoints = new();
|
||||
private readonly List<CampusBehaviorAgent> _behaviorAgents = new();
|
||||
private readonly CampusBehaviorWorld _behaviorWorld = new();
|
||||
@ -44,10 +50,16 @@ public partial class CampusController : Node2D
|
||||
private bool _navBakeReady = false;
|
||||
private Rid _navBakeMap = new();
|
||||
private uint _navBakeIterationId = 0;
|
||||
private AStarGrid2D _astarGrid;
|
||||
private Rect2I _astarRegion;
|
||||
private int _astarMapIteration;
|
||||
private Rid _astarMap;
|
||||
private const int GridSearchRadius = 6;
|
||||
|
||||
// Called when the node enters the scene tree for the first time.
|
||||
public override void _Ready()
|
||||
{
|
||||
SetProcessUnhandledInput(true);
|
||||
_taskContainer = GetNode<Control>("Task");
|
||||
_logContainer = GetNode<Control>("Log");
|
||||
|
||||
@ -84,6 +96,7 @@ public partial class CampusController : Node2D
|
||||
_studentsRoot = new Node2D { Name = "Students" };
|
||||
AddChild(_studentsRoot);
|
||||
}
|
||||
EnsureDebugGridOverlay();
|
||||
|
||||
// 使用可视化轮廓重新烘焙导航多边形,确保洞被正确识别
|
||||
RebakeNavigationPolygonFromOutlines();
|
||||
@ -98,6 +111,26 @@ public partial class CampusController : Node2D
|
||||
TrySpawnStudents();
|
||||
UpdateRoundTimer((float)delta);
|
||||
UpdateBehaviorAgents((float)delta);
|
||||
if (DebugGridEnabled)
|
||||
{
|
||||
RequestDebugGridRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
public override void _UnhandledInput(InputEvent @event)
|
||||
{
|
||||
if (@event is not InputEventKey keyEvent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!keyEvent.Pressed || keyEvent.Echo || keyEvent.Keycode != DebugGridToggleKey)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DebugGridEnabled = !DebugGridEnabled;
|
||||
RequestDebugGridRedraw();
|
||||
}
|
||||
|
||||
private void InitializeBehaviorAssets()
|
||||
@ -359,6 +392,7 @@ public partial class CampusController : Node2D
|
||||
_studentsRoot.AddChild(student);
|
||||
student.SetNavigationMap(map);
|
||||
student.MoveSpeed = AgentMoveSpeed;
|
||||
student.GridWalkableTolerance = GridWalkableTolerance;
|
||||
|
||||
// 随机放置在可行走区域,并交给行为系统控制
|
||||
var randomIndex = _random != null
|
||||
@ -590,6 +624,11 @@ public partial class CampusController : Node2D
|
||||
|
||||
private Rid GetNavigationMap()
|
||||
{
|
||||
if (_navigationRegion == null)
|
||||
{
|
||||
return new Rid();
|
||||
}
|
||||
|
||||
var map = NavigationServer2D.RegionGetMap(_navigationRegion.GetRid());
|
||||
if (!map.IsValid)
|
||||
{
|
||||
@ -602,6 +641,292 @@ public partial class CampusController : Node2D
|
||||
return map;
|
||||
}
|
||||
|
||||
public List<Vector2> RequestGridPath(Vector2 start, Vector2 target)
|
||||
{
|
||||
if (!EnsureAStarGrid())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var startCell = WorldToGrid(start);
|
||||
var targetCell = WorldToGrid(target);
|
||||
if (!IsCellInBounds(startCell) || !IsCellInBounds(targetCell))
|
||||
{
|
||||
if (DebugLogGrid)
|
||||
{
|
||||
GD.Print($"[AStarGrid] out_of_bounds start={startCell} target={targetCell} region={_astarRegion}");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
startCell = FindNearestOpenCell(startCell, GridSearchRadius);
|
||||
targetCell = FindNearestOpenCell(targetCell, GridSearchRadius);
|
||||
if (!IsCellInBounds(startCell) || !IsCellInBounds(targetCell))
|
||||
{
|
||||
if (DebugLogGrid)
|
||||
{
|
||||
GD.Print($"[AStarGrid] no_open_cell start={startCell} target={targetCell}");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_astarGrid.IsPointSolid(startCell) || _astarGrid.IsPointSolid(targetCell))
|
||||
{
|
||||
if (DebugLogGrid)
|
||||
{
|
||||
GD.Print($"[AStarGrid] solid start={startCell} target={targetCell}");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
var path = _astarGrid.GetIdPath(startCell, targetCell);
|
||||
if (path.Count == 0)
|
||||
{
|
||||
if (DebugLogGrid)
|
||||
{
|
||||
GD.Print($"[AStarGrid] empty_path start={startCell} target={targetCell}");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = new List<Vector2>(path.Count);
|
||||
foreach (Vector2I cell in path)
|
||||
{
|
||||
result.Add(GridToWorld(cell));
|
||||
}
|
||||
|
||||
if (result.Count > 0)
|
||||
{
|
||||
result.RemoveAt(0);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private bool EnsureAStarGrid()
|
||||
{
|
||||
if (!IsNavigationMapReady(out var map))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var region = BuildGridRegion();
|
||||
if (region.Size.X <= 0 || region.Size.Y <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var cellSize = new Vector2(Mathf.Max(1.0f, GridCellSize), Mathf.Max(1.0f, GridCellSize));
|
||||
var mapIteration = (int)NavigationServer2D.MapGetIterationId(map);
|
||||
var needsRebuild = _astarGrid == null
|
||||
|| _astarMapIteration != mapIteration
|
||||
|| _astarMap != map
|
||||
|| _astarRegion.Position != region.Position
|
||||
|| _astarRegion.Size != region.Size
|
||||
|| _astarGrid.CellSize != cellSize;
|
||||
|
||||
if (!needsRebuild)
|
||||
{
|
||||
return _astarGrid != null;
|
||||
}
|
||||
|
||||
_astarGrid ??= new AStarGrid2D();
|
||||
_astarGrid.Region = region;
|
||||
_astarGrid.CellSize = cellSize;
|
||||
_astarGrid.Offset = Vector2.Zero;
|
||||
_astarGrid.DiagonalMode = AStarGrid2D.DiagonalModeEnum.Never;
|
||||
_astarGrid.Update();
|
||||
|
||||
for (var x = region.Position.X; x < region.Position.X + region.Size.X; x++)
|
||||
{
|
||||
for (var y = region.Position.Y; y < region.Position.Y + region.Size.Y; y++)
|
||||
{
|
||||
var cell = new Vector2I(x, y);
|
||||
var center = GridToWorld(cell);
|
||||
var walkable = IsCellWalkable(center, map);
|
||||
_astarGrid.SetPointSolid(cell, !walkable);
|
||||
}
|
||||
}
|
||||
|
||||
_astarRegion = region;
|
||||
_astarMapIteration = mapIteration;
|
||||
_astarMap = map;
|
||||
if (DebugLogGrid)
|
||||
{
|
||||
GD.Print($"[AStarGrid] region={region} cell={cellSize}");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsNavigationMapReady(out Rid map)
|
||||
{
|
||||
map = GetNavigationMap();
|
||||
return map.IsValid && NavigationServer2D.MapGetIterationId(map) > 0;
|
||||
}
|
||||
|
||||
private Rect2I BuildGridRegion()
|
||||
{
|
||||
var bounds = BuildWorldBounds();
|
||||
var cell = Mathf.Max(1.0f, GridCellSize);
|
||||
var minX = Mathf.FloorToInt(bounds.Position.X / cell);
|
||||
var minY = Mathf.FloorToInt(bounds.Position.Y / cell);
|
||||
var maxX = Mathf.CeilToInt((bounds.Position.X + bounds.Size.X) / cell);
|
||||
var maxY = Mathf.CeilToInt((bounds.Position.Y + bounds.Size.Y) / cell);
|
||||
var sizeX = Math.Max(1, maxX - minX);
|
||||
var sizeY = Math.Max(1, maxY - minY);
|
||||
return new Rect2I(new Vector2I(minX, minY), new Vector2I(sizeX, sizeY));
|
||||
}
|
||||
|
||||
private Rect2 BuildWorldBounds()
|
||||
{
|
||||
var viewport = GetViewportRect();
|
||||
var bounds = new Rect2(viewport.Position, viewport.Size);
|
||||
|
||||
if (_navigationRegion == null || _navigationRegion.NavigationPolygon == null)
|
||||
{
|
||||
return bounds;
|
||||
}
|
||||
|
||||
var polygon = _navigationRegion.NavigationPolygon;
|
||||
var outlineCount = polygon.GetOutlineCount();
|
||||
if (outlineCount == 0)
|
||||
{
|
||||
return bounds;
|
||||
}
|
||||
|
||||
var min = Vector2.Zero;
|
||||
var max = Vector2.Zero;
|
||||
var hasPoint = false;
|
||||
|
||||
for (var i = 0; i < outlineCount; i++)
|
||||
{
|
||||
var outline = polygon.GetOutline(i);
|
||||
for (var j = 0; j < outline.Length; j++)
|
||||
{
|
||||
var world = _navigationRegion.ToGlobal(outline[j]);
|
||||
if (!hasPoint)
|
||||
{
|
||||
min = world;
|
||||
max = world;
|
||||
hasPoint = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
min = new Vector2(Mathf.Min(min.X, world.X), Mathf.Min(min.Y, world.Y));
|
||||
max = new Vector2(Mathf.Max(max.X, world.X), Mathf.Max(max.Y, world.Y));
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasPoint)
|
||||
{
|
||||
return bounds;
|
||||
}
|
||||
|
||||
var navBounds = new Rect2(min, max - min);
|
||||
return bounds.Merge(navBounds);
|
||||
}
|
||||
|
||||
private bool IsCellInRegion(Vector2I cell)
|
||||
{
|
||||
return cell.X >= _astarRegion.Position.X
|
||||
&& cell.X < _astarRegion.Position.X + _astarRegion.Size.X
|
||||
&& cell.Y >= _astarRegion.Position.Y
|
||||
&& cell.Y < _astarRegion.Position.Y + _astarRegion.Size.Y;
|
||||
}
|
||||
|
||||
private bool IsCellInBounds(Vector2I cell)
|
||||
{
|
||||
if (_astarGrid != null)
|
||||
{
|
||||
return _astarGrid.IsInBounds(cell.X, cell.Y);
|
||||
}
|
||||
|
||||
return IsCellInRegion(cell);
|
||||
}
|
||||
|
||||
private Vector2I WorldToGrid(Vector2 world)
|
||||
{
|
||||
var size = Mathf.Max(1.0f, GridCellSize);
|
||||
return new Vector2I(
|
||||
Mathf.FloorToInt(world.X / size),
|
||||
Mathf.FloorToInt(world.Y / size));
|
||||
}
|
||||
|
||||
private Vector2 GridToWorld(Vector2I grid)
|
||||
{
|
||||
var size = Mathf.Max(1.0f, GridCellSize);
|
||||
return new Vector2((grid.X + 0.5f) * size, (grid.Y + 0.5f) * size);
|
||||
}
|
||||
|
||||
private Vector2I FindNearestOpenCell(Vector2I origin, int radius)
|
||||
{
|
||||
if (_astarGrid == null) return origin;
|
||||
if (IsCellInBounds(origin) && !_astarGrid.IsPointSolid(origin)) return origin;
|
||||
|
||||
for (var r = 1; r <= radius; r++)
|
||||
{
|
||||
for (var dx = -r; dx <= r; dx++)
|
||||
{
|
||||
var dy = r - Math.Abs(dx);
|
||||
var candidateA = new Vector2I(origin.X + dx, origin.Y + dy);
|
||||
if (IsCellInBounds(candidateA) && !_astarGrid.IsPointSolid(candidateA))
|
||||
{
|
||||
return candidateA;
|
||||
}
|
||||
|
||||
if (dy == 0) continue;
|
||||
var candidateB = new Vector2I(origin.X + dx, origin.Y - dy);
|
||||
if (IsCellInBounds(candidateB) && !_astarGrid.IsPointSolid(candidateB))
|
||||
{
|
||||
return candidateB;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return origin;
|
||||
}
|
||||
|
||||
private bool IsWorldWalkable(Vector2 world, Rid map)
|
||||
{
|
||||
if (!map.IsValid) return false;
|
||||
var closest = NavigationServer2D.MapGetClosestPoint(map, world);
|
||||
return closest.DistanceTo(world) <= GetGridTolerance();
|
||||
}
|
||||
|
||||
private bool IsCellWalkable(Vector2 center, Rid map)
|
||||
{
|
||||
if (!IsWorldWalkable(center, map)) return false;
|
||||
|
||||
var half = Mathf.Max(1.0f, GridCellSize * 0.45f);
|
||||
var offsets = new[]
|
||||
{
|
||||
new Vector2(half, 0),
|
||||
new Vector2(-half, 0),
|
||||
new Vector2(0, half),
|
||||
new Vector2(0, -half),
|
||||
new Vector2(half, half),
|
||||
new Vector2(half, -half),
|
||||
new Vector2(-half, half),
|
||||
new Vector2(-half, -half)
|
||||
};
|
||||
|
||||
for (var i = 0; i < offsets.Length; i++)
|
||||
{
|
||||
if (!IsWorldWalkable(center + offsets[i], map))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private float GetGridTolerance()
|
||||
{
|
||||
return Mathf.Max(1.0f, GridWalkableTolerance);
|
||||
}
|
||||
|
||||
private static bool HasNearbyPoint(List<Vector2> points, Vector2 candidate, float minDistance)
|
||||
{
|
||||
for (int i = 0; i < points.Count; i++)
|
||||
@ -645,9 +970,94 @@ public partial class CampusController : Node2D
|
||||
{
|
||||
// 烘焙完成后再刷新导航区域,避免用旧的多边形生成地图
|
||||
_navigationRegion.NavigationPolygon = navPolygon;
|
||||
_astarGrid = null;
|
||||
_astarMap = new Rid();
|
||||
_astarMapIteration = 0;
|
||||
_navBakePending = false;
|
||||
_navBakeReady = true;
|
||||
RequestDebugGridRedraw();
|
||||
}));
|
||||
}
|
||||
|
||||
private void EnsureDebugGridOverlay()
|
||||
{
|
||||
if (_debugGridOverlay != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sprite = GetNodeOrNull<Node2D>("Sprite2D");
|
||||
_debugGridOverlay = new DebugGridOverlay
|
||||
{
|
||||
Name = "DebugGridOverlay",
|
||||
ZIndex = 1
|
||||
};
|
||||
(sprite ?? this).AddChild(_debugGridOverlay);
|
||||
_debugGridOverlay.CampusOwner = this;
|
||||
_debugGridOverlay.Visible = DebugGridEnabled;
|
||||
}
|
||||
|
||||
private void RequestDebugGridRedraw()
|
||||
{
|
||||
if (_debugGridOverlay == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_debugGridOverlay.Visible = DebugGridEnabled;
|
||||
if (DebugGridEnabled)
|
||||
{
|
||||
_debugGridOverlay.QueueRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawDebugGrid(Node2D canvas)
|
||||
{
|
||||
if (!DebugGridEnabled || canvas == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EnsureAStarGrid())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var cellSize = Mathf.Max(1.0f, GridCellSize);
|
||||
var half = new Vector2(cellSize * 0.5f, cellSize * 0.5f);
|
||||
var solidFill = new Color(1.0f, 0.3f, 0.3f, 0.25f);
|
||||
var solidOutline = new Color(1.0f, 0.3f, 0.3f, 0.6f);
|
||||
var walkFill = new Color(0.2f, 0.8f, 0.3f, 0.08f);
|
||||
var walkOutline = new Color(0.2f, 0.8f, 0.3f, 0.2f);
|
||||
|
||||
var minX = _astarRegion.Position.X;
|
||||
var maxX = _astarRegion.Position.X + _astarRegion.Size.X - 1;
|
||||
var minY = _astarRegion.Position.Y;
|
||||
var maxY = _astarRegion.Position.Y + _astarRegion.Size.Y - 1;
|
||||
|
||||
for (var x = minX; x <= maxX; x++)
|
||||
{
|
||||
for (var y = minY; y <= maxY; y++)
|
||||
{
|
||||
var cell = new Vector2I(x, y);
|
||||
var isSolid = _astarGrid.IsPointSolid(cell);
|
||||
var worldCenter = new Vector2((cell.X + 0.5f) * cellSize, (cell.Y + 0.5f) * cellSize);
|
||||
var localTopLeft = canvas.ToLocal(worldCenter) - half;
|
||||
var rect = new Rect2(localTopLeft, new Vector2(cellSize, cellSize));
|
||||
canvas.DrawRect(rect, isSolid ? solidFill : walkFill);
|
||||
canvas.DrawRect(rect, isSolid ? solidOutline : walkOutline, false, 1.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed partial class DebugGridOverlay : Node2D
|
||||
{
|
||||
public CampusController CampusOwner { get; set; }
|
||||
|
||||
public override void _Draw()
|
||||
{
|
||||
CampusOwner?.DrawDebugGrid(this);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -7,9 +7,9 @@
|
||||
[ext_resource type="PackedScene" uid="uid://drmjsqoy8htc8" path="res://scenes/ui-elements/task_list.tscn" id="3_4gjr3"]
|
||||
|
||||
[sub_resource type="NavigationPolygon" id="NavigationPolygon_8u8vn"]
|
||||
vertices = PackedVector2Array(956, 540, 4, 540, 420, 516, 460, 516, 380, 516, 204, 356, 204, 260, 228, 284, 228, 468, 4, 500, 100, 500, 380, 468, 300, 468, 148, 212, 148, 196, 156, 196, 156, 212, 244, 212, 244, 20, 268, 20, 268, 236, 436, 236, 436, 196, 444, 196, 444, 236, 612, 236, 612, 20, 636, 20, 636, 244, 708, 244, 708, 196, 732, 196, 732, 172, 756, 172, 756, 196, 772, 196, 772, 20, 796, 20, 796, 228, 844, 228, 844, 196, 852, 196, 852, 228, 860, 228, 860, 284, 828, 284, 804, 284, 932, 308, 932, 180, 956, 180, 956, 332, 828, 332, 828, 308, 828, 516, 804, 516, 572, 284, 572, 300, 548, 300, 548, 284, 956, 516, 116, 364, 124, 364, 124, 396, 196, 396, 196, 356, 180, 436, 180, 412, 140, 412, 292, 468, 292, 452, 300, 452, 140, 436, 4, 284, 4, 4, 28, 4, 28, 212, 100, 284, 116, 260, 460, 284, 420, 284)
|
||||
polygons = Array[PackedInt32Array]([PackedInt32Array(0, 1, 2, 3), PackedInt32Array(4, 2, 1), PackedInt32Array(5, 6, 7, 8), PackedInt32Array(4, 1, 9, 10), PackedInt32Array(11, 4, 10, 12), PackedInt32Array(13, 14, 15, 16), PackedInt32Array(17, 18, 19, 20), PackedInt32Array(21, 22, 23, 24), PackedInt32Array(25, 26, 27, 28), PackedInt32Array(29, 30, 31), PackedInt32Array(31, 32, 33, 34), PackedInt32Array(35, 36, 37, 38), PackedInt32Array(39, 40, 41, 42), PackedInt32Array(42, 43, 44, 45), PackedInt32Array(39, 42, 45), PackedInt32Array(38, 39, 45, 46), PackedInt32Array(47, 48, 49, 50), PackedInt32Array(47, 50, 51, 52), PackedInt32Array(51, 53, 54, 46), PackedInt32Array(52, 51, 46), PackedInt32Array(45, 52, 46), PackedInt32Array(38, 46, 55, 29), PackedInt32Array(55, 56, 57, 58), PackedInt32Array(53, 59, 0), PackedInt32Array(60, 61, 62, 10), PackedInt32Array(63, 64, 5, 8), PackedInt32Array(63, 8, 65, 66), PackedInt32Array(62, 63, 66, 67), PackedInt32Array(68, 69, 70, 12), PackedInt32Array(68, 12, 10), PackedInt32Array(8, 68, 10), PackedInt32Array(65, 8, 10, 71), PackedInt32Array(72, 73, 74, 75), PackedInt32Array(76, 72, 75, 77), PackedInt32Array(29, 31, 34, 38), PackedInt32Array(34, 35, 38), PackedInt32Array(28, 29, 55), PackedInt32Array(24, 25, 28, 55, 58), PackedInt32Array(24, 58, 78, 21), PackedInt32Array(20, 21, 78, 79, 6), PackedInt32Array(62, 67, 71, 10), PackedInt32Array(16, 17, 20, 6), PackedInt32Array(79, 7, 6), PackedInt32Array(60, 10, 76, 77), PackedInt32Array(77, 75, 13, 6), PackedInt32Array(6, 13, 16), PackedInt32Array(79, 78, 3, 2), PackedInt32Array(0, 3, 54), PackedInt32Array(0, 54, 53)])
|
||||
outlines = Array[PackedVector2Array]([PackedVector2Array(0, 0, 32, 0, 32, 208, 144, 208, 144, 192, 160, 192, 160, 208, 240, 208, 240, 16, 272, 16, 272, 232, 432, 232, 432, 192, 448, 192, 448, 232, 608, 232, 608, 16, 640, 16, 640, 240, 704, 240, 704, 192, 728, 192, 728, 168, 760, 168, 760, 192, 768, 192, 768, 16, 800, 16, 800, 224, 832, 224, 840, 224, 840, 192, 856, 192, 856, 224, 864, 224, 864, 288, 832, 288, 832, 304, 912, 304, 928, 304, 928, 288, 928, 176, 960, 176, 960, 336, 832, 336, 832, 512, 960, 512, 960, 544, 0, 544, 0, 496, 96, 496, 96, 288, 0, 288), PackedVector2Array(144, 432, 176, 432, 176, 416, 144, 416), PackedVector2Array(128, 392, 192, 392, 192, 352, 200, 352, 200, 264, 120, 264, 120, 360, 128, 360), PackedVector2Array(232, 304, 232, 464, 288, 464, 288, 448, 304, 448, 304, 464, 384, 464, 384, 512, 416, 512, 416, 288, 232, 288), PackedVector2Array(464, 288, 464, 512, 800, 512, 800, 288, 576, 288, 576, 304, 544, 304, 544, 288), PackedVector2Array(144, 464, 176, 464, 176, 480, 144, 480)])
|
||||
vertices = PackedVector2Array(956, 540, 4, 540, 420, 516, 460, 516, 380, 516, 204, 356, 204, 260, 228, 284, 228, 468, 4, 500, 100, 500, 380, 468, 308, 468, 132, 212, 132, 180, 172, 180, 172, 212, 244, 212, 244, 60, 268, 60, 268, 236, 428, 236, 428, 172, 452, 172, 452, 236, 612, 236, 612, 60, 636, 60, 636, 244, 708, 244, 708, 196, 732, 196, 732, 140, 756, 140, 756, 196, 772, 196, 772, 60, 796, 60, 796, 228, 836, 228, 836, 180, 860, 180, 860, 284, 828, 284, 932, 308, 932, 180, 956, 180, 956, 332, 828, 332, 828, 308, 828, 516, 804, 516, 804, 284, 572, 284, 572, 300, 548, 300, 548, 284, 956, 516, 116, 364, 124, 364, 124, 396, 196, 396, 196, 356, 180, 436, 180, 412, 140, 412, 284, 468, 284, 436, 308, 436, 140, 436, 4, 284, 4, 60, 28, 60, 28, 212, 100, 284, 116, 260, 460, 284, 420, 284)
|
||||
polygons = Array[PackedInt32Array]([PackedInt32Array(0, 1, 2, 3), PackedInt32Array(4, 2, 1), PackedInt32Array(5, 6, 7, 8), PackedInt32Array(4, 1, 9, 10), PackedInt32Array(11, 4, 10, 12), PackedInt32Array(13, 14, 15, 16), PackedInt32Array(17, 18, 19, 20), PackedInt32Array(21, 22, 23, 24), PackedInt32Array(25, 26, 27, 28), PackedInt32Array(29, 30, 31), PackedInt32Array(31, 32, 33, 34), PackedInt32Array(35, 36, 37, 38), PackedInt32Array(39, 40, 41, 42), PackedInt32Array(39, 42, 43, 38), PackedInt32Array(44, 45, 46, 47), PackedInt32Array(44, 47, 48, 49), PackedInt32Array(48, 50, 51, 52), PackedInt32Array(49, 48, 52), PackedInt32Array(43, 49, 52, 38), PackedInt32Array(38, 52, 53, 29), PackedInt32Array(53, 54, 55, 56), PackedInt32Array(50, 57, 0), PackedInt32Array(58, 59, 60, 10), PackedInt32Array(61, 62, 5, 8), PackedInt32Array(61, 8, 63, 64), PackedInt32Array(60, 61, 64, 65), PackedInt32Array(66, 67, 68, 12), PackedInt32Array(66, 12, 10), PackedInt32Array(8, 66, 10), PackedInt32Array(63, 8, 10, 69), PackedInt32Array(70, 71, 72, 73), PackedInt32Array(74, 70, 73, 13, 75), PackedInt32Array(29, 31, 34, 38), PackedInt32Array(34, 35, 38), PackedInt32Array(28, 29, 53), PackedInt32Array(24, 25, 28, 53, 56), PackedInt32Array(24, 56, 76, 21), PackedInt32Array(20, 21, 76, 77, 6), PackedInt32Array(60, 65, 69, 10), PackedInt32Array(16, 17, 20, 6), PackedInt32Array(77, 7, 6), PackedInt32Array(58, 10, 74, 75), PackedInt32Array(6, 75, 13, 16), PackedInt32Array(77, 76, 3, 2), PackedInt32Array(0, 3, 51), PackedInt32Array(0, 51, 50)])
|
||||
outlines = Array[PackedVector2Array]([PackedVector2Array(0, 56, 32, 56, 32, 208, 128, 208, 128, 176, 176, 176, 176, 208, 240, 208, 240, 56, 272, 56, 272, 232, 424, 232, 424, 168, 456, 168, 456, 232, 608, 232, 608, 56, 640, 56, 640, 240, 704, 240, 704, 192, 728, 192, 728, 136, 760, 136, 760, 192, 768, 192, 768, 56, 800, 56, 800, 224, 832, 224, 832, 224, 832, 176, 864, 176, 864, 224, 864, 224, 864, 288, 832, 288, 832, 304, 912, 304, 928, 304, 928, 288, 928, 176, 960, 176, 960, 336, 832, 336, 832, 512, 960, 512, 960, 544, 0, 544, 0, 496, 96, 496, 96, 288, 0, 288), PackedVector2Array(144, 432, 176, 432, 176, 416, 144, 416), PackedVector2Array(128, 392, 192, 392, 192, 352, 200, 352, 200, 264, 120, 264, 120, 360, 128, 360), PackedVector2Array(232, 304, 232, 464, 280, 464, 280, 432, 312, 432, 312, 464, 384, 464, 384, 512, 416, 512, 416, 288, 232, 288), PackedVector2Array(464, 288, 464, 512, 800, 512, 800, 288, 576, 288, 576, 304, 544, 304, 544, 288), PackedVector2Array(144, 464, 176, 464, 176, 480, 144, 480)])
|
||||
parsed_collision_mask = 4294967294
|
||||
agent_radius = 4.0
|
||||
|
||||
|
||||
@ -135,25 +135,13 @@ public partial class CampusStudent : CharacterBody2D
|
||||
/// </summary>
|
||||
private bool _gridPathPending;
|
||||
/// <summary>
|
||||
/// AStar网格
|
||||
/// </summary>
|
||||
private AStarGrid2D _astarGrid;
|
||||
/// <summary>
|
||||
/// AStar区域
|
||||
/// </summary>
|
||||
private Rect2I _astarRegion;
|
||||
/// <summary>
|
||||
/// AStar地图迭代版本
|
||||
/// </summary>
|
||||
private int _astarMapIteration;
|
||||
/// <summary>
|
||||
/// 网格重新寻路重试计时器
|
||||
/// </summary>
|
||||
private float _gridPathRetryTimer;
|
||||
/// <summary>
|
||||
/// 导航区域引用
|
||||
/// 校园控制器
|
||||
/// </summary>
|
||||
private NavigationRegion2D _navigationRegion;
|
||||
private CampusController _campusController;
|
||||
|
||||
/// <summary>
|
||||
/// 移动速度
|
||||
@ -188,42 +176,18 @@ public partial class CampusStudent : CharacterBody2D
|
||||
/// </summary>
|
||||
[Export] public bool UseGridPathfinding { get; set; } = true;
|
||||
/// <summary>
|
||||
/// 网格单元大小
|
||||
/// </summary>
|
||||
[Export] public float GridCellSize { get; set; } = 8.0f;
|
||||
/// <summary>
|
||||
/// 网格可行走容差
|
||||
/// </summary>
|
||||
[Export] public float GridWalkableTolerance { get; set; } = 2.0f;
|
||||
/// <summary>
|
||||
/// 网格搜索节点限制
|
||||
/// </summary>
|
||||
[Export] public int GridSearchNodeLimit { get; set; } = 8000;
|
||||
/// <summary>
|
||||
/// 网格重新寻路间隔
|
||||
/// </summary>
|
||||
[Export] public float GridRepathInterval { get; set; } = 0.25f;
|
||||
/// <summary>
|
||||
/// 调试绘制网格
|
||||
/// </summary>
|
||||
[Export] public bool DebugDrawGrid { get; set; }
|
||||
/// <summary>
|
||||
/// 调试仅绘制实心点
|
||||
/// </summary>
|
||||
[Export] public bool DebugDrawSolidOnly { get; set; } = true;
|
||||
/// <summary>
|
||||
/// 调试绘制路径
|
||||
/// </summary>
|
||||
[Export] public bool DebugDrawPath { get; set; }
|
||||
/// <summary>
|
||||
/// 调试绘制半径单元数
|
||||
/// </summary>
|
||||
[Export] public int DebugDrawRadiusCells { get; set; } = 20;
|
||||
/// <summary>
|
||||
/// 调试日志网格
|
||||
/// </summary>
|
||||
[Export] public bool DebugLogGrid { get; set; }
|
||||
/// <summary>
|
||||
/// 环境碰撞掩码
|
||||
/// </summary>
|
||||
[Export] public uint EnvironmentCollisionMask { get; set; } = 1u;
|
||||
@ -239,7 +203,7 @@ public partial class CampusStudent : CharacterBody2D
|
||||
{
|
||||
_navigationAgent = GetNodeOrNull<NavigationAgent2D>("NavigationAgent2D");
|
||||
_animationPlayer = GetNodeOrNull<AnimationPlayer>("AnimationPlayer");
|
||||
_navigationRegion = FindNavigationRegion();
|
||||
_campusController = GetTree()?.CurrentScene as CampusController;
|
||||
CacheSprites();
|
||||
ConfigureCollision();
|
||||
|
||||
@ -276,14 +240,10 @@ public partial class CampusStudent : CharacterBody2D
|
||||
public override void _PhysicsProcess(double delta)
|
||||
{
|
||||
_lastDelta = delta;
|
||||
if (DebugDrawGrid || DebugDrawPath)
|
||||
if (DebugDrawPath)
|
||||
{
|
||||
QueueRedraw();
|
||||
}
|
||||
if ((DebugDrawGrid || DebugDrawPath) && _astarGrid == null)
|
||||
{
|
||||
EnsureAStarGrid();
|
||||
}
|
||||
if (_gridPathRetryTimer > 0.0f)
|
||||
{
|
||||
_gridPathRetryTimer = Mathf.Max(0.0f, _gridPathRetryTimer - (float)delta);
|
||||
@ -396,48 +356,9 @@ public partial class CampusStudent : CharacterBody2D
|
||||
/// </summary>
|
||||
public override void _Draw()
|
||||
{
|
||||
if (!DebugDrawGrid && !DebugDrawPath) return;
|
||||
if (_astarGrid == null || GridCellSize <= 0.0f) return;
|
||||
if (!DebugDrawPath) return;
|
||||
|
||||
var cellSize = Mathf.Max(1.0f, GridCellSize);
|
||||
var half = new Vector2(cellSize * 0.5f, cellSize * 0.5f);
|
||||
|
||||
if (DebugDrawGrid)
|
||||
{
|
||||
var radius = Mathf.Max(1, DebugDrawRadiusCells);
|
||||
var originCell = WorldToGrid(GlobalPosition);
|
||||
var minX = Math.Max(_astarRegion.Position.X, originCell.X - radius);
|
||||
var maxX = Math.Min(_astarRegion.Position.X + _astarRegion.Size.X - 1, originCell.X + radius);
|
||||
var minY = Math.Max(_astarRegion.Position.Y, originCell.Y - radius);
|
||||
var maxY = Math.Min(_astarRegion.Position.Y + _astarRegion.Size.Y - 1, originCell.Y + radius);
|
||||
|
||||
var solidFill = new Color(1.0f, 0.3f, 0.3f, 0.25f);
|
||||
var solidOutline = new Color(1.0f, 0.3f, 0.3f, 0.6f);
|
||||
var walkFill = new Color(0.2f, 0.8f, 0.3f, 0.08f);
|
||||
var walkOutline = new Color(0.2f, 0.8f, 0.3f, 0.2f);
|
||||
|
||||
for (var x = minX; x <= maxX; x++)
|
||||
{
|
||||
for (var y = minY; y <= maxY; y++)
|
||||
{
|
||||
var cell = new Vector2I(x, y);
|
||||
var isSolid = _astarGrid.IsPointSolid(cell);
|
||||
if (DebugDrawSolidOnly && !isSolid)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var worldCenter = GridToWorld(cell);
|
||||
// Draw around the cell center to verify path alignment.
|
||||
var localTopLeft = ToLocal(worldCenter) - half;
|
||||
var rect = new Rect2(localTopLeft, new Vector2(cellSize, cellSize));
|
||||
DrawRect(rect, isSolid ? solidFill : walkFill);
|
||||
DrawRect(rect, isSolid ? solidOutline : walkOutline, false, 1.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (DebugDrawPath && _gridPath.Count > 0 && _gridPathIndex < _gridPath.Count)
|
||||
if (_gridPath.Count > 0 && _gridPathIndex < _gridPath.Count)
|
||||
{
|
||||
var pathColor = new Color(0.2f, 0.7f, 1.0f, 0.9f);
|
||||
var current = ToLocal(GlobalPosition);
|
||||
@ -602,10 +523,6 @@ public partial class CampusStudent : CharacterBody2D
|
||||
{
|
||||
// 由校园控制器传入导航地图,供本地边界夹紧使用
|
||||
_navigationMap = map;
|
||||
_astarGrid = null;
|
||||
_astarRegion = new Rect2I();
|
||||
_astarMapIteration = 0;
|
||||
_navigationRegion = FindNavigationRegion();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -772,7 +689,7 @@ public partial class CampusStudent : CharacterBody2D
|
||||
_gridPathIndex = 0;
|
||||
_gridPathActive = true;
|
||||
_gridPathPending = false;
|
||||
if (DebugDrawGrid || DebugDrawPath)
|
||||
if (DebugDrawPath)
|
||||
{
|
||||
QueueRedraw();
|
||||
}
|
||||
@ -1029,71 +946,19 @@ public partial class CampusStudent : CharacterBody2D
|
||||
/// <returns>路径点列表</returns>
|
||||
private List<Vector2> BuildGridPath(Vector2 start, Vector2 target)
|
||||
{
|
||||
if (!IsNavigationMapReady() || GridCellSize <= 0.0f)
|
||||
if (_campusController == null)
|
||||
{
|
||||
_campusController = GetTree()?.CurrentScene as CampusController;
|
||||
}
|
||||
|
||||
var path = _campusController?.RequestGridPath(start, target);
|
||||
if (path == null || path.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
EnsureAStarGrid();
|
||||
if (_astarGrid == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var startCell = WorldToGrid(start);
|
||||
var targetCell = WorldToGrid(target);
|
||||
if (!IsCellInBounds(startCell) || !IsCellInBounds(targetCell))
|
||||
{
|
||||
if (DebugLogGrid)
|
||||
{
|
||||
GD.Print($"[AStarGrid] out_of_bounds start={startCell} target={targetCell} region={_astarRegion}");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
startCell = FindNearestOpenCell(startCell, 6);
|
||||
targetCell = FindNearestOpenCell(targetCell, 6);
|
||||
if (!IsCellInBounds(startCell) || !IsCellInBounds(targetCell))
|
||||
{
|
||||
if (DebugLogGrid)
|
||||
{
|
||||
GD.Print($"[AStarGrid] no_open_cell start={startCell} target={targetCell}");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_astarGrid.IsPointSolid(startCell) || _astarGrid.IsPointSolid(targetCell))
|
||||
{
|
||||
if (DebugLogGrid)
|
||||
{
|
||||
GD.Print($"[AStarGrid] solid start={startCell} target={targetCell}");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
var path = _astarGrid.GetIdPath(startCell, targetCell);
|
||||
if (path.Count == 0)
|
||||
{
|
||||
if (DebugLogGrid)
|
||||
{
|
||||
GD.Print($"[AStarGrid] empty_path start={startCell} target={targetCell}");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = new List<Vector2>(path.Count);
|
||||
foreach (Vector2I cell in path)
|
||||
{
|
||||
result.Add(GridToWorld(cell));
|
||||
}
|
||||
|
||||
if (result.Count > 0)
|
||||
{
|
||||
result.RemoveAt(0);
|
||||
}
|
||||
|
||||
RemoveImmediateBacktracks(result);
|
||||
return SimplifyGridPath(result);
|
||||
RemoveImmediateBacktracks(path);
|
||||
return SimplifyGridPath(path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -1159,219 +1024,6 @@ public partial class CampusStudent : CharacterBody2D
|
||||
return simplified;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确保AStar网格已建立
|
||||
/// </summary>
|
||||
private void EnsureAStarGrid()
|
||||
{
|
||||
if (!IsNavigationMapReady()) return;
|
||||
|
||||
var region = BuildGridRegion();
|
||||
if (region.Size.X <= 0 || region.Size.Y <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var cellSize = new Vector2(Mathf.Max(1.0f, GridCellSize), Mathf.Max(1.0f, GridCellSize));
|
||||
var mapIteration = (int)NavigationServer2D.MapGetIterationId(_navigationMap);
|
||||
var needsRebuild = _astarGrid == null
|
||||
|| _astarMapIteration != mapIteration
|
||||
|| _astarRegion.Position != region.Position
|
||||
|| _astarRegion.Size != region.Size
|
||||
|| _astarGrid.CellSize != cellSize;
|
||||
|
||||
if (!needsRebuild)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_astarGrid ??= new AStarGrid2D();
|
||||
_astarGrid.Region = region;
|
||||
_astarGrid.CellSize = cellSize;
|
||||
_astarGrid.Offset = Vector2.Zero;
|
||||
_astarGrid.DiagonalMode = AStarGrid2D.DiagonalModeEnum.Never;
|
||||
_astarGrid.Update();
|
||||
|
||||
for (var x = region.Position.X; x < region.Position.X + region.Size.X; x++)
|
||||
{
|
||||
for (var y = region.Position.Y; y < region.Position.Y + region.Size.Y; y++)
|
||||
{
|
||||
var cell = new Vector2I(x, y);
|
||||
var center = GridToWorld(cell);
|
||||
var walkable = IsCellWalkable(center);
|
||||
_astarGrid.SetPointSolid(cell, !walkable);
|
||||
}
|
||||
}
|
||||
|
||||
_astarRegion = region;
|
||||
_astarMapIteration = mapIteration;
|
||||
if (DebugLogGrid)
|
||||
{
|
||||
GD.Print($"[AStarGrid] region={region} cell={cellSize}");
|
||||
}
|
||||
if (DebugDrawGrid || DebugDrawPath)
|
||||
{
|
||||
QueueRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建网格区域
|
||||
/// </summary>
|
||||
/// <returns>网格区域</returns>
|
||||
private Rect2I BuildGridRegion()
|
||||
{
|
||||
var bounds = BuildWorldBounds();
|
||||
var cell = Mathf.Max(1.0f, GridCellSize);
|
||||
var minX = Mathf.FloorToInt(bounds.Position.X / cell);
|
||||
var minY = Mathf.FloorToInt(bounds.Position.Y / cell);
|
||||
var maxX = Mathf.CeilToInt((bounds.Position.X + bounds.Size.X) / cell);
|
||||
var maxY = Mathf.CeilToInt((bounds.Position.Y + bounds.Size.Y) / cell);
|
||||
var sizeX = Math.Max(1, maxX - minX);
|
||||
var sizeY = Math.Max(1, maxY - minY);
|
||||
return new Rect2I(new Vector2I(minX, minY), new Vector2I(sizeX, sizeY));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建世界边界
|
||||
/// </summary>
|
||||
/// <returns>世界边界</returns>
|
||||
private Rect2 BuildWorldBounds()
|
||||
{
|
||||
var viewport = GetViewportRect();
|
||||
var bounds = new Rect2(viewport.Position, viewport.Size);
|
||||
|
||||
var navRegion = _navigationRegion ?? FindNavigationRegion();
|
||||
if (navRegion == null || navRegion.NavigationPolygon == null)
|
||||
{
|
||||
return bounds;
|
||||
}
|
||||
|
||||
var polygon = navRegion.NavigationPolygon;
|
||||
var outlineCount = polygon.GetOutlineCount();
|
||||
if (outlineCount == 0)
|
||||
{
|
||||
return bounds;
|
||||
}
|
||||
|
||||
var min = Vector2.Zero;
|
||||
var max = Vector2.Zero;
|
||||
var hasPoint = false;
|
||||
|
||||
for (var i = 0; i < outlineCount; i++)
|
||||
{
|
||||
var outline = polygon.GetOutline(i);
|
||||
for (var j = 0; j < outline.Length; j++)
|
||||
{
|
||||
var world = navRegion.ToGlobal(outline[j]);
|
||||
if (!hasPoint)
|
||||
{
|
||||
min = world;
|
||||
max = world;
|
||||
hasPoint = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
min = new Vector2(Mathf.Min(min.X, world.X), Mathf.Min(min.Y, world.Y));
|
||||
max = new Vector2(Mathf.Max(max.X, world.X), Mathf.Max(max.Y, world.Y));
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasPoint)
|
||||
{
|
||||
return bounds;
|
||||
}
|
||||
|
||||
var navBounds = new Rect2(min, max - min);
|
||||
return bounds.Merge(navBounds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单元格是否在区域内
|
||||
/// </summary>
|
||||
/// <param name="cell">单元格</param>
|
||||
/// <returns>如果在区域内返回true</returns>
|
||||
private bool IsCellInRegion(Vector2I cell)
|
||||
{
|
||||
return cell.X >= _astarRegion.Position.X
|
||||
&& cell.X < _astarRegion.Position.X + _astarRegion.Size.X
|
||||
&& cell.Y >= _astarRegion.Position.Y
|
||||
&& cell.Y < _astarRegion.Position.Y + _astarRegion.Size.Y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单元格是否在边界内
|
||||
/// </summary>
|
||||
/// <param name="cell">单元格</param>
|
||||
/// <returns>如果在边界内返回true</returns>
|
||||
private bool IsCellInBounds(Vector2I cell)
|
||||
{
|
||||
if (_astarGrid != null)
|
||||
{
|
||||
return _astarGrid.IsInBounds(cell.X, cell.Y);
|
||||
}
|
||||
|
||||
return IsCellInRegion(cell);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 世界坐标转网格坐标
|
||||
/// </summary>
|
||||
/// <param name="world">世界坐标</param>
|
||||
/// <returns>网格坐标</returns>
|
||||
private Vector2I WorldToGrid(Vector2 world)
|
||||
{
|
||||
var size = Mathf.Max(1.0f, GridCellSize);
|
||||
return new Vector2I(
|
||||
Mathf.FloorToInt(world.X / size),
|
||||
Mathf.FloorToInt(world.Y / size));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 网格坐标转世界坐标
|
||||
/// </summary>
|
||||
/// <param name="grid">网格坐标</param>
|
||||
/// <returns>世界坐标</returns>
|
||||
private Vector2 GridToWorld(Vector2I grid)
|
||||
{
|
||||
var size = Mathf.Max(1.0f, GridCellSize);
|
||||
return new Vector2((grid.X + 0.5f) * size, (grid.Y + 0.5f) * size);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找最近的开放单元格
|
||||
/// </summary>
|
||||
/// <param name="origin">起点</param>
|
||||
/// <param name="radius">搜索半径</param>
|
||||
/// <returns>最近的开放单元格</returns>
|
||||
private Vector2I FindNearestOpenCell(Vector2I origin, int radius)
|
||||
{
|
||||
if (_astarGrid == null) return origin;
|
||||
if (IsCellInBounds(origin) && !_astarGrid.IsPointSolid(origin)) return origin;
|
||||
|
||||
for (var r = 1; r <= radius; r++)
|
||||
{
|
||||
for (var dx = -r; dx <= r; dx++)
|
||||
{
|
||||
var dy = r - Math.Abs(dx);
|
||||
var candidateA = new Vector2I(origin.X + dx, origin.Y + dy);
|
||||
if (IsCellInBounds(candidateA) && !_astarGrid.IsPointSolid(candidateA))
|
||||
{
|
||||
return candidateA;
|
||||
}
|
||||
|
||||
if (dy == 0) continue;
|
||||
var candidateB = new Vector2I(origin.X + dx, origin.Y - dy);
|
||||
if (IsCellInBounds(candidateB) && !_astarGrid.IsPointSolid(candidateB))
|
||||
{
|
||||
return candidateB;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return origin;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 世界坐标是否可行走
|
||||
/// </summary>
|
||||
@ -1384,48 +1036,6 @@ public partial class CampusStudent : CharacterBody2D
|
||||
return closest.DistanceTo(world) <= GetGridTolerance();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找导航区域节点
|
||||
/// </summary>
|
||||
/// <returns>导航区域</returns>
|
||||
private NavigationRegion2D FindNavigationRegion()
|
||||
{
|
||||
return GetTree()?.CurrentScene?.FindChild("NavigationRegion2D", true, false) as NavigationRegion2D;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单元格中心是否可行走
|
||||
/// </summary>
|
||||
/// <param name="center">中心点</param>
|
||||
/// <returns>如果可行走返回true</returns>
|
||||
private bool IsCellWalkable(Vector2 center)
|
||||
{
|
||||
if (!IsWorldWalkable(center)) return false;
|
||||
|
||||
var half = Mathf.Max(1.0f, GridCellSize * 0.45f);
|
||||
var offsets = new[]
|
||||
{
|
||||
new Vector2(half, 0),
|
||||
new Vector2(-half, 0),
|
||||
new Vector2(0, half),
|
||||
new Vector2(0, -half),
|
||||
new Vector2(half, half),
|
||||
new Vector2(half, -half),
|
||||
new Vector2(-half, half),
|
||||
new Vector2(-half, -half)
|
||||
};
|
||||
|
||||
for (var i = 0; i < offsets.Length; i++)
|
||||
{
|
||||
if (!IsWorldWalkable(center + offsets[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取网格容差
|
||||
/// </summary>
|
||||
@ -1434,4 +1044,4 @@ public partial class CampusStudent : CharacterBody2D
|
||||
{
|
||||
return Mathf.Max(1.0f, GridWalkableTolerance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
163
specs/numerical_design_system.md
Normal file
163
specs/numerical_design_system.md
Normal file
@ -0,0 +1,163 @@
|
||||
# Numerical Design & Balancing System (NDBS) - Implementation Guide
|
||||
|
||||
**Version:** 2.0 (AI-Actionable)
|
||||
**Target Stack:** Python (FastAPI), Vue.js (Vite), C# (Godot .NET)
|
||||
**Role:** AI Developer Guide
|
||||
|
||||
## 1. System Overview
|
||||
The NDBS is a local web application enabling game designers to:
|
||||
1. **Edit Game Data:** Modify JSON files in `resources/definitions/` (Traits, Archetypes, Items) via a GUI.
|
||||
2. **Script Logic:** Write C# code for `RuleIds` (special game logic) in a browser-based IDE. The system auto-generates the corresponding `.cs` files in `scripts/Rules/Generated/`.
|
||||
|
||||
This guide is structured as a sequence of tasks for an AI agent to implement the system incrementally.
|
||||
|
||||
---
|
||||
|
||||
## 2. Directory Structure Plan
|
||||
The tool will reside in a new `tools/ndbs/` directory, keeping it separate from the game assets but with access to them.
|
||||
|
||||
```text
|
||||
D:\code\super-mentor\
|
||||
├── resources\definitions\ <-- Target Data Source
|
||||
├── scripts\Rules\Generated\ <-- Target Script Output
|
||||
├── tools\
|
||||
│ └── ndbs\
|
||||
│ ├── server\ <-- Python Backend
|
||||
│ │ ├── main.py
|
||||
│ │ └── ...
|
||||
│ └── client\ <-- Vue Frontend
|
||||
│ ├── src\
|
||||
│ └── ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementation Phases (AI Prompts)
|
||||
|
||||
### Phase 1: Python Backend Foundation
|
||||
**Goal:** Establish a FastAPI server that can read/write the game's JSON files.
|
||||
|
||||
**Step 1.1: Environment Setup**
|
||||
* **Instruction:** Create `tools/ndbs/server/`. Initialize a Python environment. Install `fastapi`, `uvicorn`, `pydantic`.
|
||||
* **Code Requirement:** Create `tools/ndbs/server/main.py`.
|
||||
* **Key Functionality:**
|
||||
* Enable CORS (allow all origins for dev).
|
||||
* Define a constant `PROJECT_ROOT` pointing to `../../../../`.
|
||||
|
||||
**Step 1.2: File System API**
|
||||
* **Instruction:** Implement endpoints to list and read definitions.
|
||||
* **Endpoints:**
|
||||
* `GET /api/files/definitions`: Scans `resources/definitions/` and returns a list of `.json` files.
|
||||
* `GET /api/files/content`: Takes a `filename` query param; returns the parsed JSON content.
|
||||
* `POST /api/files/content`: Takes `filename` and `content` (JSON body); writes it back to `resources/definitions/{filename}` with 2-space indentation.
|
||||
|
||||
**Step 1.3: Schema Inference (Dynamic Models)**
|
||||
* **Instruction:** Since we don't have hardcoded Pydantic models for every file, create a utility that reads a JSON file and generates a generic "Schema" description (listing keys and value types) to help the frontend build forms dynamically.
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Phase 2: Vue.js Frontend Foundation
|
||||
**Goal:** A clean UI to browse files and edit JSON data.
|
||||
|
||||
**Step 2.1: Project Scaffolding**
|
||||
* **Instruction:** Create `tools/ndbs/client` using Vite + Vue 3 (TypeScript). Install `axios`, `pinia` (state management), and `naive-ui` (or `element-plus`) for UI components.
|
||||
|
||||
**Step 2.2: File Explorer & Layout**
|
||||
* **Instruction:** Create a standard "Sidebar + Main Content" layout.
|
||||
* **Sidebar:** Fetches the list of files from `GET /api/files/definitions` and displays them as a menu.
|
||||
* **Main Content:** A RouterView to show the editor.
|
||||
|
||||
**Step 2.3: Generic JSON Editor**
|
||||
* **Instruction:** Create a component `JsonEditor.vue`.
|
||||
* Fetch content using `GET /api/files/content`.
|
||||
* Display the raw JSON in a textarea (Monaco Editor preferred later, start simple).
|
||||
* Add a "Save" button that calls `POST /api/files/content`.
|
||||
* **Enhancement:** Use a library like `jsoneditor` or `v-jsoneditor` to provide a tree view/form view.
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Phase 3: C# Rule Engine Integration (Godot Side)
|
||||
**Goal:** Prepare the game code to dynamically load the scripts we will generate later.
|
||||
|
||||
**Step 3.1: Define the Interface**
|
||||
* **Instruction:** Create `scripts/Core/Interfaces/IGameRule.cs`.
|
||||
```csharp
|
||||
namespace Core.Interfaces;
|
||||
using Models;
|
||||
public interface IGameRule {
|
||||
string Id { get; }
|
||||
// Example hooks - adjust based on GameSystems.cs analysis
|
||||
void OnEvent(GameSession session, object gameEvent);
|
||||
void ModifyStats(UnitModel unit, StatRequest request);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3.2: Rule Manager (The Loader)**
|
||||
* **Instruction:** Create `scripts/Core/Systems/RuleManager.cs`.
|
||||
* Use **Reflection** to find all classes implementing `IGameRule`.
|
||||
* Store them in a `Dictionary<string, IGameRule>`.
|
||||
* Provide a method `ExecuteRule(string ruleId, ...)` that looks up the rule and calls its methods.
|
||||
|
||||
**Step 3.3: Hook into Game Systems**
|
||||
* **Instruction:** Modify `scripts/Core/GameSystems.cs` (specifically `SynergySystem` or `TaskSystem`).
|
||||
* Find where `RuleIds` are iterated.
|
||||
* Inject a call to `RuleManager.Instance.ExecuteRule(ruleId, ...)` inside those loops.
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Phase 4: Scriptable Rule Editor (Full Stack)
|
||||
**Goal:** Allow creating new C# rules from the web UI.
|
||||
|
||||
**Step 4.1: Backend - Template Generator**
|
||||
* **Instruction:** Add endpoint `POST /api/rules/create`.
|
||||
* **Params:** `rule_id` (e.g., "grinder_stress_effect").
|
||||
* **Action:**
|
||||
1. Validate naming (alphanumeric).
|
||||
2. Load a C# template string (Class name = `Rule_{CamelCaseId}`).
|
||||
3. Write file to `scripts/Rules/Generated/Rule_{CamelCaseId}.cs`.
|
||||
4. Return success.
|
||||
|
||||
**Step 4.2: Frontend - C# Editor**
|
||||
* **Instruction:** Add a "Rules" section to the sidebar.
|
||||
* List `.cs` files in `scripts/Rules/Generated/` (add backend endpoint for this).
|
||||
* Integrate **Monaco Editor** (Vue wrapper) for syntax highlighting.
|
||||
* Save button writes the C# content back to disk.
|
||||
|
||||
**Step 4.3: Compilation Trigger (Optional but Recommended)**
|
||||
* **Instruction:** Add a "Verify" button.
|
||||
* Backend endpoint `POST /api/build`.
|
||||
* Runs `dotnet build` shell command.
|
||||
* Returns stdout/stderr to the frontend console.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 4. Technical Constraints & Conventions
|
||||
|
||||
### 4.1 File Paths
|
||||
* **Server Root:** `tools/ndbs/server`
|
||||
* **Client Root:** `tools/ndbs/client`
|
||||
* **Game Root:** `../../` (Relative to server)
|
||||
|
||||
### 4.2 Coding Style
|
||||
* **Python:** Type hints (Python 3.10+), Pydantic models for request/response bodies.
|
||||
* **Vue:** Composition API (`<script setup lang="ts">`), Scoped CSS.
|
||||
* **C#:** Namespace `Rules.Generated` for all generated scripts.
|
||||
|
||||
### 4.3 Safety
|
||||
* **No Overwrite:** When creating a new rule, fail if the file already exists.
|
||||
* **Backup:** (Bonus) Before saving a JSON file, copy the old version to `tools/ndbs/backups/`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 5. "Get Started" Prompt for AI
|
||||
|
||||
*To begin implementing this spec, paste the following prompt:*
|
||||
|
||||
> "I need to initialize the NDBS server as defined in Phase 1 of `specs/numerical_design_system.md`.
|
||||
> Please create the directory `tools/ndbs/server`, set up a basic FastAPI `main.py` application, and implement the file listing endpoint `GET /api/files/definitions` that reads from `resources/definitions/`.
|
||||
> Use `uvicorn` to run it on port 8000. Ensure CORS is enabled."
|
||||
@ -19,6 +19,7 @@
|
||||
<Content Include="resources\definitions\discipline_biology.tres" />
|
||||
<Content Include="resources\definitions\roles.json" />
|
||||
<Content Include="resources\definitions\traits.json" />
|
||||
<Content Include="specs\numerical_design_system.md" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="addons\" />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user