Завершение рефакторинга, фикс бага с запросом слайда

Теперь всё поделено по логическим классам, чтобы было помодульно, а также если выполняется запрос на слайд в момент, когда слайд не может быть выполнен, запрос отменяется
This commit is contained in:
MrTorex committed 2025-10-27 21:26:09 +03:00
1 parent e6879076a3
commit 5bf468673f
28 files changed
+809 -577

No files matched your search

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b36ec6a31fb74d608232394fd5df657c
timeCreated: 1761570832
@@ -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 режимы камеры
@@ -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();
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: fb497e2e267044a7a35f80b47c88c2c9
timeCreated: 1761570825
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d3be946108b44b33b99d2e3e4a75d47f
timeCreated: 1761571338
@@ -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)
{
_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;
}
// Устанавливаем перезарядку
_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;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5904fa03346e4751967d57500ab3d1cf
timeCreated: 1761571347
@@ -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();
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cfce19738ff34be286c953e33e515c46
timeCreated: 1761571364
@@ -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)
{
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e50ac1a55a4f4706b6385746eb9c6d1f
timeCreated: 1761571269
@@ -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);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 513b50847de24c619c380c15e084e932
timeCreated: 1761571281
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b3dfd8f0dbeb4e8690e11049cb7e2f55
timeCreated: 1761571298
@@ -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;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5ea8841f02ff4e17ab8ebd8c7a949f6f
timeCreated: 1761571326
@@ -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();
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 21cd95513cd44d52b6ae014403f9e3d1
timeCreated: 1761571312
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4028bee0a1774348bb81e0c56e400ddc
timeCreated: 1761570805
@@ -0,0 +1,17 @@
namespace Code.Gameplay.Character.Core
{
/*
* Перечисление режимов работы камеры
* Static - статичная камера без следования за целью
* FirstPerson - камера от первого лица (встроена в модель персонажа)
* ThirdPerson - камера от третьего лица (следует за персонажем на расстоянии)
* Cinematic - кинематографическая камера для скриптовых сцен
*/
public enum CameraMode
{
Static,
FirstPerson,
ThirdPerson,
Cinematic
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5a49105845a7435b9179e402fa7b8e30
timeCreated: 1761570884
@@ -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();
}
}
}
@@ -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; // Флаг указывающий, нажата ли кнопка слайда
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c3edde7108664ecc9b84a8096a3dec93
timeCreated: 1761570939
@@ -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)
{
}
}
}