349 lines
8.3 KiB
C#
349 lines
8.3 KiB
C#
using Godot;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
|
|
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; } = 6;
|
|
[Export] public float CoverageStep { get; set; } = 48.0f;
|
|
[Export] public int MaxCoveragePoints { get; set; } = 200;
|
|
|
|
private NavigationRegion2D _navigationRegion;
|
|
private Node2D _studentsRoot;
|
|
private readonly List<Vector2> _coveragePoints = new();
|
|
private bool _spawnPending = true;
|
|
private bool _navBakePending = false;
|
|
private bool _navBakeReady = false;
|
|
private Rid _navBakeMap = new();
|
|
private uint _navBakeIterationId = 0;
|
|
|
|
// Called when the node enters the scene tree for the first time.
|
|
public override void _Ready()
|
|
{
|
|
_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;
|
|
|
|
// 导航区域与学生容器初始化
|
|
_navigationRegion = GetNodeOrNull<NavigationRegion2D>("Sprite2D/NavigationRegion2D");
|
|
_studentsRoot = GetNodeOrNull<Node2D>("Students");
|
|
if (_studentsRoot == null)
|
|
{
|
|
_studentsRoot = new Node2D { Name = "Students" };
|
|
AddChild(_studentsRoot);
|
|
}
|
|
|
|
// 使用可视化轮廓重新烘焙导航多边形,确保洞被正确识别
|
|
RebakeNavigationPolygonFromOutlines();
|
|
|
|
// 等待导航地图同步完成后再生成学生
|
|
_spawnPending = true;
|
|
}
|
|
|
|
// Called every frame. 'delta' is the elapsed time since the previous frame.
|
|
public override void _Process(double delta)
|
|
{
|
|
TrySpawnStudents();
|
|
}
|
|
|
|
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.Name = $"CampusStudent_{i + 1}";
|
|
student.SetNavigationMap(map);
|
|
|
|
// 随机放置在可行走区域,并设置不同的巡游起点
|
|
var randomIndex = GD.RandRange(0, _coveragePoints.Count - 1);
|
|
student.GlobalPosition = _coveragePoints[randomIndex];
|
|
student.ConfigurePatrol(_coveragePoints, i * 7);
|
|
}
|
|
}
|
|
|
|
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()
|
|
{
|
|
var map = NavigationServer2D.RegionGetMap(_navigationRegion.GetRid());
|
|
if (!map.IsValid)
|
|
{
|
|
var world = GetWorld2D();
|
|
if (world != null)
|
|
{
|
|
map = world.NavigationMap;
|
|
}
|
|
}
|
|
return map;
|
|
}
|
|
|
|
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;
|
|
_navBakePending = false;
|
|
_navBakeReady = true;
|
|
}));
|
|
}
|
|
|
|
}
|