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; | |
| } |