246 lines
8.0 KiB
C#
246 lines
8.0 KiB
C#
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<Vector2> _patrolPoints = new();
|
|
private Sprite2D _smartphone;
|
|
private float _stuckTimer;
|
|
[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; } = true;
|
|
[Export] public float StuckRepathSeconds { get; set; } = 0.6f;
|
|
[Export] public float StuckDistanceEpsilon { get; set; } = 2.0f;
|
|
|
|
public override void _Ready()
|
|
{
|
|
_navigationAgent = GetNodeOrNull<NavigationAgent2D>("NavigationAgent2D");
|
|
_animationPlayer = GetNodeOrNull<AnimationPlayer>("AnimationPlayer");
|
|
CacheSprites();
|
|
|
|
if (_navigationAgent != null)
|
|
{
|
|
// 让寻路点更“贴近目标”,避免走到边缘时抖动
|
|
_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 || _patrolPoints.Count == 0)
|
|
{
|
|
PlayIdleAnimation();
|
|
return;
|
|
}
|
|
|
|
// 到达目标点或无路可走时,切换到下一个巡游点
|
|
if (_navigationAgent.IsNavigationFinished() || _navigationAgent.DistanceToTarget() <= TargetReachDistance)
|
|
AdvanceTarget();
|
|
|
|
var nextPosition = _navigationAgent.GetNextPathPosition();
|
|
var toNext = nextPosition - GlobalPosition;
|
|
if (toNext.LengthSquared() < 0.01f)
|
|
{
|
|
if (!EnableAvoidance)
|
|
{
|
|
Velocity = Vector2.Zero;
|
|
MoveAndSlide();
|
|
}
|
|
|
|
PlayIdleAnimation();
|
|
UpdateStuckTimer(delta);
|
|
return;
|
|
}
|
|
|
|
var direction = toNext.Normalized();
|
|
var desiredVelocity = direction * MoveSpeed;
|
|
|
|
if (EnableAvoidance)
|
|
{
|
|
// 交给导航系统做群体避让
|
|
_navigationAgent.Velocity = desiredVelocity;
|
|
}
|
|
else
|
|
{
|
|
// 未启用避让时直接移动
|
|
Velocity = desiredVelocity;
|
|
MoveAndSlide();
|
|
UpdateFacingAnimation(Velocity);
|
|
UpdateStuckTimer(delta);
|
|
}
|
|
}
|
|
|
|
public void ConfigurePatrol(List<Vector2> points, int startIndex)
|
|
{
|
|
_patrolPoints = points ?? new List<Vector2>();
|
|
if (_patrolPoints.Count == 0) return;
|
|
|
|
_patrolIndex = (startIndex % _patrolPoints.Count + _patrolPoints.Count) % _patrolPoints.Count;
|
|
_patrolConfigured = true;
|
|
ApplyRandomTheme();
|
|
|
|
if (_navigationAgent != null) AdvanceTarget();
|
|
}
|
|
|
|
public void ApplyRandomTheme()
|
|
{
|
|
// 随机替换身体与配件贴图,形成不同主题外观
|
|
if (_body == null) CacheSprites();
|
|
|
|
Debug.Assert(_body != null, nameof(_body) + " != null");
|
|
_body.Texture = ResourceLoader.Load<Texture2D>(Res.GetRandom(Res.Type.Body, Use16X16Sprites));
|
|
_hairstyle.Texture = ResourceLoader.Load<Texture2D>(Res.GetRandom(Res.Type.Hair, Use16X16Sprites));
|
|
_outfit.Texture = ResourceLoader.Load<Texture2D>(Res.GetRandom(Res.Type.Outfit, Use16X16Sprites));
|
|
_eye.Texture = ResourceLoader.Load<Texture2D>(Res.GetRandom(Res.Type.Eye, Use16X16Sprites));
|
|
_accessory.Texture = ResourceLoader.Load<Texture2D>(Res.GetRandom(Res.Type.Accessory, Use16X16Sprites));
|
|
_smartphone.Texture = ResourceLoader.Load<Texture2D>(Res.GetRandom(Res.Type.Phone, Use16X16Sprites));
|
|
}
|
|
|
|
private void CacheSprites()
|
|
{
|
|
// 缓存子节点引用,避免每帧查找
|
|
_body = GetNode<Sprite2D>("parts/body");
|
|
_hairstyle = GetNode<Sprite2D>("parts/hairstyle");
|
|
_outfit = GetNode<Sprite2D>("parts/outfit");
|
|
_eye = GetNode<Sprite2D>("parts/eye");
|
|
_accessory = GetNode<Sprite2D>("parts/accessory");
|
|
_smartphone = GetNode<Sprite2D>("parts/smartphone");
|
|
}
|
|
|
|
private void AdvanceTarget()
|
|
{
|
|
if (_patrolPoints.Count == 0 || _navigationAgent == null) 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)
|
|
{
|
|
_navigationAgent.TargetPosition = target;
|
|
return;
|
|
}
|
|
}
|
|
|
|
_navigationAgent.TargetPosition = _patrolPoints[_patrolIndex];
|
|
}
|
|
|
|
private void OnVelocityComputed(Vector2 safeVelocity)
|
|
{
|
|
// 使用安全速度移动,避免与其它角色硬碰硬卡住
|
|
Velocity = safeVelocity;
|
|
MoveAndSlide();
|
|
UpdateFacingAnimation(Velocity);
|
|
UpdateStuckTimer(_lastDelta);
|
|
}
|
|
|
|
private void UpdateStuckTimer(double delta)
|
|
{
|
|
// 若短时间内几乎没有位移,则换一个目标点脱困
|
|
if (GlobalPosition.DistanceTo(_lastPosition) <= StuckDistanceEpsilon)
|
|
_stuckTimer += (float)delta;
|
|
else
|
|
_stuckTimer = 0.0f;
|
|
|
|
_lastPosition = GlobalPosition;
|
|
|
|
if (_stuckTimer >= StuckRepathSeconds)
|
|
{
|
|
_stuckTimer = 0.0f;
|
|
AdvanceTarget();
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
} |