Files
Infratest/Assets/Code/Example/Liminal/Character/LiminalCharacterController.cs
T
ThomasCharester 5f5ace1b52 Essentials + extra crap
Loadings
Startups
Movement
Camera
2026-01-12 18:43:51 +03:00

483 lines
19 KiB
C#

using System.Collections.Generic;
using KinematicCharacterController;
using UnityEngine;
namespace Code.Example.Liminal.Character
{
public enum CharacterState
{
Default,
}
public enum OrientationMethod
{
TowardsCamera,
TowardsMovement,
}
public class PlayerCharacterInputs
{
public float MoveAxisForward;
public float MoveAxisRight;
public Quaternion CameraRotation;
public bool CrouchDown;
public bool CrouchUp;
}
public struct AICharacterInputs
{
public Vector3 MoveVector;
public Vector3 LookVector;
}
public enum BonusOrientationMethod
{
None,
TowardsGravity,
TowardsGroundSlopeAndGravity,
}
public class LiminalCharacterController : MonoBehaviour, ICharacterController
{
public KinematicCharacterMotor Motor;
[Header("Stable Movement")] public float MaxStableMoveSpeed = 10f;
public float StableMovementSharpness = 15f;
public float OrientationSharpness = 10f;
public bool InfinitelySharpRotation;
public OrientationMethod OrientationMethod = OrientationMethod.TowardsCamera;
[Header("Air Movement")] public float MaxAirMoveSpeed = 15f;
public float AirAccelerationSpeed = 15f;
public float Drag = 0.1f;
[Header("Misc")] public List<Collider> IgnoredColliders = new List<Collider>();
public BonusOrientationMethod BonusOrientationMethod = BonusOrientationMethod.None;
public float BonusOrientationSharpness = 10f;
public Vector3 Gravity = new Vector3(0, -30f, 0);
// public Transform MeshRoot;
public float CrouchedCapsuleHeight = 1f;
public CharacterState CurrentCharacterState { get; private set; }
private Collider[] _probedColliders = new Collider[8];
private RaycastHit[] _probedHits = new RaycastHit[8];
private Vector3 _moveInputVector;
private Vector3 _lookInputVector;
private Vector3 _internalVelocityAdd = Vector3.zero;
private bool _shouldBeCrouching = false;
private bool _isCrouching = false;
public void Teleport(Vector3 position, Quaternion rotation)
{
Motor.SetPosition(position);
Motor.SetRotation(rotation);
}
private void Awake()
{
// Handle initial state
TransitionToState(CharacterState.Default);
// Assign the characterController to the motor
Motor.CharacterController = this;
}
/// <summary>
/// Handles movement state transitions and enter/exit callbacks
/// </summary>
public void TransitionToState(CharacterState newState)
{
CharacterState tmpInitialState = CurrentCharacterState;
OnStateExit(tmpInitialState, newState);
CurrentCharacterState = newState;
OnStateEnter(newState, tmpInitialState);
}
/// <summary>
/// Event when entering a state
/// </summary>
private void OnStateEnter(CharacterState state, CharacterState fromState)
{
switch (state)
{
case CharacterState.Default:
{
break;
}
}
}
/// <summary>
/// Event when exiting a state
/// </summary>
private void OnStateExit(CharacterState state, CharacterState toState)
{
switch (state)
{
case CharacterState.Default:
{
break;
}
}
}
/// <summary>
/// This is called every frame by ExamplePlayer in order to tell the character what its inputs are
/// </summary>
public void SetInputs(PlayerCharacterInputs inputs)
{
// Clamp input
Vector3 moveInputVector =
Vector3.ClampMagnitude(new Vector3(inputs.MoveAxisRight, 0f, inputs.MoveAxisForward), 1f);
// Calculate camera direction and rotation on the character plane
Vector3 cameraPlanarDirection =
Vector3.ProjectOnPlane(inputs.CameraRotation * Vector3.forward, Motor.CharacterUp).normalized;
if (cameraPlanarDirection.sqrMagnitude == 0f)
{
cameraPlanarDirection = Vector3.ProjectOnPlane(inputs.CameraRotation * Vector3.up, Motor.CharacterUp)
.normalized;
}
Quaternion cameraPlanarRotation = Quaternion.LookRotation(cameraPlanarDirection, Motor.CharacterUp);
switch (CurrentCharacterState)
{
case CharacterState.Default:
{
// Move and look inputs
_moveInputVector = cameraPlanarRotation * moveInputVector;
switch (OrientationMethod)
{
case OrientationMethod.TowardsCamera:
_lookInputVector = cameraPlanarDirection;
break;
case OrientationMethod.TowardsMovement:
_lookInputVector = _moveInputVector.normalized;
break;
}
// Crouching input
if (inputs.CrouchDown)
{
_shouldBeCrouching = true;
if (!_isCrouching)
{
_isCrouching = true;
Motor.SetCapsuleDimensions(0.5f, CrouchedCapsuleHeight, CrouchedCapsuleHeight * 0.5f);
// MeshRoot.localScale = new Vector3(1f, 0.5f, 1f);
}
}
else if (inputs.CrouchUp)
{
_shouldBeCrouching = false;
}
break;
}
}
}
/// <summary>
/// This is called every frame by the AI script in order to tell the character what its inputs are
/// </summary>
public void SetInputs(ref AICharacterInputs inputs)
{
_moveInputVector = inputs.MoveVector;
_lookInputVector = inputs.LookVector;
}
private Quaternion _tmpTransientRot;
/// <summary>
/// (Called by KinematicCharacterMotor during its update cycle)
/// This is called before the character begins its movement update
/// </summary>
public void BeforeCharacterUpdate(float deltaTime)
{
}
/// <summary>
/// (Called by KinematicCharacterMotor during its update cycle)
/// This is where you tell your character what its rotation should be right now.
/// This is the ONLY place where you should set the character's rotation
/// </summary>
public void UpdateRotation(ref Quaternion currentRotation, float deltaTime)
{
switch (CurrentCharacterState)
{
case CharacterState.Default:
{
if (!InfinitelySharpRotation)
{
if (_lookInputVector.sqrMagnitude > 0f && OrientationSharpness > 0f)
{
// Smoothly interpolate from current to target look direction
Vector3 smoothedLookInputDirection = Vector3.Slerp(Motor.CharacterForward,
_lookInputVector, 1 - Mathf.Exp(-OrientationSharpness * deltaTime)).normalized;
// Set the current rotation (which will be used by the KinematicCharacterMotor)
currentRotation =
Quaternion.LookRotation(smoothedLookInputDirection, Motor.CharacterUp);
}
}
else
{
currentRotation = Quaternion.LookRotation(_lookInputVector, Motor.CharacterUp);
}
Vector3 currentUp = (currentRotation * Vector3.up);
if (BonusOrientationMethod == BonusOrientationMethod.TowardsGravity)
{
// Rotate from current up to invert gravity
Vector3 smoothedGravityDir = Vector3.Slerp(currentUp, -Gravity.normalized,
1 - Mathf.Exp(-BonusOrientationSharpness * deltaTime));
currentRotation = Quaternion.FromToRotation(currentUp, smoothedGravityDir) * currentRotation;
}
else if (BonusOrientationMethod == BonusOrientationMethod.TowardsGroundSlopeAndGravity)
{
if (Motor.GroundingStatus.IsStableOnGround)
{
Vector3 initialCharacterBottomHemiCenter =
Motor.TransientPosition + (currentUp * Motor.Capsule.radius);
Vector3 smoothedGroundNormal = Vector3.Slerp(Motor.CharacterUp,
Motor.GroundingStatus.GroundNormal,
1 - Mathf.Exp(-BonusOrientationSharpness * deltaTime));
currentRotation = Quaternion.FromToRotation(currentUp, smoothedGroundNormal) *
currentRotation;
// Move the position to create a rotation around the bottom hemi center instead of around the pivot
Motor.SetTransientPosition(initialCharacterBottomHemiCenter +
(currentRotation * Vector3.down * Motor.Capsule.radius));
}
else
{
Vector3 smoothedGravityDir = Vector3.Slerp(currentUp, -Gravity.normalized,
1 - Mathf.Exp(-BonusOrientationSharpness * deltaTime));
currentRotation = Quaternion.FromToRotation(currentUp, smoothedGravityDir) *
currentRotation;
}
}
else
{
Vector3 smoothedGravityDir = Vector3.Slerp(currentUp, Vector3.up,
1 - Mathf.Exp(-BonusOrientationSharpness * deltaTime));
currentRotation = Quaternion.FromToRotation(currentUp, smoothedGravityDir) * currentRotation;
}
break;
}
}
}
/// <summary>
/// (Called by KinematicCharacterMotor during its update cycle)
/// This is where you tell your character what its velocity should be right now.
/// This is the ONLY place where you can set the character's velocity
/// </summary>
public void UpdateVelocity(ref Vector3 currentVelocity, float deltaTime)
{
switch (CurrentCharacterState)
{
case CharacterState.Default:
{
// Ground movement
if (Motor.GroundingStatus.IsStableOnGround)
{
float currentVelocityMagnitude = currentVelocity.magnitude;
Vector3 effectiveGroundNormal = Motor.GroundingStatus.GroundNormal;
// Reorient velocity on slope
currentVelocity = Motor.GetDirectionTangentToSurface(currentVelocity, effectiveGroundNormal) *
currentVelocityMagnitude;
// Calculate target velocity
Vector3 inputRight = Vector3.Cross(_moveInputVector, Motor.CharacterUp);
Vector3 reorientedInput = Vector3.Cross(effectiveGroundNormal, inputRight).normalized *
_moveInputVector.magnitude;
Vector3 targetMovementVelocity = reorientedInput * MaxStableMoveSpeed;
// Smooth movement Velocity
currentVelocity = Vector3.Lerp(currentVelocity, targetMovementVelocity,
1f - Mathf.Exp(-StableMovementSharpness * deltaTime));
}
// Air movement
else
{
// Add move input
if (_moveInputVector.sqrMagnitude > 0f)
{
Vector3 addedVelocity = _moveInputVector * AirAccelerationSpeed * deltaTime;
Vector3 currentVelocityOnInputsPlane =
Vector3.ProjectOnPlane(currentVelocity, Motor.CharacterUp);
// Limit air velocity from inputs
if (currentVelocityOnInputsPlane.magnitude < MaxAirMoveSpeed)
{
// clamp addedVel to make total vel not exceed max vel on inputs plane
Vector3 newTotal = Vector3.ClampMagnitude(currentVelocityOnInputsPlane + addedVelocity,
MaxAirMoveSpeed);
addedVelocity = newTotal - currentVelocityOnInputsPlane;
}
else
{
// Make sure added vel doesn't go in the direction of the already-exceeding velocity
if (Vector3.Dot(currentVelocityOnInputsPlane, addedVelocity) > 0f)
{
addedVelocity = Vector3.ProjectOnPlane(addedVelocity,
currentVelocityOnInputsPlane.normalized);
}
}
// Prevent air-climbing sloped walls
if (Motor.GroundingStatus.FoundAnyGround)
{
if (Vector3.Dot(currentVelocity + addedVelocity, addedVelocity) > 0f)
{
Vector3 perpenticularObstructionNormal = Vector3
.Cross(Vector3.Cross(Motor.CharacterUp, Motor.GroundingStatus.GroundNormal),
Motor.CharacterUp).normalized;
addedVelocity =
Vector3.ProjectOnPlane(addedVelocity, perpenticularObstructionNormal);
}
}
// Apply added velocity
currentVelocity += addedVelocity;
}
// Gravity
currentVelocity += Gravity * deltaTime;
// Drag
currentVelocity *= (1f / (1f + (Drag * deltaTime)));
}
// Take into account additive velocity
if (_internalVelocityAdd.sqrMagnitude > 0f)
{
currentVelocity += _internalVelocityAdd;
_internalVelocityAdd = Vector3.zero;
}
break;
}
}
}
/// <summary>
/// (Called by KinematicCharacterMotor during its update cycle)
/// This is called after the character has finished its movement update
/// </summary>
public void AfterCharacterUpdate(float deltaTime)
{
switch (CurrentCharacterState)
{
case CharacterState.Default:
{
// Handle uncrouching
if (_isCrouching && !_shouldBeCrouching)
{
// Do an overlap test with the character's standing height to see if there are any obstructions
Motor.SetCapsuleDimensions(0.5f, 2f, 1f);
if (Motor.CharacterOverlap(
Motor.TransientPosition,
Motor.TransientRotation,
_probedColliders,
Motor.CollidableLayers,
QueryTriggerInteraction.Ignore) > 0)
{
// If obstructions, just stick to crouching dimensions
Motor.SetCapsuleDimensions(0.5f, CrouchedCapsuleHeight, CrouchedCapsuleHeight * 0.5f);
}
else
{
// If no obstructions, uncrouch
// MeshRoot.localScale = new Vector3(1f, 1f, 1f);
_isCrouching = false;
}
}
break;
}
}
}
public void PostGroundingUpdate(float deltaTime)
{
// Handle landing and leaving ground
if (Motor.GroundingStatus.IsStableOnGround && !Motor.LastGroundingStatus.IsStableOnGround)
{
OnLanded();
}
else if (!Motor.GroundingStatus.IsStableOnGround && Motor.LastGroundingStatus.IsStableOnGround)
{
OnLeaveStableGround();
}
}
public bool IsColliderValidForCollisions(Collider coll)
{
if (IgnoredColliders.Count == 0)
{
return true;
}
if (IgnoredColliders.Contains(coll))
{
return false;
}
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)
{
}
public void AddVelocity(Vector3 velocity)
{
switch (CurrentCharacterState)
{
case CharacterState.Default:
{
_internalVelocityAdd += velocity;
break;
}
}
}
public void ProcessHitStabilityReport(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint,
Vector3 atCharacterPosition, Quaternion atCharacterRotation, ref HitStabilityReport hitStabilityReport)
{
}
protected void OnLanded()
{
}
protected void OnLeaveStableGround()
{
}
public void OnDiscreteCollisionDetected(Collider hitCollider)
{
}
}
}