supervisor-simulator/scenes/CampusController.cs

243 lines
6.0 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;
// 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);
}
// 等待导航地图同步完成后再生成学生
_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 (_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;
}
// 强制刷新一次导航数据,确保首帧同步
NavigationServer2D.MapForceUpdate(map);
if (NavigationServer2D.MapGetIterationId(map) == 0)
{
return;
}
SpawnStudents(map);
_spawnPending = false;
}
private void SpawnStudents(Rid map)
{
_coveragePoints.Clear();
_coveragePoints.AddRange(BuildCoveragePoints(map));
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}";
// 随机放置在可行走区域,并设置不同的巡游起点
var randomIndex = GD.RandRange(0, _coveragePoints.Count - 1);
student.GlobalPosition = _coveragePoints[randomIndex];
student.ConfigurePatrol(_coveragePoints, i * 7);
}
}
private List<Vector2> BuildCoveragePoints(Rid map)
{
var points = new List<Vector2>();
var polygon = _navigationRegion.NavigationPolygon;
if (polygon == null || polygon.Vertices.Length == 0)
{
return points;
}
// 根据导航多边形顶点计算采样范围
var min = polygon.Vertices[0];
var max = polygon.Vertices[0];
foreach (var v in polygon.Vertices)
{
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);
var global = _navigationRegion.ToGlobal(local);
var closest = NavigationServer2D.MapGetClosestPoint(map, global);
if (closest.DistanceTo(global) > minDistance)
{
continue;
}
if (!HasNearbyPoint(points, closest, minDistance))
{
points.Add(closest);
if (points.Count >= MaxCoveragePoints)
{
return points;
}
}
}
}
// 兜底:至少给一个可行走点
if (points.Count == 0)
{
var centerLocal = (min + max) * 0.5f;
var centerGlobal = _navigationRegion.ToGlobal(centerLocal);
points.Add(NavigationServer2D.MapGetClosestPoint(map, centerGlobal));
}
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;
}
}