454 lines
18 KiB
C#
454 lines
18 KiB
C#
using Code.Configs;
|
|
using Code.Gameplay.Camera;
|
|
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 Reflex.Attributes;
|
|
using UnityEngine;
|
|
|
|
namespace Code.Gameplay.Character.Controller
|
|
{
|
|
/*
|
|
* Скрипт, отвечающий за передвижение персонажа
|
|
* Реализует физическое движение персонажа с использованием Kinematic Character Controller
|
|
* Обрабатывает ввод пользователя, применяет движение и вращение с учетом поверхности
|
|
*
|
|
* Источник: Tvtig, https://www.youtube.com/watch?v=9ibpY64wBYM
|
|
*/
|
|
public class CustomCharacterController : MonoBehaviour, ICharacterController
|
|
{
|
|
[Inject] private CharacterConfig _characterConfig; // Конфиг персонажа через DI
|
|
|
|
[SerializeField] private KinematicCharacterMotor motor; // Движок физического перемещения персонажа
|
|
|
|
[SerializeField] private Transform characterTransform; // Трансформ персонажа (с точкой отслеживания камеры)
|
|
|
|
[SerializeField] private CapsuleCollider _capsuleCollider;
|
|
[SerializeField] private Transform _cameraFollowPoint;
|
|
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 float _jumpCooldownTimer;
|
|
private float _jumpCooldownDuration => _characterConfig.jumpCooldown;
|
|
|
|
public bool CanJump => !isAbilitiesBlocked && _jumpCooldownTimer <= 0f;
|
|
|
|
// Подсистемы
|
|
private CharacterInputHandler _inputHandler;
|
|
private DashMechanic _dashMechanic;
|
|
private SlideMechanic _slideMechanic;
|
|
// private WallClimbMechanic _wallClimbMechanic;
|
|
private WallRunningMechanic _wallRunningMechanic;
|
|
private WallLeanMechanic _wallLeanMechanic;
|
|
private GroundMovementSystem _groundMovement;
|
|
private AirMovementSystem _airMovement;
|
|
|
|
public bool IsSliding => _slideMechanic.IsSliding;
|
|
public bool IsDashing => _dashMechanic.IsDashing;
|
|
public bool IsWallRunning => _wallRunningMechanic.IsWallRunning;
|
|
|
|
private float abilityBlockTimer;
|
|
private bool isAbilitiesBlocked;
|
|
|
|
private CharacterInputs _lastInputs;
|
|
|
|
public void SetPosition(Vector3 position) => motor.SetPosition(position);
|
|
public void SetRotation(Quaternion rotation) => motor.SetRotation(rotation);
|
|
|
|
[Inject]
|
|
private void Construct(CharacterConfig characterConfig)
|
|
{
|
|
_characterConfig = characterConfig;
|
|
// Инициализация подсистем с параметрами из конфига
|
|
_inputHandler = new CharacterInputHandler();
|
|
_dashMechanic = new DashMechanic(
|
|
_characterConfig.dashSpeed,
|
|
_characterConfig.dashDuration,
|
|
_characterConfig.dashCooldown
|
|
);
|
|
_slideMechanic = new SlideMechanic(
|
|
_characterConfig.slideVelocityCurve,
|
|
_characterConfig.slideMinVelocityThreshold,
|
|
_characterConfig.slideCooldown
|
|
);
|
|
_wallRunningMechanic = new WallRunningMechanic(
|
|
_characterConfig.wallRunMaxTime,
|
|
_characterConfig.wallRunSpeed,
|
|
_characterConfig.wallRunAcceleration,
|
|
_characterConfig.wallRunGravityScale,
|
|
_characterConfig.wallDetachImpulse,
|
|
_characterConfig.wallContactAngleThreshold
|
|
);
|
|
_wallLeanMechanic = new WallLeanMechanic(
|
|
_characterConfig.leanAngle,
|
|
_characterConfig.leanThreshold,
|
|
_characterConfig.leanSmoothSpeed
|
|
);
|
|
_groundMovement = new GroundMovementSystem(
|
|
_characterConfig.maxStableMoveSpeed,
|
|
_characterConfig.stableMovementSharpness,
|
|
_characterConfig.jumpSpeed
|
|
);
|
|
_airMovement = new AirMovementSystem(
|
|
_characterConfig.gravity,
|
|
_characterConfig.maxAirSpeed,
|
|
_characterConfig.airControlSharpness
|
|
);
|
|
|
|
motor.CharacterController = this; // Регистрация этого контроллера в движке
|
|
_inputHandler.FirstPersonCameraRotation =
|
|
motor.Transform.rotation; // Начальное вращение совпадает с персонажемс
|
|
_wallLeanMechanic.SetModel(characterTransform);
|
|
_wallRunningMechanic.Initialize(motor);
|
|
|
|
_originalColliderCenter = _capsuleCollider.center;
|
|
_originalColliderHeight = _capsuleCollider.height;
|
|
_originalFollowPointLocalPosition = _cameraFollowPoint.localPosition;
|
|
|
|
_currentColliderHeight = _originalColliderHeight;
|
|
_currentColliderCenter = _originalColliderCenter;
|
|
_currentFollowPointLocalPosition = _originalFollowPointLocalPosition;
|
|
}
|
|
|
|
// Текущий режим камеры для адаптации поведения
|
|
private CameraMode _currentCameraMode = CameraMode.FirstPerson;
|
|
|
|
public void SetInputs(ref CharacterInputs inputs)
|
|
{
|
|
_lastInputs = inputs;
|
|
_dashMechanic.TickCooldown(Time.deltaTime);
|
|
_slideMechanic.TickCooldown(Time.deltaTime);
|
|
_wallRunningMechanic.Tick(Time.deltaTime);
|
|
|
|
// Если активен слайд - игнорируем весь остальной ввод
|
|
if (_dashMechanic.IsDashing || _slideMechanic.IsSliding)
|
|
{
|
|
_inputHandler.MoveInputVector = Vector3.zero;
|
|
_inputHandler.LookInputVector = Vector3.one;
|
|
_groundMovement.RequestJump(inputs.JumpPressed);
|
|
return;
|
|
}
|
|
|
|
_currentCameraMode = inputs.CameraMode;
|
|
|
|
if (isAbilitiesBlocked)
|
|
{
|
|
abilityBlockTimer -= Time.deltaTime;
|
|
if (abilityBlockTimer <= 0)
|
|
{
|
|
isAbilitiesBlocked = false;
|
|
}
|
|
else
|
|
{
|
|
inputs.DashPressed = false;
|
|
inputs.SlidePressed = false;
|
|
}
|
|
}
|
|
|
|
_dashMechanic.RequestDash(inputs.DashPressed);
|
|
_slideMechanic.RequestSlide(inputs.SlidePressed);
|
|
|
|
// Обработка остального ввода
|
|
_inputHandler.ProcessInputs(ref inputs, motor);
|
|
|
|
// Передаём запрос прыжка
|
|
var jumpInput = inputs.JumpPressed;
|
|
|
|
if (jumpInput && CanJump)
|
|
{
|
|
_groundMovement.RequestJump(true);
|
|
}
|
|
else
|
|
{
|
|
_groundMovement.RequestJump(false);
|
|
}
|
|
}
|
|
|
|
public void UpdateRotation(ref Quaternion currentRotation, float deltaTime)
|
|
{
|
|
if (_slideMechanic.IsSliding) return;
|
|
|
|
switch (_currentCameraMode)
|
|
{
|
|
case CameraMode.FirstPerson when _inputHandler.ShouldApplyCameraRotation:
|
|
currentRotation = _inputHandler.FirstPersonCameraRotation;
|
|
break;
|
|
case CameraMode.ThirdPerson:
|
|
{
|
|
if (_inputHandler.LookInputVector.sqrMagnitude > 0 && _characterConfig.orientationSharpness > 0)
|
|
{
|
|
var smoothedLookInputDirection = Vector3.Slerp(
|
|
motor.CharacterForward,
|
|
_inputHandler.LookInputVector,
|
|
1f - Mathf.Exp(-_characterConfig.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 (_dashMechanic.IsDashing && _groundMovement.JumpRequested)
|
|
_dashMechanic.EndDash();
|
|
if (_slideMechanic.IsSliding && _groundMovement.JumpRequested)
|
|
_slideMechanic.EndSlide();
|
|
|
|
if (!_dashMechanic.IsDashing)
|
|
_dashMechanic.TryStartDash(motor, _currentCameraMode, _inputHandler.FirstPersonCameraRotation, _inputHandler.LookInputVector);
|
|
else
|
|
{
|
|
_dashMechanic.HandleDash(ref currentVelocity, deltaTime, motor);
|
|
return;
|
|
}
|
|
|
|
var wasSliding = _wasSlidingLastFrame;
|
|
var isSlidingNow = _slideMechanic.IsSliding;
|
|
|
|
if (!isSlidingNow)
|
|
{
|
|
_slideMechanic.TryStartSlide(motor, _currentCameraMode, _inputHandler.FirstPersonCameraRotation, _inputHandler.LookInputVector);
|
|
isSlidingNow = _slideMechanic.IsSliding;
|
|
}
|
|
|
|
switch (isSlidingNow)
|
|
{
|
|
case true when !wasSliding:
|
|
EnterSlideState();
|
|
break;
|
|
case false when wasSliding:
|
|
ExitSlideState();
|
|
break;
|
|
}
|
|
|
|
if (isSlidingNow)
|
|
{
|
|
_slideMechanic.HandleSlide(ref currentVelocity, deltaTime, motor);
|
|
}
|
|
|
|
_wasSlidingLastFrame = isSlidingNow;
|
|
|
|
// === WALL RUNNING: обработка состояния ===
|
|
if (_wallRunningMechanic.IsWallRunning)
|
|
{
|
|
// 🔄 Передаём актуальные данные для walljump
|
|
if (_groundMovement.JumpRequested) // или _lastInputs.JumpPressed — зависит от логики
|
|
{
|
|
_wallRunningMechanic.PerformWallJump(_lastInputs, _inputHandler.FirstPersonCameraRotation);
|
|
_groundMovement.CancelJumpRequest(); // сброс, чтобы не сработало дважды
|
|
}
|
|
else
|
|
{
|
|
_wallRunningMechanic.HandleWallRun(ref currentVelocity, _lastInputs, deltaTime);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Если персонаж находится на земле
|
|
if (motor.GroundingStatus.IsStableOnGround)
|
|
{
|
|
_groundMovement.UpdateVelocity(ref currentVelocity, _inputHandler, motor, deltaTime);
|
|
}
|
|
// Если персонаж не на земле — стандартное воздушное движение
|
|
else
|
|
{
|
|
_airMovement.UpdateVelocity(ref currentVelocity, _inputHandler, motor, deltaTime);
|
|
_groundMovement.CancelJumpRequest();
|
|
}
|
|
}
|
|
|
|
public void BlockAbilities(float duration)
|
|
{
|
|
isAbilitiesBlocked = true;
|
|
abilityBlockTimer = duration;
|
|
}
|
|
|
|
private void EnterSlideState()
|
|
{
|
|
_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 = _currentColliderHeight;
|
|
_capsuleCollider.center = _currentColliderCenter;
|
|
}
|
|
|
|
if (_cameraFollowPoint)
|
|
{
|
|
_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 TriggerJump()
|
|
{
|
|
_jumpCooldownTimer = _jumpCooldownDuration;
|
|
}
|
|
|
|
public void BeforeCharacterUpdate(float deltaTime)
|
|
{
|
|
}
|
|
|
|
public void PostGroundingUpdate(float deltaTime)
|
|
{
|
|
if (_dashMechanic.IsDashing && !motor.GroundingStatus.IsStableOnGround)
|
|
_dashMechanic.EndDash();
|
|
if (_slideMechanic.IsSliding && !motor.GroundingStatus.IsStableOnGround)
|
|
_slideMechanic.EndSlide();
|
|
}
|
|
|
|
public void AfterCharacterUpdate(float deltaTime)
|
|
{
|
|
UpdateSlideTransition(deltaTime);
|
|
|
|
_jumpCooldownTimer -= deltaTime;
|
|
if (_jumpCooldownTimer < 0f) _jumpCooldownTimer = 0f;
|
|
|
|
_wallLeanMechanic.Update(
|
|
_wallRunningMechanic.IsWallRunning,
|
|
_wallRunningMechanic.GetWallNormal(),
|
|
motor.CharacterForward
|
|
);
|
|
}
|
|
|
|
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 (_wallRunningMechanic.TryStartWallRun(hitNormal, hitPoint)) return;
|
|
|
|
var horizontalVelocity = Vector3.ProjectOnPlane(motor.Velocity, motor.CharacterUp).normalized;
|
|
var velocitySqrMag = horizontalVelocity.sqrMagnitude;
|
|
|
|
if (!(velocitySqrMag > 0.01f)) return;
|
|
|
|
var dot = Vector3.Dot(horizontalVelocity, hitNormal);
|
|
|
|
const float impactThreshold = -0.3f;
|
|
|
|
if (!(dot < impactThreshold)) return;
|
|
if (_dashMechanic.IsDashing)
|
|
_dashMechanic.EndDash();
|
|
|
|
if (_slideMechanic.IsSliding)
|
|
_slideMechanic.EndSlide();
|
|
}
|
|
|
|
public void ProcessHitStabilityReport(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint,
|
|
Vector3 atCharacterPosition,
|
|
Quaternion atCharacterRotation, ref HitStabilityReport hitStabilityReport)
|
|
{
|
|
}
|
|
|
|
public void OnDiscreteCollisionDetected(Collider hitCollider)
|
|
{
|
|
}
|
|
}
|
|
} |