Files
Hasebe/Assets/Code/OfficeGame/BattleMode/CameraBattleModeState.cs
T
2026-10-28 14:02:58 +03:00

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;
}
}
}