Инишиал механика наклона при залазании на стены

Нуждается в доработке (как минимум чтобы вращалось в правильную сторону), но вроде-как выглядит прикольно
This commit is contained in:
MrTorex committed 2025-10-29 15:00:25 +03:00
1 parent 07ce27618c
commit 0e188efe59
7 files changed
+170 -38

No files matched your search

@@ -28,6 +28,12 @@ namespace Code.Configs
wallClimbSpeedFactor = 0.8f, // Коэффициент преобразования горизонтальной скорости в вертикальную при залазании на стену wallClimbSpeedFactor = 0.8f, // Коэффициент преобразования горизонтальной скорости в вертикальную при залазании на стену
minWallClimbSpeed = 9f, // Минимальная горизонтальная скорость для начала залазания на стену minWallClimbSpeed = 9f, // Минимальная горизонтальная скорость для начала залазания на стену
wallDetectionAngleThreshold = 80f; // Максимальный угол (в градусах) от вертикали, при котором поверхность считается "стеной" wallDetectionAngleThreshold = 80f; // Максимальный угол (в градусах) от вертикали, при котором поверхность считается "стеной"
// Наклон при залазании на стены
public float
maxWallLeanAngle = 45f, // Максимальный угол наклона от стены (в градусах)
wallLeanSharpness = 50f, // Скорость наклона/выпрямления (чем выше — тем резче)
wallLeanHeightScale = 0.1f; // Множитель: на сколько метров подъёма = 1 единица "наклона"
// Гравитация // Гравитация
public Vector3 gravity = new(0f, -30f, 0f); // Вектор, отвечающий за применение гравитации public Vector3 gravity = new(0f, -30f, 0f); // Вектор, отвечающий за применение гравитации
@@ -14,15 +14,15 @@ namespace Code.Gameplay.Character.Controller.Abilities
// Минимальная горизонтальная скорость для начала залазания на стену // Минимальная горизонтальная скорость для начала залазания на стену
private readonly float _minWallClimbSpeed; private readonly float _minWallClimbSpeed;
// Пороговый косинус стены // Пороговый косинус стены
private readonly float _wallNormalThreshold; public float WallNormalThreshold { get; }
// Конструктор механики залазания на стену // Конструктор механики залазания на стену
public WallClimbMechanic(float wallClimbSpeedFactor, float minWallClimbSpeed, float wallDetectionAngleThreshold) public WallClimbMechanic(float wallClimbSpeedFactor, float minWallClimbSpeed, float wallDetectionAngleThreshold)
{ {
_wallClimbSpeedFactor = wallClimbSpeedFactor; _wallClimbSpeedFactor = wallClimbSpeedFactor;
_minWallClimbSpeed = minWallClimbSpeed; _minWallClimbSpeed = minWallClimbSpeed;
// Преобразуем угол в косинус порога: если |dot| > cos(threshold), то это НЕ стена // Преобразуем угол в косинус порога: если |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); var dotWithUp = Vector3.Dot(hitNormal, motor.CharacterUp);
if (Mathf.Abs(dotWithUp) > _wallNormalThreshold) return; // Не стена — это пол или потолок if (Mathf.Abs(dotWithUp) > WallNormalThreshold) return; // Не стена — это пол или потолок
// Получаем текущую скорость персонажа // Получаем текущую скорость персонажа
var currentVelocity = motor.Velocity; var currentVelocity = motor.Velocity;
@@ -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;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: dfaefcb035124edb84b7874c5938defc
timeCreated: 1761738483
@@ -20,14 +20,16 @@ namespace Code.Gameplay.Character.Controller
public class CustomCharacterController : MonoBehaviour, ICharacterController public class CustomCharacterController : MonoBehaviour, ICharacterController
{ {
[Inject] private CharacterConfig _characterConfig; // Конфиг персонажа через DI [Inject] private CharacterConfig _characterConfig; // Конфиг персонажа через DI
[SerializeField] [SerializeField] private KinematicCharacterMotor motor; // Движок физического перемещения персонажа
private KinematicCharacterMotor motor; // Движок физического перемещения персонажа
[SerializeField] private Transform characterTransform; // Трансформ персонажа (с точкой отслеживания камеры)
// Подсистемы // Подсистемы
private CharacterInputHandler _inputHandler; private CharacterInputHandler _inputHandler;
private SlideMechanic _slideMechanic; private SlideMechanic _slideMechanic;
private WallClimbMechanic _wallClimbMechanic; private WallClimbMechanic _wallClimbMechanic;
private WallLeanMechanic _wallLeanMechanic;
private GroundMovementSystem _groundMovement; private GroundMovementSystem _groundMovement;
private AirMovementSystem _airMovement; private AirMovementSystem _airMovement;
@@ -48,6 +50,11 @@ namespace Code.Gameplay.Character.Controller
_characterConfig.minWallClimbSpeed, _characterConfig.minWallClimbSpeed,
_characterConfig.wallDetectionAngleThreshold _characterConfig.wallDetectionAngleThreshold
); );
_wallLeanMechanic = new WallLeanMechanic(
_characterConfig.maxWallLeanAngle,
_characterConfig.wallLeanSharpness,
_characterConfig.wallLeanHeightScale
);
_groundMovement = new GroundMovementSystem( _groundMovement = new GroundMovementSystem(
_characterConfig.maxStableMoveSpeed, _characterConfig.maxStableMoveSpeed,
_characterConfig.sprintMoveSpeed, _characterConfig.sprintMoveSpeed,
@@ -56,7 +63,7 @@ namespace Code.Gameplay.Character.Controller
); );
_airMovement = new AirMovementSystem(_characterConfig.gravity); _airMovement = new AirMovementSystem(_characterConfig.gravity);
} }
// Текущий режим камеры для адаптации поведения // Текущий режим камеры для адаптации поведения
private CameraMode _currentCameraMode = CameraMode.FirstPerson; private CameraMode _currentCameraMode = CameraMode.FirstPerson;
@@ -64,7 +71,9 @@ namespace Code.Gameplay.Character.Controller
private void Start() private void Start()
{ {
motor.CharacterController = this; // Регистрация этого контроллера в движке 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); _slideMechanic.TickCooldown(Time.deltaTime);
// Если активен слайд - игнорируем весь остальной ввод // Если активен слайд - игнорируем весь остальной ввод
if (_slideMechanic.IsSliding) if (_slideMechanic.IsSliding)
{ {
@@ -83,16 +92,16 @@ namespace Code.Gameplay.Character.Controller
_groundMovement.RequestJump(inputs.JumpPressed); _groundMovement.RequestJump(inputs.JumpPressed);
return; return;
} }
// Сохраняем текущий режим камеры // Сохраняем текущий режим камеры
_currentCameraMode = inputs.CameraMode; _currentCameraMode = inputs.CameraMode;
// Обработка слайда // Обработка слайда
_slideMechanic.RequestSlide(inputs.SlidePressed); _slideMechanic.RequestSlide(inputs.SlidePressed);
// Обработка остального ввода // Обработка остального ввода
_inputHandler.ProcessInputs(ref inputs, motor); _inputHandler.ProcessInputs(ref inputs, motor);
// Передаём запрос прыжка напрямую в систему движения по земле // Передаём запрос прыжка напрямую в систему движения по земле
_groundMovement.RequestJump(inputs.JumpPressed); _groundMovement.RequestJump(inputs.JumpPressed);
} }
@@ -118,8 +127,8 @@ namespace Code.Gameplay.Character.Controller
{ {
// Плавная интерполяция текущего направления к целевому с экспоненциальным сглаживанием // Плавная интерполяция текущего направления к целевому с экспоненциальным сглаживанием
var smoothedLookInputDirection = Vector3.Slerp( var smoothedLookInputDirection = Vector3.Slerp(
motor.CharacterForward, motor.CharacterForward,
_inputHandler.LookInputVector, _inputHandler.LookInputVector,
1f - Mathf.Exp(-_characterConfig.orientationSharpness * deltaTime) 1f - Mathf.Exp(-_characterConfig.orientationSharpness * deltaTime)
).normalized; ).normalized;
@@ -146,11 +155,12 @@ namespace Code.Gameplay.Character.Controller
{ {
_slideMechanic.EndSlide(); _slideMechanic.EndSlide();
} }
// Проверяем запрос слайда и запускаем его если можно // Проверяем запрос слайда и запускаем его если можно
if (!_slideMechanic.IsSliding) if (!_slideMechanic.IsSliding)
{ {
_slideMechanic.TryStartSlide(motor, _currentCameraMode, _inputHandler.FirstPersonCameraRotation, _inputHandler.LookInputVector); _slideMechanic.TryStartSlide(motor, _currentCameraMode, _inputHandler.FirstPersonCameraRotation,
_inputHandler.LookInputVector);
} }
// Если активен слайд - обрабатываем отдельно // Если активен слайд - обрабатываем отдельно
else else
@@ -158,7 +168,7 @@ namespace Code.Gameplay.Character.Controller
_slideMechanic.HandleSlide(ref currentVelocity, deltaTime, motor); _slideMechanic.HandleSlide(ref currentVelocity, deltaTime, motor);
return; return;
} }
// Если персонаж находится на земле // Если персонаж находится на земле
if (motor.GroundingStatus.IsStableOnGround) if (motor.GroundingStatus.IsStableOnGround)
{ {
@@ -188,9 +198,10 @@ namespace Code.Gameplay.Character.Controller
} }
} }
// Метод вызывается после обновления персонажа (можно использовать для пост-обработки) // Метод вызывается после обновления персонажа
public void AfterCharacterUpdate(float deltaTime) 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) ref HitStabilityReport hitStabilityReport)
{ {
_wallClimbMechanic.OnMovementHit(motor, hitNormal); _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) Quaternion atCharacterRotation, ref HitStabilityReport hitStabilityReport)
{ {
} }
@@ -24,4 +24,7 @@ MonoBehaviour:
wallClimbSpeedFactor: 0.8 wallClimbSpeedFactor: 0.8
minWallClimbSpeed: 9 minWallClimbSpeed: 9
wallDetectionAngleThreshold: 80 wallDetectionAngleThreshold: 80
maxWallLeanAngle: 90
wallLeanSharpness: 100
wallLeanHeightScale: 0.1
gravity: {x: 0, y: -30, z: 0} gravity: {x: 0, y: -30, z: 0}
@@ -233,11 +233,11 @@ Transform:
m_GameObject: {fileID: 453575297} m_GameObject: {fileID: 453575297}
serializedVersion: 2 serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} 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_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0 m_ConstrainProportionsScale: 0
m_Children: [] m_Children: []
m_Father: {fileID: 1696686254} m_Father: {fileID: 723324499}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &481156066 --- !u!1 &481156066
GameObject: GameObject:
@@ -381,6 +381,39 @@ Transform:
m_ConstrainProportionsScale: 0 m_ConstrainProportionsScale: 0
m_Children: m_Children:
- {fileID: 1533098598} - {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_Father: {fileID: 1696686254}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1376862591 --- !u!1 &1376862591
@@ -709,7 +742,7 @@ GameObject:
- component: {fileID: 1696686256} - component: {fileID: 1696686256}
- component: {fileID: 1696686258} - component: {fileID: 1696686258}
m_Layer: 0 m_Layer: 0
m_Name: Character m_Name: CharacterController
m_TagString: Untagged m_TagString: Untagged
m_Icon: {fileID: 0} m_Icon: {fileID: 0}
m_NavMeshLayer: 0 m_NavMeshLayer: 0
@@ -728,8 +761,7 @@ Transform:
m_LocalScale: {x: 1, y: 1, z: 1} m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0 m_ConstrainProportionsScale: 0
m_Children: m_Children:
- {fileID: 678700802} - {fileID: 723324499}
- {fileID: 453575298}
m_Father: {fileID: 0} m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &1696686255 --- !u!114 &1696686255
@@ -824,17 +856,7 @@ MonoBehaviour:
m_Name: m_Name:
m_EditorClassIdentifier: Assembly-CSharp::Code.Gameplay.Player.CharacterController m_EditorClassIdentifier: Assembly-CSharp::Code.Gameplay.Player.CharacterController
motor: {fileID: 1696686256} motor: {fileID: 1696686256}
maxStableMoveSpeed: 8 characterTransform: {fileID: 723324499}
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}
--- !u!1 &1959874989 --- !u!1 &1959874989
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0