using System.Collections.Generic; using System.Diagnostics; using Godot; public partial class CampusStudent : CharacterBody2D { private Sprite2D _accessory; private AnimationPlayer _animationPlayer; private Sprite2D _body; private Sprite2D _eye; private Sprite2D _hairstyle; private double _lastDelta; private FacingDirection _lastFacing = FacingDirection.Down; private Vector2 _lastPosition; private NavigationAgent2D _navigationAgent; private Sprite2D _outfit; private bool _patrolConfigured; private int _patrolIndex; private List _patrolPoints = new(); private Sprite2D _smartphone; private float _stuckTimer; private Rid _navigationMap; private Vector2 _currentTarget = Vector2.Zero; private bool _hasTarget; private bool _behaviorControlEnabled; private Vector2 _behaviorTarget = Vector2.Zero; private bool _behaviorHasTarget; private bool _usePhysicsMovement = true; [Export] public float MoveSpeed { get; set; } = 60.0f; [Export] public float TargetReachDistance { get; set; } = 6.0f; [Export] public bool Use16X16Sprites { get; set; } = true; [Export] public bool EnableAvoidance { get; set; } [Export] public float StuckRepathSeconds { get; set; } = 0.6f; [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() { _navigationAgent = GetNodeOrNull("NavigationAgent2D"); _animationPlayer = GetNodeOrNull("AnimationPlayer"); CacheSprites(); ConfigureCollision(); if (_navigationAgent != null) { // 强制关闭避让,学生之间直接穿过 EnableAvoidance = false; // 让寻路点更“贴近目标”,避免走到边缘时抖动 _navigationAgent.PathDesiredDistance = TargetReachDistance; _navigationAgent.TargetDesiredDistance = TargetReachDistance; _navigationAgent.AvoidanceEnabled = EnableAvoidance; if (EnableAvoidance) // 开启避让时使用安全速度回调进行移动 _navigationAgent.VelocityComputed += OnVelocityComputed; if (_patrolConfigured) AdvanceTarget(); } PlayIdleAnimation(); _lastPosition = GlobalPosition; } public override void _PhysicsProcess(double delta) { _lastDelta = delta; if (_navigationAgent == null || (!_behaviorControlEnabled && _patrolPoints.Count == 0) || (_behaviorControlEnabled && !_behaviorHasTarget)) { PlayIdleAnimation(); return; } // 到达目标点或无路可走时,切换到下一个巡游点 if (_navigationAgent.IsNavigationFinished()) { if (_behaviorControlEnabled) { Velocity = Vector2.Zero; if (!EnableAvoidance && _usePhysicsMovement) { MoveAndSlide(); } PlayIdleAnimation(); UpdateStuckTimer(delta); return; } AdvanceTarget(); } var nextPosition = _navigationAgent.GetNextPathPosition(); var toNext = nextPosition - GlobalPosition; if (toNext.LengthSquared() < 0.01f) { if (!EnableAvoidance && _usePhysicsMovement) { Velocity = Vector2.Zero; MoveAndSlide(); } PlayIdleAnimation(); UpdateStuckTimer(delta); return; } var direction = toNext.Normalized(); var desiredVelocity = direction * MoveSpeed; if (EnableAvoidance) { // 交给导航系统做群体避让 _navigationAgent.Velocity = desiredVelocity; } else { // 未启用避让时直接移动 Velocity = ClampVelocityToNavMesh(desiredVelocity); ApplyMovement(delta); UpdateFacingAnimation(Velocity); UpdateStuckTimer(delta); } } public void ConfigurePatrol(List points, int startIndex) { _patrolPoints = points ?? new List(); if (_patrolPoints.Count == 0) return; _patrolIndex = (startIndex % _patrolPoints.Count + _patrolPoints.Count) % _patrolPoints.Count; _patrolConfigured = true; ApplyRandomTheme(); if (_navigationAgent != null) AdvanceTarget(); } public void EnableBehaviorControl() { _behaviorControlEnabled = true; _behaviorHasTarget = false; _patrolConfigured = false; _patrolPoints.Clear(); } public void SetBehaviorTarget(Vector2 target) { _behaviorControlEnabled = true; _behaviorTarget = target; _behaviorHasTarget = true; _currentTarget = target; _hasTarget = true; _stuckTimer = 0.0f; if (_navigationAgent != null) { _navigationAgent.TargetPosition = target; } } public void ClearBehaviorTarget() { _behaviorHasTarget = false; _hasTarget = false; _currentTarget = Vector2.Zero; _stuckTimer = 0.0f; } public bool HasReachedBehaviorTarget() { if (!_behaviorHasTarget || _navigationAgent == null) return true; return _navigationAgent.IsNavigationFinished(); } public void SetNavigationMap(Rid map) { // 由校园控制器传入导航地图,供本地边界夹紧使用 _navigationMap = map; } public void ApplyRandomTheme() { // 随机替换身体与配件贴图,形成不同主题外观 if (_body == null) CacheSprites(); Debug.Assert(_body != null, nameof(_body) + " != null"); _body.Texture = ResourceLoader.Load(Res.GetRandom(Res.Type.Body, Use16X16Sprites)); _hairstyle.Texture = ResourceLoader.Load(Res.GetRandom(Res.Type.Hair, Use16X16Sprites)); _outfit.Texture = ResourceLoader.Load(Res.GetRandom(Res.Type.Outfit, Use16X16Sprites)); _eye.Texture = ResourceLoader.Load(Res.GetRandom(Res.Type.Eye, Use16X16Sprites)); _accessory.Texture = ResourceLoader.Load(Res.GetRandom(Res.Type.Accessory, Use16X16Sprites)); _smartphone.Texture = ResourceLoader.Load(Res.GetRandom(Res.Type.Phone, Use16X16Sprites)); } private void CacheSprites() { // 缓存子节点引用,避免每帧查找 _body = GetNode("parts/body"); _hairstyle = GetNode("parts/hairstyle"); _outfit = GetNode("parts/outfit"); _eye = GetNode("parts/eye"); _accessory = GetNode("parts/accessory"); _smartphone = GetNode("parts/smartphone"); } private void ConfigureCollision() { // 学生只与环境碰撞,不与其它学生碰撞 CollisionLayer = StudentCollisionLayer; CollisionMask = EnvironmentCollisionMask; _usePhysicsMovement = true; } private void AdvanceTarget() { if (_patrolPoints.Count == 0 || _navigationAgent == null) return; if (_behaviorControlEnabled) return; // 避免当前点过近导致原地抖动,最多尝试一轮 for (var i = 0; i < _patrolPoints.Count; i++) { var target = _patrolPoints[_patrolIndex]; _patrolIndex = (_patrolIndex + 1) % _patrolPoints.Count; if (GlobalPosition.DistanceTo(target) > TargetReachDistance * 1.5f) { _currentTarget = target; _hasTarget = true; _navigationAgent.TargetPosition = target; _stuckTimer = 0.0f; return; } } _currentTarget = _patrolPoints[_patrolIndex]; _hasTarget = true; _navigationAgent.TargetPosition = _currentTarget; _stuckTimer = 0.0f; } private void OnVelocityComputed(Vector2 safeVelocity) { // 使用安全速度移动,避免与其它角色硬碰硬卡住 Velocity = ClampVelocityToNavMesh(safeVelocity); ApplyMovement(_lastDelta); UpdateFacingAnimation(Velocity); UpdateStuckTimer(_lastDelta); } private void UpdateStuckTimer(double delta) { if (StuckRepathSeconds <= 0.0f || !_hasTarget) { _lastPosition = GlobalPosition; return; } // 若短时间内几乎没有位移,则重新请求路径来脱困 if (GlobalPosition.DistanceTo(_lastPosition) <= StuckDistanceEpsilon) _stuckTimer += (float)delta; else _stuckTimer = 0.0f; _lastPosition = GlobalPosition; if (_stuckTimer >= StuckRepathSeconds) { _stuckTimer = 0.0f; 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) { if (velocity.LengthSquared() < 0.01f) { PlayIdleAnimation(); return; } if (Mathf.Abs(velocity.X) >= Mathf.Abs(velocity.Y)) _lastFacing = velocity.X >= 0 ? FacingDirection.Right : FacingDirection.Left; else _lastFacing = velocity.Y >= 0 ? FacingDirection.Down : FacingDirection.Up; switch (_lastFacing) { case FacingDirection.Left: PlayAnimation("walk_left"); break; case FacingDirection.Right: PlayAnimation("walk_right"); break; case FacingDirection.Up: PlayAnimation("walk_up"); break; case FacingDirection.Down: PlayAnimation("walk_down"); break; } } private void PlayIdleAnimation() { switch (_lastFacing) { case FacingDirection.Left: PlayAnimation("idle_left"); break; case FacingDirection.Right: PlayAnimation("idle_right"); break; case FacingDirection.Up: PlayAnimation("idle_back"); break; case FacingDirection.Down: PlayAnimation("idle_front"); break; } } private void PlayAnimation(string animationName) { if (_animationPlayer == null) return; if (_animationPlayer.CurrentAnimation != animationName) _animationPlayer.Play(animationName); } private enum FacingDirection { Up, Down, Left, Right } }