diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Camera.meta b/Game/CourierOld/Assets/Code/Gameplay/Character/Camera.meta new file mode 100644 index 0000000..4293bf3 --- /dev/null +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Camera.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b36ec6a31fb74d608232394fd5df657c +timeCreated: 1761570832 \ No newline at end of file diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/CharacterCamera.cs b/Game/CourierOld/Assets/Code/Gameplay/Character/Camera/CharacterCamera.cs similarity index 94% rename from Game/CourierOld/Assets/Code/Gameplay/Character/CharacterCamera.cs rename to Game/CourierOld/Assets/Code/Gameplay/Character/Camera/CharacterCamera.cs index b54bf61..9278075 100644 --- a/Game/CourierOld/Assets/Code/Gameplay/Character/CharacterCamera.cs +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Camera/CharacterCamera.cs @@ -1,23 +1,9 @@ using System; +using Code.Gameplay.Character.Core; using UnityEngine; -namespace Code.Gameplay.Character +namespace Code.Gameplay.Character.Camera { - /* - * Перечисление режимов работы камеры - * Static - статичная камера без следования за целью - * FirstPerson - камера от первого лица (встроена в модель персонажа) - * ThirdPerson - камера от третьего лица (следует за персонажем на расстоянии) - * Cinematic - кинематографическая камера для скриптовых сцен - */ - public enum CameraMode - { - Static, - FirstPerson, - ThirdPerson, - Cinematic - } - /* * Скрипт, отвечающий за поведение камеры при передвижении * Поддерживает как 3rd-person, так и 1st-person режимы камеры diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/CharacterCamera.cs.meta b/Game/CourierOld/Assets/Code/Gameplay/Character/Camera/CharacterCamera.cs.meta similarity index 100% rename from Game/CourierOld/Assets/Code/Gameplay/Character/CharacterCamera.cs.meta rename to Game/CourierOld/Assets/Code/Gameplay/Character/Camera/CharacterCamera.cs.meta diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Character.cs b/Game/CourierOld/Assets/Code/Gameplay/Character/Character.cs deleted file mode 100644 index f55d67f..0000000 --- a/Game/CourierOld/Assets/Code/Gameplay/Character/Character.cs +++ /dev/null @@ -1,118 +0,0 @@ -using Code.Gameplay.Character; -using Code.Input; -using Reflex.Attributes; -using UnityEngine; - -/* - * Главный скрипт персонажа, объединяющий камеру и контроллер персонажа - * Обрабатывает ввод пользователя и распределяет его между компонентами - * Синхронизирует состояние камеры и персонажа - * Управляет переключением режимов камеры через Input System - */ -public class Character : MonoBehaviour -{ - [SerializeField] - private CharacterCamera characterCamera; // Компонент камеры персонажа - [SerializeField] - private CustomCharacterController customCharacterController; // Компонент управления персонажем - [SerializeField] - private Transform cameraFollowPoint; // Точка следования камеры (уровень глаз для FirstPerson) - - [Inject] - private IInputService _inputService; // Сервис ввода через DI - - // Флаг для отслеживания переключения режима камеры (чтобы избежать многократных срабатываний) - private bool _cameraModeToggleHandled; - - // Инициализация персонажа - private void Start() - { - // Настройка камеры для следования за точкой камеры - characterCamera.SetFollowTransform(cameraFollowPoint); - - // Включение Gameplay мапа ввода - _inputService.EnableMap(ActionMappings.Gameplay); - } - - // Обработка ввода для камеры (вращение и зум) - private void HandleCameraInput() - { - // Получаем данные ввода из Input System - var lookInput = _inputService.LookInput; - var scrollInput = _inputService.ZoomInput; - - // Передаем данные в камеру - characterCamera.UpdateWithInput(Time.deltaTime, scrollInput, - new Vector3(lookInput.x, lookInput.y, 0)); - } - - // Обработка ввода для движения персонажа - private void HandleCharacterInput() - { - // Создаем структуру ввода с полной информацией о камере - var inputs = new CharacterInputs(); - - // Получаем данные движения из Input System - var moveInput = _inputService.MoveInput; - inputs.MoveAxisForward = moveInput.y; // Vertical ось (W/S или Up/Down) - inputs.MoveAxisRight = moveInput.x; // Horizontal ось (A/D или Left/Right) - inputs.JumpPressed = _inputService.JumpPressed; // Прыжки (Space) - inputs.SprintPressed = _inputService.SprintPressed; // Спринт (Shift) - inputs.SlidePressed = _inputService.SlidePressed; // Слайд (Ctrl) - - // Передаем информацию о камере для адаптивного поведения - inputs.CameraRotation = characterCamera.transform.rotation; - inputs.CameraMode = characterCamera.CurrentMode; - inputs.CameraPlanarDirection = characterCamera.PlanarDirection; - - // Передаем ввод в контроллер персонажа - customCharacterController.SetInputs(ref inputs); - } - - // Обработка переключения режима камеры через Input System - private void HandleCameraModeInput() - { - // Получаем состояние переключения режима камеры из Input System - var togglePressed = _inputService.ToggleCameraModePressed; - - switch (togglePressed) - { - // Обрабатываем нажатие только один раз при первом обнаружении - case true when !_cameraModeToggleHandled: - ToggleCameraMode(); - _cameraModeToggleHandled = true; - break; - // Сбрасываем флаг когда кнопка отпущена - case false: - _cameraModeToggleHandled = false; - break; - } - } - - // Метод для переключения режимов камеры - private void ToggleCameraMode() - { - var currentMode = characterCamera.CurrentMode; - var newMode = currentMode == CameraMode.FirstPerson ? - CameraMode.ThirdPerson : CameraMode.FirstPerson; - - characterCamera.SetCameraMode(newMode); - } - - // Обновление логики каждый кадр - private void Update() - { - // Обработка ввода для движения в Update (физика) - HandleCharacterInput(); - - // Обработка переключения режима камеры - HandleCameraModeInput(); - } - - // Обновление после всех вычислений (идеально для камеры) - private void LateUpdate() - { - // Обработка ввода для камеры в LateUpdate (после движения персонажа) - HandleCameraInput(); - } -} \ No newline at end of file diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Controller.meta b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller.meta new file mode 100644 index 0000000..072516c --- /dev/null +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fb497e2e267044a7a35f80b47c88c2c9 +timeCreated: 1761570825 \ No newline at end of file diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities.meta b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities.meta new file mode 100644 index 0000000..e682207 --- /dev/null +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d3be946108b44b33b99d2e3e4a75d47f +timeCreated: 1761571338 \ No newline at end of file diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities/SlideMechanic.cs b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities/SlideMechanic.cs new file mode 100644 index 0000000..f3fc114 --- /dev/null +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities/SlideMechanic.cs @@ -0,0 +1,122 @@ +using KinematicCharacterController; +using UnityEngine; +using Code.Gameplay.Character.Core; + +namespace Code.Gameplay.Character.Controller.Abilities +{ + /* + * Механика слайда для персонажа + * Отвечает за управление состоянием, таймерами и скоростью слайда + */ + public class SlideMechanic + { + // Скорость слайда + private readonly float _slideSpeed; + // Длительность слайда в секундах + private readonly float _slideDuration; + // Время перезарядки слайда + private readonly float _slideCooldown; + + // Таймер слайда + private float _slideTimer; + // Таймер перезарядки слайда + private float _slideCooldownTimer; + // Направление слайда + private Vector3 _slideDirection; + // Флаг запроса слайда + private bool _slideRequested; + + // Свойство-флаг, активен ли слайд + public bool IsSliding { get; private set; } + + // Конструктор механики слайда + public SlideMechanic(float slideSpeed, float slideDuration, float slideCooldown) + { + _slideSpeed = slideSpeed; + _slideDuration = slideDuration; + _slideCooldown = slideCooldown; + } + + // Обновляем таймер перезарядки слайда + public void TickCooldown(float deltaTime) + { + if (_slideCooldownTimer > 0f) + { + _slideCooldownTimer -= deltaTime; + } + } + + // Обработка слайда + public void RequestSlide(bool slidePressed) + { + if (slidePressed && !_slideRequested && _slideCooldownTimer <= 0f) + { + _slideRequested = true; + } + } + + // Начало слайда + public void TryStartSlide(KinematicCharacterMotor motor, + CameraMode currentCameraMode, + Quaternion firstPersonCameraRotation, + Vector3 lookInputVector) + { + // Проверяем условия для старта слайда + if (!_slideRequested || !motor.GroundingStatus.IsStableOnGround || _slideCooldownTimer > 0f) + return; + + IsSliding = true; + _slideTimer = _slideDuration; + + // НАПРАВЛЕНИЕ СЛАЙДА - ВСЕГДА ВПЕРЕД ПО КАМЕРЕ + // Используем направление камеры, но проецируем на горизонтальную плоскость + var slideDir = currentCameraMode == CameraMode.FirstPerson ? + firstPersonCameraRotation * Vector3.forward : + lookInputVector; + + // Проецируем на горизонтальную плоскость и нормализуем + _slideDirection = Vector3.ProjectOnPlane(slideDir, motor.CharacterUp).normalized; + + // Если направление нулевое (персонаж стоит на месте), используем направление вперед + if (_slideDirection.sqrMagnitude < 0.01f) + { + _slideDirection = motor.CharacterForward; + } + + // Принудительно отключаем спринт и сбрасываем прыжок во время слайда + // (это делается в вызывающем коде, но логика здесь) + + // Устанавливаем перезарядку + _slideCooldownTimer = _slideCooldown; + _slideRequested = false; + } + + // Обработка слайда + public void HandleSlide(ref Vector3 currentVelocity, float deltaTime, KinematicCharacterMotor motor) + { + // Уменьшаем таймер слайда + _slideTimer -= deltaTime; + + // Если время слайда вышло, персонаж в воздухе - заканчиваем слайд + if (_slideTimer <= 0f || !motor.GroundingStatus.IsStableOnGround) + { + EndSlide(); + return; + } + + // Устанавливаем скорость слайда вперед + // Используем прямую установку скорости вместо сложных вычислений + currentVelocity = _slideDirection * _slideSpeed; + + // Добавляем небольшую вертикальную компоненту для лучшего скольжения по неровностям + currentVelocity += motor.CharacterUp * 2f; + } + + // Завершение слайда + public void EndSlide() + { + IsSliding = false; + _slideTimer = 0f; + } + } +} \ No newline at end of file diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities/SlideMechanic.cs.meta b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities/SlideMechanic.cs.meta new file mode 100644 index 0000000..0520e3a --- /dev/null +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities/SlideMechanic.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5904fa03346e4751967d57500ab3d1cf +timeCreated: 1761571347 \ No newline at end of file diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities/WallClimbMechanic.cs b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities/WallClimbMechanic.cs new file mode 100644 index 0000000..decac53 --- /dev/null +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities/WallClimbMechanic.cs @@ -0,0 +1,51 @@ +using KinematicCharacterController; +using UnityEngine; + +namespace Code.Gameplay.Character.Controller.Abilities +{ + /* + * Механика залазания на стены + * Обрабатывает столкновения и применяет вертикальный импульс при ударе о стену с достаточной скоростью + */ + public class WallClimbMechanic + { + // Коэффициент преобразования горизонтальной скорости в вертикальную при залазании на стену + private readonly float _wallClimbSpeedFactor; + // Минимальная горизонтальная скорость для начала залазания на стену + private readonly float _minWallClimbSpeed; + + // Конструктор механики залазания на стену + public WallClimbMechanic(float wallClimbSpeedFactor, float minWallClimbSpeed) + { + _wallClimbSpeedFactor = wallClimbSpeedFactor; + _minWallClimbSpeed = minWallClimbSpeed; + } + + // Обработка столкновения при движении (вызывается при ударе о препятствие) + public void OnMovementHit(KinematicCharacterMotor motor, Vector3 hitNormal) + { + // Проверяем, что это вертикальная или почти вертикальная поверхность (стена) + var dotWithUp = Vector3.Dot(hitNormal, motor.CharacterUp); + if (Mathf.Abs(dotWithUp) > 0.1f) return; // Не стена — это пол или потолок + + // Получаем текущую скорость персонажа + var currentVelocity = motor.Velocity; + + // Проекция скорости на направление к стене (отрицательная, если летим в стену) + var velocityIntoWall = Vector3.Dot(currentVelocity, -hitNormal); + + // Если движемся в стену с достаточной скоростью + if (velocityIntoWall > _minWallClimbSpeed) + { + // Преобразуем часть горизонтальной скорости в вертикальную + var verticalBoost = velocityIntoWall * _wallClimbSpeedFactor; + + // Добавляем вертикальный импульс через BaseVelocity + motor.BaseVelocity += motor.CharacterUp * verticalBoost; + + // Предотвращаем заземление и повторное применение в этом кадре + motor.ForceUnground(); + } + } + } +} \ No newline at end of file diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities/WallClimbMechanic.cs.meta b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities/WallClimbMechanic.cs.meta new file mode 100644 index 0000000..fdf7d59 --- /dev/null +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities/WallClimbMechanic.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: cfce19738ff34be286c953e33e515c46 +timeCreated: 1761571364 \ No newline at end of file diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/CustomCharacterController.cs b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/CustomCharacterController.cs new file mode 100644 index 0000000..96b2763 --- /dev/null +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/CustomCharacterController.cs @@ -0,0 +1,213 @@ +using Code.Gameplay.Character.Controller.Abilities; +using Code.Gameplay.Character.Controller.Input; +using Code.Gameplay.Character.Controller.Movement; +using Code.Gameplay.Character.Core; +using KinematicCharacterController; +using UnityEngine; + +namespace Code.Gameplay.Character.Controller +{ + /* + * Скрипт, отвечающий за передвижение персонажа + * Реализует физическое движение персонажа с использованием Kinematic Character Controller + * Обрабатывает ввод пользователя, применяет движение и вращение с учетом поверхности + * + * Источник: Tvtig, https://www.youtube.com/watch?v=9ibpY64wBYM + */ + public class CustomCharacterController : MonoBehaviour, ICharacterController + { + [SerializeField] + private KinematicCharacterMotor motor; // Движок физического перемещения персонажа + + [SerializeField] + private float + maxStableMoveSpeed = 8f, // Максимальная скорость движения по устойчивой поверхности + sprintMoveSpeed = 12f, // Максимальная скорость спринта + slideSpeed = 20f, // Скорость слайда + slideDuration = 0.8f, // Длительность слайда в секундах + slideCooldown = 1.5f, // Время перезарядки слайда + stableMovementSharpness = 15f, // Резкость изменения скорости движения (чем выше - тем резче) + orientationSharpness = 10f, // Резкость изменения вращения персонажа (чем выше - тем резче) + jumpSpeed = 10f, // Скорость прыжка + wallClimbSpeedFactor = 0.8f, // Коэффициент преобразования горизонтальной скорости в вертикальную при залазании на стену + minWallClimbSpeed = 9f; // Минимальная горизонтальная скорость для начала залазания на стену + + [SerializeField] + private Vector3 gravity = new(0, -30f, 0); // Вектор, отвечающий за применение гравитации + + // Подсистемы + private readonly CharacterInputHandler _inputHandler; + private readonly SlideMechanic _slideMechanic; + private readonly WallClimbMechanic _wallClimbMechanic; + private readonly GroundMovementSystem _groundMovement; + private readonly AirMovementSystem _airMovement; + + // Конструктор без параметров, инициализирующий контроллер + public CustomCharacterController() + { + _inputHandler = new CharacterInputHandler(); + _slideMechanic = new SlideMechanic(slideSpeed, slideDuration, slideCooldown); + _wallClimbMechanic = new WallClimbMechanic(wallClimbSpeedFactor, minWallClimbSpeed); + _groundMovement = new GroundMovementSystem(maxStableMoveSpeed, sprintMoveSpeed, stableMovementSharpness, jumpSpeed); + _airMovement = new AirMovementSystem(gravity); + } + + // Текущий режим камеры для адаптации поведения + private CameraMode _currentCameraMode = CameraMode.ThirdPerson; + + // Инициализация контроллера + private void Start() + { + motor.CharacterController = this; // Регистрация этого контроллера в движке + _inputHandler.FirstPersonCameraRotation = motor.Transform.rotation; // Начальное вращение совпадает с персонажем + } + + // Обработка и преобразование входных данных от персонажа + public void SetInputs(ref CharacterInputs inputs) + { + // Обновляем таймер перезарядки слайда + _slideMechanic.TickCooldown(Time.deltaTime); + + // Если активен слайд - игнорируем весь остальной ввод + if (_slideMechanic.IsSliding) + { + // Во время слайда не обрабатываем движение, прыжки и спринт + _inputHandler.MoveInputVector = Vector3.zero; + _inputHandler.LookInputVector = Vector3.one; + _inputHandler.IsSprinting = false; + _groundMovement.CancelJumpRequest(); + return; + } + + // Сохраняем текущий режим камеры + _currentCameraMode = inputs.CameraMode; + + // Обработка слайда + _slideMechanic.RequestSlide(inputs.SlidePressed); + + // Обработка остального ввода + _inputHandler.ProcessInputs(ref inputs, motor); + + // Передаём запрос прыжка напрямую в систему движения по земле + _groundMovement.RequestJump(inputs.JumpPressed); + } + + // Обновление вращения персонажа на основе входных данных + public void UpdateRotation(ref Quaternion currentRotation, float deltaTime) + { + // ВО ВРЕМЯ СЛАЙДА НЕ ОБНОВЛЯЕМ ВРАЩЕНИЕ - ПЕРСОНАЖ СМОТРИТ ПРЯМО + if (_slideMechanic.IsSliding) return; + + switch (_currentCameraMode) + { + // В FirstPerson режиме применяем вращение камеры напрямую к персонажу (только горизонталь) + case CameraMode.FirstPerson when _inputHandler.ShouldApplyCameraRotation: + // Немедленно применяем горизонтальное вращение камеры к персонажу + currentRotation = _inputHandler.FirstPersonCameraRotation; + break; + // В ThirdPerson режиме плавно поворачиваем персонаж в направлении движения + case CameraMode.ThirdPerson: + { + // Вращение применяется только если есть направление движения и разрешена резкость вращения + if (_inputHandler.LookInputVector.sqrMagnitude > 0 && orientationSharpness > 0) + { + // Плавная интерполяция текущего направления к целевому с экспоненциальным сглаживанием + var smoothedLookInputDirection = Vector3.Slerp( + motor.CharacterForward, + _inputHandler.LookInputVector, + 1f - Mathf.Exp(-orientationSharpness * deltaTime) + ).normalized; + + // Установка нового вращения смотря в сглаженном направлении + currentRotation = Quaternion.LookRotation(smoothedLookInputDirection, motor.CharacterUp); + } + + break; + } + // Заготовка на будущее (если понадобится) + case CameraMode.Static: + case CameraMode.Cinematic: + default: + break; + } + } + + // Обновление скорости движения персонажа + public void UpdateVelocity(ref Vector3 currentVelocity, float deltaTime) + { + // Проверяем запрос слайда и запускаем его если можно + if (!_slideMechanic.IsSliding) + { + _slideMechanic.TryStartSlide(motor, _currentCameraMode, _inputHandler.FirstPersonCameraRotation, _inputHandler.LookInputVector); + } + // Если активен слайд - обрабатываем отдельно + else + { + _slideMechanic.HandleSlide(ref currentVelocity, deltaTime, motor); + return; + } + + // Если персонаж находится на земле + if (motor.GroundingStatus.IsStableOnGround) + { + _groundMovement.UpdateVelocity(ref currentVelocity, _inputHandler, motor, deltaTime); + } + // Если персонаж не находится на земле + else + { + _airMovement.UpdateVelocity(ref currentVelocity, deltaTime); + // Отменяем прыжок (фикс проблемы с двойным прыжком после приземления) + _groundMovement.CancelJumpRequest(); + } + } + + // Метод вызывается перед обновлением персонажа (можно использовать для подготовки) + public void BeforeCharacterUpdate(float deltaTime) + { + } + + // Метод вызывается после обработки столкновения с землей + public void PostGroundingUpdate(float deltaTime) + { + // Если во время слайда персонаж оторвался от земли - заканчиваем слайд + if (_slideMechanic.IsSliding && !motor.GroundingStatus.IsStableOnGround) + { + _slideMechanic.EndSlide(); + } + } + + // Метод вызывается после обновления персонажа (можно использовать для пост-обработки) + public void AfterCharacterUpdate(float deltaTime) + { + } + + // Проверка валидности коллайдера для обработки столкновений + public bool IsColliderValidForCollisions(Collider coll) + { + return true; // Все коллайдеры считаются валидными для столкновений + } + + // Обработка столкновения с землей (вызывается при приземлении) + public void OnGroundHit(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint, ref HitStabilityReport hitStabilityReport) + { + } + + // Обработка столкновения при движении (вызывается при ударе о препятствие) + public void OnMovementHit(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint, + ref HitStabilityReport hitStabilityReport) + { + _wallClimbMechanic.OnMovementHit(motor, hitNormal); + } + + // Обработка отчета о стабильности столкновения + public void ProcessHitStabilityReport(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint, Vector3 atCharacterPosition, + Quaternion atCharacterRotation, ref HitStabilityReport hitStabilityReport) + { + } + + // Обработка дискретных столкновений (для триггеров и специальных коллайдеров) + public void OnDiscreteCollisionDetected(Collider hitCollider) + { + } + } +} \ No newline at end of file diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/CustomCharacterController.cs.meta b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/CustomCharacterController.cs.meta similarity index 100% rename from Game/CourierOld/Assets/Code/Gameplay/Character/CustomCharacterController.cs.meta rename to Game/CourierOld/Assets/Code/Gameplay/Character/Controller/CustomCharacterController.cs.meta diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Input.meta b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Input.meta new file mode 100644 index 0000000..afd584d --- /dev/null +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Input.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e50ac1a55a4f4706b6385746eb9c6d1f +timeCreated: 1761571269 \ No newline at end of file diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Input/CharacterInputHandler.cs b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Input/CharacterInputHandler.cs new file mode 100644 index 0000000..4e2f21c --- /dev/null +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Input/CharacterInputHandler.cs @@ -0,0 +1,115 @@ +using UnityEngine; +using Code.Gameplay.Character.Core; +using KinematicCharacterController; + +namespace Code.Gameplay.Character.Controller.Input +{ + /* + * Обработчик входных данных для персонажа + * Преобразует сырые данные ввода в векторы движения и направления взгляда + */ + public class CharacterInputHandler + { + // Векторы движения и направления взгляда + public Vector3 MoveInputVector { get; set; } + public Vector3 LookInputVector { get; set; } + + // Флаг-свойство указывающий, что активен режим спринта + public bool IsSprinting { get; set; } + + // Вращение камеры в FirstPerson режиме для синхронизации (только горизонтальная компонента) + public Quaternion FirstPersonCameraRotation { get; set; } + + // Флаг-свойство указывающий, что нужно применить вращение камеры к персонажу + public bool ShouldApplyCameraRotation { get; private set; } + + // Обработка и преобразование входных данных от персонажа + public void ProcessInputs(ref CharacterInputs inputs, KinematicCharacterMotor motor) + { + // Сохраняем текущий режим камеры + var currentCameraMode = inputs.CameraMode; + + // Обработка спринта + IsSprinting = inputs is { SprintPressed: true, MoveAxisForward: > 0.1f }; // Спринт только при движении вперёд + + // Нормализация вектора движения и ограничение его величины до 1 + var moveInputVector = + Vector3.ClampMagnitude(new Vector3(inputs.MoveAxisRight, 0, inputs.MoveAxisForward), 1f); + + // Раздельная обработка в зависимости от режима камеры + if (currentCameraMode == CameraMode.FirstPerson) + { + HandleFirstPersonInputs(ref inputs, moveInputVector, motor); + } + else + { + HandleThirdPersonInputs(ref inputs, moveInputVector, motor); + } + } + + // Обработка ввода для FirstPerson режима + private void HandleFirstPersonInputs(ref CharacterInputs inputs, Vector3 moveInputVector, KinematicCharacterMotor motor) + { + // Сохраняем вращение камеры для применения в UpdateRotation (только горизонтальная компонента) + FirstPersonCameraRotation = ExtractHorizontalRotation(inputs.CameraRotation, motor); + ShouldApplyCameraRotation = true; + + // В FirstPerson режиме движение всегда относительно направления камеры + // Получаем направление камеры в горизонтальной плоскости + var cameraPlanarDirection = inputs.CameraPlanarDirection; + + // Создание вращения на основе направления камеры + var cameraPlanarRotation = Quaternion.LookRotation(cameraPlanarDirection, motor.CharacterUp); + + // Преобразование локального ввода в мировые координаты относительно камеры + MoveInputVector = cameraPlanarRotation * moveInputVector; + + // В FirstPerson режиме направление взгляда всегда совпадает с направлением камеры (горизонтально) + LookInputVector = cameraPlanarDirection; + } + + // Обработка ввода для ThirdPerson режима + private void HandleThirdPersonInputs(ref CharacterInputs inputs, Vector3 moveInputVector, KinematicCharacterMotor motor) + { + // Сбрасываем флаг применения вращения камеры + ShouldApplyCameraRotation = false; + + // Получение направления камеры в горизонтальной плоскости (игнорируя наклон) + var cameraPlanarDirection = inputs.CameraPlanarDirection; + + // Резервный расчет направления, если основное направление параллельно вертикали + if (cameraPlanarDirection.sqrMagnitude == 0f) + { + cameraPlanarDirection = Vector3.ProjectOnPlane(inputs.CameraRotation * Vector3.up, motor.CharacterUp); + } + + // Создание вращения на основе направления камеры + var cameraPlanarRotation = Quaternion.LookRotation(cameraPlanarDirection, motor.CharacterUp); + + // Преобразование локального ввода в мировые координаты относительно камеры + MoveInputVector = cameraPlanarRotation * moveInputVector; + + // Направление взгляда совпадает с направлением движения (нормализованное) + LookInputVector = MoveInputVector.normalized; + } + + // Извлечение только горизонтальной компоненты вращения (игнорируя наклон камеры) + private static Quaternion ExtractHorizontalRotation(Quaternion cameraRotation, KinematicCharacterMotor motor) + { + // Получаем вектор направления камеры + var cameraForward = cameraRotation * Vector3.forward; + + // Проецируем на горизонтальную плоскость + var horizontalForward = Vector3.ProjectOnPlane(cameraForward, motor.CharacterUp).normalized; + + // Если проекция вырождена, используем текущее направление персонажа + if (horizontalForward.sqrMagnitude < 0.01f) + { + horizontalForward = motor.CharacterForward; + } + + // Создаем вращение только по горизонтали + return Quaternion.LookRotation(horizontalForward, motor.CharacterUp); + } + } +} \ No newline at end of file diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Input/CharacterInputHandler.cs.meta b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Input/CharacterInputHandler.cs.meta new file mode 100644 index 0000000..c904140 --- /dev/null +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Input/CharacterInputHandler.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 513b50847de24c619c380c15e084e932 +timeCreated: 1761571281 \ No newline at end of file diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Movement.meta b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Movement.meta new file mode 100644 index 0000000..4637a28 --- /dev/null +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Movement.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b3dfd8f0dbeb4e8690e11049cb7e2f55 +timeCreated: 1761571298 \ No newline at end of file diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Movement/AirMovementSystem.cs b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Movement/AirMovementSystem.cs new file mode 100644 index 0000000..71676cc --- /dev/null +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Movement/AirMovementSystem.cs @@ -0,0 +1,26 @@ +using UnityEngine; + +namespace Code.Gameplay.Character.Controller.Movement +{ + /* + * Система движения персонажа в воздухе + */ + public class AirMovementSystem + { + // Вектор, отвечающий за применение гравитации + private readonly Vector3 _gravity; + + // Конструктор системы движения в воздухе + public AirMovementSystem(Vector3 gravity) + { + _gravity = gravity; + } + + // Обновление скорости движения персонажа в воздухе + public void UpdateVelocity(ref Vector3 currentVelocity, float deltaTime) + { + // Применяем гравитацию + currentVelocity += _gravity * deltaTime; + } + } +} \ No newline at end of file diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Movement/AirMovementSystem.cs.meta b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Movement/AirMovementSystem.cs.meta new file mode 100644 index 0000000..53aecf8 --- /dev/null +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Movement/AirMovementSystem.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5ea8841f02ff4e17ab8ebd8c7a949f6f +timeCreated: 1761571326 \ No newline at end of file diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Movement/GroundMovementSystem.cs b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Movement/GroundMovementSystem.cs new file mode 100644 index 0000000..767c0b8 --- /dev/null +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Movement/GroundMovementSystem.cs @@ -0,0 +1,87 @@ +using UnityEngine; +using Code.Gameplay.Character.Controller.Input; +using KinematicCharacterController; + +namespace Code.Gameplay.Character.Controller.Movement +{ + /* + * Система движения персонажа по устойчивой поверхности + */ + public class GroundMovementSystem + { + // Максимальная скорость движения по устойчивой поверхности + private readonly float _maxStableMoveSpeed; + // Максимальная скорость спринта + private readonly float _sprintMoveSpeed; + // Резкость изменения скорости движения (чем выше - тем резче) + private readonly float _stableMovementSharpness; + // Скорость прыжка + private readonly float _jumpSpeed; + // Флаг указывающий, что был запрошен прыжок + private bool _jumpRequested; + + // Конструктор системы движения на земле + public GroundMovementSystem(float maxStableMoveSpeed, float sprintMoveSpeed, float stableMovementSharpness, float jumpSpeed) + { + _maxStableMoveSpeed = maxStableMoveSpeed; + _sprintMoveSpeed = sprintMoveSpeed; + _stableMovementSharpness = stableMovementSharpness; + _jumpSpeed = jumpSpeed; + } + + // Метод для установки запроса на прыжок извне + public void RequestJump(bool jumpPressed) + { + if (jumpPressed) + _jumpRequested = true; + } + + // Метод для отмены запроса на прыжок извне + public void CancelJumpRequest() + { + _jumpRequested = false; + } + + // Обновление скорости движения персонажа на земле + public void UpdateVelocity( + ref Vector3 currentVelocity, + CharacterInputHandler inputHandler, + KinematicCharacterMotor motor, + float deltaTime) + { + // Сохранение текущей величины скорости + var currentVelocityMagnitude = currentVelocity.magnitude; + + // Получение нормали поверхности для корректного движения по наклонным плоскостям + var effectiveGroundNormal = motor.GroundingStatus.GroundNormal; + + // Переориентация текущей скорости вдоль поверхности + currentVelocity = motor.GetDirectionTangentToSurface(currentVelocity, effectiveGroundNormal) * + currentVelocityMagnitude; + + // Переориентация входного вектора движения вдоль поверхности + var inputRight = Vector3.Cross(inputHandler.MoveInputVector, motor.CharacterUp); + var reorientedInput = Vector3.Cross(effectiveGroundNormal, inputRight).normalized * + inputHandler.MoveInputVector.magnitude; + + // Расчет целевой скорости движения с учетом ограничения максимальной скорости и спринта + var targetSpeed = inputHandler.IsSprinting ? _sprintMoveSpeed : _maxStableMoveSpeed; + var targetMovementVelocity = reorientedInput * targetSpeed; + + // Плавная интерполяция текущей скорости к целевой с экспоненциальным сглаживанием + currentVelocity = Vector3.Lerp( + currentVelocity, + targetMovementVelocity, + 1f - Mathf.Exp(-_stableMovementSharpness * deltaTime) + ); + + // Если запрошен прыжок + if (_jumpRequested) + { + currentVelocity += motor.CharacterUp * _jumpSpeed - Vector3.Project(currentVelocity, motor.CharacterUp); + _jumpRequested = false; // Сброс сразу после применения + motor.ForceUnground(); + } + } + } +} \ No newline at end of file diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Movement/GroundMovementSystem.cs.meta b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Movement/GroundMovementSystem.cs.meta new file mode 100644 index 0000000..ad5d533 --- /dev/null +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Movement/GroundMovementSystem.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 21cd95513cd44d52b6ae014403f9e3d1 +timeCreated: 1761571312 \ No newline at end of file diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Core.meta b/Game/CourierOld/Assets/Code/Gameplay/Character/Core.meta new file mode 100644 index 0000000..fb072d7 --- /dev/null +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Core.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 4028bee0a1774348bb81e0c56e400ddc +timeCreated: 1761570805 \ No newline at end of file diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Core/CameraMode.cs b/Game/CourierOld/Assets/Code/Gameplay/Character/Core/CameraMode.cs new file mode 100644 index 0000000..92dfaf6 --- /dev/null +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Core/CameraMode.cs @@ -0,0 +1,17 @@ +namespace Code.Gameplay.Character.Core +{ + /* + * Перечисление режимов работы камеры + * Static - статичная камера без следования за целью + * FirstPerson - камера от первого лица (встроена в модель персонажа) + * ThirdPerson - камера от третьего лица (следует за персонажем на расстоянии) + * Cinematic - кинематографическая камера для скриптовых сцен + */ + public enum CameraMode + { + Static, + FirstPerson, + ThirdPerson, + Cinematic + } +} \ No newline at end of file diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Core/CameraMode.cs.meta b/Game/CourierOld/Assets/Code/Gameplay/Character/Core/CameraMode.cs.meta new file mode 100644 index 0000000..1ae16fb --- /dev/null +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Core/CameraMode.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5a49105845a7435b9179e402fa7b8e30 +timeCreated: 1761570884 \ No newline at end of file diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Core/Character.cs b/Game/CourierOld/Assets/Code/Gameplay/Character/Core/Character.cs new file mode 100644 index 0000000..f0b82be --- /dev/null +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Core/Character.cs @@ -0,0 +1,117 @@ +using Code.Gameplay.Character.Camera; +using Code.Gameplay.Character.Controller; +using Code.Input; +using Reflex.Attributes; +using UnityEngine; + +namespace Code.Gameplay.Character.Core +{ +/* + * Главный скрипт персонажа, объединяющий камеру и контроллер персонажа + * Обрабатывает ввод пользователя и распределяет его между компонентами + * Синхронизирует состояние камеры и персонажа + * Управляет переключением режимов камеры через Input System + */ + public class Character : MonoBehaviour + { + [SerializeField] private CharacterCamera characterCamera; // Компонент камеры персонажа + [SerializeField] private CustomCharacterController customCharacterController; // Компонент управления персонажем + [SerializeField] private Transform cameraFollowPoint; // Точка следования камеры (уровень глаз для FirstPerson) + + [Inject] private IInputService _inputService; // Сервис ввода через DI + + // Флаг для отслеживания переключения режима камеры (чтобы избежать многократных срабатываний) + private bool _cameraModeToggleHandled; + + // Инициализация персонажа + private void Start() + { + // Настройка камеры для следования за точкой камеры + characterCamera.SetFollowTransform(cameraFollowPoint); + + // Включение Gameplay мапа ввода + _inputService.EnableMap(ActionMappings.Gameplay); + } + + // Обработка ввода для камеры (вращение и зум) + private void HandleCameraInput() + { + // Получаем данные ввода из Input System + var lookInput = _inputService.LookInput; + var scrollInput = _inputService.ZoomInput; + + // Передаем данные в камеру + characterCamera.UpdateWithInput(Time.deltaTime, scrollInput, + new Vector3(lookInput.x, lookInput.y, 0)); + } + + // Обработка ввода для движения персонажа + private void HandleCharacterInput() + { + // Создаем структуру ввода с полной информацией о камере + var inputs = new CharacterInputs(); + + // Получаем данные движения из Input System + var moveInput = _inputService.MoveInput; + inputs.MoveAxisForward = moveInput.y; // Vertical ось (W/S или Up/Down) + inputs.MoveAxisRight = moveInput.x; // Horizontal ось (A/D или Left/Right) + inputs.JumpPressed = _inputService.JumpPressed; // Прыжки (Space) + inputs.SprintPressed = _inputService.SprintPressed; // Спринт (Shift) + inputs.SlidePressed = _inputService.SlidePressed; // Слайд (Ctrl) + + // Передаем информацию о камере для адаптивного поведения + inputs.CameraRotation = characterCamera.transform.rotation; + inputs.CameraMode = characterCamera.CurrentMode; + inputs.CameraPlanarDirection = characterCamera.PlanarDirection; + + // Передаем ввод в контроллер персонажа + customCharacterController.SetInputs(ref inputs); + } + + // Обработка переключения режима камеры через Input System + private void HandleCameraModeInput() + { + // Получаем состояние переключения режима камеры из Input System + var togglePressed = _inputService.ToggleCameraModePressed; + + switch (togglePressed) + { + // Обрабатываем нажатие только один раз при первом обнаружении + case true when !_cameraModeToggleHandled: + ToggleCameraMode(); + _cameraModeToggleHandled = true; + break; + // Сбрасываем флаг когда кнопка отпущена + case false: + _cameraModeToggleHandled = false; + break; + } + } + + // Метод для переключения режимов камеры + private void ToggleCameraMode() + { + var currentMode = characterCamera.CurrentMode; + var newMode = currentMode == CameraMode.FirstPerson ? CameraMode.ThirdPerson : CameraMode.FirstPerson; + + characterCamera.SetCameraMode(newMode); + } + + // Обновление логики каждый кадр + private void Update() + { + // Обработка ввода для движения в Update (физика) + HandleCharacterInput(); + + // Обработка переключения режима камеры + HandleCameraModeInput(); + } + + // Обновление после всех вычислений (идеально для камеры) + private void LateUpdate() + { + // Обработка ввода для камеры в LateUpdate (после движения персонажа) + HandleCameraInput(); + } + } +} \ No newline at end of file diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Character.cs.meta b/Game/CourierOld/Assets/Code/Gameplay/Character/Core/Character.cs.meta similarity index 100% rename from Game/CourierOld/Assets/Code/Gameplay/Character/Character.cs.meta rename to Game/CourierOld/Assets/Code/Gameplay/Character/Core/Character.cs.meta diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Core/CharacterInputs.cs b/Game/CourierOld/Assets/Code/Gameplay/Character/Core/CharacterInputs.cs new file mode 100644 index 0000000..3557d5c --- /dev/null +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Core/CharacterInputs.cs @@ -0,0 +1,20 @@ +using UnityEngine; + +namespace Code.Gameplay.Character.Core +{ + /* + * Структура для хранения входных данных от персонажа + * Содержит информацию о движении и состоянии камеры + */ + public struct CharacterInputs + { + public float MoveAxisForward; // Ввод движения вперед/назад (W/S или Up/Down) + public float MoveAxisRight; // Ввод движения влево/вправо (A/D или Left/Right) + public Quaternion CameraRotation; // Текущее вращение камеры для определения направления движения + public CameraMode CameraMode; // Текущий режим камеры для адаптации поведения + public Vector3 CameraPlanarDirection; // Направление камеры в горизонтальной плоскости + public bool JumpPressed; // Флаг указывающий, нажата ли кнопка прыжка + public bool SprintPressed; // Флаг указывающий, нажата ли кнопка спринта + public bool SlidePressed; // Флаг указывающий, нажата ли кнопка слайда + } +} \ No newline at end of file diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Core/CharacterInputs.cs.meta b/Game/CourierOld/Assets/Code/Gameplay/Character/Core/CharacterInputs.cs.meta new file mode 100644 index 0000000..3154df5 --- /dev/null +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Core/CharacterInputs.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c3edde7108664ecc9b84a8096a3dec93 +timeCreated: 1761570939 \ No newline at end of file diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/CustomCharacterController.cs b/Game/CourierOld/Assets/Code/Gameplay/Character/CustomCharacterController.cs deleted file mode 100644 index 631052e..0000000 --- a/Game/CourierOld/Assets/Code/Gameplay/Character/CustomCharacterController.cs +++ /dev/null @@ -1,443 +0,0 @@ -using KinematicCharacterController; -using UnityEngine; - -namespace Code.Gameplay.Character -{ - /* - * Структура для хранения входных данных от персонажа - * Содержит информацию о движении и состоянии камеры - */ - public struct CharacterInputs - { - public float MoveAxisForward; // Ввод движения вперед/назад (W/S или Up/Down) - public float MoveAxisRight; // Ввод движения влево/вправо (A/D или Left/Right) - public Quaternion CameraRotation; // Текущее вращение камеры для определения направления движения - public CameraMode CameraMode; // Текущий режим камеры для адаптации поведения - public Vector3 CameraPlanarDirection; // Направление камеры в горизонтальной плоскости - public bool JumpPressed; // Флаг указывающий, нажата ли кнопка прыжка - public bool SprintPressed; // Флаг указывающий, нажата ли кнопка спринта - public bool SlidePressed; // Флаг указывающий, нажата ли кнопка слайда - } - - /* - * Скрипт, отвечающий за передвижение персонажа - * Реализует физическое движение персонажа с использованием Kinematic Character Controller - * Обрабатывает ввод пользователя, применяет движение и вращение с учетом поверхности - * - * Источник: Tvtig, https://www.youtube.com/watch?v=9ibpY64wBYM - */ - public class CustomCharacterController : MonoBehaviour, ICharacterController - { - [SerializeField] - private KinematicCharacterMotor motor; // Движок физического перемещения персонажа - - [SerializeField] - private float - maxStableMoveSpeed = 8f, // Максимальная скорость движения по устойчивой поверхности - sprintMoveSpeed = 12f, // Максимальная скорость спринта - slideSpeed = 20f, // Скорость слайда - slideDuration = 0.8f, // Длительность слайда в секундах - slideCooldown = 1.5f, // Время перезарядки слайда - stableMovementSharpness = 15f, // Резкость изменения скорости движения (чем выше - тем резче) - orientationSharpness = 10f, // Резкость изменения вращения персонажа (чем выше - тем резче) - jumpSpeed = 10f, // Скорость прыжка - wallClimbSpeedFactor = 0.8f, // Коэффициент преобразования горизонтальной скорости в вертикальную при залазании на стену - minWallClimbSpeed = 9f; // Минимальная горизонтальная скорость для начала залазания на стену - - [SerializeField] - private Vector3 gravity = new(0, -30f, 0); // Вектор, отвечающий за применение гравитации - - // Векторы движения и направления взгляда - private Vector3 _moveInputVector, _lookInputVector; - - // Текущий режим камеры для адаптации поведения - private CameraMode _currentCameraMode = CameraMode.ThirdPerson; - - // Вращение камеры в FirstPerson режиме для синхронизации (только горизонтальная компонента) - private Quaternion _firstPersonCameraRotation; - - private bool - _shouldApplyCameraRotation, // Флаг указывающий, что нужно применить вращение камеры к персонажу - _jumpRequested, // Флаг указывающий, что был запрошен прыжок - _isSprinting; // Флаг указывающий, что активен режим спринта - - // Переменные для управления слайдом - private bool _isSliding; // Флаг активного слайда - private float _slideTimer; // Таймер слайда - private float _slideCooldownTimer; // Таймер перезарядки слайда - private Vector3 _slideDirection; // Направление слайда - private bool _slideRequested; // Флаг запроса слайда - - // Флаг для предотвращения повторного залазания на стену в один и тот же кадр - private bool _wallClimbAppliedThisFrame; - - // Инициализация контроллера - private void Start() - { - motor.CharacterController = this; // Регистрация этого контроллера в движке - _firstPersonCameraRotation = motor.Transform.rotation; // Начальное вращение совпадает с персонажем - } - - // Обработка и преобразование входных данных от персонажа - public void SetInputs(ref CharacterInputs inputs) - { - // Обновляем таймер перезарядки слайда - if (_slideCooldownTimer > 0f) - { - _slideCooldownTimer -= Time.deltaTime; - } - - // Если активен слайд - игнорируем весь остальной ввод - if (_isSliding) - { - // Во время слайда не обрабатываем движение, прыжки и спринт - _moveInputVector = Vector3.zero; - _lookInputVector = Vector3.one; - _isSprinting = false; - return; - } - - // Сохраняем текущий режим камеры - _currentCameraMode = inputs.CameraMode; - - // Обработка спринта - _isSprinting = inputs is { SprintPressed: true, MoveAxisForward: > 0.1f }; // Спринт только при движении вперёд - - // Обработка слайда - if (inputs.SlidePressed && !_slideRequested && _slideCooldownTimer <= 0f) - { - _slideRequested = true; - } - - // Нормализация вектора движения и ограничение его величины до 1 - var moveInputVector = - Vector3.ClampMagnitude(new Vector3(inputs.MoveAxisRight, 0, inputs.MoveAxisForward), 1f); - - // Раздельная обработка в зависимости от режима камеры - if (_currentCameraMode == CameraMode.FirstPerson) - { - HandleFirstPersonInputs(ref inputs, moveInputVector); - } - else - { - HandleThirdPersonInputs(ref inputs, moveInputVector); - } - - // При запрашивании прыжка - if (inputs.JumpPressed) - _jumpRequested = true; - } - - // Начало слайда - private void StartSlide() - { - // Проверяем условия для старта слайда - if (!motor.GroundingStatus.IsStableOnGround || _slideCooldownTimer > 0f) - { - _slideRequested = false; - return; - } - - _isSliding = true; - _slideTimer = slideDuration; - - // НАПРАВЛЕНИЕ СЛАЙДА - ВСЕГДА ВПЕРЕД ПО КАМЕРЕ - // Используем направление камеры, но проецируем на горизонтальную плоскость - var slideDir = _currentCameraMode == CameraMode.FirstPerson ? - _firstPersonCameraRotation * Vector3.forward : - _lookInputVector; - - // Проецируем на горизонтальную плоскость и нормализуем - _slideDirection = Vector3.ProjectOnPlane(slideDir, motor.CharacterUp).normalized; - - // Если направление нулевое (персонаж стоит на месте), используем направление вперед - if (_slideDirection.sqrMagnitude < 0.01f) - { - _slideDirection = motor.CharacterForward; - } - - // Принудительно отключаем спринт и сбрасываем прыжок во время слайда - _isSprinting = false; - _jumpRequested = false; - _slideRequested = false; - - // Устанавливаем перезарядку - _slideCooldownTimer = slideCooldown; - } - - // Обработка слайда - private void HandleSlide(ref Vector3 currentVelocity, float deltaTime) - { - // Уменьшаем таймер слайда - _slideTimer -= deltaTime; - - // Если время слайда вышло, персонаж в воздухе - заканчиваем слайд - if (_slideTimer <= 0f || !motor.GroundingStatus.IsStableOnGround) - { - EndSlide(); - return; - } - - // Устанавливаем скорость слайда вперед - // Используем прямую установку скорости вместо сложных вычислений - currentVelocity = _slideDirection * slideSpeed; - - // Добавляем небольшую вертикальную компоненту для лучшего скольжения по неровностям - currentVelocity += motor.CharacterUp * 2f; - } - - // Завершение слайда - private void EndSlide() - { - _isSliding = false; - _slideTimer = 0f; - } - - // Обработка ввода для FirstPerson режима - private void HandleFirstPersonInputs(ref CharacterInputs inputs, Vector3 moveInputVector) - { - // Сохраняем вращение камеры для применения в UpdateRotation (только горизонтальная компонента) - _firstPersonCameraRotation = ExtractHorizontalRotation(inputs.CameraRotation); - _shouldApplyCameraRotation = true; - - // В FirstPerson режиме движение всегда относительно направления камеры - // Получаем направление камеры в горизонтальной плоскости - var cameraPlanarDirection = inputs.CameraPlanarDirection; - - // Создание вращения на основе направления камеры - var cameraPlanarRotation = Quaternion.LookRotation(cameraPlanarDirection, motor.CharacterUp); - - // Преобразование локального ввода в мировые координаты относительно камеры - _moveInputVector = cameraPlanarRotation * moveInputVector; - - // В FirstPerson режиме направление взгляда всегда совпадает с направлением камеры (горизонтально) - _lookInputVector = cameraPlanarDirection; - } - - // Обработка ввода для ThirdPerson режима - private void HandleThirdPersonInputs(ref CharacterInputs inputs, Vector3 moveInputVector) - { - // Сбрасываем флаг применения вращения камеры - _shouldApplyCameraRotation = false; - - // Получение направления камеры в горизонтальной плоскости (игнорируя наклон) - var cameraPlanarDirection = inputs.CameraPlanarDirection; - - // Резервный расчет направления, если основное направление параллельно вертикали - if (cameraPlanarDirection.sqrMagnitude == 0f) - { - cameraPlanarDirection = Vector3.ProjectOnPlane(inputs.CameraRotation * Vector3.up, motor.CharacterUp); - } - - // Создание вращения на основе направления камеры - var cameraPlanarRotation = Quaternion.LookRotation(cameraPlanarDirection, motor.CharacterUp); - - // Преобразование локального ввода в мировые координаты относительно камеры - _moveInputVector = cameraPlanarRotation * moveInputVector; - - // Направление взгляда совпадает с направлением движения (нормализованное) - _lookInputVector = _moveInputVector.normalized; - } - - // Извлечение только горизонтальной компоненты вращения (игнорируя наклон камеры) - private Quaternion ExtractHorizontalRotation(Quaternion cameraRotation) - { - // Получаем вектор направления камеры - var cameraForward = cameraRotation * Vector3.forward; - - // Проецируем на горизонтальную плоскость - var horizontalForward = Vector3.ProjectOnPlane(cameraForward, motor.CharacterUp).normalized; - - // Если проекция вырождена, используем текущее направление персонажа - if (horizontalForward.sqrMagnitude < 0.01f) - { - horizontalForward = motor.CharacterForward; - } - - // Создаем вращение только по горизонтали - return Quaternion.LookRotation(horizontalForward, motor.CharacterUp); - } - - // Обновление вращения персонажа на основе входных данных - public void UpdateRotation(ref Quaternion currentRotation, float deltaTime) - { - // ВО ВРЕМЯ СЛАЙДА НЕ ОБНОВЛЯЕМ ВРАЩЕНИЕ - ПЕРСОНАЖ СМОТРИТ ПРЯМО - if (_isSliding) return; - - switch (_currentCameraMode) - { - // В FirstPerson режиме применяем вращение камеры напрямую к персонажу (только горизонталь) - case CameraMode.FirstPerson when _shouldApplyCameraRotation: - // Немедленно применяем горизонтальное вращение камеры к персонажу - currentRotation = _firstPersonCameraRotation; - _shouldApplyCameraRotation = false; - break; - // В ThirdPerson режиме плавно поворачиваем персонаж в направлении движения - case CameraMode.ThirdPerson: - { - // Вращение применяется только если есть направление движения и разрешена резкость вращения - if (_lookInputVector.sqrMagnitude > 0 && orientationSharpness > 0) - { - // Плавная интерполяция текущего направления к целевому с экспоненциальным сглаживанием - var smoothedLookInputDirection = Vector3.Slerp( - motor.CharacterForward, - _lookInputVector, - 1f - Mathf.Exp(-orientationSharpness * deltaTime) - ).normalized; - - // Установка нового вращения смотря в сглаженном направлении - currentRotation = Quaternion.LookRotation(smoothedLookInputDirection, motor.CharacterUp); - } - - break; - } - // Заготовка на будущее (если понадобится) - case CameraMode.Static: - case CameraMode.Cinematic: - default: - break; - } - } - - // Обновление скорости движения персонажа - public void UpdateVelocity(ref Vector3 currentVelocity, float deltaTime) - { - // Сбрасываем флаг залазания на стену в начале каждого кадра - _wallClimbAppliedThisFrame = false; - - // Проверяем запрос слайда и запускаем его если можно - if (_slideRequested && !_isSliding && motor.GroundingStatus.IsStableOnGround && _slideCooldownTimer <= 0f) - { - StartSlide(); - } - - // Если активен слайд - обрабатываем отдельно - if (_isSliding) - { - HandleSlide(ref currentVelocity, deltaTime); - return; - } - - // Если персонаж находится на земле - if (motor.GroundingStatus.IsStableOnGround) - { - // Сохранение текущей величины скорости - var currentVelocityMagnitude = currentVelocity.magnitude; - - // Получение нормали поверхности для корректного движения по наклонным плоскостям - var effectiveGroundNormal = motor.GroundingStatus.GroundNormal; - - // Переориентация текущей скорости вдоль поверхности - currentVelocity = motor.GetDirectionTangentToSurface(currentVelocity, effectiveGroundNormal) * - currentVelocityMagnitude; - - // Переориентация входного вектора движения вдоль поверхности - var inputRight = Vector3.Cross(_moveInputVector, motor.CharacterUp); - var reorientedInput = Vector3.Cross(effectiveGroundNormal, inputRight).normalized * - _moveInputVector.magnitude; - - // Расчет целевой скорости движения с учетом ограничения максимальной скорости и спринта - var targetSpeed = _isSprinting ? sprintMoveSpeed : maxStableMoveSpeed; - var targetMovementVelocity = reorientedInput * targetSpeed; - - // Плавная интерполяция текущей скорости к целевой с экспоненциальным сглаживанием - currentVelocity = Vector3.Lerp( - currentVelocity, - targetMovementVelocity, - 1f - Mathf.Exp(-stableMovementSharpness * deltaTime) - ); - - // Если запрошен прыжок - if (_jumpRequested) - { - currentVelocity += motor.CharacterUp * jumpSpeed - Vector3.Project(currentVelocity, motor.CharacterUp); - _jumpRequested = false; - motor.ForceUnground(); - } - } - // Если персонаж не находится на земле - else - { - // Применяем гравитацию - currentVelocity += gravity * deltaTime; - - // Отменяем прыжок (фикс проблемы с двойным прыжком после приземления) - _jumpRequested = false; - } - } - - // Метод вызывается перед обновлением персонажа (можно использовать для подготовки) - public void BeforeCharacterUpdate(float deltaTime) - { - } - - // Метод вызывается после обработки столкновения с землей - public void PostGroundingUpdate(float deltaTime) - { - // Если во время слайда персонаж оторвался от земли - заканчиваем слайд - if (_isSliding && !motor.GroundingStatus.IsStableOnGround) - { - EndSlide(); - } - } - - // Метод вызывается после обновления персонажа (можно использовать для пост-обработки) - public void AfterCharacterUpdate(float deltaTime) - { - } - - // Проверка валидности коллайдера для обработки столкновений - public bool IsColliderValidForCollisions(Collider coll) - { - return true; // Все коллайдеры считаются валидными для столкновений - } - - // Обработка столкновения с землей (вызывается при приземлении) - public void OnGroundHit(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint, ref HitStabilityReport hitStabilityReport) - { - } - - // Обработка столкновения при движении (вызывается при ударе о препятствие) - public void OnMovementHit(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint, - ref HitStabilityReport hitStabilityReport) - { - // Игнорируем, если уже залезли на стену в этом кадре или если слайд активен - if (_wallClimbAppliedThisFrame || _isSliding) return; - - // Проверяем, что это вертикальная или почти вертикальная поверхность (стена) - var dotWithUp = Vector3.Dot(hitNormal, motor.CharacterUp); - if (Mathf.Abs(dotWithUp) > 0.1f) return; // Не стена — это пол или потолок - - // Получаем текущую скорость персонажа - var currentVelocity = motor.Velocity; - - // Проекция скорости на направление к стене (отрицательная, если летим в стену) - var velocityIntoWall = Vector3.Dot(currentVelocity, -hitNormal); - - // Если движемся в стену с достаточной скоростью - if (velocityIntoWall > minWallClimbSpeed) - { - // Преобразуем часть горизонтальной скорости в вертикальную - var verticalBoost = velocityIntoWall * wallClimbSpeedFactor; - - // Добавляем вертикальный импульс через BaseVelocity - motor.BaseVelocity += motor.CharacterUp * verticalBoost; - - // Предотвращаем заземление и повторное применение в этом кадре - motor.ForceUnground(); - _wallClimbAppliedThisFrame = true; - } - } - - // Обработка отчета о стабильности столкновения - public void ProcessHitStabilityReport(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint, Vector3 atCharacterPosition, - Quaternion atCharacterRotation, ref HitStabilityReport hitStabilityReport) - { - } - - // Обработка дискретных столкновений (для триггеров и специальных коллайдеров) - public void OnDiscreteCollisionDetected(Collider hitCollider) - { - } - } -} \ No newline at end of file