rabidus-test/Assets/BNG Framework/Scripts/Core/SmoothLocomotion.cs

611 lines
22 KiB
C#
Raw Permalink Normal View History

2023-07-24 16:38:13 +03:00
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
namespace BNG {
public enum MovementVector {
HMD,
Controller
}
public class SmoothLocomotion : MonoBehaviour {
public PlayerControllerType ControllerType = PlayerControllerType.CharacterController;
[Header("CharacterController Settings : ")]
public float MovementSpeed = 1.25f;
[Header("Rigidbody Settings : ")]
[Tooltip("How much force to apply to the rigidbody when the player is grounded")]
public float MovementForce = 500f;
[Tooltip("Maximium velocity of the player's rigidbody in X, Z. Used to constrain max speed movement.")]
public float MaxHorizontalVelocity = 5f;
[Tooltip("Maximium velocity of the player's rigidbody on the Y axis. Used to constrain max vertical movement, such as jumping and falling.")]
public float MaxVerticalVelocity = 10f;
[Tooltip("How far up from the characters feet is considered a step. Anything below this will be considered grounded.")]
public float StepHeight = 0.1f;
[Tooltip("Maximium angle a slope can be to be considered grounded. Negated if contact distance from the player's feet is less than StepHeight.")]
public float MaxSlopeAngle = 45f;
[Tooltip("Physics Material to apply to the sphere collider while moving. Use this to dynamically adjust friction and bounciness.")]
public PhysicMaterial MovementMaterial;
[Tooltip("Physics Material to apply to the sphere collider when no controls are being issues. Use this to slow t he player down, or allow them to slide across surfaces.")]
public PhysicMaterial FrictionMaterial;
[Tooltip("How much drag to apply to the player while moving")]
public float MovementDrag = 1f;
[Tooltip("How much drag to apply to the player while standing still. Used to slow down the player or prevent from sliding down a hill.")]
public float StaticDrag = 5f;
[Tooltip("How much drag to apply to the player when in the air / not grounded.")]
public float AirDrag = 1f;
[Header("Forward Direction : ")]
[Tooltip("(Optional) If specified, this transform's forward direction will determine the movement direction ")]
public Transform ForwardDirection;
[Header("Input : ")]
[Tooltip("Set to false if you do not want to respond to input commands. (For example, if paused or in a menu)")]
public bool AllowInput = true;
[Header("Input : ")]
[Tooltip("Set to false if you do not want SmoothLocomotion to update movement at all.")]
public bool UpdateMovement = true;
[Tooltip("Used to determine which direction to move. Example : Left Thumbstick Axis or Touchpad. ")]
public List<InputAxis> inputAxis = new List<InputAxis>() { InputAxis.LeftThumbStickAxis };
[Tooltip("Input Action used to affect movement")]
public InputActionReference MoveAction;
[Tooltip("If true, movement events will only be sent if the Application has focus (Or Play window, if running in the Unity Editor)")]
public bool RequireAppFocus = true;
[Header("Sprint : ")]
public float SprintSpeed = 1.5f;
[Tooltip("The key(s) to use to initiate sprint. You can also override the SprintKeyDown() function to determine your sprint criteria.")]
public List<ControllerBinding> SprintInput = new List<ControllerBinding>() { ControllerBinding.None };
[Tooltip("Unity Input Action used to enable sprinting")]
public InputActionReference SprintAction;
[Header("Strafe : ")]
public float StrafeSpeed = 1f;
public float StrafeSprintSpeed = 1.25f;
[Header("Jump : ")]
[Tooltip("Amount of 'force' to apply to the player during Jump")]
public float JumpForce = 3f;
[Tooltip("The key(s) to use to initiate a jump. You can also override the CheckJump() function to determine your jump criteria.")]
public List<ControllerBinding> JumpInput = new List<ControllerBinding>() { ControllerBinding.None };
[Tooltip("Unity Input Action used to initiate a jump")]
public InputActionReference JumpAction;
[Header("Air Control : ")]
[Tooltip("Can the player move when not grounded? Set to true if you want to be able to move the joysticks and have the player respond to input even when not grounded.")]
public bool AirControl = true;
[Tooltip("How fast the player can move in the air if AirControl = true. Example : 0.5 = Player will move at half the speed of MovementSpeed")]
public float AirControlSpeed = 1f;
BNGPlayerController playerController;
CharacterController characterController;
Rigidbody playerRigid;
SphereCollider playerSphere;
// Left / Right
float movementX;
// Up / Down
float movementY;
// Forwards / Backwards
float movementZ;
private float _verticalSpeed = 0; // Keep track of vertical speed
/// <summary>
/// Any movement to apply after controller movement has been calculated
/// </summary>
Vector3 additionalMovement;
#region Events
public delegate void OnBeforeMoveAction();
public static event OnBeforeMoveAction OnBeforeMove;
public delegate void OnAfterMoveAction();
public static event OnAfterMoveAction OnAfterMove;
#endregion
public virtual void Update() {
CheckControllerReferences();
UpdateInputs();
if(UpdateMovement) {
MoveCharacter();
}
}
public virtual void FixedUpdate() {
if (UpdateMovement && ControllerType == PlayerControllerType.Rigidbody) {
MoveRigidCharacter();
// Reset GroundContacts. This will be updated in OnCollisionStay
GroundContacts = 0;
}
}
bool playerInitialized = false;
public virtual void CheckControllerReferences() {
// Component may be called while disabled, so check for references here
if (playerController == null) {
playerController = GetComponentInParent<BNGPlayerController>();
}
if(playerInitialized == false) {
// Check CharacterController initialization
if (ControllerType == PlayerControllerType.CharacterController && characterController == null) {
SetupCharacterController();
playerInitialized = true;
}
// Rigidbody Initialization
if (ControllerType == PlayerControllerType.Rigidbody && playerRigid == null) {
playerRigid = GetComponent<Rigidbody>();
playerSphere = GetComponent<SphereCollider>();
// If it is still null, then we'll need to setup the player rigid body
if (playerRigid == null) {
SetupRigidbodyPlayer();
}
}
}
}
public virtual void UpdateInputs() {
// Start by resetting our previous frame's inputs
movementX = 0;
movementY = 0;
movementZ = 0;
// Keep values zeroed out if not allowing input
if(AllowInput == false) {
return;
}
// Start with VR Controller Input
Vector2 primaryAxis = GetMovementAxis();
if (IsGrounded()) {
movementX = primaryAxis.x;
movementZ = primaryAxis.y;
}
else if(AirControl) {
movementX = primaryAxis.x * AirControlSpeed;
movementZ = primaryAxis.y * AirControlSpeed;
}
// Add Jump Force
if (CheckJump()) {
// Add movement directly to CC type
if(ControllerType == PlayerControllerType.CharacterController) {
movementY += JumpForce;
}
else if (ControllerType == PlayerControllerType.Rigidbody) {
DoRigidBodyJump();
}
}
// Attach any additional speed
if(CheckSprint()) {
movementX *= StrafeSprintSpeed;
movementZ *= SprintSpeed;
}
else {
movementX *= StrafeSpeed;
movementZ *= MovementSpeed;
}
}
float lastJumpTime;
float lastMoveTime;
public virtual void DoRigidBodyJump() {
if(Time.time - lastJumpTime > 0.2f) {
playerRigid.AddForce(new Vector3(playerRigid.velocity.x, JumpForce, playerRigid.velocity.z), ForceMode.VelocityChange);
lastJumpTime = Time.time;
}
}
public virtual Vector2 GetMovementAxis() {
// Use the largest, non-zero value we find in our input list
Vector3 lastAxisValue = Vector3.zero;
// Check raw input bindings
if(inputAxis != null) {
for (int i = 0; i < inputAxis.Count; i++) {
Vector3 axisVal = InputBridge.Instance.GetInputAxisValue(inputAxis[i]);
// Always take this value if our last entry was 0.
if (lastAxisValue == Vector3.zero) {
lastAxisValue = axisVal;
}
else if (axisVal != Vector3.zero && axisVal.magnitude > lastAxisValue.magnitude) {
lastAxisValue = axisVal;
}
}
}
// Check Unity Input Action if we have application focus
bool hasRequiredFocus = RequireAppFocus == false || RequireAppFocus && Application.isFocused;
if (MoveAction != null && hasRequiredFocus) {
Vector3 axisVal = MoveAction.action.ReadValue<Vector2>();
// Always take this value if our last entry was 0.
if (lastAxisValue == Vector3.zero) {
lastAxisValue = axisVal;
}
else if (axisVal != Vector3.zero && axisVal.magnitude > lastAxisValue.magnitude) {
lastAxisValue = axisVal;
}
}
return lastAxisValue;
}
public virtual void MoveCharacter() {
// Bail early if no elligible for movement
if (UpdateMovement == false) {
return;
}
Vector3 moveDirection = new Vector3(movementX, movementY, movementZ);
if(ForwardDirection != null) {
moveDirection = ForwardDirection.TransformDirection(moveDirection);
}
else {
moveDirection = transform.TransformDirection(moveDirection);
}
// Check for jump value
if (playerController != null && playerController.IsGrounded()) {
// Reset jump speed if grounded
_verticalSpeed = 0;
if (CheckJump()) {
_verticalSpeed = JumpForce;
}
}
moveDirection.y = _verticalSpeed;
if(playerController) {
playerController.LastPlayerMoveTime = Time.time;
}
if(moveDirection != Vector3.zero) {
MoveCharacter(moveDirection * Time.deltaTime);
}
}
public virtual void MoveRigidCharacter(Vector3 moveTo) {
// Incremental move not yet implemented
}
public virtual void MoveRigidCharacter() {
float maxVelocityChange = 10f;
if (playerRigid) {
Vector3 moveDirection = new Vector3(movementX, movementY, movementZ);
if (ForwardDirection != null) {
moveDirection = ForwardDirection.TransformDirection(moveDirection);
}
else {
moveDirection = transform.TransformDirection(moveDirection);
}
Vector3 targetVelocity = moveDirection * MovementForce;
Vector3 movement = Vector3.zero;
// Apply a force that attempts to reach our target velocity
Vector3 currentVelocity = playerRigid.velocity;
Vector3 velocityTarget = (targetVelocity - currentVelocity);
bool recentlyJumped = Time.time - lastJumpTime < 0.1f;
// Do Grounded Movement
if (IsGrounded()) {
// Do movement if grounded and not recently jumped
if(!recentlyJumped) {
if (maxVelocityChange > 0) {
velocityTarget.x = Mathf.Clamp(velocityTarget.x, -maxVelocityChange, maxVelocityChange);
// velocityChange.y = Mathf.Clamp(velocityChange.y, -maxVelocityChange, maxVelocityChange);
velocityTarget.z = Mathf.Clamp(velocityTarget.z, -maxVelocityChange, maxVelocityChange);
}
// Adjust physics material if applying movement or waiting to come to a stop
if (moveDirection.magnitude > 0.001f) {
if(playerSphere) {
playerSphere.material = MovementMaterial;
}
playerRigid.drag = MovementDrag;
}
else {
if (playerSphere) {
playerSphere.material = FrictionMaterial;
}
playerRigid.drag = StaticDrag;
}
// Apply movement
if (moveDirection.magnitude > 0.001f) {
playerRigid.AddForce(velocityTarget, ForceMode.VelocityChange);
}
}
}
// Air Control Movement
else {
playerRigid.drag = AirDrag;
if (AirControl) {
if (!recentlyJumped) {
Vector3 move = (targetVelocity) * AirControlSpeed;
move.y = 0;
playerRigid.AddForce(move, ForceMode.Acceleration);
}
}
}
// Cap our min / max velocity.
var adjustedVelocity = new Vector3(playerRigid.velocity.x, 0, playerRigid.velocity.z);
if(adjustedVelocity.magnitude > MaxHorizontalVelocity) {
adjustedVelocity = Vector3.ClampMagnitude(adjustedVelocity, MaxHorizontalVelocity);
adjustedVelocity = new Vector3(adjustedVelocity.x, playerRigid.velocity.y, adjustedVelocity.z);
// Apply changes if there was a difference
playerRigid.velocity = adjustedVelocity;
}
// Clamp the Y axis separately
if (Mathf.Abs(playerRigid.velocity.y) > MaxVerticalVelocity) {
playerRigid.velocity = new Vector3(playerRigid.velocity.x, Mathf.Clamp(playerRigid.velocity.y, -MaxVerticalVelocity, MaxVerticalVelocity), playerRigid.velocity.z);
}
}
}
public float Magnitude;
public virtual void MoveCharacter(Vector3 motion) {
// Can bail immediately if no movement is required
if(motion == null || motion == Vector3.zero) {
return;
}
Magnitude = (float)Math.Round(motion.magnitude * 1000f) / 1000f;
bool callEvents = Magnitude > 0.0f;
CheckControllerReferences();
// Call any Before Move Events
if(callEvents) {
OnBeforeMove?.Invoke();
}
// CharacterController Movement Type
if(ControllerType == PlayerControllerType.CharacterController) {
if (characterController && characterController.enabled) {
characterController.Move(motion);
}
}
// RigidBody Movement Type moved in Fixed Update
else if (ControllerType == PlayerControllerType.Rigidbody) {
// playerRigid.MovePosition(transform.position + motion * Time.deltaTime);
}
// Call any After Move Events
if (callEvents) {
OnAfterMove?.Invoke();
}
}
public virtual bool CheckJump() {
// Don't jump if not grounded
if (!IsGrounded()) {
return false;
}
// Check for bound controller button
for (int x = 0; x < JumpInput.Count; x++) {
if (InputBridge.Instance.GetControllerBindingValue(JumpInput[x])) {
return true;
}
}
// Check Unity Input Action value
if (JumpAction != null && JumpAction.action.ReadValue<float>() > 0) {
return true;
}
return false;
}
public virtual bool CheckSprint() {
// Check for bound controller button
for (int x = 0; x < SprintInput.Count; x++) {
if (InputBridge.Instance.GetControllerBindingValue(SprintInput[x])) {
return true;
}
}
// Check Unity Input Action
if (SprintAction != null) {
return SprintAction.action.ReadValue<float>() == 1f;
}
return false;
}
public virtual bool IsGrounded() {
// PlayerController has a IsGrounded method we can check
if(playerController && playerController.IsGrounded()) {
return true;
}
// OnCollision Contacts
else if(GroundContacts > 0) {
return true;
}
// Could use raycast, oncollisionstay, etc.
return false;
}
public virtual void SetupCharacterController() {
playerRigid = GetComponent<Rigidbody>();
BNGPlayerController bng = GetComponent<BNGPlayerController>();
if (bng) {
bng.DistanceFromGroundOffset = 0f;
}
// Add the CharacterController
characterController = GetComponent<CharacterController>();
if (characterController == null) {
characterController = gameObject.AddComponent<CharacterController>();
characterController.skinWidth = 0.001f;
characterController.center = new Vector3(0, -0.25f, 0);
characterController.radius = 0.1f;
characterController.height = 1.5f;
}
playerInitialized = true;
}
public virtual void SetupRigidbodyPlayer() {
playerRigid = gameObject.AddComponent<Rigidbody>();
playerRigid.mass = 50f;
playerRigid.drag = 1f;
playerRigid.angularDrag = 0.05f;
playerRigid.freezeRotation = true;
// playerRigid.useGravity = false; // Gravity is applied manually
// Remove any CharacterControllers, if any
CharacterController charController = GetComponent<CharacterController>();
if (charController != null) {
GameObject.Destroy(charController);
}
BNGPlayerController bng = GetComponent<BNGPlayerController>();
if (bng) {
bng.DistanceFromGroundOffset = -0.087f;
}
// Add the colliders
CapsuleCollider cc = gameObject.AddComponent<CapsuleCollider>();
cc.radius = 0.1f;
cc.center = new Vector3(0, 0.785f, 0);
cc.height = 1.25f;
playerSphere = gameObject.AddComponent<SphereCollider>();
playerSphere.center = new Vector3(0, 0.079f, 0);
playerSphere.radius = 0.08f;
playerInitialized = true;
}
public virtual void EnableMovement() {
UpdateMovement = true;
}
public virtual void DisableMovement() {
UpdateMovement = false;
}
[Header("Shown for Debug : ")]
public int GroundContacts = 0;
public float SurfaceAngle = 0f;
public float SurfaceHeight = 0f;
//public virtual void OnCollisionEnter(Collision collision) {
// if(ControllerType == PlayerControllerType.Rigidbody) {
// // May want to check for sphere collider here, contact normals, etc. Keeping it simple for now.
// GroundContacts++;
// }
//}
//public virtual void OnCollisionExit(Collision collision) {
// if(ControllerType == PlayerControllerType.Rigidbody) {
// GroundContacts--;
// }
//}
void OnCollisionStay(Collision collisionInfo) {
if (ControllerType == PlayerControllerType.Rigidbody) {
//= collisionInfo.contacts.Length;
for (int x = 0; x < collisionInfo.contacts.Length; x++) {
ContactPoint contact = collisionInfo.contacts[x];
SurfaceAngle = Vector3.Angle(contact.normal, transform.up);
SurfaceHeight = Mathf.Abs(contact.point.y - transform.position.y);
// Smooth floating point out for debug
if(SurfaceHeight < 0.0001) {
SurfaceHeight = 0;
}
// Is this a valid contact?
if(SurfaceHeight <= StepHeight) {
GroundContacts++;
}
else if (SurfaceAngle <= MaxSlopeAngle) {
GroundContacts++;
}
// Debug - draw all contact points and normals
//Debug.DrawRay(contact.point, contact.normal * 10, Color.white);
Debug.DrawLine(contact.point, transform.position);
}
}
}
}
public enum PlayerControllerType {
CharacterController,
Rigidbody
}
}