266 lines
12 KiB
C#
266 lines
12 KiB
C#
using Code.Infrastructure.Camera;
|
|
using Code.Infrastructure.Camera.CameraStates;
|
|
using Code.Infrastructure.Input;
|
|
using Cysharp.Threading.Tasks;
|
|
using UnityEngine;
|
|
using UnityEngine.InputSystem;
|
|
|
|
namespace Code.Game.OfficeGame.BattleMode
|
|
{
|
|
[Infrastructure.Generators.State(typeof(CameraMode), typeof(CameraStateMachineContext), ManualInherit = true)]
|
|
public partial class CameraBattleModeState : BaseCameraState
|
|
{
|
|
public bool battleDeciding = false;
|
|
private bool _rightClickHold = false;
|
|
|
|
private Vector2 _mouseDelta;
|
|
private Vector2 _rotation = Vector2.zero;
|
|
private Vector2 _movement;
|
|
private float _verticalMovement; // Если захотите добавить Space / Ctrl как в Minecraft
|
|
|
|
private float _sensitivityX = 0.1f;
|
|
private float _sensitivityY = 0.1f;
|
|
|
|
private float _moveSpeed = 12f;
|
|
private float _cameraRadius = 0.25f; // Должен быть больше nearClipPlane камеры (обычно 0.05 - 0.1)
|
|
private LayerMask _obstacleMask = ~0; // По умолчанию все слои (настройте, исключив триггеры/юнитов)
|
|
|
|
private const float SKIN_WIDTH = 0.015f;
|
|
private const int MAX_SLIDE_BOUNCES = 3;
|
|
|
|
// Буферы без аллокаций памяти для проверки застревания
|
|
private readonly Collider[] _overlapBuffer = new Collider[8];
|
|
private SphereCollider _dummySphere;
|
|
|
|
public CameraBattleModeState(CameraStateMachineContext context) : base(context)
|
|
{
|
|
}
|
|
|
|
public override CameraMode MyState => CameraMode.BattleMode;
|
|
|
|
public override UniTask EnterState()
|
|
{
|
|
Context.InputService.BindActionPerformed(
|
|
(int)ActionMappings.BattleMode,
|
|
(int)BattleModeActions.Movement,
|
|
SetMovement);
|
|
Context.InputService.BindActionCancelled(
|
|
(int)ActionMappings.BattleMode,
|
|
(int)BattleModeActions.Movement,
|
|
ResetMovement);
|
|
|
|
Context.InputService.BindActionPerformed(
|
|
(int)ActionMappings.BattleMode,
|
|
(int)BattleModeActions.Watchment,
|
|
LookPerformed);
|
|
Context.InputService.BindActionCancelled(
|
|
(int)ActionMappings.BattleMode,
|
|
(int)BattleModeActions.Watchment,
|
|
LookCancelled);
|
|
|
|
Context.InputService.BindActionPerformed(
|
|
(int)ActionMappings.BattleMode,
|
|
(int)BattleModeActions.RightClick,
|
|
RightClickPerformed);
|
|
Context.InputService.BindActionCancelled(
|
|
(int)ActionMappings.BattleMode,
|
|
(int)BattleModeActions.RightClick,
|
|
RightClickCancelled);
|
|
|
|
var camTransform = Context.cameraLoadedData.Camera.transform;
|
|
|
|
// 1. Синхронизируем стартовые углы с текущим поворотом камеры без рывка
|
|
Vector3 euler = camTransform.rotation.eulerAngles;
|
|
_rotation.x = euler.y;
|
|
_rotation.y = euler.x > 180f ? euler.x - 360f : euler.x;
|
|
|
|
// 2. Гарантируем, что nearClipPlane меньше радиуса сферы (иначе вплотную к стене будет видна дыра)
|
|
if (Context.cameraLoadedData.Camera.nearClipPlane >= _cameraRadius)
|
|
{
|
|
Context.cameraLoadedData.Camera.nearClipPlane = 0.05f;
|
|
}
|
|
|
|
EnsureDummySphere(camTransform);
|
|
|
|
return UniTask.CompletedTask;
|
|
}
|
|
|
|
public override UniTask ExitState()
|
|
{
|
|
Context.InputService.UnbindActionPerformed(
|
|
(int)ActionMappings.BattleMode,
|
|
(int)BattleModeActions.Movement,
|
|
SetMovement);
|
|
Context.InputService.UnbindActionCancelled(
|
|
(int)ActionMappings.BattleMode,
|
|
(int)BattleModeActions.Movement,
|
|
ResetMovement);
|
|
|
|
Context.InputService.UnbindActionPerformed(
|
|
(int)ActionMappings.BattleMode,
|
|
(int)BattleModeActions.Watchment,
|
|
LookPerformed);
|
|
Context.InputService.UnbindActionCancelled(
|
|
(int)ActionMappings.BattleMode,
|
|
(int)BattleModeActions.Watchment,
|
|
LookCancelled);
|
|
|
|
return UniTask.CompletedTask;
|
|
}
|
|
|
|
private void SetMovement(InputAction.CallbackContext obj) => _movement = obj.ReadValue<Vector2>();
|
|
private void ResetMovement(InputAction.CallbackContext obj) => _movement = Vector2.zero;
|
|
|
|
private void LookCancelled(InputAction.CallbackContext obj) => _mouseDelta = Vector2.zero;
|
|
private void LookPerformed(InputAction.CallbackContext obj) => _mouseDelta = obj.ReadValue<Vector2>();
|
|
|
|
private void RightClickCancelled(InputAction.CallbackContext obj) => _rightClickHold = false;
|
|
private void RightClickPerformed(InputAction.CallbackContext obj) => _rightClickHold = true;
|
|
|
|
public override void Update()
|
|
{
|
|
}
|
|
|
|
public override void LateUpdate()
|
|
{
|
|
var camTransform = Context.cameraLoadedData.Camera.transform;
|
|
float dt = Time.unscaledDeltaTime;
|
|
|
|
if (!(battleDeciding && !_rightClickHold))
|
|
{
|
|
_rotation.x += _mouseDelta.x * _sensitivityX;
|
|
_rotation.y -= _mouseDelta.y * _sensitivityY;
|
|
_rotation.y = Mathf.Clamp(_rotation.y, -89.9f, 89.9f);
|
|
|
|
camTransform.rotation = Quaternion.Euler(_rotation.y, _rotation.x, 0f);
|
|
}
|
|
|
|
Vector3 wishDir = camTransform.forward * _movement.y + camTransform.right * _movement.x;
|
|
|
|
// Если добавите кнопки вверх/вниз (Space/Ctrl), раскомментируйте:
|
|
// wishDir += Vector3.up * _verticalMovement;
|
|
|
|
if (wishDir.sqrMagnitude > 1f)
|
|
wishDir.Normalize();
|
|
|
|
Vector3 currentPos = camTransform.position;
|
|
|
|
currentPos = Depenetrate(currentPos);
|
|
|
|
if (wishDir.sqrMagnitude > 0.0001f)
|
|
{
|
|
Vector3 displacement = wishDir * (_moveSpeed * dt);
|
|
currentPos = CollideAndSlide(currentPos, displacement);
|
|
}
|
|
|
|
camTransform.position = currentPos;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Если камера оказалась зажата в коллайдере, мягко выталкивает её наружу
|
|
/// </summary>
|
|
private Vector3 Depenetrate(Vector3 position)
|
|
{
|
|
int count = Physics.OverlapSphereNonAlloc(
|
|
position,
|
|
_cameraRadius,
|
|
_overlapBuffer,
|
|
_obstacleMask,
|
|
QueryTriggerInteraction.Ignore);
|
|
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
Collider col = _overlapBuffer[i];
|
|
if (col == _dummySphere) continue;
|
|
|
|
if (Physics.ComputePenetration(
|
|
_dummySphere, position, Quaternion.identity,
|
|
col, col.transform.position, col.transform.rotation,
|
|
out Vector3 dir, out float dist))
|
|
{
|
|
position += dir * (dist + SKIN_WIDTH);
|
|
}
|
|
}
|
|
|
|
return position;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Алгоритм Quake/Source FlyMove: скольжение вдоль стен и по стыкам двух плоскостей
|
|
/// </summary>
|
|
private Vector3 CollideAndSlide(Vector3 position, Vector3 displacement)
|
|
{
|
|
Vector3 remaining = displacement;
|
|
Vector3 prevNormal = Vector3.zero;
|
|
|
|
for (int bounce = 0; bounce < MAX_SLIDE_BOUNCES; bounce++)
|
|
{
|
|
float dist = remaining.magnitude;
|
|
if (dist < 0.0001f) break;
|
|
|
|
Vector3 dir = remaining / dist;
|
|
|
|
if (Physics.SphereCast(
|
|
position,
|
|
_cameraRadius,
|
|
dir,
|
|
out RaycastHit hit,
|
|
dist + SKIN_WIDTH,
|
|
_obstacleMask,
|
|
QueryTriggerInteraction.Ignore))
|
|
{
|
|
// Подлетаем к препятствию вплотную, оставляя защитный зазор SKIN_WIDTH
|
|
float safeDist = Mathf.Max(0f, hit.distance - SKIN_WIDTH);
|
|
position += dir * safeDist;
|
|
|
|
float fraction = safeDist / dist;
|
|
Vector3 leftover = remaining * (1f - fraction);
|
|
|
|
// Если это первый удар о плоскость — просто скользим вдоль неё
|
|
if (bounce == 0)
|
|
{
|
|
remaining = Vector3.ProjectOnPlane(leftover, hit.normal);
|
|
prevNormal = hit.normal;
|
|
}
|
|
// Если после отскока от первой стены мы сразу упёрлись во вторую (острый угол / стык стены и пола)
|
|
else if (bounce == 1)
|
|
{
|
|
// Направляем вектор строго вдоль линии пересечения двух стен (Crease)
|
|
Vector3 crease = Vector3.Cross(prevNormal, hit.normal).normalized;
|
|
remaining = crease * Vector3.Dot(leftover, crease);
|
|
}
|
|
else
|
|
{
|
|
// На 3-м отскоке (глухой угол из 3 плоскостей) полностью гасим остаток
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
position += remaining;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return position;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Создаёт отключённый триггерный коллайдер на камере, нужный только для математического расчёта Physics.ComputePenetration
|
|
/// </summary>
|
|
private void EnsureDummySphere(Transform camTransform)
|
|
{
|
|
if (_dummySphere != null) return;
|
|
|
|
_dummySphere = camTransform.GetComponent<SphereCollider>();
|
|
if (_dummySphere == null)
|
|
{
|
|
_dummySphere = camTransform.gameObject.AddComponent<SphereCollider>();
|
|
}
|
|
|
|
_dummySphere.radius = _cameraRadius;
|
|
_dummySphere.isTrigger = true;
|
|
}
|
|
}
|
|
|
|
} |