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

Нуждается в доработке (как минимум чтобы вращалось в правильную сторону), но вроде-как выглядит прикольно
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, // Коэффициент преобразования горизонтальной скорости в вертикальную при залазании на стену
minWallClimbSpeed = 9f, // Минимальная горизонтальная скорость для начала залазания на стену
wallDetectionAngleThreshold = 80f; // Максимальный угол (в градусах) от вертикали, при котором поверхность считается "стеной"
// Наклон при залазании на стены
public float
maxWallLeanAngle = 45f, // Максимальный угол наклона от стены (в градусах)
wallLeanSharpness = 50f, // Скорость наклона/выпрямления (чем выше — тем резче)
wallLeanHeightScale = 0.1f; // Множитель: на сколько метров подъёма = 1 единица "наклона"
// Гравитация
public Vector3 gravity = new(0f, -30f, 0f); // Вектор, отвечающий за применение гравитации
@@ -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;
@@ -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
{
[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)
{
}
@@ -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}
@@ -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