修复student运动bugs

This commit is contained in:
wjsjwr 2025-12-28 22:49:43 +08:00
parent 1776244fac
commit 1690981c70
3 changed files with 216 additions and 32 deletions

View File

@ -18,6 +18,10 @@ public partial class CampusController : Node2D
private Node2D _studentsRoot; private Node2D _studentsRoot;
private readonly List<Vector2> _coveragePoints = new(); private readonly List<Vector2> _coveragePoints = new();
private bool _spawnPending = true; 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. // Called when the node enters the scene tree for the first time.
public override void _Ready() public override void _Ready()
@ -45,6 +49,9 @@ public partial class CampusController : Node2D
AddChild(_studentsRoot); AddChild(_studentsRoot);
} }
// 使用可视化轮廓重新烘焙导航多边形,确保洞被正确识别
RebakeNavigationPolygonFromOutlines();
// 等待导航地图同步完成后再生成学生 // 等待导航地图同步完成后再生成学生
_spawnPending = true; _spawnPending = true;
} }
@ -93,6 +100,11 @@ public partial class CampusController : Node2D
return; return;
} }
if (_navBakePending || !_navBakeReady)
{
return;
}
// 已生成过学生则不重复生成 // 已生成过学生则不重复生成
if (_studentsRoot != null && _studentsRoot.GetChildCount() > 0) if (_studentsRoot != null && _studentsRoot.GetChildCount() > 0)
{ {
@ -119,13 +131,26 @@ public partial class CampusController : Node2D
return; return;
} }
// 强制刷新一次导航数据,确保首帧同步 var iterationId = NavigationServer2D.MapGetIterationId(map);
NavigationServer2D.MapForceUpdate(map); if (iterationId == 0)
if (NavigationServer2D.MapGetIterationId(map) == 0)
{ {
return; return;
} }
// 等待导航图完成新一轮同步,避免采样到旧的/空的地图
if (_navBakeMap.IsValid)
{
if (map == _navBakeMap && iterationId == _navBakeIterationId)
{
return;
}
if (NavigationServer2D.MapGetRegions(map).Count == 0)
{
return;
}
}
SpawnStudents(map); SpawnStudents(map);
_spawnPending = false; _spawnPending = false;
} }
@ -133,7 +158,7 @@ public partial class CampusController : Node2D
private void SpawnStudents(Rid map) private void SpawnStudents(Rid map)
{ {
_coveragePoints.Clear(); _coveragePoints.Clear();
_coveragePoints.AddRange(BuildCoveragePoints(map)); _coveragePoints.AddRange(BuildCoveragePoints());
if (_coveragePoints.Count == 0) if (_coveragePoints.Count == 0)
{ {
GD.PushWarning("未采样到可行走区域,跳过学生生成。"); GD.PushWarning("未采样到可行走区域,跳过学生生成。");
@ -152,6 +177,7 @@ public partial class CampusController : Node2D
_studentsRoot.AddChild(student); _studentsRoot.AddChild(student);
student.Name = $"CampusStudent_{i + 1}"; student.Name = $"CampusStudent_{i + 1}";
student.SetNavigationMap(map);
// 随机放置在可行走区域,并设置不同的巡游起点 // 随机放置在可行走区域,并设置不同的巡游起点
var randomIndex = GD.RandRange(0, _coveragePoints.Count - 1); var randomIndex = GD.RandRange(0, _coveragePoints.Count - 1);
@ -160,7 +186,7 @@ public partial class CampusController : Node2D
} }
} }
private List<Vector2> BuildCoveragePoints(Rid map) private List<Vector2> BuildCoveragePoints()
{ {
var points = new List<Vector2>(); var points = new List<Vector2>();
var polygon = _navigationRegion.NavigationPolygon; var polygon = _navigationRegion.NavigationPolygon;
@ -169,10 +195,27 @@ public partial class CampusController : Node2D
return points; return points;
} }
// 根据导航多边形顶点计算采样范围 var outlineCount = polygon.GetOutlineCount();
var min = polygon.Vertices[0]; var outer = polygon.GetOutline(0);
var max = polygon.Vertices[0]; if (outer.Length == 0)
foreach (var v in polygon.Vertices) {
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)); 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)); max = new Vector2(Mathf.Max(max.X, v.X), Mathf.Max(max.Y, v.Y));
@ -185,16 +228,29 @@ public partial class CampusController : Node2D
for (float y = min.Y; y <= max.Y; y += step) for (float y = min.Y; y <= max.Y; y += step)
{ {
var local = new Vector2(x, y); var local = new Vector2(x, y);
var global = _navigationRegion.ToGlobal(local); if (!Geometry2D.IsPointInPolygon(local, outer))
var closest = NavigationServer2D.MapGetClosestPoint(map, global);
if (closest.DistanceTo(global) > minDistance)
{ {
continue; continue;
} }
if (!HasNearbyPoint(points, closest, minDistance)) var insideHole = false;
for (int i = 0; i < holes.Count; i++)
{ {
points.Add(closest); 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) if (points.Count >= MaxCoveragePoints)
{ {
return points; return points;
@ -203,12 +259,25 @@ public partial class CampusController : Node2D
} }
} }
// 补充采样轮廓顶点,避免狭窄区域缺少巡游点
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) if (points.Count == 0)
{ {
var centerLocal = (min + max) * 0.5f; var centerLocal = (min + max) * 0.5f;
var centerGlobal = _navigationRegion.ToGlobal(centerLocal); points.Add(_navigationRegion.ToGlobal(centerLocal));
points.Add(NavigationServer2D.MapGetClosestPoint(map, centerGlobal));
} }
return points; return points;
@ -239,4 +308,41 @@ public partial class CampusController : Node2D
} }
return false; 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;
}));
}
} }

View File

@ -7,13 +7,12 @@
[ext_resource type="PackedScene" uid="uid://drmjsqoy8htc8" path="res://scenes/ui-elements/task_list.tscn" id="3_4gjr3"] [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"] [sub_resource type="NavigationPolygon" id="NavigationPolygon_8u8vn"]
vertices = PackedVector2Array(960, 544, 0, 544, 416, 512, 464, 512, 384, 512, 384, 464, 176, 496, 304, 464, 144, 496, 200, 352, 200, 264, 232, 304, 232, 464, 232, 288, 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, 840, 224, 840, 192, 856, 192, 856, 224, 864, 224, 864, 288, 832, 288, 832, 224, 800, 288, 912, 304, 912, 288, 936, 288, 952, 336, 936, 184, 952, 184, 832, 336, 832, 304, 832, 512, 800, 512, 576, 288, 576, 304, 544, 304, 544, 288, 960, 512, 120, 360, 128, 360, 128, 400, 192, 400, 192, 352, 288, 464, 288, 448, 304, 448, 176, 464, 144, 464, 0, 496, 96, 496, 96, 288, 120, 264, 0, 288, 0, 0, 32, 0, 32, 208, 416, 288, 464, 288) 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, 916, 308, 916, 292, 940, 292, 948, 332, 940, 188, 948, 188, 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, 4, 1, 6, 7), PackedInt32Array(8, 6, 1), PackedInt32Array(9, 10, 11, 12), PackedInt32Array(13, 11, 10), PackedInt32Array(14, 15, 16, 17), PackedInt32Array(18, 19, 20, 21), PackedInt32Array(22, 23, 24, 25), PackedInt32Array(26, 27, 28, 29), PackedInt32Array(30, 31, 32), PackedInt32Array(32, 33, 34, 35), PackedInt32Array(36, 37, 38, 39), PackedInt32Array(40, 41, 42, 43), PackedInt32Array(43, 44, 45, 46), PackedInt32Array(40, 43, 46), PackedInt32Array(47, 40, 46), PackedInt32Array(39, 47, 46, 48), PackedInt32Array(49, 50, 51, 52), PackedInt32Array(51, 53, 54, 52), PackedInt32Array(49, 52, 55, 56), PackedInt32Array(55, 57, 58, 48), PackedInt32Array(56, 55, 48), PackedInt32Array(46, 56, 48), PackedInt32Array(39, 48, 59, 30), PackedInt32Array(59, 60, 61, 62), PackedInt32Array(57, 63, 0), PackedInt32Array(64, 65, 66), PackedInt32Array(67, 68, 9, 12), PackedInt32Array(69, 70, 71, 7), PackedInt32Array(69, 7, 6), PackedInt32Array(12, 69, 6), PackedInt32Array(12, 6, 72, 67), PackedInt32Array(66, 67, 72, 73), PackedInt32Array(1, 74, 75), PackedInt32Array(8, 1, 75), PackedInt32Array(73, 8, 75, 76, 66), PackedInt32Array(64, 66, 76), PackedInt32Array(77, 64, 76), PackedInt32Array(78, 79, 80, 81), PackedInt32Array(76, 78, 81, 14, 77), PackedInt32Array(10, 77, 14, 17), PackedInt32Array(10, 17, 18, 21, 13), PackedInt32Array(82, 13, 21, 22), PackedInt32Array(82, 22, 25), PackedInt32Array(30, 32, 35, 39), PackedInt32Array(82, 25, 26, 83), PackedInt32Array(35, 36, 39), PackedInt32Array(29, 30, 59), PackedInt32Array(26, 29, 59, 62), PackedInt32Array(26, 62, 83), PackedInt32Array(82, 83, 3, 2), PackedInt32Array(0, 3, 58), PackedInt32Array(0, 58, 57)]) 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(49, 51, 52, 50), PackedInt32Array(47, 50, 53, 54), PackedInt32Array(53, 55, 56, 46), PackedInt32Array(54, 53, 46), PackedInt32Array(45, 54, 46), PackedInt32Array(38, 46, 57, 29), PackedInt32Array(57, 58, 59, 60), PackedInt32Array(55, 61, 0), PackedInt32Array(62, 63, 64, 10), PackedInt32Array(65, 66, 5, 8), PackedInt32Array(65, 8, 67, 68), PackedInt32Array(64, 65, 68, 69), PackedInt32Array(70, 71, 72, 12), PackedInt32Array(70, 12, 10), PackedInt32Array(8, 70, 10), PackedInt32Array(67, 8, 10, 73), PackedInt32Array(74, 75, 76, 77), PackedInt32Array(78, 74, 77, 79), PackedInt32Array(29, 31, 34, 38), PackedInt32Array(34, 35, 38), PackedInt32Array(28, 29, 57), PackedInt32Array(24, 25, 28, 57, 60), PackedInt32Array(24, 60, 80, 21), PackedInt32Array(20, 21, 80, 81, 6), PackedInt32Array(64, 69, 73, 10), PackedInt32Array(16, 17, 20, 6), PackedInt32Array(81, 7, 6), PackedInt32Array(62, 10, 78, 79), PackedInt32Array(79, 77, 13, 6), PackedInt32Array(6, 13, 16), PackedInt32Array(81, 80, 3, 2), PackedInt32Array(0, 3, 56), PackedInt32Array(0, 56, 55)])
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, 912, 288, 936, 288, 936, 184, 952, 184, 952, 336, 832, 336, 832, 512, 960, 512, 960, 544, 0, 544, 0, 496, 96, 496, 96, 288, 0, 288), PackedVector2Array(144, 496, 176, 496, 176, 464, 144, 464), PackedVector2Array(128, 400, 192, 400, 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, 416, 176, 416, 176, 448, 144, 448)]) 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, 912, 288, 936, 288, 936, 184, 952, 184, 952, 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)])
parsed_geometry_type = 1 parsed_geometry_type = 1
parsed_collision_mask = 4294967294 parsed_collision_mask = 4294967294
source_geometry_mode = 1 agent_radius = 4.0
agent_radius = 8.0
[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_4gjr3"] [sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_4gjr3"]
texture = ExtResource("1_p4tmp") texture = ExtResource("1_p4tmp")

View File

@ -21,21 +21,32 @@ public partial class CampusStudent : CharacterBody2D
private List<Vector2> _patrolPoints = new(); private List<Vector2> _patrolPoints = new();
private Sprite2D _smartphone; private Sprite2D _smartphone;
private float _stuckTimer; private float _stuckTimer;
private Rid _navigationMap;
private Vector2 _currentTarget = Vector2.Zero;
private bool _hasTarget;
private bool _usePhysicsMovement = true;
[Export] public float MoveSpeed { get; set; } = 60.0f; [Export] public float MoveSpeed { get; set; } = 60.0f;
[Export] public float TargetReachDistance { get; set; } = 6.0f; [Export] public float TargetReachDistance { get; set; } = 6.0f;
[Export] public bool Use16X16Sprites { get; set; } = true; [Export] public bool Use16X16Sprites { get; set; } = true;
[Export] public bool EnableAvoidance { get; set; } = true; [Export] public bool EnableAvoidance { get; set; }
[Export] public float StuckRepathSeconds { get; set; } = 0.6f; [Export] public float StuckRepathSeconds { get; set; } = 0.6f;
[Export] public float StuckDistanceEpsilon { get; set; } = 2.0f; [Export] public float StuckDistanceEpsilon { get; set; } = 2.0f;
[Export] public float NavMeshClampDistance { get; set; } = 6.0f;
[Export] public uint EnvironmentCollisionMask { get; set; } = 1u;
[Export] public uint StudentCollisionLayer { get; set; } = 1u << 1;
public override void _Ready() public override void _Ready()
{ {
_navigationAgent = GetNodeOrNull<NavigationAgent2D>("NavigationAgent2D"); _navigationAgent = GetNodeOrNull<NavigationAgent2D>("NavigationAgent2D");
_animationPlayer = GetNodeOrNull<AnimationPlayer>("AnimationPlayer"); _animationPlayer = GetNodeOrNull<AnimationPlayer>("AnimationPlayer");
CacheSprites(); CacheSprites();
ConfigureCollision();
if (_navigationAgent != null) if (_navigationAgent != null)
{ {
// 强制关闭避让,学生之间直接穿过
EnableAvoidance = false;
// 让寻路点更“贴近目标”,避免走到边缘时抖动 // 让寻路点更“贴近目标”,避免走到边缘时抖动
_navigationAgent.PathDesiredDistance = TargetReachDistance; _navigationAgent.PathDesiredDistance = TargetReachDistance;
_navigationAgent.TargetDesiredDistance = TargetReachDistance; _navigationAgent.TargetDesiredDistance = TargetReachDistance;
@ -62,14 +73,14 @@ public partial class CampusStudent : CharacterBody2D
} }
// 到达目标点或无路可走时,切换到下一个巡游点 // 到达目标点或无路可走时,切换到下一个巡游点
if (_navigationAgent.IsNavigationFinished() || _navigationAgent.DistanceToTarget() <= TargetReachDistance) if (_navigationAgent.IsNavigationFinished())
AdvanceTarget(); AdvanceTarget();
var nextPosition = _navigationAgent.GetNextPathPosition(); var nextPosition = _navigationAgent.GetNextPathPosition();
var toNext = nextPosition - GlobalPosition; var toNext = nextPosition - GlobalPosition;
if (toNext.LengthSquared() < 0.01f) if (toNext.LengthSquared() < 0.01f)
{ {
if (!EnableAvoidance) if (!EnableAvoidance && _usePhysicsMovement)
{ {
Velocity = Vector2.Zero; Velocity = Vector2.Zero;
MoveAndSlide(); MoveAndSlide();
@ -91,8 +102,8 @@ public partial class CampusStudent : CharacterBody2D
else else
{ {
// 未启用避让时直接移动 // 未启用避让时直接移动
Velocity = desiredVelocity; Velocity = ClampVelocityToNavMesh(desiredVelocity);
MoveAndSlide(); ApplyMovement(delta);
UpdateFacingAnimation(Velocity); UpdateFacingAnimation(Velocity);
UpdateStuckTimer(delta); UpdateStuckTimer(delta);
} }
@ -110,6 +121,12 @@ public partial class CampusStudent : CharacterBody2D
if (_navigationAgent != null) AdvanceTarget(); if (_navigationAgent != null) AdvanceTarget();
} }
public void SetNavigationMap(Rid map)
{
// 由校园控制器传入导航地图,供本地边界夹紧使用
_navigationMap = map;
}
public void ApplyRandomTheme() public void ApplyRandomTheme()
{ {
// 随机替换身体与配件贴图,形成不同主题外观 // 随机替换身体与配件贴图,形成不同主题外观
@ -135,6 +152,14 @@ public partial class CampusStudent : CharacterBody2D
_smartphone = GetNode<Sprite2D>("parts/smartphone"); _smartphone = GetNode<Sprite2D>("parts/smartphone");
} }
private void ConfigureCollision()
{
// 学生只与环境碰撞,不与其它学生碰撞
CollisionLayer = StudentCollisionLayer;
CollisionMask = EnvironmentCollisionMask;
_usePhysicsMovement = true;
}
private void AdvanceTarget() private void AdvanceTarget()
{ {
if (_patrolPoints.Count == 0 || _navigationAgent == null) return; if (_patrolPoints.Count == 0 || _navigationAgent == null) return;
@ -146,26 +171,38 @@ public partial class CampusStudent : CharacterBody2D
_patrolIndex = (_patrolIndex + 1) % _patrolPoints.Count; _patrolIndex = (_patrolIndex + 1) % _patrolPoints.Count;
if (GlobalPosition.DistanceTo(target) > TargetReachDistance * 1.5f) if (GlobalPosition.DistanceTo(target) > TargetReachDistance * 1.5f)
{ {
_currentTarget = target;
_hasTarget = true;
_navigationAgent.TargetPosition = target; _navigationAgent.TargetPosition = target;
_stuckTimer = 0.0f;
return; return;
} }
} }
_navigationAgent.TargetPosition = _patrolPoints[_patrolIndex]; _currentTarget = _patrolPoints[_patrolIndex];
_hasTarget = true;
_navigationAgent.TargetPosition = _currentTarget;
_stuckTimer = 0.0f;
} }
private void OnVelocityComputed(Vector2 safeVelocity) private void OnVelocityComputed(Vector2 safeVelocity)
{ {
// 使用安全速度移动,避免与其它角色硬碰硬卡住 // 使用安全速度移动,避免与其它角色硬碰硬卡住
Velocity = safeVelocity; Velocity = ClampVelocityToNavMesh(safeVelocity);
MoveAndSlide(); ApplyMovement(_lastDelta);
UpdateFacingAnimation(Velocity); UpdateFacingAnimation(Velocity);
UpdateStuckTimer(_lastDelta); UpdateStuckTimer(_lastDelta);
} }
private void UpdateStuckTimer(double delta) private void UpdateStuckTimer(double delta)
{ {
// 若短时间内几乎没有位移,则换一个目标点脱困 if (StuckRepathSeconds <= 0.0f || !_hasTarget)
{
_lastPosition = GlobalPosition;
return;
}
// 若短时间内几乎没有位移,则重新请求路径来脱困
if (GlobalPosition.DistanceTo(_lastPosition) <= StuckDistanceEpsilon) if (GlobalPosition.DistanceTo(_lastPosition) <= StuckDistanceEpsilon)
_stuckTimer += (float)delta; _stuckTimer += (float)delta;
else else
@ -176,10 +213,52 @@ public partial class CampusStudent : CharacterBody2D
if (_stuckTimer >= StuckRepathSeconds) if (_stuckTimer >= StuckRepathSeconds)
{ {
_stuckTimer = 0.0f; _stuckTimer = 0.0f;
AdvanceTarget(); RepathToCurrentTarget();
} }
} }
private void RepathToCurrentTarget()
{
if (_navigationAgent == null || !_hasTarget) return;
// 重新请求到同一目标点的路径,避免“固定时间换目的地”
_navigationAgent.TargetPosition = _currentTarget;
}
private Vector2 ClampVelocityToNavMesh(Vector2 velocity)
{
if (!_navigationMap.IsValid) return velocity;
if (NavigationServer2D.MapGetIterationId(_navigationMap) == 0) return velocity;
if (_lastDelta <= 0.0) return velocity;
// 预测下一帧位置,若偏离导航网格过远则夹回网格内
var candidate = GlobalPosition + velocity * (float)_lastDelta;
var closest = NavigationServer2D.MapGetClosestPoint(_navigationMap, candidate);
if (closest.DistanceTo(candidate) > NavMeshClampDistance)
{
var corrected = closest - GlobalPosition;
if (corrected.LengthSquared() < 0.01f)
{
return Vector2.Zero;
}
return corrected / (float)_lastDelta;
}
return velocity;
}
private void ApplyMovement(double delta)
{
if (_usePhysicsMovement)
{
MoveAndSlide();
return;
}
// 关闭碰撞时直接平移,避免 CharacterBody2D 在无碰撞层时不移动
GlobalPosition += Velocity * (float)delta;
}
private void UpdateFacingAnimation(Vector2 velocity) private void UpdateFacingAnimation(Vector2 velocity)
{ {
if (velocity.LengthSquared() < 0.01f) if (velocity.LengthSquared() < 0.01f)