Refactored and updated to Unity 5 version of the tutorial here.
The FirstPersonController.cs
is mostly the standard asset, but did have to change a couple things to enable/disable sprinting and jumping.
using System; | |
using UnityEngine; | |
using UnityStandardAssets.CrossPlatformInput; | |
using UnityStandardAssets.Utility; | |
using Random = UnityEngine.Random; | |
namespace UnityStandardAssets.Characters.FirstPerson | |
{ | |
[RequireComponent(typeof (CharacterController))] | |
[RequireComponent(typeof (AudioSource))] | |
public class FirstPersonController : MonoBehaviour | |
{ | |
[SerializeField] public bool m_IsWalking; | |
[SerializeField] private float m_WalkSpeed; | |
[SerializeField] private float m_RunSpeed; | |
[SerializeField] [Range(0f, 1f)] private float m_RunstepLenghten; | |
[SerializeField] private float m_JumpSpeed; | |
[SerializeField] private float m_StickToGroundForce; | |
[SerializeField] private float m_GravityMultiplier; | |
[SerializeField] private MouseLook m_MouseLook; | |
[SerializeField] private bool m_UseFovKick; | |
[SerializeField] private FOVKick m_FovKick = new FOVKick(); | |
[SerializeField] private bool m_UseHeadBob; | |
[SerializeField] private CurveControlledBob m_HeadBob = new CurveControlledBob(); | |
[SerializeField] private LerpControlledBob m_JumpBob = new LerpControlledBob(); | |
[SerializeField] private float m_StepInterval; | |
[SerializeField] private AudioClip[] m_FootstepSounds; // an array of footstep sounds that will be randomly selected from. | |
[SerializeField] private AudioClip m_JumpSound; // the sound played when character leaves the ground. | |
[SerializeField] private AudioClip m_LandSound; // the sound played when character touches back on ground. | |
private Camera m_Camera; | |
public bool m_Jump; | |
public bool m_CanSprint; | |
public bool m_CanJump; | |
private float m_YRotation; | |
private Vector2 m_Input; | |
private Vector3 m_MoveDir = Vector3.zero; | |
private CharacterController m_CharacterController; | |
private CollisionFlags m_CollisionFlags; | |
private bool m_PreviouslyGrounded; | |
private Vector3 m_OriginalCameraPosition; | |
private float m_StepCycle; | |
private float m_NextStep; | |
public bool m_Jumping; | |
private AudioSource m_AudioSource; | |
// Use this for initialization | |
private void Start() | |
{ | |
m_CharacterController = GetComponent<CharacterController>(); | |
m_Camera = Camera.main; | |
m_OriginalCameraPosition = m_Camera.transform.localPosition; | |
m_FovKick.Setup(m_Camera); | |
m_HeadBob.Setup(m_Camera, m_StepInterval); | |
m_StepCycle = 0f; | |
m_NextStep = m_StepCycle/2f; | |
m_Jumping = false; | |
m_AudioSource = GetComponent<AudioSource>(); | |
m_MouseLook.Init(transform , m_Camera.transform); | |
} | |
// Update is called once per frame | |
private void Update() | |
{ | |
RotateView(); | |
// the jump state needs to read here to make sure it is not missed | |
if (!m_Jump && m_CanJump) | |
{ | |
m_Jump = CrossPlatformInputManager.GetButtonDown("Jump"); | |
} | |
if (!m_PreviouslyGrounded && m_CharacterController.isGrounded) | |
{ | |
StartCoroutine(m_JumpBob.DoBobCycle()); | |
PlayLandingSound(); | |
m_MoveDir.y = 0f; | |
m_Jumping = false; | |
} | |
if (!m_CharacterController.isGrounded && !m_Jumping && m_PreviouslyGrounded) | |
{ | |
m_MoveDir.y = 0f; | |
} | |
m_PreviouslyGrounded = m_CharacterController.isGrounded; | |
} | |
private void PlayLandingSound() | |
{ | |
m_AudioSource.clip = m_LandSound; | |
m_AudioSource.Play(); | |
m_NextStep = m_StepCycle + .5f; | |
} | |
private void FixedUpdate() | |
{ | |
float speed; | |
GetInput(out speed); | |
// always move along the camera forward as it is the direction that it being aimed at | |
Vector3 desiredMove = transform.forward*m_Input.y + transform.right*m_Input.x; | |
// get a normal for the surface that is being touched to move along it | |
RaycastHit hitInfo; | |
Physics.SphereCast(transform.position, m_CharacterController.radius, Vector3.down, out hitInfo, | |
m_CharacterController.height/2f, ~0, QueryTriggerInteraction.Ignore); | |
desiredMove = Vector3.ProjectOnPlane(desiredMove, hitInfo.normal).normalized; | |
m_MoveDir.x = desiredMove.x*speed; | |
m_MoveDir.z = desiredMove.z*speed; | |
if (m_CharacterController.isGrounded) | |
{ | |
m_MoveDir.y = -m_StickToGroundForce; | |
if (m_Jump) | |
{ | |
m_MoveDir.y = m_JumpSpeed; | |
PlayJumpSound(); | |
m_Jump = false; | |
m_Jumping = true; | |
} | |
} | |
else | |
{ | |
m_MoveDir += Physics.gravity*m_GravityMultiplier*Time.fixedDeltaTime; | |
} | |
m_CollisionFlags = m_CharacterController.Move(m_MoveDir*Time.fixedDeltaTime); | |
ProgressStepCycle(speed); | |
UpdateCameraPosition(speed); | |
m_MouseLook.UpdateCursorLock(); | |
} | |
private void PlayJumpSound() | |
{ | |
m_AudioSource.clip = m_JumpSound; | |
m_AudioSource.Play(); | |
} | |
private void ProgressStepCycle(float speed) | |
{ | |
if (m_CharacterController.velocity.sqrMagnitude > 0 && (m_Input.x != 0 || m_Input.y != 0)) | |
{ | |
m_StepCycle += (m_CharacterController.velocity.magnitude + (speed*(m_IsWalking ? 1f : m_RunstepLenghten)))* | |
Time.fixedDeltaTime; | |
} | |
if (!(m_StepCycle > m_NextStep)) | |
{ | |
return; | |
} | |
m_NextStep = m_StepCycle + m_StepInterval; | |
PlayFootStepAudio(); | |
} | |
private void PlayFootStepAudio() | |
{ | |
if (!m_CharacterController.isGrounded) | |
{ | |
return; | |
} | |
// pick & play a random footstep sound from the array, | |
// excluding sound at index 0 | |
int n = Random.Range(1, m_FootstepSounds.Length); | |
m_AudioSource.clip = m_FootstepSounds[n]; | |
m_AudioSource.PlayOneShot(m_AudioSource.clip); | |
// move picked sound to index 0 so it's not picked next time | |
m_FootstepSounds[n] = m_FootstepSounds[0]; | |
m_FootstepSounds[0] = m_AudioSource.clip; | |
} | |
private void UpdateCameraPosition(float speed) | |
{ | |
Vector3 newCameraPosition; | |
if (!m_UseHeadBob) | |
{ | |
return; | |
} | |
if (m_CharacterController.velocity.magnitude > 0 && m_CharacterController.isGrounded) | |
{ | |
m_Camera.transform.localPosition = | |
m_HeadBob.DoHeadBob(m_CharacterController.velocity.magnitude + | |
(speed*(m_IsWalking ? 1f : m_RunstepLenghten))); | |
newCameraPosition = m_Camera.transform.localPosition; | |
newCameraPosition.y = m_Camera.transform.localPosition.y - m_JumpBob.Offset(); | |
} | |
else | |
{ | |
newCameraPosition = m_Camera.transform.localPosition; | |
newCameraPosition.y = m_OriginalCameraPosition.y - m_JumpBob.Offset(); | |
} | |
m_Camera.transform.localPosition = newCameraPosition; | |
} | |
private void GetInput(out float speed) | |
{ | |
// Read input | |
float horizontal = CrossPlatformInputManager.GetAxis("Horizontal"); | |
float vertical = CrossPlatformInputManager.GetAxis("Vertical"); | |
bool waswalking = m_IsWalking; | |
#if !MOBILE_INPUT | |
// On standalone builds, walk/run speed is modified by a key press. | |
// keep track of whether or not the character is walking or running | |
m_IsWalking = !Input.GetKey(KeyCode.LeftShift); | |
#endif | |
// set the desired speed to be walking or running | |
speed = m_IsWalking ? m_WalkSpeed : m_RunSpeed; | |
// Handle dynamic setting of whether the character can sprint or not | |
speed = m_CanSprint ? speed : m_WalkSpeed; | |
m_IsWalking = m_CanSprint ? m_IsWalking : true; | |
m_Input = new Vector2(horizontal, vertical); | |
// normalize input if it exceeds 1 in combined length: | |
if (m_Input.sqrMagnitude > 1) | |
{ | |
m_Input.Normalize(); | |
} | |
// handle speed change to give an fov kick | |
// only if the player is going to a run, is running and the fovkick is to be used | |
if (m_IsWalking != waswalking && m_UseFovKick && m_CharacterController.velocity.sqrMagnitude > 0) | |
{ | |
StopAllCoroutines(); | |
StartCoroutine(!m_IsWalking ? m_FovKick.FOVKickUp() : m_FovKick.FOVKickDown()); | |
} | |
} | |
private void RotateView() | |
{ | |
m_MouseLook.LookRotation (transform, m_Camera.transform); | |
} | |
private void OnControllerColliderHit(ControllerColliderHit hit) | |
{ | |
Rigidbody body = hit.collider.attachedRigidbody; | |
//dont move the rigidbody if the character is on top of it | |
if (m_CollisionFlags == CollisionFlags.Below) | |
{ | |
return; | |
} | |
if (body == null || body.isKinematic) | |
{ | |
return; | |
} | |
body.AddForceAtPosition(m_CharacterController.velocity*0.1f, hit.point, ForceMode.Impulse); | |
} | |
} | |
} |
#pragma strict | |
import System.Collections.Generic; | |
var size : Vector2 = new Vector2(240, 40); | |
var barX : int = 20; | |
var barY : int = 20; | |
var barSpacing : int = 40; | |
var healthDrainRate : int = 150; | |
var hungerDrainRate : int = 150; | |
var thirstDrainRate : int = 100; | |
var staminaDrainRate : int = 35; | |
class Player | |
{ | |
var health : Stat; | |
var hunger : Stat; | |
var thirst : Stat; | |
var stamina : Stat; | |
var stats : List.<Stat>; | |
function Player(healthDrainRate : int, hungerDrainRate : int, thirstDrainRate : int, staminaDrainRate : int) | |
{ | |
this.stats = new List.<Stat>(); | |
this.health = this.CreateStat(healthDrainRate); | |
this.hunger = this.CreateStat(hungerDrainRate); | |
this.thirst = this.CreateStat(thirstDrainRate); | |
this.stamina = this.CreateStat(staminaDrainRate); | |
} | |
function CreateStat(drainRate : int) | |
{ | |
var stat = Stat(drainRate); | |
this.stats.Add(stat); | |
return stat; | |
} | |
} | |
class Stat | |
{ | |
var value : float; | |
var drainRate : int; | |
var displayBar : Bar; | |
function Stat(drainRate : int) | |
{ | |
this.value = 1; this.drainRate = drainRate; | |
} | |
function Decrease() | |
{ | |
this.Sub(Time.deltaTime / this.drainRate); | |
} | |
function Increase() | |
{ | |
// recover at half the drain rate | |
this.Add(Time.deltaTime / (this.drainRate * 2)); | |
} | |
function Add(v : float) | |
{ | |
this.value += v; | |
if (this.value >= 1) | |
{ | |
this.value = 1; | |
} | |
this.displayBar.Display = this.value; | |
} | |
function Sub(v : float) | |
{ | |
this.value -= v; | |
if (this.value <= 0) | |
{ | |
this.value = 0; | |
} | |
this.displayBar.Display = this.value; | |
} | |
} | |
var player = Player(healthDrainRate, hungerDrainRate, thirstDrainRate, staminaDrainRate); | |
class Bar | |
{ | |
var Pos : Vector2; | |
var Display : float; | |
var EmptyTexture : Texture2D; | |
var FullTexture : Texture2D; | |
function Bar(x : int, y : int) | |
{ | |
this.Pos = new Vector2(x, y); | |
this.Display = 1; | |
} | |
} | |
for (var i : int = 0; i < len(player.stats); i++) | |
{ | |
var localBarY = barY + barSpacing * i; | |
var newBar = Bar(barX, localBarY); | |
player.stats[i].displayBar = newBar; | |
} | |
private var chMotor : UnityStandardAssets.Characters.FirstPerson.FirstPersonController; | |
private var controller : CharacterController; | |
var staminaDrainedWait : float = 0.5; // How long to wait when we drain stamina before it starts recovering | |
var staminaJumpCost : float = 2; // Multiplier from default drain rate | |
var recoveringStamina : boolean = false; | |
var regenEnabled : boolean = true; | |
function Start() | |
{ | |
chMotor = GetComponent(UnityStandardAssets.Characters.FirstPerson.FirstPersonController); | |
controller = GetComponent(CharacterController); | |
} | |
function OnGUI() | |
{ | |
for (var stat in player.stats) | |
{ | |
var bar = stat.displayBar; | |
GUI.BeginGroup(new Rect (bar.Pos.x, bar.Pos.y, size.x, size.y)); | |
GUI.Box(Rect(0,0, size.x, size.y), bar.EmptyTexture); | |
GUI.BeginGroup(new Rect (0, 0, size.x * bar.Display, size.y)); | |
GUI.Box(Rect(0,0, size.x, size.y), bar.FullTexture); | |
GUI.EndGroup(); | |
GUI.EndGroup(); | |
} | |
} | |
function TickHealth() | |
{ | |
if (player.hunger.value <= 0) | |
{ | |
player.health.Decrease(); | |
} | |
if (player.thirst.value <= 0) | |
{ | |
player.health.Decrease(); | |
} | |
if (player.health.value <= 0) | |
{ | |
Die(); | |
} | |
} | |
function TickStamina() | |
{ | |
if (!chMotor.m_IsWalking) | |
{ | |
player.stamina.Decrease(); | |
} | |
if (chMotor.m_Jumping) | |
{ | |
player.stamina.Sub((Time.deltaTime / staminaDrainRate) * staminaJumpCost); | |
} | |
if (!chMotor.m_Jumping && chMotor.m_IsWalking && regenEnabled) | |
{ | |
player.stamina.Increase(); | |
} | |
if (player.stamina.value <= 0 && !recoveringStamina) | |
{ | |
chMotor.m_CanJump = false; | |
chMotor.m_CanSprint = false; | |
recoveringStamina = true; // We need to be recovering next time we get over 0 | |
} else if (recoveringStamina) | |
{ | |
regenEnabled = false; | |
recoveringStamina = false; // So we don't start a bunch of coroutines | |
StartCoroutine(RecoverStamina()); | |
} else { | |
// Defaults | |
chMotor.m_CanJump = true; | |
chMotor.m_CanSprint = true; | |
} | |
} | |
function Die() | |
{ | |
} | |
function Update() | |
{ | |
TickHealth(); | |
player.hunger.Decrease(); | |
player.thirst.Decrease(); | |
TickStamina(); | |
} | |
function RecoverStamina() | |
{ | |
yield WaitForSeconds(staminaDrainedWait); | |
chMotor.m_CanJump = true; | |
chMotor.m_CanSprint = true; | |
regenEnabled = true; | |
} |