using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace BNG {
public enum LocomotionType {
Teleport,
SmoothLocomotion,
None
}
///
/// The BNGPlayerController handles basic player movement
///
public class BNGPlayerController : MonoBehaviour {
[Header("Camera Options : ")]
[Tooltip("If true the CharacterController will move along with the HMD, as long as there are no obstacle's in the way")]
public bool MoveCharacterWithCamera = true;
[Tooltip("If true the CharacterController will rotate it's Y angle to match the HMD's Y angle")]
public bool RotateCharacterWithCamera = true;
[Tooltip("If true the CharacterController will resize to match the calculated player height (distance from floor to camera)")]
public bool ResizeCharacterHeightWithCamera = true;
[Header("Transform Setup ")]
[Tooltip("The TrackingSpace represents your tracking space origin.")]
public Transform TrackingSpace;
[Tooltip("The CameraRig is a Transform that is used to offset the main camera. The main camera should be parented to this.")]
public Transform CameraRig;
[Tooltip("The CenterEyeAnchor is typically the Transform that contains your Main Camera")]
public Transform CenterEyeAnchor;
[Header("Ground checks : ")]
[Tooltip("Raycast against these layers to check if player is grounded")]
public LayerMask GroundedLayers;
///
/// 0 means we are grounded
///
[Tooltip("How far off the ground the player currently is. 0 = Grounded, 1 = 1 Meter in the air.")]
public float DistanceFromGround = 0;
[Tooltip("DistanceFromGround will subtract this value when determining distance from ground")]
public float DistanceFromGroundOffset = 0;
[Header("Player Capsule Settings : ")]
///
/// Minimum Height our Player's capsule collider can be (in meters)
///
[Tooltip("Minimum Height our Player's capsule collider can be (in meters)")]
public float MinimumCapsuleHeight = 0.4f;
///
/// Maximum Height our Player's capsule collider can be (in meters)
///
[Tooltip("Maximum Height our Player's capsule collider can be (in meters)")]
public float MaximumCapsuleHeight = 3f;
[HideInInspector]
public float LastTeleportTime;
[Header("Player Y Offset : ")]
///
/// Offset the height of the CharacterController by this amount
///
[Tooltip("Offset the height of the CharacterController by this amount")]
public float CharacterControllerYOffset = -0.025f;
///
/// Height of our camera in local coords
///
[HideInInspector]
public float CameraHeight;
[Header("Misc : ")]
[Tooltip("If true the Camera will be offset by ElevateCameraHeight if no HMD is active or connected. This prevents the camera from falling to the floor and can allow you to use keyboard controls.")]
public bool ElevateCameraIfNoHMDPresent = true;
[Tooltip("How high (in meters) to elevate the player camera if no HMD is present and ElevateCameraIfNoHMDPresent is true. 1.65 = about 5.4' tall. ")]
public float ElevateCameraHeight = 1.65f;
///
/// If player goes below this elevation they will be reset to their initial starting position.
/// If the player goes too far away from the center they may start to jitter due to floating point precisions.
/// Can also use this to detect if player somehow fell through a floor. Or if the "floor is lava".
///
[Tooltip("Minimum Y position our player is allowed to go. Useful for floating point precision and making sure player didn't fall through the map.")]
public float MinElevation = -6000f;
///
/// If player goes above this elevation they will be reset to their initial starting position.
/// If the player goes too far away from the center they may start to jitter due to floating point precisions.
///
public float MaxElevation = 6000f;
[HideInInspector]
public float LastPlayerMoveTime;
// The controller to manipulate
protected CharacterController characterController;
// The controller to manipulate
protected Rigidbody playerRigid;
protected CapsuleCollider playerCapsule;
// Use smooth movement if available
protected SmoothLocomotion smoothLocomotion;
// Optional components can be used to update LastMoved Time
protected PlayerClimbing playerClimbing;
protected bool isClimbing, wasClimbing = false;
// This the object that is currently beneath us
public RaycastHit groundHit;
// Stored for GC
protected RaycastHit hit;
protected Transform mainCamera;
private Vector3 _initialPosition;
void Start() {
characterController = GetComponentInChildren();
playerRigid = GetComponent();
playerCapsule = GetComponent();
smoothLocomotion = GetComponentInChildren();
mainCamera = GameObject.FindGameObjectWithTag("MainCamera").transform;
if (characterController) {
_initialPosition = characterController.transform.position;
}
else if(playerRigid) {
_initialPosition = playerRigid.position;
}
else {
_initialPosition = transform.position;
}
playerClimbing = GetComponentInChildren();
}
void Update() {
// Sanity check for camera
if (mainCamera == null && Camera.main != null) {
mainCamera = Camera.main.transform;
}
isClimbing = playerClimbing != null && playerClimbing.GrippingAtLeastOneClimbable();
if (isClimbing != wasClimbing) {
OnClimbingChange();
}
// Update the Character Controller's Capsule Height to match our Camera position
if(ResizeCharacterHeightWithCamera) {
UpdateCharacterHeight();
}
// Update the position of our camera rig to account for our player's height
UpdateCameraRigPosition();
// JPTODO : Testing character height
if(playerClimbing != null && playerClimbing.GrippingAtLeastOneClimbable() && characterController != null) {
characterController.height = playerClimbing.ClimbingCapsuleHeight;
}
if(playerClimbing != null && playerClimbing.GrippingAtLeastOneClimbable() && playerRigid != null) {
playerCapsule.height = playerClimbing.ClimbingCapsuleHeight;
}
// After positioning the camera rig, we can update our main camera's height
UpdateCameraHeight();
CheckCharacterCollisionMove();
// Align TrackingSpace with Camera
if (RotateCharacterWithCamera) {
RotateTrackingSpaceToCamera();
}
}
void FixedUpdate() {
UpdateDistanceFromGround();
CheckPlayerElevationRespawn();
}
///
/// Check if the player has moved beyond the specified min / max elevation
/// Player should never go above or below 6000 units as physics can start to jitter due to floating point precision
/// Maybe they clipped through a floor, touched a set "lava" height, etc.
///
public virtual void CheckPlayerElevationRespawn() {
// No need for elevation checks
if(MinElevation == 0 && MaxElevation == 0) {
return;
}
// Check Elevation based on Character Controller height
if(characterController != null && (characterController.transform.position.y < MinElevation || characterController.transform.position.y > MaxElevation)) {
Debug.Log("Player out of bounds; Returning to initial position.");
characterController.transform.position = _initialPosition;
}
// Check Elevation based on Character Controller height
if(playerRigid != null && (playerRigid.transform.position.y < MinElevation || playerRigid.transform.position.y > MaxElevation)) {
Debug.Log("Player out of bounds; Returning to initial position.");
playerRigid.transform.position = _initialPosition;
}
}
public virtual void UpdateDistanceFromGround() {
if(characterController) {
if (Physics.Raycast(characterController.transform.position, -characterController.transform.up, out groundHit, 20, GroundedLayers, QueryTriggerInteraction.Ignore)) {
DistanceFromGround = Vector3.Distance(characterController.transform.position, groundHit.point);
DistanceFromGround += characterController.center.y;
DistanceFromGround -= (characterController.height * 0.5f) + characterController.skinWidth;
// Round to nearest thousandth
DistanceFromGround = (float)Math.Round(DistanceFromGround * 1000f) / 1000f;
}
else {
DistanceFromGround = float.MaxValue;
}
}
if(playerRigid) {
if (Physics.Raycast(playerCapsule.transform.position, -playerCapsule.transform.up, out groundHit, 20, GroundedLayers, QueryTriggerInteraction.Ignore)) {
DistanceFromGround = Vector3.Distance(playerCapsule.transform.position, groundHit.point);
DistanceFromGround += playerCapsule.center.y;
DistanceFromGround -= (playerCapsule.height * 0.5f);
// Round to nearest thousandth
DistanceFromGround = (float)Math.Round(DistanceFromGround * 1000f) / 1000f;
}
else {
DistanceFromGround = float.MaxValue;
}
}
// No CharacterController found. Update Distance based on current transform position
else {
if (Physics.Raycast(transform.position, -transform.up, out groundHit, 20, GroundedLayers, QueryTriggerInteraction.Ignore)) {
DistanceFromGround = Vector3.Distance(transform.position, groundHit.point) - 0.0875f;
// Round to nearest thousandth
DistanceFromGround = (float)Math.Round(DistanceFromGround * 1000f) / 1000f;
}
else {
DistanceFromGround = float.MaxValue;
}
}
if (DistanceFromGround != float.MaxValue) {
DistanceFromGround -= DistanceFromGroundOffset;
}
// Smooth floating point issues from thousandths
if(DistanceFromGround < 0.001f && DistanceFromGround > -0.001f) {
DistanceFromGround = 0;
}
}
public virtual void RotateTrackingSpaceToCamera() {
Vector3 initialPosition = TrackingSpace.position;
Quaternion initialRotation = TrackingSpace.rotation;
// Move the character controller to the proper rotation / alignment
if(characterController) {
characterController.transform.rotation = Quaternion.Euler(0.0f, CenterEyeAnchor.rotation.eulerAngles.y, 0.0f);
// Now we can rotate our tracking space back to initial position / rotation
TrackingSpace.position = initialPosition;
TrackingSpace.rotation = initialRotation;
}
else if(playerRigid) {
playerRigid.transform.rotation = Quaternion.Euler(0.0f, CenterEyeAnchor.rotation.eulerAngles.y, 0.0f);
// Now we can rotate our tracking space back to initial position / rotation
TrackingSpace.position = initialPosition;
TrackingSpace.rotation = initialRotation;
}
}
public virtual void UpdateCameraRigPosition() {
float yPos = CharacterControllerYOffset;
// Get character controller position based on the height and center of the capsule
if (characterController != null) {
yPos = -(0.5f * characterController.height) + characterController.center.y + CharacterControllerYOffset;
}
// Get character controller position based on the height and center of the capsule
else if (playerRigid != null) {
yPos = -(0.5f * playerCapsule.height) + playerCapsule.center.y + CharacterControllerYOffset;
}
// Offset the capsule a bit while climbing. This allows the player to more easily hoist themselves onto a ledge / platform.
if (playerClimbing != null && playerClimbing.GrippingAtLeastOneClimbable()) {
//yPos = yPos - (playerClimbing.ClimbingCapsuleHeight - playerClimbing.ClimbingCapsuleCenter);
}
// If no HMD is active, bump our rig up a bit so it doesn't sit on the floor
if (!InputBridge.Instance.HMDActive && ElevateCameraIfNoHMDPresent) {
yPos += ElevateCameraHeight;
}
CameraRig.transform.localPosition = new Vector3(CameraRig.transform.localPosition.x, yPos, CameraRig.transform.localPosition.z);
}
public virtual void UpdateCharacterHeight() {
float minHeight = MinimumCapsuleHeight;
// Increase Min Height if no HMD is present. This prevents our character from being really small
if(!InputBridge.Instance.HMDActive && minHeight < 1f) {
minHeight = 1f;
}
// Update Character Height based on Camera Height.
if(characterController) {
characterController.height = Mathf.Clamp(CameraHeight + CharacterControllerYOffset - characterController.skinWidth, minHeight, MaximumCapsuleHeight);
// If we are climbing set the capsule center upwards
if (playerClimbing != null && playerClimbing.GrippingAtLeastOneClimbable()) {
playerCapsule.height = playerClimbing.ClimbingCapsuleHeight;
playerCapsule.center = new Vector3(0, playerClimbing.ClimbingCapsuleCenter * 2, 0);
}
else if(playerClimbing != null) {
characterController.center = new Vector3(0, playerClimbing.ClimbingCapsuleCenter, 0);
}
}
else if(playerRigid && playerCapsule) {
playerCapsule.height = Mathf.Clamp(CameraHeight + CharacterControllerYOffset, minHeight, MaximumCapsuleHeight);
playerCapsule.center = new Vector3(0, playerCapsule.height / 2 + (SphereColliderRadius * 2), 0);
}
}
public float SphereColliderRadius = 0.08f;
public virtual void UpdateCameraHeight() {
// update camera height
if (CenterEyeAnchor) {
CameraHeight = CenterEyeAnchor.localPosition.y;
}
}
///
/// Move the character controller to new camera position
///
public virtual void CheckCharacterCollisionMove() {
if(!MoveCharacterWithCamera) {
return;
}
Vector3 initialCameraRigPosition = CameraRig.transform.position;
Vector3 cameraPosition = CenterEyeAnchor.position;
Vector3 movePosition = new Vector3(cameraPosition.x, transform.position.y, cameraPosition.z);
Vector3 delta = cameraPosition - transform.position;
// Ignore Y position
delta.y = 0;
// Move Character Controller and Camera Rig to Camera's delta
if (delta.magnitude > 0.0f) {
if(smoothLocomotion && smoothLocomotion.ControllerType == PlayerControllerType.CharacterController) {
smoothLocomotion.MoveCharacter(delta);
}
else if (smoothLocomotion && smoothLocomotion.ControllerType == PlayerControllerType.Rigidbody) {
CheckRigidbodyCapsuleMove(movePosition);
}
else if(characterController) {
characterController.Move(delta);
}
// Move Camera Rig back into position
CameraRig.transform.position = initialCameraRigPosition;
}
}
Vector3 moveTest;
public virtual void CheckRigidbodyCapsuleMove(Vector3 movePosition) {
bool noCollision = true;
float capsuleRadius = 0.2f;
moveTest = movePosition;
// Cast capsule shape at the desired position to see if it is about to hit anything
if (Physics.SphereCast(movePosition, capsuleRadius, transform.up, out hit, playerCapsule.height / 2, GroundedLayers, QueryTriggerInteraction.Ignore)) {
Debug.Log(hit.collider);
noCollision = false;
}
if (noCollision) {
transform.position = movePosition;
}
}
public virtual bool IsGrounded() {
// Immediately check for a positive from a CharacterController if it's present
if(characterController != null) {
if(characterController.isGrounded) {
return true;
}
}
// DistanceFromGround is a bit more reliable as we can give a bit of leniency in what's considered grounded
return DistanceFromGround <= 0.007f;
}
public virtual void OnClimbingChange() {
// Climbing
if(playerClimbing.GrippingAtLeastOneClimbable()) {
}
// Just let go
else {
}
}
//#if UNITY_EDITOR
// public static void DrawWireCapsule(Vector3 _pos, Vector3 _pos2, float _radius, float _height, Color _color = default) {
// if (_color != default) {
// UnityEditor.Handles.color = _color;
// }
// var forward = _pos2 - _pos;
// var _rot = Quaternion.LookRotation(forward);
// var pointOffset = _radius / 2f;
// var length = forward.magnitude;
// var center2 = new Vector3(0f, 0, length);
// Matrix4x4 angleMatrix = Matrix4x4.TRS(_pos, _rot, UnityEditor.Handles.matrix.lossyScale);
// using (new UnityEditor.Handles.DrawingScope(angleMatrix)) {
// UnityEditor.Handles.DrawWireDisc(Vector3.zero, Vector3.forward, _radius);
// UnityEditor.Handles.DrawWireArc(Vector3.zero, Vector3.up, Vector3.left * pointOffset, -180f, _radius);
// UnityEditor.Handles.DrawWireArc(Vector3.zero, Vector3.left, Vector3.down * pointOffset, -180f, _radius);
// UnityEditor.Handles.DrawWireDisc(center2, Vector3.forward, _radius);
// UnityEditor.Handles.DrawWireArc(center2, Vector3.up, Vector3.right * pointOffset, -180f, _radius);
// UnityEditor.Handles.DrawWireArc(center2, Vector3.left, Vector3.up * pointOffset, -180f, _radius);
// DrawLine(_radius, 0f, length);
// DrawLine(-_radius, 0f, length);
// DrawLine(0f, _radius, length);
// DrawLine(0f, -_radius, length);
// }
// }
// private static void DrawLine(float arg1, float arg2, float forward) {
// UnityEditor.Handles.DrawLine(new Vector3(arg1, arg2, 0f), new Vector3(arg1, arg2, forward));
// }
// void OnDrawGizmosSelected() {
// DrawWireCapsule(moveTest, moveTest + new Vector3(0, playerCapsule.height), 0.2f, playerCapsule.height / 2);
// }
//#endif
}
}