supervisor-simulator/scenes/CampusController.cs
2026-01-17 14:27:36 +08:00

1064 lines
27 KiB
C#

using Godot;
using System;
using System.Collections.Generic;
using Core;
using Models;
public partial class CampusController : Node2D
{
private Control _taskContainer;
private Control _logContainer;
private Button _taskToggle;
private Button _logToggle;
[Export] public PackedScene StudentScene { get; set; }
[Export] public int StudentCount { get; set; } = 5;
[Export] public float CoverageStep { get; set; } = 48.0f;
[Export] public int MaxCoveragePoints { get; set; } = 200;
[Export] public string BehaviorConfigPath { get; set; } = "res://resources/definitions/campus_behavior.json";
[Export] public int RandomSeed { get; set; } = 0;
[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();
private readonly CampusLocationRegistry _locationRegistry = new();
private CampusBehaviorConfig _behaviorConfig;
private GameContentDatabase _contentDatabase;
private List<string> _archetypeIds = new();
private List<string> _roleIds = new();
private List<string> _traitIds = new();
private List<string> _disciplineIds = new();
private Random _random;
private float _roundElapsed;
private int _roundIndex;
private bool _roundActive;
private bool _spawnPending = true;
private bool _navBakePending = false;
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");
// Path to buttons based on scene structure
_taskToggle = GetNode<Button>("TopBar/HBox/CC1/TaskToggle");
_logToggle = GetNode<Button>("TopBar/HBox/CC2/LogToggle");
// Sync initial state
_taskToggle.ButtonPressed = _taskContainer.Visible;
_logToggle.ButtonPressed = _logContainer.Visible;
_taskToggle.Toggled += OnTaskToggled;
_logToggle.Toggled += OnLogToggled;
_topBar = GetNodeOrNull<TopBar>("TopBar");
if (_topBar != null)
{
_topBar.ResetRound(RoundDurationSeconds);
}
_logLabel = GetNodeOrNull<RichTextLabel>("Log/VBoxContainer/RichTextLabel");
if (_logLabel != null)
{
_logLabel.Text = string.Empty;
}
InitializeBehaviorAssets();
// 导航区域与学生容器初始化
_navigationRegion = GetNodeOrNull<NavigationRegion2D>("Sprite2D/NavigationRegion2D");
_studentsRoot = GetNodeOrNull<Node2D>("Students");
if (_studentsRoot == null)
{
_studentsRoot = new Node2D { Name = "Students" };
AddChild(_studentsRoot);
}
EnsureDebugGridOverlay();
// 使用可视化轮廓重新烘焙导航多边形,确保洞被正确识别
RebakeNavigationPolygonFromOutlines();
// 等待导航地图同步完成后再生成学生
_spawnPending = true;
}
// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
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()
{
_behaviorConfig = CampusBehaviorConfig.Load(BehaviorConfigPath);
_random = RandomSeed == 0 ? new Random() : new Random(RandomSeed);
LoadContentDatabase();
CacheLocations();
}
private void LoadContentDatabase()
{
var registry = new ContentRegistry();
var jsonSource = new JsonContentSource(0);
jsonSource.DataPaths.Add("res://resources/definitions/disciplines.json");
jsonSource.DataPaths.Add("res://resources/definitions/archetypes.json");
jsonSource.DataPaths.Add("res://resources/definitions/roles.json");
jsonSource.DataPaths.Add("res://resources/definitions/traits.json");
registry.RegisterSource(jsonSource);
_contentDatabase = registry.BuildDatabase();
_archetypeIds = new List<string>(_contentDatabase.Archetypes.Keys);
_roleIds = new List<string>(_contentDatabase.Roles.Keys);
_traitIds = new List<string>(_contentDatabase.Traits.Keys);
_disciplineIds = new List<string>(_contentDatabase.Disciplines.Keys);
if (_archetypeIds.Count == 0 || _roleIds.Count == 0 || _traitIds.Count == 0)
{
GD.PushWarning("Behavior content definitions are missing; random tags will be limited.");
}
}
private void CacheLocations()
{
var locationsRoot = GetNodeOrNull<Node2D>("Locations");
if (locationsRoot == null)
{
GD.PushWarning("Campus scene is missing Locations root; agents will wander only.");
return;
}
RegisterLocation(locationsRoot, "Location_Lab", CampusLocationId.Laboratory);
RegisterLocation(locationsRoot, "Location_Library", CampusLocationId.Library);
RegisterLocation(locationsRoot, "Location_Canteen", CampusLocationId.Canteen);
RegisterLocation(locationsRoot, "Location_Dorm", CampusLocationId.Dormitory);
RegisterLocation(locationsRoot, "Location_Lake", CampusLocationId.ArtificialLake);
RegisterLocation(locationsRoot, "Location_Coffee", CampusLocationId.CoffeeShop);
RegisterLocation(locationsRoot, "Location_Admin", CampusLocationId.AdministrationBuilding);
RegisterLocation(locationsRoot, "Location_Field", CampusLocationId.FootballField);
}
private void RegisterLocation(Node2D root, string nodeName, CampusLocationId id)
{
var node = root.GetNodeOrNull<Node2D>(nodeName);
if (node == null)
{
GD.PushWarning($"Campus location marker not found: {nodeName}");
return;
}
_locationRegistry.Register(id, node.GlobalPosition);
}
private void UpdateBehaviorAgents(float delta)
{
if (_behaviorAgents.Count == 0) return;
_behaviorWorld.Clear();
foreach (var agent in _behaviorAgents)
{
_behaviorWorld.AddOccupant(agent.Runtime.CurrentLocationId);
}
foreach (var agent in _behaviorAgents)
{
agent.Tick(delta);
}
}
private void UpdateRoundTimer(float delta)
{
if (_behaviorAgents.Count == 0 || _spawnPending) return;
if (!_roundActive)
{
StartRound();
}
if (!_roundActive) return;
_roundElapsed += delta;
_topBar?.UpdateRoundProgress(_roundElapsed, RoundDurationSeconds);
if (_roundElapsed >= RoundDurationSeconds)
{
EndRound();
}
}
private void StartRound()
{
_roundActive = true;
_roundElapsed = 0.0f;
_roundIndex += 1;
_topBar?.ResetRound(RoundDurationSeconds);
AppendLog($"第{_roundIndex}轮开始");
foreach (var agent in _behaviorAgents)
{
EnsureRoundTask(agent.Runtime);
agent.StartRound(RoundDurationSeconds);
}
}
private void EndRound()
{
_roundActive = false;
foreach (var agent in _behaviorAgents)
{
agent.EndRound();
}
AppendLog($"第{_roundIndex}轮结束");
}
private void EnsureRoundTask(CampusAgentRuntime runtime)
{
if (runtime.AssignedTask != null) return;
if (TaskTypePool.Length == 0) return;
var rng = _random ?? Random.Shared;
if (rng.Next(0, 100) >= AssignedTaskChancePercent) return;
var taskType = TaskTypePool[rng.Next(0, TaskTypePool.Length)];
var maxDuration = Math.Max(10, (int)RoundDurationSeconds - 5);
var duration = rng.Next(8, Math.Max(9, maxDuration));
runtime.AssignedTask = new CampusTask(taskType, duration);
}
private void AppendLog(string message)
{
if (_logLabel == null || string.IsNullOrWhiteSpace(message)) return;
_logLabel.AppendText(message + "\n");
}
private void OnTaskToggled(bool pressed)
{
AnimateVisibility(_taskContainer, pressed);
}
private void OnLogToggled(bool pressed)
{
AnimateVisibility(_logContainer, pressed);
}
private void AnimateVisibility(Control container, bool visible)
{
var tween = CreateTween();
if (visible)
{
if (!container.Visible)
{
var col = container.Modulate;
col.A = 0;
container.Modulate = col;
container.Visible = true;
}
tween.TweenProperty(container, "modulate:a", 1.0f, 0.2f);
}
else
{
tween.TweenProperty(container, "modulate:a", 0.0f, 0.2f);
tween.TweenCallback(Callable.From(() => container.Visible = false));
}
}
private void TrySpawnStudents()
{
if (!_spawnPending)
{
return;
}
if (_navBakePending || !_navBakeReady)
{
return;
}
// 已生成过学生则不重复生成
if (_studentsRoot != null && _studentsRoot.GetChildCount() > 0)
{
_spawnPending = false;
return;
}
if (StudentScene == null)
{
StudentScene = ResourceLoader.Load<PackedScene>("res://scenes/student_16_native.tscn");
}
if (_navigationRegion == null || _navigationRegion.NavigationPolygon == null)
{
GD.PushWarning("校园导航区域未配置或缺失导航多边形,无法生成巡游学生。");
_spawnPending = false;
return;
}
// 导航地图可能还未就绪,需要等待同步完成后再采样
var map = GetNavigationMap();
if (!map.IsValid)
{
return;
}
var iterationId = NavigationServer2D.MapGetIterationId(map);
if (iterationId == 0)
{
return;
}
// 等待导航图完成新一轮同步,避免采样到旧的/空的地图
if (_navBakeMap.IsValid)
{
if (map == _navBakeMap && iterationId == _navBakeIterationId)
{
return;
}
if (NavigationServer2D.MapGetRegions(map).Count == 0)
{
return;
}
}
SpawnStudents(map);
_spawnPending = false;
}
private void SpawnStudents(Rid map)
{
_coveragePoints.Clear();
_coveragePoints.AddRange(BuildCoveragePoints());
if (_coveragePoints.Count == 0)
{
GD.PushWarning("未采样到可行走区域,跳过学生生成。");
return;
}
for (int i = 0; i < StudentCount; i++)
{
var instance = StudentScene.Instantiate();
if (instance is not CampusStudent student)
{
GD.PushWarning("student_16_native.tscn 需要挂载 CampusStudent 脚本。");
instance.QueueFree();
continue;
}
_studentsRoot.AddChild(student);
student.SetNavigationMap(map);
student.MoveSpeed = AgentMoveSpeed;
student.GridWalkableTolerance = GridWalkableTolerance;
// 随机放置在可行走区域,并交给行为系统控制
var randomIndex = _random != null
? _random.Next(0, _coveragePoints.Count)
: (int)GD.RandRange(0, _coveragePoints.Count - 1);
student.GlobalPosition = _coveragePoints[randomIndex];
student.ApplyRandomTheme();
var runtime = BuildRandomAgentRuntime(i);
student.Name = runtime.Name;
var agent = new CampusBehaviorAgent(
student,
runtime,
_behaviorConfig,
_locationRegistry,
_behaviorWorld,
_random,
_coveragePoints,
AppendLog);
_behaviorAgents.Add(agent);
LogSpawn(runtime);
}
}
private static readonly CampusTaskType[] TaskTypePool =
{
CampusTaskType.Experiment,
CampusTaskType.Writing,
CampusTaskType.Administration,
CampusTaskType.Exercise,
CampusTaskType.Coding,
CampusTaskType.Social
};
private CampusAgentRuntime BuildRandomAgentRuntime(int index)
{
var rng = _random ?? Random.Shared;
var name = $"Agent_{index + 1}";
var student = new StudentModel(name, rng);
AssignRandomTags(student.Core, rng);
ApplyRandomStatus(student, rng);
var needs = new CampusAgentNeeds(
rng.Next(20, 100),
rng.Next(20, 100),
rng.Next(20, 100),
rng.Next(60, 100));
var runtime = new CampusAgentRuntime(student, needs);
if (rng.Next(0, 100) < AssignedTaskChancePercent && TaskTypePool.Length > 0)
{
var taskType = TaskTypePool[rng.Next(0, TaskTypePool.Length)];
var duration = rng.Next(8, 16);
runtime.AssignedTask = new CampusTask(taskType, duration);
}
return runtime;
}
private void AssignRandomTags(UnitModel unit, Random rng)
{
unit.Tags.ArchetypeIds.Clear();
unit.Tags.RoleIds.Clear();
unit.Tags.TraitIds.Clear();
var archetypeId = PickRandomId(_archetypeIds, rng);
if (!string.IsNullOrWhiteSpace(archetypeId))
{
unit.Tags.ArchetypeIds.Add(archetypeId);
}
var roleId = PickRandomId(_roleIds, rng);
if (!string.IsNullOrWhiteSpace(roleId))
{
unit.Tags.RoleIds.Add(roleId);
}
var traitCount = _traitIds.Count == 0 ? 0 : rng.Next(1, Math.Min(3, _traitIds.Count + 1));
for (var i = 0; i < traitCount; i++)
{
var traitId = PickRandomId(_traitIds, rng);
if (!string.IsNullOrWhiteSpace(traitId) && !unit.Tags.TraitIds.Contains(traitId))
{
unit.Tags.TraitIds.Add(traitId);
}
}
var disciplineId = PickRandomId(_disciplineIds, rng);
if (!string.IsNullOrWhiteSpace(disciplineId))
{
unit.Tags.DisciplineId = disciplineId;
}
}
private void ApplyRandomStatus(StudentModel student, Random rng)
{
var unit = student.Core;
unit.Statuses.Stress.Current.Value = rng.Next(0, 60);
unit.Statuses.Sanity.Current.Value = rng.Next(50, 100);
unit.Statuses.Mood.Value = rng.Next(30, 90);
student.Progress.Stamina.Current.Value = rng.Next(25, 100);
}
private static string PickRandomId(List<string> ids, Random rng)
{
if (ids == null || ids.Count == 0) return null;
return ids[rng.Next(0, ids.Count)];
}
private void LogSpawn(CampusAgentRuntime runtime)
{
var unit = runtime.Unit;
var archetypes = unit.Tags.ArchetypeIds.Count == 0 ? "none" : string.Join(",", unit.Tags.ArchetypeIds);
var roles = unit.Tags.RoleIds.Count == 0 ? "none" : string.Join(",", unit.Tags.RoleIds);
var traits = unit.Tags.TraitIds.Count == 0 ? "none" : string.Join(",", unit.Tags.TraitIds);
var taskInfo = runtime.AssignedTask != null
? $"{runtime.AssignedTask.Type} ({runtime.AssignedTask.RemainingSeconds:0}s)"
: "none";
var attributes =
$"A:{unit.Attributes.Academic.DisplayInt()} " +
$"E:{unit.Attributes.Engineering.DisplayInt()} " +
$"W:{unit.Attributes.Writing.DisplayInt()} " +
$"F:{unit.Attributes.Financial.DisplayInt()} " +
$"S:{unit.Attributes.Social.DisplayInt()} " +
$"Act:{unit.Attributes.Activation.DisplayInt()}";
GD.Print($"[CampusAI] Spawned {runtime.Name} archetype={archetypes} role={roles} traits={traits} task={taskInfo} attrs={attributes}");
}
private List<Vector2> BuildCoveragePoints()
{
var points = new List<Vector2>();
var polygon = _navigationRegion.NavigationPolygon;
if (polygon == null || polygon.Vertices.Length == 0)
{
return points;
}
var outlineCount = polygon.GetOutlineCount();
var outer = polygon.GetOutline(0);
if (outer.Length == 0)
{
return points;
}
var holes = new List<Vector2[]>();
for (int i = 1; i < outlineCount; i++)
{
var hole = polygon.GetOutline(i);
if (hole.Length > 0)
{
holes.Add(hole);
}
}
// 根据外轮廓计算采样范围
var min = outer[0];
var max = outer[0];
foreach (var v in outer)
{
min = new Vector2(Mathf.Min(min.X, v.X), Mathf.Min(min.Y, v.Y));
max = new Vector2(Mathf.Max(max.X, v.X), Mathf.Max(max.Y, v.Y));
}
var step = Mathf.Max(8.0f, CoverageStep);
var minDistance = step * 0.45f;
for (float x = min.X; x <= max.X; x += step)
{
for (float y = min.Y; y <= max.Y; y += step)
{
var local = new Vector2(x, y);
if (!Geometry2D.IsPointInPolygon(local, outer))
{
continue;
}
var insideHole = false;
for (int i = 0; i < holes.Count; i++)
{
if (Geometry2D.IsPointInPolygon(local, holes[i]))
{
insideHole = true;
break;
}
}
if (insideHole)
{
continue;
}
var global = _navigationRegion.ToGlobal(local);
if (!HasNearbyPoint(points, global, minDistance))
{
points.Add(global);
if (points.Count >= MaxCoveragePoints)
{
return points;
}
}
}
}
// 补充采样轮廓顶点,避免狭窄区域缺少巡游点
foreach (var vertex in outer)
{
var global = _navigationRegion.ToGlobal(vertex);
if (!HasNearbyPoint(points, global, minDistance))
{
points.Add(global);
if (points.Count >= MaxCoveragePoints)
{
return points;
}
}
}
// 兜底:至少给一个可行走点
if (points.Count == 0)
{
var centerLocal = (min + max) * 0.5f;
points.Add(_navigationRegion.ToGlobal(centerLocal));
}
return points;
}
private Rid GetNavigationMap()
{
if (_navigationRegion == null)
{
return new Rid();
}
var map = NavigationServer2D.RegionGetMap(_navigationRegion.GetRid());
if (!map.IsValid)
{
var world = GetWorld2D();
if (world != null)
{
map = world.NavigationMap;
}
}
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++)
{
if (points[i].DistanceTo(candidate) <= minDistance)
{
return true;
}
}
return false;
}
private void RebakeNavigationPolygonFromOutlines()
{
if (_navigationRegion == null || _navigationRegion.NavigationPolygon == null)
{
return;
}
var navPolygon = _navigationRegion.NavigationPolygon;
var outlineCount = navPolygon.GetOutlineCount();
if (outlineCount == 0)
{
return;
}
_navBakePending = true;
_navBakeReady = false;
_navBakeMap = GetNavigationMap();
_navBakeIterationId = _navBakeMap.IsValid ? NavigationServer2D.MapGetIterationId(_navBakeMap) : 0;
// 将第一个轮廓作为可通行区域,其余轮廓作为障碍洞
var sourceGeometry = new NavigationMeshSourceGeometryData2D();
sourceGeometry.AddTraversableOutline(navPolygon.GetOutline(0));
for (int i = 1; i < outlineCount; i++)
{
sourceGeometry.AddObstructionOutline(navPolygon.GetOutline(i));
}
NavigationServer2D.BakeFromSourceGeometryData(navPolygon, sourceGeometry, Callable.From(() =>
{
// 烘焙完成后再刷新导航区域,避免用旧的多边形生成地图
_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);
}
}
}