using System; 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 { private Vector2 _mouseDelta; private Vector2 _rotation = Vector2.zero; private Vector2 _movement; private float _verticalMovement; // Если захотите добавить Space / Ctrl как в Minecraft [Header("Look Settings")] private float _sensitivityX = 0.1f; private float _sensitivityY = 0.1f; [Header("Spectator Flight Settings")] 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); 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(); 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(); public override void Update() { } public override void LateUpdate() { var camTransform = Context.cameraLoadedData.Camera.transform; float dt = Time.unscaledDeltaTime; // --- 1. СВОБОДНЫЙ ОБЗОР ОТ ПЕРВОГО ЛИЦА (Yaw + Pitch) --- _rotation.x += _mouseDelta.x * _sensitivityX; _rotation.y -= _mouseDelta.y * _sensitivityY; _rotation.y = Mathf.Clamp(_rotation.y, -89.9f, 89.9f); // Используем мировое вращение, чтобы камера всегда держала горизонт ровно (без завала Roll) camTransform.rotation = Quaternion.Euler(_rotation.y, _rotation.x, 0f); // --- 2. ВЕКТОР ПОЛЁТА КАК В CS / MINECRAFT SPECTATOR --- 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; // --- 3. ВЫТАЛКИВАНИЕ (Если камера начала кадр внутри стены или двери) --- currentPos = Depenetrate(currentPos); // --- 4. ДВИЖЕНИЕ И СКОЛЬЖЕНИЕ ПО СТЕНАМ И УГЛАМ --- if (wishDir.sqrMagnitude > 0.0001f) { Vector3 displacement = wishDir * (_moveSpeed * dt); currentPos = CollideAndSlide(currentPos, displacement); } camTransform.position = currentPos; } /// /// Если камера оказалась зажата в коллайдере, мягко выталкивает её наружу /// 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; } /// /// Алгоритм Quake/Source FlyMove: скольжение вдоль стен и по стыкам двух плоскостей /// 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; } /// /// Создаёт отключённый триггерный коллайдер на камере, нужный только для математического расчёта Physics.ComputePenetration /// private void EnsureDummySphere(Transform camTransform) { if (_dummySphere != null) return; _dummySphere = camTransform.GetComponent(); if (_dummySphere == null) { _dummySphere = camTransform.gameObject.AddComponent(); } _dummySphere.radius = _cameraRadius; _dummySphere.isTrigger = true; } } }