From 0e188efe59cbdbeeba190ae530db65feb737ed04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C5=A1a=20Ceviel?= <39222425+MrTorex@users.noreply.github.com> Date: Wed, 29 Oct 2025 14:59:53 +0300 Subject: [PATCH] =?UTF-8?q?=D0=98=D0=BD=D0=B8=D1=88=D0=B8=D0=B0=D0=BB=20?= =?UTF-8?q?=D0=BC=D0=B5=D1=85=D0=B0=D0=BD=D0=B8=D0=BA=D0=B0=20=D0=BD=D0=B0?= =?UTF-8?q?=D0=BA=D0=BB=D0=BE=D0=BD=D0=B0=20=D0=BF=D1=80=D0=B8=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BB=D0=B0=D0=B7=D0=B0=D0=BD=D0=B8=D0=B8=20=D0=BD=D0=B0?= =?UTF-8?q?=20=D1=81=D1=82=D0=B5=D0=BD=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Нуждается в доработке (как минимум чтобы вращалось в правильную сторону), но вроде-как выглядит прикольно --- .../Assets/Code/Configs/CharacterConfig.cs | 6 ++ .../Controller/Abilities/WallClimbMechanic.cs | 8 +- .../Controller/Abilities/WallLeanMechanic.cs | 79 +++++++++++++++++++ .../Abilities/WallLeanMechanic.cs.meta | 3 + .../Controller/CustomCharacterController.cs | 55 ++++++++----- .../Configs/App/CharacterConfig.asset | 3 + .../Assets/Resources/Scenes/MovementDev.unity | 54 +++++++++---- 7 files changed, 170 insertions(+), 38 deletions(-) create mode 100644 Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities/WallLeanMechanic.cs create mode 100644 Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities/WallLeanMechanic.cs.meta diff --git a/Game/CourierOld/Assets/Code/Configs/CharacterConfig.cs b/Game/CourierOld/Assets/Code/Configs/CharacterConfig.cs index 6aad509..d650184 100644 --- a/Game/CourierOld/Assets/Code/Configs/CharacterConfig.cs +++ b/Game/CourierOld/Assets/Code/Configs/CharacterConfig.cs @@ -28,6 +28,12 @@ namespace Code.Configs wallClimbSpeedFactor = 0.8f, // Коэффициент преобразования горизонтальной скорости в вертикальную при залазании на стену minWallClimbSpeed = 9f, // Минимальная горизонтальная скорость для начала залазания на стену wallDetectionAngleThreshold = 80f; // Максимальный угол (в градусах) от вертикали, при котором поверхность считается "стеной" + + // Наклон при залазании на стены + public float + maxWallLeanAngle = 45f, // Максимальный угол наклона от стены (в градусах) + wallLeanSharpness = 50f, // Скорость наклона/выпрямления (чем выше — тем резче) + wallLeanHeightScale = 0.1f; // Множитель: на сколько метров подъёма = 1 единица "наклона" // Гравитация public Vector3 gravity = new(0f, -30f, 0f); // Вектор, отвечающий за применение гравитации diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities/WallClimbMechanic.cs b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities/WallClimbMechanic.cs index 96c140e..667250f 100644 --- a/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities/WallClimbMechanic.cs +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities/WallClimbMechanic.cs @@ -14,15 +14,15 @@ namespace Code.Gameplay.Character.Controller.Abilities // Минимальная горизонтальная скорость для начала залазания на стену private readonly float _minWallClimbSpeed; // Пороговый косинус стены - private readonly float _wallNormalThreshold; - + public float WallNormalThreshold { get; } + // Конструктор механики залазания на стену public WallClimbMechanic(float wallClimbSpeedFactor, float minWallClimbSpeed, float wallDetectionAngleThreshold) { _wallClimbSpeedFactor = wallClimbSpeedFactor; _minWallClimbSpeed = minWallClimbSpeed; // Преобразуем угол в косинус порога: если |dot| > cos(threshold), то это НЕ стена - _wallNormalThreshold = Mathf.Cos(Mathf.Deg2Rad * wallDetectionAngleThreshold); + WallNormalThreshold = Mathf.Cos(Mathf.Deg2Rad * wallDetectionAngleThreshold); } // Обработка столкновения при движении (вызывается при ударе о препятствие) @@ -30,7 +30,7 @@ namespace Code.Gameplay.Character.Controller.Abilities { // Проверяем, что это вертикальная или почти вертикальная поверхность (стена) var dotWithUp = Vector3.Dot(hitNormal, motor.CharacterUp); - if (Mathf.Abs(dotWithUp) > _wallNormalThreshold) return; // Не стена — это пол или потолок + if (Mathf.Abs(dotWithUp) > WallNormalThreshold) return; // Не стена — это пол или потолок // Получаем текущую скорость персонажа var currentVelocity = motor.Velocity; diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities/WallLeanMechanic.cs b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities/WallLeanMechanic.cs new file mode 100644 index 0000000..189f15d --- /dev/null +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities/WallLeanMechanic.cs @@ -0,0 +1,79 @@ +using UnityEngine; +using KinematicCharacterController; + +namespace Code.Gameplay.Character.Controller.Abilities +{ + /* + * Механика наклона персонажа при контакте со стеной + * Управляет визуальным наклоном модели в зависимости от высоты подъёма по стене + */ + public class WallLeanMechanic + { + private readonly float _maxLeanAngle; + private readonly float _leanSharpness; + private readonly float _heightScale; + + private Transform _characterTransform; + private Vector3 _lastWallNormal; + private float _lastWallContactHeight; + private float _leanFactor; + + public WallLeanMechanic(float maxLeanAngle, float leanSharpness, float heightScale) + { + _maxLeanAngle = maxLeanAngle; + _leanSharpness = leanSharpness; + _heightScale = heightScale; + } + + // Назначаем трансформ визуальной модели один раз + public void SetCharacterTransform(Transform transform) + { + _characterTransform = transform; + } + + // Вызывается из OnMovementHit + public void OnWallContact(Vector3 wallNormal, float contactHeight) + { + _lastWallNormal = wallNormal; + _lastWallContactHeight = contactHeight; } + + // Обновление каждый кадр + public void UpdateLean(KinematicCharacterMotor motor, float deltaTime) + { + if (!_characterTransform) return; + + var isGrounded = motor.GroundingStatus.IsStableOnGround; + + if (!isGrounded) + { + var heightDelta = motor.TransientPosition.y - _lastWallContactHeight; + var targetLeanFactor = Mathf.Clamp01(heightDelta * _heightScale); + _leanFactor = Mathf.Lerp(_leanFactor, targetLeanFactor, 1f - Mathf.Exp(-_leanSharpness * deltaTime)); + } + else + { + _leanFactor = Mathf.Lerp(_leanFactor, 0f, 1f - Mathf.Exp(-_leanSharpness * deltaTime)); + } + + ApplyLeanRotation(motor); + } + + private void ApplyLeanRotation(KinematicCharacterMotor motor) + { + if (_leanFactor <= 0.01f) + { + _characterTransform.localRotation = Quaternion.identity; + return; + } + + var leanDirection = -_lastWallNormal; + leanDirection = Vector3.ProjectOnPlane(leanDirection, motor.CharacterUp).normalized; + + if (leanDirection.sqrMagnitude < 0.01f) return; + + var leanAngle = _leanFactor * _maxLeanAngle; + var leanRotation = Quaternion.AngleAxis(leanAngle, Vector3.Cross(motor.CharacterUp, leanDirection)); + _characterTransform.localRotation = leanRotation; + } + } +} \ No newline at end of file diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities/WallLeanMechanic.cs.meta b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities/WallLeanMechanic.cs.meta new file mode 100644 index 0000000..ee5e36d --- /dev/null +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities/WallLeanMechanic.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: dfaefcb035124edb84b7874c5938defc +timeCreated: 1761738483 \ 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 index 156aed6..e81561b 100644 --- a/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/CustomCharacterController.cs +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/CustomCharacterController.cs @@ -20,14 +20,16 @@ namespace Code.Gameplay.Character.Controller public class CustomCharacterController : MonoBehaviour, ICharacterController { [Inject] private CharacterConfig _characterConfig; // Конфиг персонажа через DI - - [SerializeField] - private KinematicCharacterMotor motor; // Движок физического перемещения персонажа - + + [SerializeField] private KinematicCharacterMotor motor; // Движок физического перемещения персонажа + + [SerializeField] private Transform characterTransform; // Трансформ персонажа (с точкой отслеживания камеры) + // Подсистемы private CharacterInputHandler _inputHandler; private SlideMechanic _slideMechanic; private WallClimbMechanic _wallClimbMechanic; + private WallLeanMechanic _wallLeanMechanic; private GroundMovementSystem _groundMovement; private AirMovementSystem _airMovement; @@ -48,6 +50,11 @@ namespace Code.Gameplay.Character.Controller _characterConfig.minWallClimbSpeed, _characterConfig.wallDetectionAngleThreshold ); + _wallLeanMechanic = new WallLeanMechanic( + _characterConfig.maxWallLeanAngle, + _characterConfig.wallLeanSharpness, + _characterConfig.wallLeanHeightScale + ); _groundMovement = new GroundMovementSystem( _characterConfig.maxStableMoveSpeed, _characterConfig.sprintMoveSpeed, @@ -56,7 +63,7 @@ namespace Code.Gameplay.Character.Controller ); _airMovement = new AirMovementSystem(_characterConfig.gravity); } - + // Текущий режим камеры для адаптации поведения private CameraMode _currentCameraMode = CameraMode.FirstPerson; @@ -64,7 +71,9 @@ namespace Code.Gameplay.Character.Controller private void Start() { motor.CharacterController = this; // Регистрация этого контроллера в движке - _inputHandler.FirstPersonCameraRotation = motor.Transform.rotation; // Начальное вращение совпадает с персонажем + _inputHandler.FirstPersonCameraRotation = + motor.Transform.rotation; // Начальное вращение совпадает с персонажемс + _wallLeanMechanic.SetCharacterTransform(characterTransform); } // Обработка и преобразование входных данных от персонажа @@ -72,7 +81,7 @@ namespace Code.Gameplay.Character.Controller { // Обновляем таймер перезарядки слайда _slideMechanic.TickCooldown(Time.deltaTime); - + // Если активен слайд - игнорируем весь остальной ввод if (_slideMechanic.IsSliding) { @@ -83,16 +92,16 @@ namespace Code.Gameplay.Character.Controller _groundMovement.RequestJump(inputs.JumpPressed); return; } - + // Сохраняем текущий режим камеры _currentCameraMode = inputs.CameraMode; - + // Обработка слайда _slideMechanic.RequestSlide(inputs.SlidePressed); // Обработка остального ввода _inputHandler.ProcessInputs(ref inputs, motor); - + // Передаём запрос прыжка напрямую в систему движения по земле _groundMovement.RequestJump(inputs.JumpPressed); } @@ -118,8 +127,8 @@ namespace Code.Gameplay.Character.Controller { // Плавная интерполяция текущего направления к целевому с экспоненциальным сглаживанием var smoothedLookInputDirection = Vector3.Slerp( - motor.CharacterForward, - _inputHandler.LookInputVector, + motor.CharacterForward, + _inputHandler.LookInputVector, 1f - Mathf.Exp(-_characterConfig.orientationSharpness * deltaTime) ).normalized; @@ -146,11 +155,12 @@ namespace Code.Gameplay.Character.Controller { _slideMechanic.EndSlide(); } - + // Проверяем запрос слайда и запускаем его если можно if (!_slideMechanic.IsSliding) { - _slideMechanic.TryStartSlide(motor, _currentCameraMode, _inputHandler.FirstPersonCameraRotation, _inputHandler.LookInputVector); + _slideMechanic.TryStartSlide(motor, _currentCameraMode, _inputHandler.FirstPersonCameraRotation, + _inputHandler.LookInputVector); } // Если активен слайд - обрабатываем отдельно else @@ -158,7 +168,7 @@ namespace Code.Gameplay.Character.Controller _slideMechanic.HandleSlide(ref currentVelocity, deltaTime, motor); return; } - + // Если персонаж находится на земле if (motor.GroundingStatus.IsStableOnGround) { @@ -188,9 +198,10 @@ namespace Code.Gameplay.Character.Controller } } - // Метод вызывается после обновления персонажа (можно использовать для пост-обработки) + // Метод вызывается после обновления персонажа public void AfterCharacterUpdate(float deltaTime) { + _wallLeanMechanic.UpdateLean(motor, deltaTime); } // Проверка валидности коллайдера для обработки столкновений @@ -200,7 +211,8 @@ namespace Code.Gameplay.Character.Controller } // Обработка столкновения с землей (вызывается при приземлении) - public void OnGroundHit(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint, ref HitStabilityReport hitStabilityReport) + public void OnGroundHit(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint, + ref HitStabilityReport hitStabilityReport) { } @@ -209,10 +221,17 @@ namespace Code.Gameplay.Character.Controller ref HitStabilityReport hitStabilityReport) { _wallClimbMechanic.OnMovementHit(motor, hitNormal); + + var dotWithUp = Vector3.Dot(hitNormal, motor.CharacterUp); + if (Mathf.Abs(dotWithUp) <= _wallClimbMechanic.WallNormalThreshold) + { + _wallLeanMechanic.OnWallContact(hitNormal, motor.TransientPosition.y); + } } // Обработка отчета о стабильности столкновения - public void ProcessHitStabilityReport(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint, Vector3 atCharacterPosition, + public void ProcessHitStabilityReport(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint, + Vector3 atCharacterPosition, Quaternion atCharacterRotation, ref HitStabilityReport hitStabilityReport) { } diff --git a/Game/CourierOld/Assets/Resources/Configs/App/CharacterConfig.asset b/Game/CourierOld/Assets/Resources/Configs/App/CharacterConfig.asset index 1be96eb..d89d157 100644 --- a/Game/CourierOld/Assets/Resources/Configs/App/CharacterConfig.asset +++ b/Game/CourierOld/Assets/Resources/Configs/App/CharacterConfig.asset @@ -24,4 +24,7 @@ MonoBehaviour: wallClimbSpeedFactor: 0.8 minWallClimbSpeed: 9 wallDetectionAngleThreshold: 80 + maxWallLeanAngle: 90 + wallLeanSharpness: 100 + wallLeanHeightScale: 0.1 gravity: {x: 0, y: -30, z: 0} diff --git a/Game/CourierOld/Assets/Resources/Scenes/MovementDev.unity b/Game/CourierOld/Assets/Resources/Scenes/MovementDev.unity index 1280f66..e3c2be0 100644 --- a/Game/CourierOld/Assets/Resources/Scenes/MovementDev.unity +++ b/Game/CourierOld/Assets/Resources/Scenes/MovementDev.unity @@ -233,11 +233,11 @@ Transform: m_GameObject: {fileID: 453575297} serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} - m_LocalPosition: {x: -0, y: 1.395, z: 0.59} + m_LocalPosition: {x: 0, y: 1.395, z: 0.59} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] - m_Father: {fileID: 1696686254} + m_Father: {fileID: 723324499} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &481156066 GameObject: @@ -381,6 +381,39 @@ Transform: m_ConstrainProportionsScale: 0 m_Children: - {fileID: 1533098598} + m_Father: {fileID: 723324499} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &723324498 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 723324499} + m_Layer: 0 + m_Name: Character + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &723324499 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 723324498} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 678700802} + - {fileID: 453575298} m_Father: {fileID: 1696686254} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1376862591 @@ -709,7 +742,7 @@ GameObject: - component: {fileID: 1696686256} - component: {fileID: 1696686258} m_Layer: 0 - m_Name: Character + m_Name: CharacterController m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 @@ -728,8 +761,7 @@ Transform: m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: - - {fileID: 678700802} - - {fileID: 453575298} + - {fileID: 723324499} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!114 &1696686255 @@ -824,17 +856,7 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: Assembly-CSharp::Code.Gameplay.Player.CharacterController motor: {fileID: 1696686256} - maxStableMoveSpeed: 8 - sprintMoveSpeed: 12 - slideSpeed: 20 - slideDuration: 0.8 - slideCooldown: 1.5 - stableMovementSharpness: 15 - orientationSharpness: 10 - jumpSpeed: 10 - wallClimbSpeedFactor: 0.8 - minWallClimbSpeed: 9 - gravity: {x: 0, y: -30, z: 0} + characterTransform: {fileID: 723324499} --- !u!1 &1959874989 GameObject: m_ObjectHideFlags: 0