supervisor-simulator/scripts/CampusStudent.cs

741 lines
21 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using Godot;
using Views;
/// <summary>
/// 校园学生角色控制器
/// </summary>
public partial class CampusStudent : CharacterBody2D {
/// <summary>
/// 网格路径列表
/// </summary>
private readonly List<Vector2> _gridPath = new();
/// <summary>
/// 外观主题
/// </summary>
private readonly SuitTheme _theme = new();
/// <summary>
/// 获取外观主题ID
/// </summary>
/// <returns>主题ID</returns>
public ulong GetSuitThemeId() {
return _theme.GetThemeId();
}
/// <summary>
/// 动画播放器
/// </summary>
private AnimationPlayer _animationPlayer;
/// <summary>
/// 是否启用行为控制
/// </summary>
private bool _behaviorControlEnabled;
/// <summary>
/// 是否有行为目标
/// </summary>
private bool _behaviorHasTarget;
/// <summary>
/// 行为目标点
/// </summary>
private Vector2 _behaviorTarget = Vector2.Zero;
/// <summary>
/// 校园控制器
/// </summary>
private CampusController _campusController;
/// <summary>
/// 当前目标点
/// </summary>
private Vector2 _currentTarget = Vector2.Zero;
/// <summary>
/// 网格路径是否激活
/// </summary>
private bool _gridPathActive;
/// <summary>
/// 当前网格路径索引
/// </summary>
private int _gridPathIndex;
/// <summary>
/// 网格路径是否挂起
/// </summary>
private bool _gridPathPending;
/// <summary>
/// 网格重新寻路重试计时器
/// </summary>
private float _gridPathRetryTimer;
/// <summary>
/// 是否有目标
/// </summary>
private bool _hasTarget;
/// <summary>
/// 上一帧的时间间隔
/// </summary>
private double _lastDelta;
/// <summary>
/// 上一次的面朝方向
/// </summary>
private FacingDirection _lastFacing = FacingDirection.Down;
/// <summary>
/// 上一次的位置
/// </summary>
private Vector2 _lastPosition;
/// <summary>
/// 是否已配置巡逻
/// </summary>
private bool _patrolConfigured;
/// <summary>
/// 当前巡逻点索引
/// </summary>
private int _patrolIndex;
/// <summary>
/// 巡逻点列表
/// </summary>
private List<Vector2> _patrolPoints = new();
/// <summary>
/// 手机退出动作锁定
/// </summary>
private bool _phoneExitLocked;
/// <summary>
/// 手机闲置动画是否激活
/// </summary>
private bool _phoneIdleActive;
/// <summary>
/// 卡住计时器
/// </summary>
private float _stuckTimer;
/// <summary>
/// 是否使用物理移动
/// </summary>
private bool _usePhysicsMovement = true;
/// <summary>
/// 移动速度
/// </summary>
[Export]
public float MoveSpeed { get; set; } = 60.0f;
/// <summary>
/// 目标到达判定距离
/// </summary>
[Export]
public float TargetReachDistance { get; set; } = 6.0f;
/// <summary>
/// 是否使用16x16精灵
/// </summary>
[Export]
public bool Use16X16Sprites { get; set; } = true;
/// <summary>
/// 卡住重新寻路时间
/// </summary>
[Export]
public float StuckRepathSeconds { get; set; } = 0.6f;
/// <summary>
/// 卡住距离阈值
/// </summary>
[Export]
public float StuckDistanceEpsilon { get; set; } = 2.0f;
/// <summary>
/// 网格重新寻路间隔
/// </summary>
[Export]
public float GridRepathInterval { get; set; } = 0.25f;
/// <summary>
/// 调试绘制路径
/// </summary>
[Export]
public bool DebugDrawPath { get; set; }
/// <summary>
/// 环境碰撞掩码
/// </summary>
[Export]
public uint EnvironmentCollisionMask { get; set; } = 1u;
/// <summary>
/// 学生碰撞层
/// </summary>
[Export]
public uint StudentCollisionLayer { get; set; } = 1u << 1;
/// <summary>
/// 准备就绪时调用
/// </summary>
public override void _Ready() {
_animationPlayer = GetNodeOrNull<AnimationPlayer>("AnimationPlayer");
_campusController = GetTree()?.CurrentScene as CampusController;
_theme.IsPortrait = false;
_theme.Use16 = Use16X16Sprites;
CacheSprites();
ConfigureCollision();
if (_animationPlayer != null) _animationPlayer.AnimationFinished += OnAnimationFinished;
if (_patrolConfigured) AdvanceTarget();
PlayIdleAnimation();
_lastPosition = GlobalPosition;
}
/// <summary>
/// 物理处理
/// </summary>
/// <param name="delta">时间间隔</param>
public override void _PhysicsProcess(double delta) {
_lastDelta = delta;
if (DebugDrawPath) QueueRedraw();
if (_gridPathRetryTimer > 0.0f) _gridPathRetryTimer = Mathf.Max(0.0f, _gridPathRetryTimer - (float)delta);
if (_phoneExitLocked) {
Velocity = Vector2.Zero;
if (_usePhysicsMovement) MoveAndSlide();
return;
}
var hasPatrolTarget = !_behaviorControlEnabled && _patrolPoints.Count > 0;
if (_behaviorControlEnabled && _behaviorHasTarget) {
TryBuildPendingGridPath();
}
else if (hasPatrolTarget) {
if (!_hasTarget || (!_gridPathPending && !_gridPathActive)) AdvanceTarget();
TryBuildPendingGridPath();
}
else {
if (!_phoneIdleActive && !_phoneExitLocked) PlayIdleAnimation();
return;
}
if (_gridPathActive)
if (ProcessGridPathMovement(delta)) {
if (hasPatrolTarget && !_gridPathActive) AdvanceTarget();
return;
}
if (_gridPathPending) {
Velocity = Vector2.Zero;
if (_usePhysicsMovement) MoveAndSlide();
if (!_phoneIdleActive && !_phoneExitLocked) PlayIdleAnimation();
UpdateStuckTimer(delta);
return;
}
if (!_phoneIdleActive && !_phoneExitLocked) PlayIdleAnimation();
}
/// <summary>
/// 绘制调试信息
/// </summary>
public override void _Draw() {
if (!DebugDrawPath) return;
if (_gridPath.Count > 0 && _gridPathIndex < _gridPath.Count) {
var pathColor = new Color(0.2f, 0.7f, 1.0f, 0.9f);
var current = ToLocal(GlobalPosition);
for (var i = _gridPathIndex; i < _gridPath.Count; i++) {
var next = ToLocal(_gridPath[i]);
DrawLine(current, next, pathColor, 2.0f);
DrawCircle(next, 2.5f, pathColor);
current = next;
}
}
}
/// <summary>
/// 配置巡逻
/// </summary>
/// <param name="points">巡逻点列表</param>
/// <param name="startIndex">起始索引</param>
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();
AdvanceTarget();
}
/// <summary>
/// 启用行为控制
/// </summary>
public void EnableBehaviorControl() {
_behaviorControlEnabled = true;
_behaviorHasTarget = false;
_patrolConfigured = false;
_patrolPoints.Clear();
}
/// <summary>
/// 设置行为目标
/// </summary>
/// <param name="target">目标位置</param>
public void SetBehaviorTarget(Vector2 target) {
_behaviorControlEnabled = true;
_behaviorTarget = target;
_behaviorHasTarget = true;
BeginGridTarget(target);
}
/// <summary>
/// 清除行为目标
/// </summary>
public void ClearBehaviorTarget() {
_behaviorHasTarget = false;
_hasTarget = false;
_currentTarget = Vector2.Zero;
_stuckTimer = 0.0f;
_gridPathActive = false;
_gridPathPending = false;
_gridPathIndex = 0;
_gridPathRetryTimer = 0.0f;
_gridPath.Clear();
}
/// <summary>
/// 进入建筑时同步头像
/// </summary>
/// <param name="locationId">地点ID</param>
public void EnterBuilding(CampusLocationId locationId) {
_campusController ??= GetTree()?.CurrentScene as CampusController;
_campusController?.HandleStudentEnterBuilding(this, locationId);
}
/// <summary>
/// 离开建筑时移除头像
/// </summary>
/// <param name="locationId">地点ID</param>
public void ExitBuilding(CampusLocationId locationId) {
_campusController ??= GetTree()?.CurrentScene as CampusController;
_campusController?.HandleStudentExitBuilding(this, locationId);
}
/// <summary>
/// 设置网格寻路目标
/// </summary>
/// <param name="target">目标位置</param>
private void BeginGridTarget(Vector2 target) {
_currentTarget = target;
_hasTarget = true;
_stuckTimer = 0.0f;
_gridPathActive = false;
_gridPathPending = true;
_gridPathIndex = 0;
_gridPathRetryTimer = 0.0f;
_gridPath.Clear();
TryBuildPendingGridPath();
}
/// <summary>
/// 是否已到达行为目标
/// </summary>
/// <returns>到达返回true</returns>
public bool HasReachedBehaviorTarget() {
if (!_behaviorHasTarget) return true;
if (_gridPathActive) return _gridPathIndex >= _gridPath.Count;
if (_gridPathPending) return false;
return true;
}
/// <summary>
/// 开始玩手机
/// </summary>
public void StartPhoneIdle() {
if (_animationPlayer == null || !_animationPlayer.HasAnimation("phone_up")) return;
if (_phoneIdleActive) return;
_phoneExitLocked = false;
_phoneIdleActive = true;
_animationPlayer.Play("phone_up");
}
/// <summary>
/// 停止玩手机
/// </summary>
/// <param name="lockMovement">是否锁定移动</param>
public void StopPhoneIdle(bool lockMovement = false) {
if (_animationPlayer == null) {
_phoneIdleActive = false;
_phoneExitLocked = false;
return;
}
if (_phoneExitLocked && !lockMovement) return;
if (!_phoneIdleActive && !_phoneExitLocked) return;
_phoneIdleActive = false;
if (_animationPlayer.HasAnimation("phone_down")) {
_phoneExitLocked = lockMovement;
_animationPlayer.Play("phone_down");
}
else {
_phoneExitLocked = false;
PlayIdleAnimation();
}
}
/// <summary>
/// 应用随机主题
/// </summary>
public void ApplyRandomTheme() {
// 随机替换身体与配件贴图,形成不同主题外观
if (_theme.IsEmpty()) CacheSprites();
Debug.Assert(_theme != null, nameof(_theme) + " != null");
for (Res.Type typeId = 0; typeId < Res.Type.ResTypeMax; typeId++)
_theme.ApplyComponent(typeId, Res.GetResourcePathWithId(0, typeId, Use16X16Sprites, false, true));
}
/// <summary>
/// 缓存精灵节点
/// </summary>
private void CacheSprites() {
// 缓存子节点引用,避免每帧查找
_theme.CacheComponent(Res.Type.Accessory, GetNode<Sprite2D>("parts/accessory"));
_theme.CacheComponent(Res.Type.Body, GetNode<Sprite2D>("parts/body"));
_theme.CacheComponent(Res.Type.Eye, GetNode<Sprite2D>("parts/eye"));
_theme.CacheComponent(Res.Type.Hair, GetNode<Sprite2D>("parts/hairstyle"));
_theme.CacheComponent(Res.Type.Outfit, GetNode<Sprite2D>("parts/outfit"));
_theme.CacheComponent(Res.Type.Phone, GetNode<Sprite2D>("parts/smartphone"));
}
/// <summary>
/// 配置碰撞
/// </summary>
private void ConfigureCollision() {
// 学生只与环境碰撞,不与其它学生碰撞
CollisionLayer = StudentCollisionLayer;
CollisionMask = EnvironmentCollisionMask;
_usePhysicsMovement = true;
}
/// <summary>
/// 推进到下一个目标
/// </summary>
private void AdvanceTarget() {
if (_patrolPoints.Count == 0) 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) {
BeginGridTarget(target);
return;
}
}
BeginGridTarget(_patrolPoints[_patrolIndex]);
}
/// <summary>
/// 更新卡住计时器
/// </summary>
/// <param name="delta">时间间隔</param>
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();
}
}
/// <summary>
/// 重新规划路径到当前目标
/// </summary>
private void RepathToCurrentTarget() {
if (!_hasTarget) return;
_gridPathActive = false;
_gridPathPending = true;
_gridPathIndex = 0;
_gridPath.Clear();
TryBuildPendingGridPath();
}
/// <summary>
/// 尝试构建挂起的网格路径
/// </summary>
private void TryBuildPendingGridPath() {
if (!_gridPathPending) return;
if (_gridPathRetryTimer > 0.0f) return;
var path = BuildGridPath(GlobalPosition, _currentTarget);
if (path == null || path.Count == 0) {
_gridPathRetryTimer = Mathf.Max(0.1f, GridRepathInterval);
return;
}
_gridPath.Clear();
_gridPath.AddRange(path);
_gridPathIndex = 0;
_gridPathActive = true;
_gridPathPending = false;
if (DebugDrawPath) QueueRedraw();
}
/// <summary>
/// 应用移动
/// </summary>
/// <param name="delta">时间间隔</param>
private void ApplyMovement(double delta) {
if (_usePhysicsMovement) {
MoveAndSlide();
return;
}
// 关闭碰撞时直接平移,避免 CharacterBody2D 在无碰撞层时不移动
GlobalPosition += Velocity * (float)delta;
}
/// <summary>
/// 更新面朝向动画
/// </summary>
/// <param name="velocity">当前速度</param>
private void UpdateFacingAnimation(Vector2 velocity) {
if (_phoneIdleActive || _phoneExitLocked) return;
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;
}
}
/// <summary>
/// 播放闲置动画
/// </summary>
private void PlayIdleAnimation() {
if (_phoneIdleActive || _phoneExitLocked) return;
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;
}
}
/// <summary>
/// 播放指定动画
/// </summary>
/// <param name="animationName">动画名称</param>
private void PlayAnimation(string animationName) {
if (_animationPlayer == null) return;
if (_animationPlayer.CurrentAnimation != animationName) _animationPlayer.Play(animationName);
}
/// <summary>
/// 动画结束回调
/// </summary>
/// <param name="animationName">动画名称</param>
private void OnAnimationFinished(StringName animationName) {
if (_animationPlayer == null) return;
if (animationName == "phone_up" && _phoneIdleActive) {
if (_animationPlayer.HasAnimation("phone_loop")) _animationPlayer.Play("phone_loop");
return;
}
if (animationName == "phone_down" && !_phoneIdleActive) {
_phoneExitLocked = false;
PlayIdleAnimation();
}
}
/// <summary>
/// 处理网格路径移动
/// </summary>
/// <param name="delta">时间间隔</param>
/// <returns>如果正在移动返回true</returns>
private bool ProcessGridPathMovement(double delta) {
if (_gridPathIndex >= _gridPath.Count) {
Velocity = Vector2.Zero;
if (_usePhysicsMovement) MoveAndSlide();
if (!_phoneIdleActive && !_phoneExitLocked) PlayIdleAnimation();
UpdateStuckTimer(delta);
_gridPathActive = false;
return true;
}
var target = _gridPath[_gridPathIndex];
var toNext = target - GlobalPosition;
if (toNext.LengthSquared() <= TargetReachDistance * TargetReachDistance) {
_gridPathIndex += 1;
return true;
}
if (delta <= 0.0) return true;
var axisVelocity = ToAxisVelocity(toNext);
var step = axisVelocity * MoveSpeed * (float)delta;
Velocity = step / (float)delta;
ApplyMovement(delta);
UpdateFacingAnimation(Velocity);
UpdateStuckTimer(delta);
return true;
}
/// <summary>
/// 转换为轴向速度
/// </summary>
/// <param name="delta">位移</param>
/// <returns>轴向速度</returns>
private Vector2 ToAxisVelocity(Vector2 delta) {
if (Mathf.Abs(delta.X) >= Mathf.Abs(delta.Y)) return new Vector2(Mathf.Sign(delta.X), 0);
return new Vector2(0, Mathf.Sign(delta.Y));
}
/// <summary>
/// 构建网格路径
/// </summary>
/// <param name="start">起点</param>
/// <param name="target">终点</param>
/// <returns>路径点列表</returns>
private List<Vector2> BuildGridPath(Vector2 start, Vector2 target) {
if (_campusController == null) _campusController = GetTree()?.CurrentScene as CampusController;
var path = _campusController?.RequestGridPath(start, target);
if (path == null || path.Count == 0) return null;
RemoveImmediateBacktracks(path);
return SimplifyGridPath(path);
}
/// <summary>
/// 移除直接回头的路径点
/// </summary>
/// <param name="path">路径</param>
private void RemoveImmediateBacktracks(List<Vector2> path) {
if (path == null || path.Count < 3) return;
var index = 2;
while (index < path.Count) {
if (path[index].DistanceTo(path[index - 2]) <= 0.01f) {
path.RemoveAt(index - 1);
index = Math.Max(2, index - 1);
continue;
}
index += 1;
}
}
/// <summary>
/// 简化网格路径(移除共线点)
/// </summary>
/// <param name="path">路径</param>
/// <returns>简化后的路径</returns>
private List<Vector2> SimplifyGridPath(List<Vector2> path) {
if (path == null || path.Count < 3) return path;
var simplified = new List<Vector2> { path[0] };
var lastDir = Vector2.Zero;
for (var i = 1; i < path.Count; i++) {
var delta = path[i] - path[i - 1];
if (delta.LengthSquared() < 0.01f) continue;
var axisDir = ToAxisVelocity(delta);
if (simplified.Count == 1) {
simplified.Add(path[i]);
lastDir = axisDir;
continue;
}
if (axisDir == lastDir) {
simplified[simplified.Count - 1] = path[i];
}
else {
simplified.Add(path[i]);
lastDir = axisDir;
}
}
return simplified;
}
/// <summary>
/// 面朝方向
/// </summary>
private enum FacingDirection {
Up,
Down,
Left,
Right
}
}