Hi. When my characterController-controlled fps character goes airborne, I made him keep his current velocity until he lands again. This works, but I want my character to still receive movement input and apply it to the airVelocity variable. I tried this for while, but I couldn’t figure it out. Here’s my script (there are no bugs, and doesn’t contain any failed attempts at this problem):
using SerializableDictionary.Scripts;
using System;
using Ami.BroAudio;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(CharacterController))]
public class Player : MonoBehaviour
{
[Header("Motion")]
[SerializeField] private Movement movement; // struct for movement values
[SerializeField] private GadgetSettings gadgetSettings; // struct for gadget values
[SerializeField] private float gravityScale; // how strong gravity is in this particular scene (remember to move this to manager GameObject)
[SerializeField] private float buoyancy; // how quickly the player stops falling when entering water
[SerializeField] private float swimSpeed; // how quickly the player swims
[SerializeField, Frequency] private float freq = 800; // value for BroAudio effect when underwater
[SerializeField] public float fadeTime = 1; // another value for BroAudio effect when underwater
[Header("References")]
[SerializeField] private CharacterController charcontrol; //character controller reference
[SerializeField] private Weapon weaponScript; //Weapon script reference
[SerializeField] private Inventory inventoryScript; //Inventory script reference
[SerializeField] private Animator anim; // Animator Reference
[SerializeField] private Animator armAnim; // Arm Animator Reference
[Header("Other")]
[SerializeField] public int maxWeapons;// how many weapons player can carry
[SerializeField] private Transform groundDetector; // gameObject at player foot for detecting ground
[SerializeField] private LayerMask ground; // objects considered as ground
[SerializeField] private SerializableDictionary<string, SoundID> stepSounds; // dictionary for footstep audio clips
[SerializeField] private SoundID[] grunts; // array holding the grunt sounds according to landing velocity in ascending order
[HideInInspector] public int waterLevel; // how deep the player is in water
// References
private GameObject postprocess; // Main PostProcessing object reference
private GameObject postprocesswater; // Underwater PostProcessing object reference
// Physics and Movement
public float sprintLeft; // sprinting energy remaining
private bool sprintOut; // whether player can sprint
private float gravity = -9.81f; // gravity multiplier
public float fallVelocity; // current falling speed
private float landVelocity; // speed the player was falling last time they landed
private Vector2 airVelocity; // player's xz-axis velocity last time they left the ground
private static Vector3 velocity; // current movement velocity in all axes
// Misc.
private List<AudioClip> prevSteps = new List<AudioClip>(); // previous footstep noises played
private float timeSinceLastFootstep; // time since the last footstep
private Vector2 input; // xz-movement values input through controller
// States
private bool sprint; // whether player is pressing sprint key
private bool jump; // whether player is pressing jump key
[HideInInspector] public bool crouch; // whether player is crouching
private bool walk; // whether player is walking
// States
private bool isGrounded; // // whether player is grounded
private bool isJumping; // whether player is actually jumping
private bool isSprinting; // whether player is actually sprinting
[Range(0, 200)]
public int hp = 100; // health remaining;
[Range(0, 100)]
public int shield = 0; // shield remaining;
private void Start()
{
sprintLeft = 100; // initialize sprint
Spawn(); //spawn Player
if (GameObject.Find("PostProcessing") != null)
postprocess = GameObject.Find("PostProcessing");
if (GameObject.Find("PostProcessing Water") != null)
postprocesswater = GameObject.Find("PostProcessing Water");
}//Start
private void Update()
{
UpdateVars();
Animate();
}//Update
void FixedUpdate()
{
UpdateVars();
ApplyRotation();
ApplyGravity();
ApplyMovement();
Footsteps();
}//FixedUpdate
void UpdateVars()
{
//TASK: Update the variables
if (charcontrol.isGrounded && !isGrounded) // the moment the player lands
{
landVelocity = fallVelocity; // lock the current fallingVelocity into landVelocity
Landing();
}//if
if (isGrounded && !charcontrol.isGrounded) // the moment the player leaves the ground
{
airVelocity.x = velocity.x; // lock the current xz-axis velocity into airVelocity
airVelocity.y = velocity.z;
}//if
isGrounded = charcontrol.isGrounded; // update isGrounded
//States
isJumping = jump && (isGrounded || waterLevel == 3); // decide whether player can jump
isSprinting = sprint && waterLevel < 2 && !sprintOut; // decide whether player can sprint
if (crouch) // update the collision geometry when crouching
{
charcontrol.height = 0.5f;
charcontrol.center = new Vector3(0, -0.5f, 0);
}//if
else
{
charcontrol.height = 2;
charcontrol.center = new Vector3(0, 0, 0);
}//else
if (postprocesswater != null) // adjust postprocessing according to state
{
if (waterLevel == 3) // if player is submerged
{
postprocess.SetActive(false);
postprocesswater.SetActive(true);
BroAudio.SetEffect(Effect.LowPass(freq, fadeTime));
}//if
else
{
postprocess.SetActive(true);
postprocesswater.SetActive(false);
BroAudio.SetEffect(Effect.ResetLowPass(fadeTime));
}//else
}//if
if (isJumping)
{
if (inventoryScript.gadgets[2] == "Jump" && inventoryScript.gadgets[3] == "Jump") // if player has two jump boots
fallVelocity = gadgetSettings.doubleJumpBootMultiplier * movement.jumpHeight;
else if (inventoryScript.gadgets[2] == "Jump" || inventoryScript.gadgets[3] == "Jump") // if player has only one jump boot
fallVelocity = gadgetSettings.jumpBootMultiplier * movement.jumpHeight;
else
fallVelocity = movement.jumpHeight;
jump = false; // player will not jump again until jump key is released and pressed again
} //if
}//UpdateVars
void Animate()
{
// TASK: adjust animator parameters
armAnim.SetInteger("Weapon", weaponScript.loadout[weaponScript.currentIndex].GunNum); // adjust arms to conform to current gun
if (Mathf.Abs(velocity.x) > 5 || Mathf.Abs(velocity.z) > 5) // if player is moving
{
if (walk)
anim.SetInteger("Speed", 3); // walking
else if (isSprinting)
anim.SetInteger("Speed", 1); // sprinting
else
anim.SetInteger("Speed", 2); // running
}//if
else
anim.SetInteger("Speed", 0); // standing
if (velocity.z > 0)
anim.SetInteger("Run Dir", 0); // running forwards
else
anim.SetInteger("Run Dir", 3); // running backwards
anim.SetBool("IsGrounded", isGrounded); // decide whether to fall or stand
}//Animate
private void ApplyGravity()
{
// TASK: apply gravity
if (isGrounded && fallVelocity < 0) // if on ground
fallVelocity = -1; // set to -1 so that player doesn't bounce
else
fallVelocity += gravity * gravityScale * Time.deltaTime; // increase falling speed
if (inventoryScript.gadgets[0] == "Parachute" && jump && !isGrounded && fallVelocity < 0) // if parachute is open and player is not moving up
fallVelocity = -gadgetSettings.parachuteSpeed; // adjust accordingly
if (inventoryScript.gadgets[0] == "Jetpack" && jump && !isGrounded) // if jetpack is active
{
fallVelocity += gadgetSettings.jetpackStrength; // increase velocity gradually
fallVelocity = Mathf.Clamp(fallVelocity, -2, gadgetSettings.maxJetpack); // clamp at maxJetpack
}//if
velocity.y = fallVelocity; // apply falling speed to main velocity Vector3
}//ApplyGravity
private void ApplyRotation()
{
velocity = Quaternion.Euler(0.0f, GameObject.Find("Camera").transform.eulerAngles.y, 0.0f) * new Vector3(input.x, 0.0f, input.y); // adjust input to conform to looking direction
}//ApplyRotation
private void ApplyMovement()
{
var targetSpeed = 0.0f; // initialize targetSpeed
targetSpeed = isSprinting ? movement.speed * movement.sprintMultiplier : movement.speed; // adjust speed depending on whether player is sprinting
targetSpeed = walk || crouch ? movement.speed * movement.walkSpeed : targetSpeed; // adjust speed depending on whether player is walking or crouching
if (inventoryScript.gadgets[2] == "Speed" && inventoryScript.gadgets[3] == "Speed") // if player has two speed boots
targetSpeed *= gadgetSettings.doubleSpeedBootMultiplier;
else if (inventoryScript.gadgets[2] == "Speed" || inventoryScript.gadgets[3] == "Speed") // if player has only one speed boot
targetSpeed *= gadgetSettings.speedBootMultiplier;
if (!isGrounded)
velocity = new Vector3(airVelocity.x, fallVelocity, airVelocity.y); // if player is airborne, override current speed and replace it with airVelocity
movement.currentSpeed = Mathf.MoveTowards(movement.currentSpeed, targetSpeed, movement.acceleration * Time.deltaTime); // accelerate
charcontrol.Move(velocity * movement.currentSpeed * Time.deltaTime); // apply the movement
if (isSprinting)
{
sprintLeft -= movement.sprintTime; // detract sprint energy
if (sprintLeft < 0) // if you run out of sprint
{
sprintLeft = 0;
sprintOut = true;
}//if
}//if
else if (sprintLeft < 100) // if not sprinting and sprint is not full
sprintLeft += movement.sprintRecover; // restore some energy
Mathf.Clamp(sprintLeft, 0, 100);
if (sprintLeft > 10) // when sprintLeft reaches the 10% mark after being depleted
sprintOut = false; // player can sprint again
}//ApplyMovement
void Footsteps()
{
//TASK: Play footstep sounds
RaycastHit hit;
if (Physics.Raycast(groundDetector.position, Vector3.down, out hit, 0.1F, ground)) // Detect what player is standing on
if ((Mathf.Abs(velocity.x) > 5 || Mathf.Abs(velocity.z) > 5) && isGrounded) // if player is moving on ground
{
float timeTillNextFootstep; // initiate timeCounter
if (walk || crouch)
timeTillNextFootstep = 0.7f; // a longer period of time
else if (isSprinting)
timeTillNextFootstep = 0.35f; // a shorter peripd of time
else
timeTillNextFootstep = 0.5f; // a moderate period of time
if (Time.time - timeSinceLastFootstep > timeTillNextFootstep) // if long enough has gone by since last footstep
{
string clip = "Rock"; // if item player is on is not in the dictionary, play this sound
if (waterLevel == 0) // if player is not in contact with water
{
if (stepSounds.ContainsKey(hit.transform.gameObject.tag)) // if dictionary contains a key for what the player is on
clip = hit.transform.gameObject.tag; // set the target clip to that key
}
else if (waterLevel == 1) // below waist
clip = "ShallowWater";
else if (waterLevel >= 2) // above waist
clip = "DeepWater";
BroAudio.Play(stepSounds.Get(clip)); // play the sound
timeSinceLastFootstep = Time.time; // next tick
} //if
}//if
}//Footsteps
void LoseHealth(int lostHealth)
{
//TASK: Subtract health from player
if (shield > 0) // if player has shield
shield -= lostHealth; // lose shield
else if (hp < 20) // Lose less health when low on health to pump more adrenaline into human player
hp -= lostHealth / 2;
else
hp -= lostHealth;
hp = Mathf.Clamp(hp, 0, 200); // player can have up to 200 hp
shield = Mathf.Clamp(shield, 0, 100); // but only 100 shield
}//LoseHealth
void Spawn()
{
//TASK: Spawn player
GameObject[] spawnPoints = GameObject.FindGameObjectsWithTag("Spawn Point"); // make an array of every spawn point
if (spawnPoints.Length > 0) // if there are spawn points
{
Vector3 spawnPos = spawnPoints[UnityEngine.Random.Range(0, spawnPoints.Length)].transform.position; // pick a random one and find its position
transform.position = spawnPos; // spawn at point
}//if
}//Spawn
void Landing()
{
RaycastHit hit;
if (Physics.Raycast(groundDetector.position, Vector3.down, out hit, 0.1F, ground)) // Detect what player has landed on
{
string clip = "Rock"; // if item is not in dictionary, it will play a rock landing sound
if (waterLevel == 0) // not in contact at all
{
if (stepSounds.ContainsKey(hit.transform.gameObject.tag)) // check if dictionary contains this item
clip = hit.transform.gameObject.tag; // if it does, assign it
}
else if (waterLevel == 1) // less than waist deep
clip = "ShallowWater";
BroAudio.Play(stepSounds.Get(clip)); // play sound
}
int landForce = -1; // initial declaration
if (landVelocity < -9) // if player hit the ground HARD
landForce = 3; // play a painful grunt (there is no fall damage)
else if (landVelocity < -7) // if player hit the ground at a medium-high speed
landForce = 2; // play a moderate grunt
else if (landVelocity < -5) // if player hit the ground at a medium speed
landForce = 1; // play a smaller grunt
else if (landVelocity < -3) // if player hit the ground at a low speed
landForce = 0; // play a quiet grunt
if (landForce != -1) // falls from very low heights will not trigger a grunt
BroAudio.Play(grunts[landForce]); // play the grunt
}
#region Inputs
public void OnMove(InputAction.CallbackContext context)
{
input = context.ReadValue<Vector2>();
velocity = new Vector3(input.x, 0.0f, input.y);
}
public void OnJump(InputAction.CallbackContext context)
{
if (waterLevel == 3) // if completely submerged
{
if (context.performed) // player can continually jump
jump = true;
}
else
{
if (context.started) // otherwise they can only jump once
jump = true;
}
if (context.canceled)
jump = false;
}
public void OnSprint(InputAction.CallbackContext context)
{
crouch = false; // sprinting will uncrouch you
sprint = context.performed;
}
public void OnWalk(InputAction.CallbackContext context)
{
walk = context.performed;
}
public void OnCrouch(InputAction.CallbackContext context)
{
crouch = context.performed;
}
public void OnCrouchToggle(InputAction.CallbackContext context)
{
if (context.started)
crouch = !crouch;
}
public void OnMelee(InputAction.CallbackContext context)
{
if (context.started)
armAnim.SetTrigger("Melee");
}
#endregion
}// class
[Serializable]
public struct Movement
{
public float speed; // how fast player moves when not sprinting
public float sprintMultiplier; // value to multiply to speed when sprinting
public float acceleration; // how quickly player reaches top speed
public float walkSpeed; // how fast player moves when holding walk
public float sprintTime; // how much sprint energy is subtracted each tick while sprinting
public float sprintRecover; // how much sprint energy is added each tick while not sprinting
public float jumpHeight; // how high player jumps
[HideInInspector] public float currentSpeed; // current velocity along the xz-axis
}
[Serializable]
public struct GadgetSettings
{
public float speedBootMultiplier; // value to multiply to speed when speed boot is equipped
public float doubleSpeedBootMultiplier; // value to multiply to speed when two speed boots are equipped
public float jumpBootMultiplier; // value to multiply to jumpHeight when jump boot is equipped
public float doubleJumpBootMultiplier; // value to multiply to jumpHeight when two jump boots are equipped
public float maxJetpack; // max flying velocity in jetpack
public float jetpackStrength; // how quickly player acclerates upwards with jetpack
public float parachuteSpeed; // how quickly player falls with open parachute
}
Here are the particular areas related to this:
private void ApplyGravity()
{
// TASK: apply gravity
if (isGrounded && fallVelocity < 0) // if on ground
fallVelocity = -1; // set to -1 so that player doesn't bounce
else
fallVelocity += gravity * gravityScale * Time.deltaTime; // increase falling speed
if (inventoryScript.gadgets[0] == "Parachute" && jump && !isGrounded && fallVelocity < 0) // if parachute is open and player is not moving up
fallVelocity = -gadgetSettings.parachuteSpeed; // adjust accordingly
if (inventoryScript.gadgets[0] == "Jetpack" && jump && !isGrounded) // if jetpack is active
{
fallVelocity += gadgetSettings.jetpackStrength; // increase velocity gradually
fallVelocity = Mathf.Clamp(fallVelocity, -2, gadgetSettings.maxJetpack); // clamp at maxJetpack
}//if
velocity.y = fallVelocity; // apply falling speed to main velocity Vector3
}//ApplyGravity
private void ApplyRotation()
{
velocity = Quaternion.Euler(0.0f, GameObject.Find("Camera").transform.eulerAngles.y, 0.0f) * new Vector3(input.x, 0.0f, input.y); // adjust input to conform to looking direction
}//ApplyRotation
private void ApplyMovement()
{
var targetSpeed = 0.0f; // initialize targetSpeed
targetSpeed = isSprinting ? movement.speed * movement.sprintMultiplier : movement.speed; // adjust speed depending on whether player is sprinting
targetSpeed = walk || crouch ? movement.speed * movement.walkSpeed : targetSpeed; // adjust speed depending on whether player is walking or crouching
if (inventoryScript.gadgets[2] == "Speed" && inventoryScript.gadgets[3] == "Speed") // if player has two speed boots
targetSpeed *= gadgetSettings.doubleSpeedBootMultiplier;
else if (inventoryScript.gadgets[2] == "Speed" || inventoryScript.gadgets[3] == "Speed") // if player has only one speed boot
targetSpeed *= gadgetSettings.speedBootMultiplier;
if (!isGrounded)
velocity = new Vector3(airVelocity.x, fallVelocity, airVelocity.y); // if player is airborne, override current speed and replace it with airVelocity
movement.currentSpeed = Mathf.MoveTowards(movement.currentSpeed, targetSpeed, movement.acceleration * Time.deltaTime); // accelerate
charcontrol.Move(velocity * movement.currentSpeed * Time.deltaTime); // apply the movement
if (isSprinting)
{
sprintLeft -= movement.sprintTime; // detract sprint energy
if (sprintLeft < 0) // if you run out of sprint
{
sprintLeft = 0;
sprintOut = true;
}//if
}//if
else if (sprintLeft < 100) // if not sprinting and sprint is not full
sprintLeft += movement.sprintRecover; // restore some energy
Mathf.Clamp(sprintLeft, 0, 100);
if (sprintLeft > 10) // when sprintLeft reaches the 10% mark after being depleted
sprintOut = false; // player can sprint again
}//ApplyMovement