I found a very detailed Unity FPS Controller Unity Tutorial on YouTube and decided to rework the series.
First person camera movement
Following this tutorial, I modified some of the code.
public class PlayerController : MonoBehaviour
{
// Whether it can be moved
public bool CanMove { get; private set; } = true;
// Parameters related to character movement
[Header("Movement Parameters")]
[SerializeField] private float walkSpeed = 3.0 f;
[SerializeField] private float gravity = 9.81 f;
[SerializeField] private bool isGrounded;
// Camera Angle parameters
[Header("Look Parameters")]
// Mouse X axis speed, Y axis speed
[SerializeField, Range(1, 100)] private float lookSpeedX = 2.0 f;
[SerializeField, Range(1, 100)] private float lookSpeedY = 2.0 f;
[SerializeField, Range(0, 90)] private float upperLookLimit = 89.0 f;
[SerializeField, Range(0, 90)] private float lowerLookLimit = 89.0 f;
// Camera and character controller variables
private Camera playerCamera;
private CharacterController characterController;
// Store player movement speed
private Vector3 moveDirection;
// Store "Horizontal" and "Vertical" inputs
private Vector2 moveInput;
// Mouse down, the camera's X axis rotation Angle
private float rotationX;
// Save mouse input
private float mouseY;
private float mouseX;
private void Awake()
{
// Camera is a child of player
playerCamera = GetComponentInChildren<Camera>();
characterController = GetComponent<CharacterController>();
// Lock the computer cursor
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false; }}Copy the code
The tutorial is divided into moving input, mouse-viewing, and finally applying moving
void Update()
{
GroundCheck();
if(CanMove) { HandleMovementInput(); HandleMouseLook(); ApplyFinalMovement(); }}Copy the code
Processing moving input
private void HandleMovementInput()
{
// Get WASD input * speed
moveInput = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical")) * walkSpeed;
// On the Y-axis, the vertical velocity is saved
float moveDirectionY = moveDirection.y;
// Get input in three directions
moveDirection = transform.right * moveInput.x + transform.forward * moveInput.y;
moveDirection.y = moveDirectionY;
}
Copy the code
Handling mouse input
private void HandleMouseLook()
{
mouseX = Input.GetAxis("Mouse Y") * lookSpeedY * Time.deltaTime;
mouseY = Input.GetAxis("Mouse X") * lookSpeedX * Time.deltaTime;
rotationX -= mouseX;
// Hold your left hand in a thumbs up gesture with your thumb pointing in the positive direction and your fingers curling in the positive direction
// So the minimum value is up, negative
rotationX = Mathf.Clamp(rotationX, -upperLookLimit, lowerLookLimit);
playerCamera.transform.localRotation = Quaternion.Euler(rotationX, 0.0);
// We can only use multiplication because of coordinate changes.
transform.rotation *= Quaternion.Euler(0, mouseY, 0);
// This will do
// transform.Rotate(transform.up * mouseY);
}
Copy the code
Handling movement, gravity implementation as described in the previous article.
private void ApplyFinalMovement()
{
if(! isGrounded) { moveDirection.y += gravity * Time.deltaTime; } characterController.Move(moveDirection * Time.deltaTime); }private void GroundCheck()
{
isGrounded = Physics.CheckSphere(checkGround.position, groundCheckRadius, groundLayer);
if (isGrounded && moveDirection.y < 0.0 f)
{
moveDirection.y = 2.0 f; }}Copy the code
The sprint
add
public class PlayerController : MonoBehaviour
{
public bool CanMove { get; private set; } = true;
// You can sprint, and press the sprint button to enter the sprint state
private bool IsSprinting => canSprint && Input.GetKey(sprintKey);
[Header("Functional Options")]
[SerializeField] private bool canSprint = true;
[Header("Controls")]
// You can also add an Input from the Unity top menu bar Edit -> Project Setting -> Input Manager
[SerializeField] private KeyCode sprintKey = KeyCode.LeftShift;
[Header("Movement Parameters")]
[SerializeField] private float walkSpeed = 3.0 f;
[SerializeField] private float sprintSpeed = 6.0 f;
}
Copy the code
float speed = IsSprinting ? sprintSpeed : walkSpeed;
private void HandleMovementInput()
{
float speed = IsSprinting ? sprintSpeed : walkSpeed;
// Get WASD input * speed
moveInput = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical")) * speed;
}
Copy the code
You can also add an Input, and input.getButton (“Run”);
jumping
private bool ShouldJump => canJump && Input.GetKeyDown(jumpKey); The jump function is enabled and the jump key is pressed to determine that the jump should be performed
public class PlayerController : MonoBehaviour
{
private bool ShouldJump => canJump && Input.GetKeyDown(jumpKey);
// Function switch
[Header("Functional Options")]
[SerializeField] private bool canJump = true;
[Header("Controls")]
[SerializeField] private KeyCode jumpKey = KeyCode.Space;
[Header("Jumping Parameters")]
// Maximum number of jumps
[SerializeField] private int maxJumpCount = 3;
// Current number of jumps
[SerializeField] private int jumpCount = 0;
// Last jump time
[SerializeField] private float lastJumpTime;
// Jump count resets the cooldown
[SerializeField] private float jumpCoolDownTime = 0.1 f;
// Jump height
[SerializeField] private float jumpHeight = 5.0 f;
[SerializeField] private float gravity = 9.81 f;
}
Copy the code
void Update()
{
GroundCheck();
if (CanMove)
{
HandleMovementInput();
HandleMouseLook();
// If jump is enabled
if (canJump)
{
// Perform the jumpHandleJump(); } ApplyFinalMovement(); }}Copy the code
Because there is a small amount of time off the ground, it will still be judged on the ground, so add a small amount of time to prevent the first jump off the ground, not counting the use of the jump
private void HandleJump()
{
if (ShouldJump && jumpCount < maxJumpCount)
{
moveDirection.y = Mathf.Sqrt(-gravity * 2f * jumpHeight);
jumpCount++;
lastJumpTime = Time.time;
}
if (Time.time - lastJumpTime > jumpCoolDownTime && isGrounded) jumpCount = 0;
}
Copy the code
She lay down
Switch squats
Most tutorials on the Internet either don’t teach you how to do the crouch directly, or simply change the ‘Height’ of ‘CharacterControllerd’ by pressing the button and adding a smooth overtone, but it’s hard to find a tutorial where you can hold down the crouch and release the stand automatically. Here, press “C” to switch between standing and squatting.
public class PlayerController : MonoBehaviour
{
// When the function is enabled and the "C" key is pressed, it is not already squatting/standing up, and it is on the ground (this can be determined according to requirements)
private boolShouldCrouch => canSwitchCrouch && Input.GetKeyDown(switchCrouchKey) && ! duringCrouchAnimation && isGrounded; [Header("Functional Options")]
// Whether to enable the switch squat function
[SerializeField] private bool canSwitchCrouch = false;
[Header("Controls")]
// You can also add an Input from the Unity top menu bar Edit -> Project Setting -> Input Manager
[SerializeField] private KeyCode switchCrouchKey = KeyCode.C;
[Header("Movement Parameters")]
// The speed of movement while crouching
[SerializeField] private float crouchSpeed = 1.0 f;
[Header("Crouch Parameters")]
// Squat height and standing height
[SerializeField] private float crouchHeight = 1.35 f;
[SerializeField] private float standingHeight = 2f;
[SerializeField] private float timeToCrouch = 0.25 f;
[SerializeField] private bool isCrouching;
[SerializeField] private bool duringCrouchAnimation;
Copy the code
void Update()
{
GroundCheck();
if (CanMove)
{
HandleMovementInput();
HandleMouseLook();
if (canJump)
HandleJump();
// If the switch squatting process is enabled, perform squatting
if(canSwitchCrouch) HandleCrouch(); ApplyFinalMovement(); }}Copy the code
Press squat, should squat or stand up, perform a coroutine
private void HandleCrouch()
{
if (ShouldCrouch)
StartCoroutine(CrouchStand());
}
Copy the code
The coroutine does these things:
(1) Judge if it is in the state of squatting, then it is necessary to perform standing below, judge whether there are obstacles on the top of the head, and do not perform standing if there are obstacles
If “isCrouching” is “True”, it indicates that the crouching state is in the crouching state, and the “targetHeight” is the “standingHeight”
(3) Get the current height, and “timeElapsed” is a timer that Lerp will use to smooth the standing squats
< span style = “font-size: 10.5pt; color: RGB (0, 0, 0)
⑤ After completing the height change, the height will be set as the target height because there will be some errors
⑥ The state of the squat changes, the end of the squat animation
private IEnumerator CrouchStand()
{
if (isCrouching && Physics.Raycast(playerCamera.transform.position, transform.up, 1f)) yield break;
duringCrouchAnimation = true;
float timeElapsed = 0;
float targetHeight = isCrouching ? standingHeight : crouchHeight;
float currentHeight = characterController.height;
while (timeElapsed < timeToCrouch)
{
float chrouchPercentage = timeElapsed / timeToCrouch;
characterController.height = Mathf.Lerp(currentHeight, targetHeight, chrouchPercentage);
timeElapsed += Time.deltaTime;
yield return null; } characterController.height = targetHeight; isCrouching = ! isCrouching; duringCrouchAnimation =false;
}
Copy the code
Up to this point, the height can be smooth and uniform for a set timeToCrouch time. However, the camera height will not change, so it is necessary to change the position of “Center” to move the collider up.
[Header("Crouch Parameters")]
[SerializeField] private float crouchHeight = 1.35 f;
[SerializeField] private float standingHeight = 2f;
// While squatting, the center position of the collider changes
[SerializeField] private float timeToCrouch = 0.25 f;
[SerializeField] private Vector3 crouchingCenter = Vector3.zero;
[SerializeField] private Vector3 standingCenter = Vector3.zero;
[SerializeField] private bool isCrouching;
[SerializeField] private bool duringCrouchAnimation;
// Ground detects object and camera position changes
// changedLength = (standingHeight - crouchHeight) / 2
[SerializeField] private float changedDistanceY;
Copy the code
The crouchingCenter height can be set, which I calculate here in Start().
void Start()
{
changedDistanceY = (standingHeight - crouchHeight) / 2f;
crouchingCenter.Set(standingCenter.x, standingCenter.y + changedDistanceY, standingCenter.z);
changedDistanceY + crouchingCenter.y, 0);
}
Copy the code
In coroutines, it’s the same operation as height.
private IEnumerator CrouchStand()
{
if (isCrouching && Physics.Raycast(playerCamera.transform.position, transform.up, 1f)) yield break;
duringCrouchAnimation = true;
float timeElapsed = 0;
float targetHeight = isCrouching ? standingHeight : crouchHeight;
float currentHeight = characterController.height;
Vector3 targetCenter = isCrouching ? standingCenter : crouchingCenter;
Vector3 currentCenter = characterController.center;
while (timeElapsed < timeToCrouch)
{
float chrouchPercentage = timeElapsed / timeToCrouch;
characterController.height = Mathf.Lerp(currentHeight, targetHeight, chrouchPercentage);
characterController.center = Vector3.Lerp(currentCenter, targetCenter, chrouchPercentage);
timeElapsed += Time.deltaTime;
yield return null; } characterController.height = targetHeight; characterController.center = targetCenter; isCrouching = ! isCrouching; duringCrouchAnimation =false;
}
Copy the code
When I tested it here, if I set it to center down, it might go through the wall when I stand up.
The position of groundCheck moves with it, the same operation.
[SerializeField] private Vector3 groundCheckStandingPosition;
[SerializeField] private Vector3 groundCheckCrouchPosition;
void Start()
{
// Squat related data
changedDistanceY = (standingHeight - crouchHeight) / 2f;
crouchingCenter.Set(standingCenter.x, standingCenter.y + changedDistanceY, standingCenter.z);
groundCheckStandingPosition = new Vector3(0, groundCheck.localPosition.y, 0);
groundCheckCrouchPosition = new Vector3(0, groundCheck.localPosition.y + changedDistanceY + crouchingCenter.y, 0);
}
private IEnumerator CrouchStand()
{
Vector3 targetGroundCheckPosition = isCrouching ? groundCheckStandingPosition : groundCheckCrouchPosition;
Vector3 currentGroundCheckPosition = groundCheck.localPosition;
while (timeElapsed < timeToCrouch)
{
groundCheck.localPosition = Vector3.Lerp(currentGroundCheckPosition, targetGroundCheckPosition, chrouchPercentage);
timeElapsed += Time.deltaTime;
yield return null; } groundCheck.localPosition = targetGroundCheckPosition; isCrouching = ! isCrouching; duringCrouchAnimation =false;
}
Copy the code
Press “LeftCtrl” to squat
The second phase has a better solution: (2) the first person perspective controller – open lens, head swing, slope slide
This is really can not find a good tutorial, feel how many have a bit of a problem, the following is my own writing, it is not too big a problem.
public class PlayerController : MonoBehaviour
{
// Function switch
[Header("Functional Options")]
[SerializeField] private bool canSwitchCrouch = false;
// Press the squat switch
[SerializeField] private bool canHoldToCrouch = true;
[Header("Controls")]
// You can also add an Input from the Unity top menu bar Edit -> Project Setting -> Input Manager
[SerializeField] private KeyCode holdToCrouchKey = KeyCode.LeftControl;
[SerializeField] private KeyCode switchCrouchKey = KeyCode.C;
[Header("Crouch Parameters")]
// Squat speed
[SerializeField, Range(50f, 100f)] private float crouchStandSpeed = 50f;
/ / squat lock
[SerializeField] private bool crouchLock;
}
Copy the code
I divided into two kinds of switches, one is the top, press C to switch squat and stand. One is to hold down squat, can also achieve squatting and standing at the same time.
void Update()
{
GroundCheck();
if (CanMove)
{
HandleMovementInput();
HandleMouseLook();
if (canJump)
HandleJump();
if(canSwitchCrouch || canHoldToCrouch) HandleCrouch(); ApplyFinalMovement(); }}Copy the code
① If you press “C” to switch the squatting position at the beginning of the squatting position lock, the squatting position lock will be switched
② If the squat lock is open, but the “leftControl” is pressed, the squat lock is closed
③ isCrouching is determined by whether the crouching lock is opened or the crouching lock is pressed
private void HandleCrouch()
{
// If press and hold squat is enabled, it has priority to press and hold squat
if (canHoldToCrouch)
{
// If squat is pressed to switch, the lock switch is switched
if(Input.GetKeyDown(switchCrouchKey)) crouchLock = ! crouchLock;// If the hold crouch key is pressed in the lock state, the lock is closed
if (crouchLock && Input.GetKeyDown(holdToCrouchKey)) crouchLock = false;
// Press and hold the Hold squat button, or lock it to crouch, to display the crouch state
isCrouching = Input.GetKey(holdToCrouchKey) || crouchLock;
// Adjust height
AdjustHeight();
}
// Otherwise, squat via coroutine
else if (ShouldCrouch)
StartCoroutine(CrouchStand());
}
Copy the code
In update, adjust height in real time.
① If the target height is already the current height, return directly.
② If the error between the current height and the target height is small, the target height is directly set
③ If it is other cases, it is necessary to adjust the height
Because it was implemented in update, the timer wasn’t as good as it should have been, and the “leftControl” was released in the middle of the squat.
I tried timers, or mathf.Smoothdamp (), and nothing worked.
The third time of the race, using fixed parameters or timers, or height ratios, has a stalling or other troublesome situation.
Test down, so far set a squat speed, and then multiply Time. DeltaTime is still relatively good.
Mathf.Lerp(currentHeight, targetHeight, crouchStandSpeed * Time.deltaTime);
Want to know if there is a more mature implementation method, online search is in is unable to find.
Check this out for details on Lerp and SmoothDamp.
Lerp is Mathf.Lerp(a, b, t(0.5)). If t is 0.5, then in this frame, we take the (b – a) segment and multiply it by 0.5, which means we cut it in half, and then the new position is a + half of the length, and then, if we take the coordinates of a + half of the length, As a new starting point, I’m going to continue to take half, which is 3/4 of the original position. So it’s going to get closer and closer to B, but it’s going to get slower and slower.
But if you take time, you’re going to be able to have constant velocity in your coroutine, instead of slowing down like this.
A SmoothDamp, on the other hand, is similar to the feeling of a car accelerating to start up, then slowing down, and finally stopping.
private void AdjustHeight()
{
float targetHeight = isCrouching ? crouchHeight : standingHeight;
float currentHeight = characterController.height;
// If yes, return
if (targetHeight == currentHeight) return;
Vector3 targetCenter = isCrouching ? crouchingCenter : standingCenter;
Vector3 currentCenter = characterController.center;
Vector3 targetGroundCheckPosition = isCrouching ? groundCheckCrouchPosition : groundCheckStandingPosition;
Vector3 currentGroundCheckPosition = groundCheck.localPosition;
if (Mathf.Abs(targetHeight - currentHeight) < 0.01 f)
{
characterController.height = targetHeight;
characterController.center = targetCenter;
groundCheck.localPosition = targetGroundCheckPosition;
}
else
{
// Squat -> Standing, if there is a block on the head, can not change the height
if (Physics.Raycast(playerCamera.transform.position, transform.up, 1f) && !isCrouching) return; characterController.height = Mathf.Lerp(currentHeight, targetHeight, crouchStandSpeed * Time.deltaTime); characterController.center = Vector3.Lerp(currentCenter, targetCenter, crouchStandSpeed * Time.deltaTime); groundCheck.localPosition = Vector3.Lerp(currentGroundCheckPosition, targetGroundCheckPosition, crouchStandSpeed * Time.deltaTime); }}Copy the code
And the speed of movement while crouching, changed here.
private void HandleMovementInput()
{
float speed = isCrouching ? crouchSpeed : IsSprinting ? sprintSpeed : walkSpeed;
// Debug.Log("speed: " + speed);
// Get WASD input * speed
moveInput = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical")) * speed;
// On the Y-axis, the vertical velocity is saved
float moveDirectionY = moveDirection.y;
// Get input in three directions
moveDirection = transform.right * moveInput.x + transform.forward * moveInput.y;
moveDirection.y = moveDirectionY;
}
Copy the code