282 lines
12 KiB
C#
282 lines
12 KiB
C#
using Code.Game.Gameplay.Player;
|
|
using Code.OfficeGame.BattleMode.Actions.Deciders;
|
|
using Code.OfficeGame.BattleMode.Timeline;
|
|
using KinematicCharacterController;
|
|
using UnityEngine;
|
|
using UnityEngine.AI;
|
|
|
|
namespace Code.OfficeGame.BattleMode.Actions
|
|
{
|
|
public class BattleActionMove : BattleAction
|
|
{
|
|
private const float StoppingDistance = 0.15f;
|
|
private const float RotateDistance = 1f;
|
|
private const float NavMeshSampleRadius = 1.5f;
|
|
private const float TurnSpeedDegPerSec = 720f; // Скорость вращения на месте (градусов/сек)
|
|
|
|
private int MoveStateHash => Animator.StringToHash("WalkBlend");
|
|
|
|
public override BattleActionType ActionType => BattleActionType.Move;
|
|
public override DeciderType DeciderType => DeciderType.Position;
|
|
|
|
public override BattleActionData MountAndGetFrames(EntityProperties entity, BattleActionDecisionResult result)
|
|
{
|
|
MovementBattleActionData data = new();
|
|
var positionDecisionResult = (PositionBattleActionDeciderResult)result;
|
|
|
|
Vector3 startPos = entity.KinematicCharacterMotor.TransientPosition;
|
|
Vector3 targetPos = positionDecisionResult.Position;
|
|
|
|
// 1. Проверяем дистанцию в плоскости XZ
|
|
Vector3 toTargetXZ = targetPos - startPos;
|
|
toTargetXZ.y = 0f;
|
|
float distanceXZ = toTargetXZ.magnitude;
|
|
|
|
// ЕСЛИ ВПЛОТНУЮ: отключаем перемещение, оставляем только поворот
|
|
if (distanceXZ <= StoppingDistance)
|
|
{
|
|
data.IsRotateOnly = true;
|
|
data.Corners = null;
|
|
|
|
float angle = Quaternion.Angle(entity.KinematicCharacterMotor.TransientRotation, positionDecisionResult.Rotation);
|
|
|
|
// Если персонаж уже смотрит в нужную сторону — 0 кадров
|
|
if (angle < 1f)
|
|
{
|
|
data.TotalFrames = 0;
|
|
}
|
|
else
|
|
{
|
|
float stepPerFrame = TurnSpeedDegPerSec * BattleTimeline.TickDelta;
|
|
data.TotalFrames = Mathf.Max(2, Mathf.CeilToInt(angle / stepPerFrame));
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
// 2. ДВИЖЕНИЕ: проецируем точки строго на полигоны NavMesh
|
|
bool hasStart = NavMesh.SamplePosition(startPos, out NavMeshHit startHit, NavMeshSampleRadius, NavMesh.AllAreas);
|
|
bool hasTarget = NavMesh.SamplePosition(targetPos, out NavMeshHit targetHit, NavMeshSampleRadius, NavMesh.AllAreas);
|
|
|
|
if (!hasStart || !hasTarget)
|
|
{
|
|
data.TotalFrames = 0;
|
|
return data;
|
|
}
|
|
|
|
Vector3 validStart = startHit.position;
|
|
Vector3 validTarget = targetHit.position;
|
|
|
|
// 3. Прямая видимость: если препятствий нет, идём по идеальной прямой без квантования полигонов NavMesh
|
|
if (!NavMesh.Raycast(validStart, validTarget, out _, NavMesh.AllAreas))
|
|
{
|
|
data.Corners = new[] { startPos, validTarget };
|
|
data.CurrentCornerIndex = 1;
|
|
data.TotalFrames = CalculatePathFrames(data.Corners, entity.MovementController);
|
|
return data;
|
|
}
|
|
|
|
// 4. Обход препятствий: строим путь и отсекаем частичные обрубки (PathPartial)
|
|
NavMeshPath path = new();
|
|
if (NavMesh.CalculatePath(validStart, validTarget, NavMesh.AllAreas, path)
|
|
&& path.status == NavMeshPathStatus.PathComplete
|
|
&& path.corners.Length > 1)
|
|
{
|
|
// Привязываем нулевую и финальную точки к фактическим координатам
|
|
path.corners[0] = startPos;
|
|
path.corners[^1] = validTarget;
|
|
|
|
data.Corners = path.corners;
|
|
data.CurrentCornerIndex = 1;
|
|
data.TotalFrames = CalculatePathFrames(data.Corners, entity.MovementController);
|
|
}
|
|
else
|
|
{
|
|
// Если NavMesh не смог построить полный путь (PathPartial/Invalid) — не даём ходить огрызками
|
|
data.TotalFrames = 0;
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
public override void Tick(
|
|
float tickDelta,
|
|
EntityProperties entity,
|
|
BattleActionDecisionResult battleActionDecisionResult,
|
|
BattleActionData battleActionData)
|
|
{
|
|
if (battleActionData is not MovementBattleActionData movementActionData
|
|
|| battleActionDecisionResult is not PositionBattleActionDeciderResult positionDecisionResult)
|
|
{
|
|
battleActionData.Finished = true;
|
|
return;
|
|
}
|
|
|
|
var movementController = entity.MovementController;
|
|
|
|
// -------------------------------------------------------------
|
|
// РЕЖИМ 1: ТОЛЬКО ПОВОРОТ НА МЕСТЕ (КОГДА ПОЗИЦИЯ ВПЛОТНУЮ)
|
|
// -------------------------------------------------------------
|
|
if (movementActionData.IsRotateOnly)
|
|
{
|
|
movementActionData.CurrentFrame++;
|
|
|
|
float angle = Quaternion.Angle(movementController.Motor.TransientRotation, positionDecisionResult.Rotation);
|
|
int framesLeft = Mathf.Max(1, movementActionData.TotalFrames - movementActionData.CurrentFrame + 1);
|
|
float stepAngle = angle / framesLeft;
|
|
|
|
Quaternion nextRot = Quaternion.RotateTowards(
|
|
movementController.Motor.TransientRotation,
|
|
positionDecisionResult.Rotation,
|
|
stepAngle
|
|
);
|
|
|
|
entity.KinematicCharacterMotor.SetRotation(nextRot);
|
|
StopMovementAndFinish(movementController);
|
|
|
|
KinematicCharacterSystem.Simulate(
|
|
tickDelta,
|
|
KinematicCharacterSystem.CharacterMotors,
|
|
KinematicCharacterSystem.PhysicsMovers
|
|
);
|
|
|
|
if (movementActionData.CurrentFrame >= movementActionData.TotalFrames || angle <= 0.5f)
|
|
{
|
|
entity.KinematicCharacterMotor.SetRotation(positionDecisionResult.Rotation);
|
|
battleActionData.Finished = true;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// РЕЖИМ 2: ПЕРЕДВИЖЕНИЕ ПО ПУТИ
|
|
// -------------------------------------------------------------
|
|
if (movementActionData.Corners == null || movementActionData.CurrentCornerIndex >= movementActionData.Corners.Length)
|
|
{
|
|
entity.KinematicCharacterMotor.SetRotation(positionDecisionResult.Rotation);
|
|
StopMovementAndFinish(movementController);
|
|
battleActionData.Finished = true;
|
|
return;
|
|
}
|
|
|
|
if (!movementActionData.WasStarted)
|
|
{
|
|
movementActionData.WasStarted = true;
|
|
float transitionDurationSeconds = 10 * BattleTimeline.TickDelta;
|
|
entity.animator.CrossFadeInFixedTime(MoveStateHash, transitionDurationSeconds);
|
|
}
|
|
|
|
Vector3 currentPos = entity.KinematicCharacterMotor.TransientPosition;
|
|
|
|
// Пропускаем пройденные углы
|
|
while (movementActionData.CurrentCornerIndex < movementActionData.Corners.Length)
|
|
{
|
|
Vector3 toCorner = movementActionData.Corners[movementActionData.CurrentCornerIndex] - currentPos;
|
|
toCorner.y = 0f;
|
|
|
|
if (toCorner.sqrMagnitude <= StoppingDistance * StoppingDistance)
|
|
movementActionData.CurrentCornerIndex++;
|
|
else
|
|
break;
|
|
}
|
|
|
|
// Если все точки пройдены до шага симуляции
|
|
if (movementActionData.CurrentCornerIndex >= movementActionData.Corners.Length)
|
|
{
|
|
entity.KinematicCharacterMotor.SetRotation(positionDecisionResult.Rotation);
|
|
StopMovementAndFinish(movementController);
|
|
KinematicCharacterSystem.Simulate(
|
|
tickDelta,
|
|
KinematicCharacterSystem.CharacterMotors,
|
|
KinematicCharacterSystem.PhysicsMovers
|
|
);
|
|
|
|
battleActionData.Finished = true;
|
|
return;
|
|
}
|
|
|
|
Vector3 targetCorner = movementActionData.Corners[movementActionData.CurrentCornerIndex];
|
|
Vector3 moveDirection = targetCorner - currentPos;
|
|
moveDirection.y = 0f;
|
|
|
|
float distance = moveDirection.magnitude;
|
|
Vector3 moveNormal = distance > 0.0001f ? moveDirection / distance : Vector3.zero;
|
|
|
|
bool isLastCorner = movementActionData.CurrentCornerIndex == movementActionData.Corners.Length - 1;
|
|
bool preLastPoint = isLastCorner && distance < RotateDistance;
|
|
|
|
float maxStep = movementController.MaxStableMoveSpeed * tickDelta;
|
|
|
|
float inputMagnitude = (isLastCorner && distance < maxStep && maxStep > 0f)
|
|
? distance / maxStep
|
|
: 1f;
|
|
|
|
Quaternion lookRotation = preLastPoint
|
|
? positionDecisionResult.Rotation
|
|
: (moveNormal.sqrMagnitude > 0.0001f
|
|
? Quaternion.LookRotation(moveNormal, movementController.Motor.CharacterUp)
|
|
: movementController.Motor.TransientRotation);
|
|
|
|
var aiInputs = new AICharacterInputs
|
|
{
|
|
MoveVector = moveNormal * inputMagnitude,
|
|
Look = lookRotation
|
|
};
|
|
|
|
movementController.SetInputs(ref aiInputs);
|
|
|
|
KinematicCharacterSystem.Simulate(
|
|
tickDelta,
|
|
KinematicCharacterSystem.CharacterMotors,
|
|
KinematicCharacterSystem.PhysicsMovers
|
|
);
|
|
|
|
// Проверка достижения конечной точки после физического тика
|
|
Vector3 postMovePos = movementController.Motor.TransientPosition;
|
|
Vector3 toFinal = movementActionData.Corners[^1] - postMovePos;
|
|
toFinal.y = 0f;
|
|
|
|
if (isLastCorner && toFinal.sqrMagnitude <= StoppingDistance * StoppingDistance)
|
|
{
|
|
entity.KinematicCharacterMotor.SetRotation(positionDecisionResult.Rotation);
|
|
StopMovementAndFinish(movementController);
|
|
battleActionData.Finished = true;
|
|
}
|
|
}
|
|
|
|
private void StopMovementAndFinish(MovementController movementController)
|
|
{
|
|
var emptyInputs = new AICharacterInputs
|
|
{
|
|
MoveVector = Vector3.zero,
|
|
Look = movementController.Motor.TransientRotation
|
|
};
|
|
movementController.SetInputs(ref emptyInputs);
|
|
movementController.Motor.BaseVelocity = Vector3.zero;
|
|
}
|
|
|
|
private int CalculatePathFrames(Vector3[] corners, MovementController movementController)
|
|
{
|
|
if (corners == null || corners.Length < 2)
|
|
return 0;
|
|
|
|
float totalDistanceXZ = 0f;
|
|
|
|
for (int i = 1; i < corners.Length; i++)
|
|
{
|
|
Vector3 segment = corners[i] - corners[i - 1];
|
|
segment.y = 0f;
|
|
totalDistanceXZ += segment.magnitude;
|
|
}
|
|
|
|
float effectiveDistance = Mathf.Max(0f, totalDistanceXZ - StoppingDistance);
|
|
float stepPerFrame = movementController.MaxStableMoveSpeed * BattleTimeline.TickDelta;
|
|
|
|
if (stepPerFrame <= 0.0001f)
|
|
return 0;
|
|
|
|
return Mathf.Max(1, Mathf.CeilToInt(effectiveDistance / stepPerFrame));
|
|
}
|
|
}
|
|
} |