working on fpp locomotion, camera spring

This commit is contained in:
2025-06-07 10:39:06 +02:00
parent 0abddece7e
commit e99314d5d8
40 changed files with 4112 additions and 462 deletions

View File

@@ -0,0 +1,59 @@
using UnityEngine;
namespace RealityReboot.jelycho.Player {
public class CameraSpring : MonoBehaviour {
[Min(0.01f), SerializeField] float m_HalfLife = 0.075f;
[SerializeField] float m_Frequency = 10.0f;
[SerializeField] float m_ForwardAngularDisplacement = 1.0f;
[SerializeField] float m_SidewaysAngularDisplacement = 1.0f;
[SerializeField] float m_HeightAngularDisplacement = 2.0f;
[SerializeField] float m_LinearDisplacement = 0.05f;
Vector3 m_SpringPosition;
Vector3 m_SpringVelocity;
public void Initialize() {
m_SpringPosition = transform.position;
m_SpringVelocity = Vector3.zero;
}
public void UpdateSpring(float deltaTime, Vector3 up, Vector3 right, Vector3 forward) {
Spring(ref m_SpringPosition,
ref m_SpringVelocity,
transform.position,
m_HalfLife,
m_Frequency,
deltaTime);
Vector3 localSpringPosition = m_SpringPosition - transform.position;
float springHeight = Vector3.Dot(localSpringPosition, up);
float springForward = Vector3.Dot(localSpringPosition, forward);
float springSideways = Vector3.Dot(localSpringPosition, right);
float pitch = -springHeight * m_HeightAngularDisplacement + -springForward * m_ForwardAngularDisplacement;
transform.localEulerAngles = new Vector3(pitch,
0.0f,
springSideways * m_SidewaysAngularDisplacement);
}
// Source: https://allenchou.net/2015/04/game-math-precise-control-over-numeric-springing/
static void Spring(ref Vector3 current,
ref Vector3 velocity,
Vector3 target,
float halfLife,
float frequency,
float timeStep) {
float dampingRatio = -Mathf.Log(0.5f) / (frequency * halfLife);
float f = 1.0f + 2.0f * timeStep * dampingRatio * frequency;
float oo = frequency * frequency;
float hoo = timeStep * oo;
float hhoo = timeStep * hoo;
float detInv = 1.0f / (f + hhoo);
Vector3 detX = f * current + timeStep * velocity + hhoo * target;
Vector3 detV = velocity + hoo * (target - current);
current = detX * detInv;
velocity = detV * detInv;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f3305bd258b340d69d9b813f76dc5a01
timeCreated: 1749115409

View File

@@ -0,0 +1,59 @@
using RebootKit.Engine.Foundation;
using RebootKit.Engine.Services.Simulation.Sensors;
using Unity.Cinemachine;
using UnityEngine;
namespace RealityReboot.jelycho.Player {
[AddComponentMenu(GameConsts.k_AddComponentMenu + "Player/First Person Camera")]
public class FPPCamera : MonoBehaviour {
[ConfigVar("fpp.camera.fov", 60.0f, "Field of view for the first person camera.")]
static ConfigVar s_cameraFOV;
[SerializeField] float _pickDistance = 5.0f;
[SerializeField] LayerMask _pickLayer;
[field: SerializeField]
public float Sensitivity { get; set; }
[SerializeField] float _pitchMin = -80f;
[SerializeField] float _pitchMax = 80f;
[field: SerializeField]
public CinemachineCamera Camera { get; private set; }
readonly RaycastSensor _raycastSensor = new();
public float Pitch { get; private set; }
public float Yaw { get; private set; }
public ISensor Sensor {
get {
_raycastSensor.ray = new Ray(Camera.transform.position, Camera.transform.forward);
_raycastSensor.maxDistance = _pickDistance;
_raycastSensor.layerMask = _pickLayer;
return _raycastSensor;
}
}
public void Tick() {
Camera.Lens.FieldOfView = s_cameraFOV.FloatValue;
Camera.transform.localRotation = Quaternion.Euler(Pitch, 0f, 0f);
}
public void Rotate(float x, float y) {
float sens = Sensitivity;
Pitch -= y * sens;
Pitch = Mathf.Clamp(Pitch, _pitchMin, _pitchMax);
Yaw += x * sens;
}
public void SetLookDirection(Vector3 forward) {
Pitch = Mathf.Asin(-forward.y) * Mathf.Rad2Deg;
Yaw = Mathf.Atan2(forward.x, forward.z) * Mathf.Rad2Deg;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b82691de261d95842bde9bca81db0893

View File

@@ -1,7 +1,6 @@
using RebootKit.Engine.Extensions;
using RebootKit.Engine.Foundation;
using RebootKit.Engine.Services.Simulation;
using RebootKit.FPPKit;
using Unity.Mathematics;
using UnityEngine;
using Logger = RebootKit.Engine.Foundation.Logger;
@@ -9,22 +8,33 @@ using Logger = RebootKit.Engine.Foundation.Logger;
namespace RealityReboot.jelycho.Player {
public class PlayerActor : Actor {
static readonly Logger s_logger = new(nameof(PlayerActor));
[SerializeField] PlayerFPPLocomotion m_Locomotion;
[SerializeField] FPPCamera m_Camera;
[SerializeField] Animator m_Animator;
[SerializeField] Transform m_DragGutStartPosition;
[SerializeField] Animator m_Animator;
[Header("Movement")]
[SerializeField] PlayerFPPLocomotion m_Locomotion;
[Header("Camera")]
[SerializeField] FPPCamera m_Camera;
[SerializeField] CameraSpring m_CameraSpring;
[Header("Dragging")]
[SerializeField] Transform m_DragGutStartPosition;
[SerializeField] PhysicsObjectDragger m_PhysicsDragger;
[SerializeField] FloatRange m_DragDistanceRange = new(1.0f, 5.0f);
[SerializeField] LineRenderer m_LineRenderer;
public override void OnBeginPlay() {
m_CameraSpring.Initialize();
}
public override void OnTick() {
m_Locomotion.YawRotation = m_Camera.Yaw;
m_Camera.Tick();
m_PhysicsDragger.TargetWorldPosition = m_Camera.Camera.transform.position + m_Camera.Camera.transform.forward * 2.0f;
if (m_PhysicsDragger.Current.OrNull() != null) {
if (m_PhysicsDragger.Current.OrNull() is not null) {
m_LineRenderer.enabled = true;
m_LineRenderer.SetPosition(0, m_LineRenderer.transform.InverseTransformPoint(m_DragGutStartPosition.position));
m_LineRenderer.SetPosition(1, m_LineRenderer.transform.InverseTransformPoint(m_PhysicsDragger.Current.position));
@@ -32,8 +42,17 @@ namespace RealityReboot.jelycho.Player {
m_LineRenderer.enabled = false;
}
Vector3 actorUp = transform.up;
Vector3 actorRight = transform.right;
Vector3 actorForward = transform.forward;
m_CameraSpring.UpdateSpring(Time.deltaTime, actorUp, actorRight, actorForward);
UpdateAnimator();
}
public void SetSprint(bool isSprinting) {
m_Locomotion.SetSprint(isSprinting);
}
public void Jump() {
m_Locomotion.Jump();
@@ -44,7 +63,8 @@ namespace RealityReboot.jelycho.Player {
}
public void SetMoveInput(Vector2 input) {
float3 direction = Quaternion.AngleAxis(m_Camera.Yaw, Vector3.up) * new float3(input.x, 0.0f, input.y);
float3 direction = Quaternion.AngleAxis(transform.eulerAngles.y, Vector3.up) *
new float3(input.x, 0.0f, input.y);
m_Locomotion.SetWishDirection(direction);
}
@@ -66,12 +86,12 @@ namespace RealityReboot.jelycho.Player {
}
void UpdateAnimator() {
float forwardNormalized = m_Locomotion.LocalVelocity.z / m_Locomotion.runSpeed;
m_Animator.SetFloat(AnimatorParamHashes.s_VelocityForwardNormalized, forwardNormalized);
m_Animator.SetFloat(AnimatorParamHashes.s_VelocityRightNormalized, 0.0f);
m_Animator.SetBool(AnimatorParamHashes.s_IsGrounded, m_Locomotion.IsGrounded);
// float forwardNormalized = m_Locomotion.LocalVelocity.z / m_Locomotion.runSpeed;
//
// m_Animator.SetFloat(AnimatorParamHashes.s_VelocityForwardNormalized, forwardNormalized);
// m_Animator.SetFloat(AnimatorParamHashes.s_VelocityRightNormalized, 0.0f);
//
// m_Animator.SetBool(AnimatorParamHashes.s_IsGrounded, m_Locomotion.IsGrounded);
}
}
}

View File

@@ -5,7 +5,7 @@ using RebootKit.Engine;
using RebootKit.Engine.Foundation;
using RebootKit.Engine.Main;
using RebootKit.Engine.Services.Input;
using RebootKit.FPPKit;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.InputSystem;
@@ -57,6 +57,8 @@ namespace RealityReboot.jelycho.Player {
Vector2 moveInput = m_Config.moveActionReference.action.ReadValue<Vector2>();
m_FPPActor.SetMoveInput(moveInput);
m_FPPActor.SetSprint(m_Config.sprintActionReference.action.IsPressed());
if (m_Config.jumpActionReference.action.WasPerformedThisFrame()) {
m_FPPActor.Jump();
}
@@ -77,9 +79,40 @@ namespace RealityReboot.jelycho.Player {
public InputActionReference moveActionReference;
public InputActionReference lookActionReference;
public InputActionReference jumpActionReference;
public InputActionReference sprintActionReference;
public InputActionReference dragObjectActionReference;
public InputActionReference primaryActionReference;
public InputActionReference secondaryActionReference;
}
}
public static class FPPConfig {
[ConfigVar("fpp.mouse.sens", 0.25f, "Mouse look sensitivity")] public static ConfigVar s_MouseSensitivity;
[ConfigVar("fpp.mouse.invert.y", 0, "Invert mouse look")] public static ConfigVar s_MouseInvertY;
[ConfigVar("fpp.gamepad.sens", 4.0f, "Gamepad look sensitivity")] public static ConfigVar s_GamepadSensitivity;
}
public static class SensitivityReader {
public static float2 Read(InputAction action) {
if (action == null || action.activeControl == null) {
return float2.zero;
}
float2 sensitivity = action.ReadValue<Vector2>();
bool isGamepad = action.activeControl.device is Gamepad;
if (isGamepad) {
sensitivity *= FPPConfig.s_GamepadSensitivity.FloatValue;
} else {
sensitivity *= FPPConfig.s_MouseSensitivity.FloatValue;
if (FPPConfig.s_MouseInvertY.IndexValue == 1) {
sensitivity.y *= -1;
}
}
return sensitivity;
}
}
}

View File

@@ -1,17 +1,15 @@
using System;
using System.Runtime.CompilerServices;
using RebootKit.Engine.Extensions;
using KinematicCharacterController;
using Unity.Mathematics;
using UnityEngine;
using Logger = RebootKit.Engine.Foundation.Logger;
namespace RealityReboot.jelycho.Player {
[RequireComponent(typeof(Rigidbody), typeof(CapsuleCollider))]
public class PlayerFPPLocomotion : MonoBehaviour {
static readonly RaycastHit[] s_HitBuffer = new RaycastHit[32];
[SerializeField] Rigidbody m_Rigidbody;
[SerializeField] CapsuleCollider m_CapsuleCollider;
[SerializeField] LayerMask m_GroundLayerMask;
public class PlayerFPPLocomotion : MonoBehaviour, ICharacterController {
static readonly Logger s_logger = new(nameof(PlayerFPPLocomotion));
[SerializeField] KinematicCharacterMotor m_Motor;
public float runSpeed = 10.0f;
public float sprintSpeed = 20.0f;
@@ -19,146 +17,170 @@ namespace RealityReboot.jelycho.Player {
public float groundAcceleration = 10.0f;
public float groundFriction = 10.0f;
public float airFriction = 1.0f;
public float jumpHeight = 2.0f;
public float inAirAcceleration = 10.0f;
public float jumpInputDuration = 0.2f;
public float coyoteDuration = 0.2f;
[Range(0.0f, 1.0f)] public float airControl = 0.5f;
public float gravity = 9.8f;
public float3 Velocity => m_Rigidbody.linearVelocity;
public float3 LocalVelocity => m_Rigidbody.transform.InverseTransformDirection(m_Rigidbody.linearVelocity);
float3 m_WishDir;
bool m_IsSprinting;
public bool IsGrounded { get; private set; }
public float YawRotation { get; set; }
Vector3 m_WishDir;
bool m_IsSprinting;
bool m_IsJumpRequested;
float m_JumpRequestedTime;
float m_LastGroundedTime;
Vector3 m_LastVelocity;
public bool IsGrounded => m_Motor.GroundingStatus.IsStableOnGround;
void Awake() {
m_Motor.CharacterController = this;
}
void FixedUpdate() {
IsGrounded = CheckGrounded();
float3 vel = m_Rigidbody.linearVelocity;
if (IsGrounded) {
UpdateGroundVelocity(ref vel, Time.fixedDeltaTime);
} else {
UpdateAirVelocity(ref vel, Time.fixedDeltaTime);
}
m_Rigidbody.linearVelocity = vel;
}
void ApplyGroundFriction(ref float3 vel, float dt) {
float speed = math.length(vel);
if (speed < 0.1f) {
vel = float3.zero;
return;
}
float drop = speed * groundFriction * dt;
float newSpeed = speed - drop;
if (newSpeed < 0.0f) {
newSpeed = 0.0f;
}
newSpeed /= speed;
vel *= newSpeed;
}
void ApplyGravity(ref float3 vel, float dt) {
vel.y -= gravity * dt;
}
void Accelerate(ref float3 vel, float3 wishDir, float wishSpeed, float acceleration, float dt) {
float currentSpeed = math.dot(vel, wishDir);
float addSpeed = wishSpeed - currentSpeed;
if (addSpeed <= 0.0f) {
return;
}
float accelSpeed = acceleration * dt * wishSpeed;
if (accelSpeed > addSpeed) {
accelSpeed = addSpeed;
}
vel += accelSpeed * wishDir;
}
void UpdateGroundVelocity(ref float3 vel, float dt) {
ApplyGroundFriction(ref vel, dt);
float wishSpeed;
if (m_IsSprinting) {
wishSpeed = sprintSpeed;
} else {
wishSpeed = runSpeed;
}
Accelerate(ref vel, m_WishDir, wishSpeed, groundAcceleration, dt);
ApplyGravity(ref vel, dt);
}
void UpdateAirVelocity(ref float3 vel, float dt) {
ApplyGravity(ref vel, dt);
Accelerate(ref vel, m_WishDir, runSpeed, inAirAcceleration, dt);
}
void OnValidate() {
if (m_Rigidbody == null) {
m_Rigidbody = GetComponent<Rigidbody>();
}
}
void OnDrawGizmosSelected() {
float3 origin = transform.position;
void OnDrawGizmos() {
Gizmos.color = Color.blue;
Gizmos.DrawLine(origin, origin + m_WishDir);
Gizmos.color = Color.green;
Gizmos.DrawLine(origin, origin + Velocity);
Gizmos.DrawLine(transform.position,
transform.position + m_WishDir * 2.0f);
}
bool CheckGrounded() {
float3 origin = transform.position + Vector3.up * 0.1f;
float3 direction = Vector3.down;
float distance = 0.2f;
int hitCount = Physics.SphereCastNonAlloc(origin,
m_CapsuleCollider.radius,
direction,
s_HitBuffer,
distance,
m_GroundLayerMask,
QueryTriggerInteraction.Ignore);
for (int i = 0; i < hitCount; i++) {
RaycastHit hit = s_HitBuffer[i];
if (hit.collider is not null && hit.collider.gameObject != gameObject) {
return true;
}
}
return false;
void OnGUI() {
GUI.Label(new Rect(0, 0, Screen.width, Screen.height),
$"Wish Direction: {m_WishDir}\n" +
$"Is Grounded: {m_Motor.GroundingStatus.IsStableOnGround}\n" +
$"Is Jump Requested: {m_IsJumpRequested}\n" +
$"Motor Velocity: {m_Motor.Velocity}, magnitude: {m_Motor.Velocity.magnitude}\n" +
$"Motor Base Velocity: {m_Motor.BaseVelocity}, magnitude: {m_Motor.BaseVelocity.magnitude}\n" +
$"Last Velocity: {m_LastVelocity}, magnitude: {math.length(m_LastVelocity)}\n");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetWishDirection(float3 dir) {
public void SetWishDirection(Vector3 dir) {
m_WishDir = dir;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Jump() {
if (IsGrounded) {
m_Rigidbody.AddForce(Vector3.up * CalculateJumpVelocity(jumpHeight, gravity), ForceMode.VelocityChange);
}
m_IsJumpRequested = true;
m_JumpRequestedTime = Time.time;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetSprint(bool isSprinting) {
m_IsSprinting = isSprinting;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static float CalculateJumpVelocity(float jumpHeight, float gravity) {
return math.sqrt(2.0f * gravity * jumpHeight);
}
public void UpdateRotation(ref Quaternion currentRotation, float deltaTime) {
currentRotation = Quaternion.AngleAxis(YawRotation, Vector3.up);
}
void Accelerate(ref Vector3 currentVelocity,
float wishSpeed,
float control,
float deltaTime) {
float currentSpeed = Vector3.Dot(currentVelocity, m_WishDir);
float addSpeed = wishSpeed - currentSpeed;
if (addSpeed <= 0.0f) {
return;
}
float accelSpeed = groundAcceleration * deltaTime * wishSpeed;
if (accelSpeed > addSpeed) {
accelSpeed = addSpeed;
}
currentVelocity += accelSpeed * control * m_WishDir;
}
void ApplyFriction(ref Vector3 currentVelocity, float friction, float deltaTime) {
float speed = currentVelocity.magnitude;
if (speed <= 0.0f) {
return;
}
float drop = speed * friction * deltaTime;
if (drop < speed) {
currentVelocity *= (speed - drop) / speed;
} else {
currentVelocity = Vector3.zero;
}
}
void GroundMovement(ref Vector3 currentVelocity, float deltaTime) {
float wishSpeed = m_IsSprinting ? sprintSpeed : runSpeed;
Accelerate(ref currentVelocity, wishSpeed, 1.0f, deltaTime);
ApplyFriction(ref currentVelocity, groundFriction, deltaTime);
}
void ApplyGravity(ref Vector3 currentVelocity, float deltaTime) {
currentVelocity.y -= gravity * deltaTime;
}
public void UpdateVelocity(ref Vector3 currentVelocity, float deltaTime) {
if (Time.time > m_JumpRequestedTime + jumpInputDuration) {
m_IsJumpRequested = false;
}
if (m_Motor.GroundingStatus.IsStableOnGround) {
m_LastGroundedTime = Time.time;
}
bool canJump = m_Motor.GroundingStatus.IsStableOnGround ||
Time.time < m_LastGroundedTime + coyoteDuration;
if (m_IsJumpRequested && canJump) {
currentVelocity.y = CalculateJumpVelocity(jumpHeight, gravity);
m_Motor.ForceUnground();
m_IsJumpRequested = false;
m_LastGroundedTime = 0.0f;
}
if (m_Motor.GroundingStatus.IsStableOnGround) {
GroundMovement(ref currentVelocity, deltaTime);
} else {
Accelerate(ref currentVelocity, runSpeed, airControl, deltaTime);
ApplyFriction(ref currentVelocity, airFriction, deltaTime);
ApplyGravity(ref currentVelocity, deltaTime);
}
m_LastVelocity = currentVelocity;
}
public void BeforeCharacterUpdate(float deltaTime) {
}
public void PostGroundingUpdate(float deltaTime) {
}
public void AfterCharacterUpdate(float deltaTime) {
}
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) {
}
public void ProcessHitStabilityReport(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint, Vector3 atCharacterPosition, Quaternion atCharacterRotation, ref HitStabilityReport hitStabilityReport) {
}
public void OnDiscreteCollisionDetected(Collider hitCollider) {
}
}
}