From eb4dff803e9853399406f0149f1e1684bfc287fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C5=A1a=20Ceviel?= Date: Thu, 4 Dec 2025 20:09:02 +0300 Subject: [PATCH 1/3] =?UTF-8?q?=D0=9F=D1=80=D0=B8=20=D0=BA=D0=B0=D1=81?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=20=D1=81=20=D0=B7=D0=B5=D0=BC=D0=BB=D1=91?= =?UTF-8?q?=D0=B9=20=D0=BF=D1=80=D0=B5=D0=BA=D1=80=D0=B0=D1=89=D0=B0=D0=B5?= =?UTF-8?q?=D1=82=D1=81=D1=8F=20=D0=B7=D0=B0=D0=BB=D0=B0=D0=B7=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5,=20=D1=83=D0=B1=D1=80=D0=B0=D0=BD=D0=BE=20=D0=B4?= =?UTF-8?q?=D0=B2=D0=B8=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BD=D0=B0=20A/D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Abilities/WallRunningMechanic.cs | 42 +++++++------------ .../Controller/CustomCharacterController.cs | 2 +- 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities/WallRunningMechanic.cs b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities/WallRunningMechanic.cs index 41d47f6..9473735 100644 --- a/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities/WallRunningMechanic.cs +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/Abilities/WallRunningMechanic.cs @@ -1,4 +1,3 @@ -using Code.Gameplay.Character.Controller.Input; using Code.Gameplay.Character.Core; using KinematicCharacterController; using UnityEngine; @@ -25,7 +24,6 @@ namespace Code.Gameplay.Character.Controller.Abilities private bool _canWallRun; private KinematicCharacterMotor _motor; - private CharacterInputHandler _inputHandler; public bool IsWallRunning { get; private set; } @@ -45,10 +43,9 @@ namespace Code.Gameplay.Character.Controller.Abilities _wallContactAngleThreshold = wallContactAngleThreshold; } - public void Initialize(KinematicCharacterMotor motor, CharacterInputHandler inputHandler) + public void Initialize(KinematicCharacterMotor motor) { _motor = motor; - _inputHandler = inputHandler; _surfaceAngleCos = Mathf.Cos(Mathf.Deg2Rad * _wallContactAngleThreshold); } @@ -88,13 +85,16 @@ namespace Code.Gameplay.Character.Controller.Abilities _wallNormal = wallNormal.normalized; _wallContactPoint = contactPoint; - if (_wallTangentUp.sqrMagnitude < 0.01f) _wallTangentUp = Vector3.up; + var forwardInWallPlane = Vector3.ProjectOnPlane(_motor.CharacterForward, _wallNormal).normalized; + if (forwardInWallPlane.sqrMagnitude < 0.01f) + { + var fallbackRight = Vector3.Cross(_wallNormal, _wallTangentUp).normalized; + var dotWithRight = Vector3.Dot(_motor.CharacterRight, _wallNormal); + forwardInWallPlane = dotWithRight < 0 ? fallbackRight : -fallbackRight; + } - _wallTangentRight = Vector3.Cross(_wallNormal, _wallTangentUp).normalized; - - _wallTangentUp = Vector3.Cross(_wallTangentRight, _wallNormal).normalized; - if (Vector3.Dot(_wallTangentUp, Vector3.up) < 0) - _wallTangentUp = -_wallTangentUp; + _wallTangentRight = forwardInWallPlane; + _wallTangentUp = Vector3.Cross(_wallTangentRight, -_wallNormal).normalized; _motor.ForceUnground(); } @@ -104,23 +104,11 @@ namespace Code.Gameplay.Character.Controller.Abilities if (!IsWallRunning) return; if (!IsWallStillPresent()) { DetachFromWall(false); return; } - var moveInputWorld = _inputHandler.MoveInputVector; - var moveInWallPlane = Vector3.ProjectOnPlane(moveInputWorld, _wallNormal); - - var wsMove = Vector3.Project(moveInWallPlane, _wallTangentRight); + var forwardInput = inputs.MoveAxisForward; + var moveDirection = _wallTangentRight * forwardInput; - var ad = inputs.MoveAxisRight; - - var sideDot = Vector3.Dot(_wallNormal, _motor.CharacterRight); - if (sideDot < 0) - ad = -ad; - - var adMove = _wallTangentUp * (-ad); - - var finalMove = wsMove + adMove; - finalMove = finalMove.normalized * moveInputWorld.magnitude; - - var targetVelocity = finalMove * _wallRunSpeed; + var targetSpeed = moveDirection.sqrMagnitude > 0.01f ? _wallRunSpeed : currentVelocity.magnitude; + var targetVelocity = moveDirection.normalized * targetSpeed; currentVelocity = Vector3.Lerp( currentVelocity, @@ -146,6 +134,8 @@ namespace Code.Gameplay.Character.Controller.Abilities private bool IsWallStillPresent() { if (!_motor) return false; + + if (_motor.GroundingStatus.IsStableOnGround) return false; var characterCenter = _motor.TransientPosition; var characterRight = _motor.CharacterRight; diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/CustomCharacterController.cs b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/CustomCharacterController.cs index fe03ce1..3bfc083 100644 --- a/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/CustomCharacterController.cs +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/CustomCharacterController.cs @@ -91,7 +91,7 @@ namespace Code.Gameplay.Character.Controller _inputHandler.FirstPersonCameraRotation = motor.Transform.rotation; // Начальное вращение совпадает с персонажемс _wallLeanMechanic.SetModel(characterTransform); - _wallRunningMechanic.Initialize(motor, _inputHandler); + _wallRunningMechanic.Initialize(motor); } // Текущий режим камеры для адаптации поведения From ecff1e69cd56b4ec7a46542b02adf5db635fd904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C5=A1a=20Ceviel?= Date: Thu, 4 Dec 2025 21:33:37 +0300 Subject: [PATCH 2/3] =?UTF-8?q?=D0=9C=D0=B5=D0=BD=D1=8F=D0=B5=D1=82=D1=81?= =?UTF-8?q?=D1=8F=20=D1=82=D0=B0=D1=80=D0=B3=D0=B5=D1=82=20=D0=BA=D0=B0?= =?UTF-8?q?=D0=BC=D0=B5=D1=80=D1=8B=20(=D0=B8=20=D0=B2=D1=8B=D1=81=D0=BE?= =?UTF-8?q?=D1=82=D0=B0=20=D0=BA=D0=BE=D0=BB=D0=BB=D0=B0=D0=B9=D0=B4=D0=B5?= =?UTF-8?q?=D1=80=D0=B0)=20=D0=BF=D1=80=D0=B8=20=D1=81=D0=BB=D0=B0=D0=B9?= =?UTF-8?q?=D0=B4=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Assets/Code/Configs/CharacterConfig.cs | 6 +- .../Controller/CustomCharacterController.cs | 70 ++++++++++++++++++- .../Prefabs/Player/CharacterController.prefab | 2 + 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/Game/CourierOld/Assets/Code/Configs/CharacterConfig.cs b/Game/CourierOld/Assets/Code/Configs/CharacterConfig.cs index fca6ad5..9ef0d8f 100644 --- a/Game/CourierOld/Assets/Code/Configs/CharacterConfig.cs +++ b/Game/CourierOld/Assets/Code/Configs/CharacterConfig.cs @@ -23,7 +23,11 @@ namespace Code.Configs ); public float slideMinVelocityThreshold = 3f, - slideCooldown = 1.5f; + slideCooldown = 1.5f, + slideColliderHeightScale = 0.5f, + slideCameraOffsetY = -0.5f, + slideEnterDuration = 0.2f, + slideExitDuration = 0.25f; public float leanAngle = 25f, diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/CustomCharacterController.cs b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/CustomCharacterController.cs index 3bfc083..5a2ad14 100644 --- a/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/CustomCharacterController.cs +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/CustomCharacterController.cs @@ -15,7 +15,7 @@ namespace Code.Gameplay.Character.Controller * Реализует физическое движение персонажа с использованием Kinematic Character Controller * Обрабатывает ввод пользователя, применяет движение и вращение с учетом поверхности * - * Источник: Tvtig, https://www.youtube.com/watch?v=9ibpY64wBYM + * Источник: Tvtig, https://www.youtube.com/watch?v=9ibpY64wBYM */ public class CustomCharacterController : MonoBehaviour, ICharacterController { @@ -25,6 +25,12 @@ namespace Code.Gameplay.Character.Controller [SerializeField] private Transform characterTransform; // Трансформ персонажа (с точкой отслеживания камеры) + [SerializeField] private CapsuleCollider _capsuleCollider; + [SerializeField] private Transform _cameraFollowPoint; + private Vector3 _originalColliderCenter; + private float _originalColliderHeight; + private Vector3 _originalFollowPointLocalPosition; + // Подсистемы private CharacterInputHandler _inputHandler; private DashMechanic _dashMechanic; @@ -92,6 +98,17 @@ namespace Code.Gameplay.Character.Controller motor.Transform.rotation; // Начальное вращение совпадает с персонажемс _wallLeanMechanic.SetModel(characterTransform); _wallRunningMechanic.Initialize(motor); + + if (_capsuleCollider != null) + { + _originalColliderCenter = _capsuleCollider.center; + _originalColliderHeight = _capsuleCollider.height; + } + + if (_cameraFollowPoint != null) + { + _originalFollowPointLocalPosition = _cameraFollowPoint.localPosition; + } } // Текущий режим камеры для адаптации поведения @@ -185,11 +202,26 @@ namespace Code.Gameplay.Character.Controller } if (!_slideMechanic.IsSliding) + { _slideMechanic.TryStartSlide(motor, _currentCameraMode, _inputHandler.FirstPersonCameraRotation, _inputHandler.LookInputVector); + + if (_slideMechanic.IsSliding) + { + EnterSlideState(); + } + else + { + ExitSlideState(); + } + } else { _slideMechanic.HandleSlide(ref currentVelocity, deltaTime, motor); - return; + + if (!_slideMechanic.IsSliding) + { + ExitSlideState(); + } } // === WALL RUNNING: обработка состояния === @@ -226,6 +258,40 @@ namespace Code.Gameplay.Character.Controller isAbilitiesBlocked = true; abilityBlockTimer = duration; } + + private void EnterSlideState() + { + if (_capsuleCollider) + { + var newHeight = _originalColliderHeight * _characterConfig.slideColliderHeightScale; + + _capsuleCollider.height = newHeight; + + var newCenterY = newHeight * 0.5f - _originalColliderHeight * 0.5f; + _capsuleCollider.center = _originalColliderCenter + Vector3.up * newCenterY; + } + + if (!_cameraFollowPoint) return; + + var offset = Vector3.up * _characterConfig.slideCameraOffsetY; + _cameraFollowPoint.localPosition = _originalFollowPointLocalPosition + offset; + } + + private void ExitSlideState() + { + if (_capsuleCollider) + { + _capsuleCollider.height = _originalColliderHeight; + _capsuleCollider.center = _originalColliderCenter; + } + + if (_cameraFollowPoint) + { + _cameraFollowPoint.localPosition = _originalFollowPointLocalPosition; + } + } + + public void BeforeCharacterUpdate(float deltaTime) { diff --git a/Game/CourierOld/Assets/Resources_moved/Prefabs/Player/CharacterController.prefab b/Game/CourierOld/Assets/Resources_moved/Prefabs/Player/CharacterController.prefab index 8661a96..e7b4a46 100644 --- a/Game/CourierOld/Assets/Resources_moved/Prefabs/Player/CharacterController.prefab +++ b/Game/CourierOld/Assets/Resources_moved/Prefabs/Player/CharacterController.prefab @@ -173,6 +173,8 @@ MonoBehaviour: m_EditorClassIdentifier: Assembly-CSharp::Code.Gameplay.Player.CharacterController motor: {fileID: 8476708390679839925} characterTransform: {fileID: 4122797476271397740} + _capsuleCollider: {fileID: 8541228187116599584} + _cameraFollowPoint: {fileID: 1047840484662430186} --- !u!114 &3283650228831796584 MonoBehaviour: m_ObjectHideFlags: 0 From 6732aef384e03b0645580c81d270f2204a2dce95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C5=A1a=20Ceviel?= Date: Thu, 4 Dec 2025 21:44:11 +0300 Subject: [PATCH 3/3] =?UTF-8?q?=D0=9F=D0=BB=D0=B0=D0=B2=D0=BD=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D1=8C=20=D1=8D=D1=82=D0=BE=20=D0=BA=D1=80=D1=83=D1=82?= =?UTF-8?q?=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controller/CustomCharacterController.cs | 161 +++++++++++++----- 1 file changed, 117 insertions(+), 44 deletions(-) diff --git a/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/CustomCharacterController.cs b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/CustomCharacterController.cs index 5a2ad14..8cbc84a 100644 --- a/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/CustomCharacterController.cs +++ b/Game/CourierOld/Assets/Code/Gameplay/Character/Controller/CustomCharacterController.cs @@ -30,6 +30,14 @@ namespace Code.Gameplay.Character.Controller private Vector3 _originalColliderCenter; private float _originalColliderHeight; private Vector3 _originalFollowPointLocalPosition; + private bool _isEnteringSlide; + private bool _isExitingSlide; + private float _slideTransitionTime; + private float _slideTransitionDuration; + private float _currentColliderHeight; + private Vector3 _currentColliderCenter; + private Vector3 _currentFollowPointLocalPosition; + private bool _wasSlidingLastFrame; // Подсистемы private CharacterInputHandler _inputHandler; @@ -99,16 +107,13 @@ namespace Code.Gameplay.Character.Controller _wallLeanMechanic.SetModel(characterTransform); _wallRunningMechanic.Initialize(motor); - if (_capsuleCollider != null) - { - _originalColliderCenter = _capsuleCollider.center; - _originalColliderHeight = _capsuleCollider.height; - } + _originalColliderCenter = _capsuleCollider.center; + _originalColliderHeight = _capsuleCollider.height; + _originalFollowPointLocalPosition = _cameraFollowPoint.localPosition; - if (_cameraFollowPoint != null) - { - _originalFollowPointLocalPosition = _cameraFollowPoint.localPosition; - } + _currentColliderHeight = _originalColliderHeight; + _currentColliderCenter = _originalColliderCenter; + _currentFollowPointLocalPosition = _originalFollowPointLocalPosition; } // Текущий режим камеры для адаптации поведения @@ -201,29 +206,32 @@ namespace Code.Gameplay.Character.Controller return; } - if (!_slideMechanic.IsSliding) + var wasSliding = _wasSlidingLastFrame; + var isSlidingNow = _slideMechanic.IsSliding; + + if (!isSlidingNow) { _slideMechanic.TryStartSlide(motor, _currentCameraMode, _inputHandler.FirstPersonCameraRotation, _inputHandler.LookInputVector); - - if (_slideMechanic.IsSliding) - { - EnterSlideState(); - } - else - { - ExitSlideState(); - } + isSlidingNow = _slideMechanic.IsSliding; } - else + + switch (isSlidingNow) + { + case true when !wasSliding: + EnterSlideState(); + break; + case false when wasSliding: + ExitSlideState(); + break; + } + + if (isSlidingNow) { _slideMechanic.HandleSlide(ref currentVelocity, deltaTime, motor); - - if (!_slideMechanic.IsSliding) - { - ExitSlideState(); - } } + _wasSlidingLastFrame = isSlidingNow; + // === WALL RUNNING: обработка состояния === if (_wallRunningMechanic.IsWallRunning) { @@ -261,37 +269,100 @@ namespace Code.Gameplay.Character.Controller private void EnterSlideState() { - if (_capsuleCollider) - { - var newHeight = _originalColliderHeight * _characterConfig.slideColliderHeightScale; - - _capsuleCollider.height = newHeight; - - var newCenterY = newHeight * 0.5f - _originalColliderHeight * 0.5f; - _capsuleCollider.center = _originalColliderCenter + Vector3.up * newCenterY; - } - - if (!_cameraFollowPoint) return; - - var offset = Vector3.up * _characterConfig.slideCameraOffsetY; - _cameraFollowPoint.localPosition = _originalFollowPointLocalPosition + offset; + _isEnteringSlide = true; + _isExitingSlide = false; + _slideTransitionTime = 0f; + _slideTransitionDuration = _characterConfig.slideEnterDuration; } private void ExitSlideState() { + _isExitingSlide = true; + _isEnteringSlide = false; + _slideTransitionTime = 0f; + _slideTransitionDuration = _characterConfig.slideExitDuration; + } + + private void UpdateSlideTransition(float deltaTime) + { + if (!_isEnteringSlide && !_isExitingSlide) return; + + _slideTransitionTime += deltaTime; + var t = Mathf.Clamp01(_slideTransitionTime / _slideTransitionDuration); + + t = Mathf.SmoothStep(0f, 1f, t); + + if (_isEnteringSlide) + { + var targetHeight = _originalColliderHeight * _characterConfig.slideColliderHeightScale; + var targetCenterY = targetHeight * 0.5f - _originalColliderHeight * 0.5f; + var targetCenter = _originalColliderCenter + Vector3.up * targetCenterY; + + var targetFollowPos = _originalFollowPointLocalPosition + Vector3.up * _characterConfig.slideCameraOffsetY; + + _currentColliderHeight = Mathf.Lerp(_originalColliderHeight, targetHeight, t); + _currentColliderCenter = Vector3.Lerp(_originalColliderCenter, targetCenter, t); + _currentFollowPointLocalPosition = Vector3.Lerp(_originalFollowPointLocalPosition, targetFollowPos, t); + } + else if (_isExitingSlide) + { + _currentColliderHeight = Mathf.Lerp(_originalColliderHeight * _characterConfig.slideColliderHeightScale, _originalColliderHeight, t); + + var prevHeight = _originalColliderHeight * _characterConfig.slideColliderHeightScale; + var prevCenterY = prevHeight * 0.5f - _originalColliderHeight * 0.5f; + var prevCenter = _originalColliderCenter + Vector3.up * prevCenterY; + + var prevFollowPos = _originalFollowPointLocalPosition + Vector3.up * _characterConfig.slideCameraOffsetY; + + _currentColliderCenter = Vector3.Lerp(prevCenter, _originalColliderCenter, t); + _currentFollowPointLocalPosition = Vector3.Lerp(prevFollowPos, _originalFollowPointLocalPosition, t); + } + if (_capsuleCollider) { - _capsuleCollider.height = _originalColliderHeight; - _capsuleCollider.center = _originalColliderCenter; + _capsuleCollider.height = _currentColliderHeight; + _capsuleCollider.center = _currentColliderCenter; } if (_cameraFollowPoint) { - _cameraFollowPoint.localPosition = _originalFollowPointLocalPosition; + _cameraFollowPoint.localPosition = _currentFollowPointLocalPosition; } + + if (!(t >= 1f)) return; + + if (_isEnteringSlide) + { + _isEnteringSlide = false; + if (_capsuleCollider) + { + _capsuleCollider.height = _originalColliderHeight * _characterConfig.slideColliderHeightScale; + var h = _capsuleCollider.height; + _capsuleCollider.center = _originalColliderCenter + Vector3.up * (h * 0.5f - _originalColliderHeight * 0.5f); + } + if (_cameraFollowPoint) + { + _cameraFollowPoint.localPosition = _originalFollowPointLocalPosition + Vector3.up * _characterConfig.slideCameraOffsetY; + } + } + else if (_isExitingSlide) + { + _isExitingSlide = false; + if (_capsuleCollider) + { + _capsuleCollider.height = _originalColliderHeight; + _capsuleCollider.center = _originalColliderCenter; + } + if (_cameraFollowPoint) + { + _cameraFollowPoint.localPosition = _originalFollowPointLocalPosition; + } + } + + _currentColliderHeight = _capsuleCollider ? _capsuleCollider.height : _originalColliderHeight; + _currentColliderCenter = _capsuleCollider ? _capsuleCollider.center : _originalColliderCenter; + _currentFollowPointLocalPosition = _cameraFollowPoint ? _cameraFollowPoint.localPosition : _originalFollowPointLocalPosition; } - - public void BeforeCharacterUpdate(float deltaTime) { @@ -307,6 +378,8 @@ namespace Code.Gameplay.Character.Controller public void AfterCharacterUpdate(float deltaTime) { + UpdateSlideTransition(deltaTime); + _wallLeanMechanic.Update( _wallRunningMechanic.IsWallRunning, _wallRunningMechanic.GetWallNormal(),