diff --git a/FPSController.cs b/FPSController.cs new file mode 100644 index 0000000000000000000000000000000000000000..57c8c7d52e7865a3b96cce210d56d3a790443373 --- /dev/null +++ b/FPSController.cs @@ -0,0 +1,215 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +// Generic First-Person-Shooter-like Controller +[RequireComponent(typeof(CharacterController))] +public class FPSController : MonoBehaviour +{ + [Tooltip("How fast the player moves when walking (default move speed).")] + [SerializeField] + private float movementWalkSpeed = 6.0f; + + [Tooltip("How fast the player moves when running.")] + [SerializeField] + private float movementRunSpeed = 11.0f; + + [Tooltip("If true, diagonal speed (when strafing + moving forward or back) can't exceed normal move speed; otherwise it's about 1.4 times faster.")] + [SerializeField] + public bool movementLimitDiagonalSpeed = true; + + [Tooltip("If checked, the run key toggles between running and walking. Otherwise player runs if the key is held down.")] + [SerializeField] + private bool movementToggleRun = false; + + [Tooltip("How high the player jumps when hitting the jump button.")] + [SerializeField] + private float movementJumpSpeed = 8.0f; + + [Tooltip("How fast the player falls when not standing on anything.")] + [SerializeField] + private float movementGravity = 20.0f; + + [Tooltip("Units that player can fall before a falling function is run. To disable, type \"infinity\" in the inspector.")] + [SerializeField] + private float movementFallingThreshold = 10.0f; + + [Tooltip("If the player ends up on a slope which is at least the Slope Limit as set on the character controller, then he will slide down.")] + [SerializeField] + private bool movementSlideWhenOverSlopeLimit = false; + + [Tooltip("If checked and the player is on an object tagged \"Slide\", he will slide down it regardless of the slope limit.")] + [SerializeField] + private bool movementSlideOnTaggedObjects = false; + + [Tooltip("How fast the player slides when on slopes as defined above.")] + [SerializeField] + private float movementSlideSpeed = 12.0f; + + [Tooltip("If checked, then the player can change direction while in the air.")] + [SerializeField] + private bool movementAirControl = false; + + [Tooltip("Small amounts of this results in bumping when walking down slopes, but large amounts results in falling too fast.")] + [SerializeField] + private float movementAntiBumpFactor = .75f; + + [Tooltip("Player must be grounded for at least this many physics frames before being able to jump again; set to 0 to allow bunny hopping.")] + [SerializeField] + private int movementAntiBunnyHopFactor = 1; + + private Vector3 movementMoveDirection = Vector3.zero; + private bool movementGrounded = false; + private CharacterController movementController; + private Transform movementTransform; + private float movementSpeed; + private RaycastHit movementHit; + private float movementFallStartLevel; + private bool movementFalling; + private float movementSlideLimit; + private float movementRayDistance; + private Vector3 movementContactPoint; + private bool movementPlayerControl = false; + private int movementJumpTimer; + + + private void Start() + { + // Saving component references to improve performance. + movementTransform = GetComponent<Transform>(); + movementController = GetComponent<CharacterController>(); + + // Setting initial values. + movementSpeed = movementWalkSpeed; + movementRayDistance = movementController.height * .5f + movementController.radius; + movementSlideLimit = movementController.slopeLimit - .1f; + movementJumpTimer = movementAntiBunnyHopFactor; + } + + + private void Update() + { + // If the run button is set to toggle, then switch between walk/run speed. (We use Update for this... + // FixedUpdate is a poor place to use GetButtonDown, since it doesn't necessarily run every frame and can miss the event) + if (movementToggleRun && movementGrounded && Input.GetButtonDown("Run")) + { + movementSpeed = (movementSpeed == movementWalkSpeed ? movementRunSpeed : movementWalkSpeed); + } + } + + + private void FixedUpdate() + { + float inputX = Input.GetAxis("Horizontal"); + float inputY = Input.GetAxis("Vertical"); + + // If both horizontal and vertical are used simultaneously, limit speed (if allowed), so the total doesn't exceed normal move speed + float inputModifyFactor = (inputX != 0.0f && inputY != 0.0f && movementLimitDiagonalSpeed) ? .7071f : 1.0f; + + if (movementGrounded) + { + bool sliding = false; + // See if surface immediately below should be slid down. We use this normally rather than a ControllerColliderHit point, + // because that interferes with step climbing amongst other annoyances + if (Physics.Raycast(movementTransform.position, -Vector3.up, out movementHit, movementRayDistance)) + { + if (Vector3.Angle(movementHit.normal, Vector3.up) > movementSlideLimit) + { + sliding = true; + } + } + // However, just raycasting straight down from the center can fail when on steep slopes + // So if the above raycast didn't catch anything, raycast down from the stored ControllerColliderHit point instead + else + { + Physics.Raycast(movementContactPoint + Vector3.up, -Vector3.up, out movementHit); + if (Vector3.Angle(movementHit.normal, Vector3.up) > movementSlideLimit) + { + sliding = true; + } + } + + // If we were falling, and we fell a vertical distance greater than the threshold, run a falling damage routine + if (movementFalling) + { + movementFalling = false; + if (movementTransform.position.y < movementFallStartLevel - movementFallingThreshold) + { + OnFell(movementFallStartLevel - movementTransform.position.y); + } + } + + // If running isn't on a toggle, then use the appropriate speed depending on whether the run button is down + if (!movementToggleRun) + { + movementSpeed = Input.GetKey(KeyCode.LeftShift) ? movementRunSpeed : movementWalkSpeed; + } + + // If sliding (and it's allowed), or if we're on an object tagged "Slide", get a vector pointing down the slope we're on + if ((sliding && movementSlideWhenOverSlopeLimit) || (movementSlideOnTaggedObjects && movementHit.collider.tag == "Slide")) + { + Vector3 hitNormal = movementHit.normal; + movementMoveDirection = new Vector3(hitNormal.x, -hitNormal.y, hitNormal.z); + Vector3.OrthoNormalize(ref hitNormal, ref movementMoveDirection); + movementMoveDirection *= movementSlideSpeed; + movementPlayerControl = false; + } + // Otherwise recalculate moveDirection directly from axes, adding a bit of -y to avoid bumping down inclines + else + { + movementMoveDirection = new Vector3(inputX * inputModifyFactor, -movementAntiBumpFactor, inputY * inputModifyFactor); + movementMoveDirection = movementTransform.TransformDirection(movementMoveDirection) * movementSpeed; + movementPlayerControl = true; + } + + // Jump! But only if the jump button has been released and player has been grounded for a given number of frames + if (!Input.GetButton("Jump")) + { + movementJumpTimer++; + } + else if (movementJumpTimer >= movementAntiBunnyHopFactor) + { + movementMoveDirection.y = movementJumpSpeed; + movementJumpTimer = 0; + } + } + else + { + // If we stepped over a cliff or something, set the height at which we started falling + if (!movementFalling) + { + movementFalling = true; + movementFallStartLevel = movementTransform.position.y; + } + + // If air control is allowed, check movement but don't touch the y component + if (movementAirControl && movementPlayerControl) + { + movementMoveDirection.x = inputX * movementSpeed * inputModifyFactor; + movementMoveDirection.z = inputY * movementSpeed * inputModifyFactor; + movementMoveDirection = movementTransform.TransformDirection(movementMoveDirection); + } + } + + // Apply gravity + movementMoveDirection.y -= movementGravity * Time.deltaTime; + + // Move the controller, and set grounded true or false depending on whether we're standing on something + movementGrounded = (movementController.Move(movementMoveDirection * Time.deltaTime) & CollisionFlags.Below) != 0; + } + + + // Store point that we're in contact with for use in FixedUpdate if needed + private void OnControllerColliderHit(ControllerColliderHit hit) + { + movementContactPoint = hit.point; + } + + + // This is the place to apply things like fall damage. You can give the player hitpoints and remove some + // of them based on the distance fallen, play sound effects, etc. + private void OnFell(float fallDistance) + { + print("Ouch! Fell " + fallDistance + " units!"); + } +} \ No newline at end of file