FPSWalkerEnhanced

From Unify Community Wiki
(Difference between revisions)
Jump to: navigation, search
m (Text replace - "</javascript>" to "</syntaxhighlight>")
(Removed Boo and JavaScript. Improved C# and updated category tags.)
 
(4 intermediate revisions by 2 users not shown)
Line 1: Line 1:
Author: Eric Haines (Eric5h5)
 
 
 
== Description ==
 
== Description ==
 +
This is an enhanced C# port of the original FPSWalker script found in the Unity 2.x Standard Assets. It adds walking and running, sliding down slopes (both those above the Slope Limit and/or specific tagged objects), air control, anti-bunny hopping, the ability to check fall distances, and special anti-bump code that eliminates that irritating "bumpity bumpity bumpa bump" you get when trying to walk down moderately inclined slopes when using the default script.
  
This is an enhanced version of the FPSWalker script found in Standard Assets. It allows for both walking and running (either by toggle or by holding down a run key), the capability of detecting falling distances, optionally sliding down slopes (both those above the Slope Limit and/or objects specifically tagged "Slide"), optional air control, optional anti-bunny hopping control, and special patented* Anti-Bump™ code that eliminates that irritating "bumpity bumpity bumpa bump" you get when trying to walk down moderately inclined slopes when using the default script.
+
Original author: Eric Haines (Eric5h5)
  
<nowiki>*It's not really patented...it's just a small change actually.</nowiki>
+
==== Anti Bump Factor ====
 +
An amount used to reduce or eliminate the "bumping" that can occur when walking down slopes, which is a result of the player constantly toggling between walking and falling small distances. The default of .75 is sufficient for most cases, although a little bit can still occur on steep slopes, assuming sliding isn't enabled. Larger amounts will stop this from ever happening, but too much can result in excessive falling speeds when stepping over an edge. Very small amounts will enable bumping, if that's desired for some reason.
  
Note that the new first-person controller prefab in Unity 3 handles most of this, as well as having other functionality.
+
==== Slide When Over Slope Limit ==== 
 +
If checked, the player will slide down slopes that are greater than the Slope Limit as defined by the CharacterController. Attempting to jump up such slopes will also fail. The player has no control until resting on a surface that is under the Slope Limit.
  
== Usage ==
+
==== Slide On Tagged Objects ====
 +
If checked, the player will slide down any objects tagged "Slide" when standing on them, regardless of how much or little they are sloped. (The tag "Slide" must be added to the Tag Manager.) This can be used to create chutes, icy surfaces, etc. Note that tagged objects with zero slope will cause sliding in an undefined direction.
  
In any situation where you'd normally use the standard FPSWalker script, you can replace it with this instead. The original parameters are still there and work as usual (although "speed" is now "walk speed").
+
==== Anti Bunny Hop Factor ====
 +
Bunny hopping is repeated jumping by virtue of continuously holding down the jump button. Often considered annoying and silly, especially in multiplayer games. If the anti-hop value is at least 1, the player must release the jump button (and the release-and-hold-in-the-air trick is ineffective), and be grounded for the specified number of physics frames before being able to jump again. If the value is 0, then bunny hopping is allowed. If using noticeably large values, implementing some kind of visual indicator of jump availability is recommended to avoid player frustration.
  
* '''Walk Speed''': How fast the player moves when walking (the default).
+
==== Falling Threshold ====
* '''Run Speed''': How fast the player moves when running.
+
How many units the player can fall before triggering the OnFell function. The script as-is merely prints the total number of units that the player fell if this happens but the function can be changed to do anything the programmer desires. The "fallDistance" variable in this function contains the number of vertical units between the initial point of "catching air" and the final impact point. If falling function is irrelevant simply change the number to "Infinity" in the inspector, and the function will never be called.
* '''Limit Diagonal Speed''': If checked, strafing combined with moving forward or backward can't exceed the normal movement rate. The horizontal and vertical movement axes are computed independently, so if this isn't checked, then diagonal speed is about 1.4 times faster than normal.
+
* '''Toggle Run''': If checked, the player can toggle between running and walking by pressing the run button (there must be a button set up in the Input Manager called "Run"). If not checked, the player normally walks unless holding down the run button.
+
* '''Jump Speed''': How high the player jumps when hitting the jump button (there must be a button set up in the Input Manager called "Jump", which there is by default).
+
* '''Gravity''': How fast the player falls when not standing on anything.
+
* '''Falling Damage Threshold''': How many units the player can fall before taking damage when landing. The script as-is merely prints the total number of units that the player fell if this happens, but the FallingDamageAlert function can be changed to do anything the programmer desires. The "fallDistance" local variable in this function contains the number of vertical units between the initial point of "catching air" and the final impact point. If falling damage is irrelevant, change the number to "Infinity" in the inspector, and the function will never be called.
+
* '''Slide When Over Slope Limit''': If checked, the player will slide down slopes that are greater than the Slope Limit as defined by the Character Controller. Attempting to jump up such slopes will also fail. The player has no control until resting on a surface that is under the Slope Limit.
+
* '''Slide On Tagged Objects''': If checked, the player will slide down any objects tagged "Slide" when standing on them, regardless of how much or little they are sloped. (The tag "Slide" must be added to the Tag Manager.) This can be used to create chutes, icy surfaces, etc. Note that tagged objects with zero slope will cause sliding in an undefined direction.
+
* '''Slide Speed''': How fast the player slides when on slopes as defined above.
+
* '''Air Control''': If checked, the player will be able to control movement while in the air, except when Slide When Over Slope Limit/Slide On Tagged Objects is enabled and the player is jumping off a slope over the limit (otherwise the player would be able to jump up supposedly inaccessible slopes).
+
* '''Anti Bump Factor''': An amount used to reduce or eliminate the "bumping" that can occur when walking down slopes, which is a result of the player constantly toggling between walking and falling small distances. The default of .75 is sufficient for most cases, although a little bit can still occur on steep slopes, assuming sliding isn't enabled. Larger amounts will stop this from ever happening, but too much can result in excessive falling speeds when stepping over an edge. Very small amounts will enable bumping, if that's desired for some reason.
+
* '''Anti Bunny Hop Factor''': Bunny hopping is repeated jumping by virtue of continuously holding down the jump button. Often considered annoying and silly, especially in multiplayer games. If the anti-hop value is at least 1, the player must release the jump button (and the release-and-hold-in-the-air trick is ineffective), and be grounded for the specified number of physics frames before being able to jump again. If the value is 0, then bunny hopping is allowed. If using noticeably large values, implementing some kind of visual indicator of jump availability is recommended to avoid player frustration.
+
  
The slope sliding is quite basic: the player is either sliding or not, and has no lateral control when sliding. One issue that may surface is that, under some circumstances, attempting to force oneself up a slippery slope will result in annoying jittering, as the character controller moves forward a bit one frame only to slide back the next frame, then moves forward a bit again, etc. Possible solutions are: add code to the script so this doesn't happen, design levels so as to minimize this, or ignore it and repeat to yourself "If it's good enough for UT2004, it's good enough for me." It's also not a good idea to create situations where the player slides into a V-shaped wedge valley where both slopes are unscalable, since the player will be stuck at the bottom, jittering for eternity. Not fun.
+
== FPSWalkerEnhanced.cs ==
 +
<syntaxhighlight lang="csharp">using UnityEngine;
  
== JavaScript Version (FPSWalkerEnhanced.js) ==
+
[RequireComponent(typeof(CharacterController))]
 +
public class FPSWalkerEnhanced : MonoBehaviour
 +
{
 +
    [Tooltip("How fast the player moves when walking (default move speed).")]
 +
    [SerializeField]
 +
    private float m_WalkSpeed = 6.0f;
  
<syntaxhighlight lang="javascript">var walkSpeed = 6.0;
+
    [Tooltip("How fast the player moves when running.")]
var runSpeed = 11.0;
+
    [SerializeField]
 +
    private float m_RunSpeed = 11.0f;
  
// If true, diagonal speed (when strafing + moving forward or back) can't exceed normal move speed; otherwise it's about 1.4 times faster
+
    [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.")]
var limitDiagonalSpeed = true;
+
    [SerializeField]
 +
    public bool m_LimitDiagonalSpeed = true;
  
// If checked, the run key toggles between running and walking. Otherwise player runs if the key is held down and walks otherwise
+
    [Tooltip("If checked, the run key toggles between running and walking. Otherwise player runs if the key is held down.")]
// There must be a button set up in the Input Manager called "Run"
+
    [SerializeField]
var toggleRun = false;
+
    private bool m_ToggleRun = false;
  
var jumpSpeed = 8.0;
+
    [Tooltip("How high the player jumps when hitting the jump button.")]
var gravity = 20.0;
+
    [SerializeField]
 +
    private float m_JumpSpeed = 8.0f;
  
// Units that player can fall before a falling damage function is run. To disable, type "infinity" in the inspector
+
    [Tooltip("How fast the player falls when not standing on anything.")]
var fallingDamageThreshold = 10.0;
+
    [SerializeField]
 +
    private float m_Gravity = 20.0f;
  
// 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
+
    [Tooltip("Units that player can fall before a falling function is run. To disable, type \"infinity\" in the inspector.")]
var slideWhenOverSlopeLimit = false;
+
    [SerializeField]
 +
    private float m_FallingThreshold = 10.0f;
  
// If checked and the player is on an object tagged "Slide", he will slide down it regardless of the slope limit
+
    [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.")]
var slideOnTaggedObjects = false;
+
    [SerializeField]
 +
    private bool m_SlideWhenOverSlopeLimit = false;
  
var slideSpeed = 12.0;
+
    [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 m_SlideOnTaggedObjects = false;
  
// If checked, then the player can change direction while in the air
+
    [Tooltip("How fast the player slides when on slopes as defined above.")]
var airControl = false;
+
    [SerializeField]
 +
    private float m_SlideSpeed = 12.0f;
  
// Small amounts of this results in bumping when walking down slopes, but large amounts results in falling too fast
+
    [Tooltip("If checked, then the player can change direction while in the air.")]
var antiBumpFactor = .75;
+
    [SerializeField]
 +
    private bool m_AirControl = false;
  
// Player must be grounded for at least this many physics frames before being able to jump again; set to 0 to allow bunny hopping
+
    [Tooltip("Small amounts of this results in bumping when walking down slopes, but large amounts results in falling too fast.")]
var antiBunnyHopFactor = 1;
+
    [SerializeField]
 +
    private float m_AntiBumpFactor = .75f;
  
private var moveDirection = Vector3.zero;
+
    [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.")]
private var grounded = false;
+
    [SerializeField]
private var controller : CharacterController;
+
    private int m_AntiBunnyHopFactor = 1;
private var myTransform : Transform;
+
private var speed : float;
+
private var hit : RaycastHit;
+
private var fallStartLevel : float;
+
private var falling = false;
+
private var slideLimit : float;
+
private var rayDistance : float;
+
private var contactPoint : Vector3;
+
private var playerControl = false;
+
private var jumpTimer : int;
+
  
function Start () {
+
    private Vector3 m_MoveDirection = Vector3.zero;
controller = GetComponent(CharacterController);
+
    private bool m_Grounded = false;
myTransform = transform;
+
    private CharacterController m_Controller;
speed = walkSpeed;
+
    private Transform m_Transform;
rayDistance = controller.height * .5 + controller.radius;
+
    private float m_Speed;
slideLimit = controller.slopeLimit - .1;
+
    private RaycastHit m_Hit;
jumpTimer = antiBunnyHopFactor;
+
    private float m_FallStartLevel;
oldPos = transform.position;
+
    private bool m_Falling;
}
+
    private float m_SlideLimit;
 +
    private float m_RayDistance;
 +
    private Vector3 m_ContactPoint;
 +
    private bool m_PlayerControl = false;
 +
    private int m_JumpTimer;
  
function FixedUpdate() {
 
var inputX = Input.GetAxis("Horizontal");
 
var 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
 
var inputModifyFactor = (inputX != 0.0 && inputY != 0.0 && limitDiagonalSpeed)? .7071 : 1.0;
 
 
if (grounded) {
 
var 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(myTransform.position, -Vector3.up, hit, rayDistance)) {
 
if (Vector3.Angle(hit.normal, Vector3.up) > slideLimit)
 
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(contactPoint + Vector3.up, -Vector3.up, hit);
 
if (Vector3.Angle(hit.normal, Vector3.up) > slideLimit)
 
sliding = true;
 
}
 
  
// If we were falling, and we fell a vertical distance greater than the threshold, run a falling damage routine
+
    private void Start()
if (falling) {
+
    {
falling = false;
+
        // Saving component references to improve performance.
if (myTransform.position.y < fallStartLevel - fallingDamageThreshold)
+
        m_Transform = GetComponent<Transform>();
FallingDamageAlert (fallStartLevel - myTransform.position.y);
+
        m_Controller = GetComponent<CharacterController>();
}
+
+
// If running isn't on a toggle, then use the appropriate speed depending on whether the run button is down
+
if (!toggleRun)
+
speed = Input.GetButton("Run")? runSpeed : walkSpeed;
+
  
// 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
+
        // Setting initial values.
if ( (sliding && slideWhenOverSlopeLimit) || (slideOnTaggedObjects && hit.collider.tag == "Slide") ) {
+
        m_Speed = m_WalkSpeed;
var hitNormal = hit.normal;
+
        m_RayDistance = m_Controller.height * .5f + m_Controller.radius;
moveDirection = Vector3(hitNormal.x, -hitNormal.y, hitNormal.z);
+
        m_SlideLimit = m_Controller.slopeLimit - .1f;
Vector3.OrthoNormalize (hitNormal, moveDirection);
+
        m_JumpTimer = m_AntiBunnyHopFactor;
moveDirection *= slideSpeed;
+
    }
playerControl = false;
+
}
+
// Otherwise recalculate moveDirection directly from axes, adding a bit of -y to avoid bumping down inclines
+
else {
+
moveDirection = Vector3(inputX * inputModifyFactor, -antiBumpFactor, inputY * inputModifyFactor);
+
moveDirection = myTransform.TransformDirection(moveDirection) * speed;
+
playerControl = 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"))
 
jumpTimer++;
 
else if (jumpTimer >= antiBunnyHopFactor) {
 
moveDirection.y = jumpSpeed;
 
jumpTimer = 0;
 
}
 
}
 
else {
 
// If we stepped over a cliff or something, set the height at which we started falling
 
if (!falling) {
 
falling = true;
 
fallStartLevel = myTransform.position.y;
 
}
 
 
// If air control is allowed, check movement but don't touch the y component
 
if (airControl && playerControl) {
 
moveDirection.x = inputX * speed * inputModifyFactor;
 
moveDirection.z = inputY * speed * inputModifyFactor;
 
moveDirection = myTransform.TransformDirection(moveDirection);
 
}
 
}
 
  
// Apply gravity
+
    private void Update()
moveDirection.y -= gravity * Time.deltaTime;
+
    {
 +
        // 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 (m_ToggleRun && m_Grounded && Input.GetButtonDown("Run"))
 +
        {
 +
            m_Speed = (m_Speed == m_WalkSpeed ? m_RunSpeed : m_WalkSpeed);
 +
        }
 +
    }
  
// Move the controller, and set grounded true or false depending on whether we're standing on something
 
grounded = (controller.Move(moveDirection * Time.deltaTime) & CollisionFlags.Below) != 0;
 
}
 
  
function Update () {
+
     private void FixedUpdate()
// 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 (toggleRun && grounded && Input.GetButtonDown("Run"))
+
speed = (speed == walkSpeed? runSpeed : walkSpeed);
+
}
+
 
+
// Store point that we're in contact with for use in FixedUpdate if needed
+
function OnControllerColliderHit (hit : ControllerColliderHit) {
+
contactPoint = hit.point;
+
}
+
 
+
// If falling damage occured, this is the place to do something about it. You can make the player
+
// have hitpoints and remove some of them based on the distance fallen, add sound effects, etc.
+
function FallingDamageAlert (fallDistance : float) {
+
Debug.Log ("Ouch! Fell " + fallDistance + " units!");
+
}
+
 
+
@script RequireComponent(CharacterController)
+
</syntaxhighlight>
+
 
+
== C# Version ==
+
Identical script, just converted to C#.
+
 
+
Converted by: --[[User:TwiiK|TwiiK]] 09:40, 1 May 2010 (PDT)
+
 
+
<syntaxhighlight lang="javascript">using UnityEngine;
+
using System.Collections;
+
 
+
[RequireComponent (typeof (CharacterController))]
+
public class FPSWalkerEnhanced: MonoBehaviour {
+
 
+
    public float walkSpeed = 6.0f;
+
 
+
    public float runSpeed = 11.0f;
+
 
+
    // If true, diagonal speed (when strafing + moving forward or back) can't exceed normal move speed; otherwise it's about 1.4 times faster
+
    public bool limitDiagonalSpeed = true;
+
 
+
    // If checked, the run key toggles between running and walking. Otherwise player runs if the key is held down and walks otherwise
+
    // There must be a button set up in the Input Manager called "Run"
+
    public bool toggleRun = false;
+
 
+
    public float jumpSpeed = 8.0f;
+
    public float gravity = 20.0f;
+
 
+
    // Units that player can fall before a falling damage function is run. To disable, type "infinity" in the inspector
+
    public float fallingDamageThreshold = 10.0f;
+
 
+
    // 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
+
    public bool slideWhenOverSlopeLimit = false;
+
 
+
    // If checked and the player is on an object tagged "Slide", he will slide down it regardless of the slope limit
+
    public bool slideOnTaggedObjects = false;
+
 
+
    public float slideSpeed = 12.0f;
+
 
+
    // If checked, then the player can change direction while in the air
+
    public bool airControl = false;
+
 
+
    // Small amounts of this results in bumping when walking down slopes, but large amounts results in falling too fast
+
    public float antiBumpFactor = .75f;
+
 
+
    // Player must be grounded for at least this many physics frames before being able to jump again; set to 0 to allow bunny hopping
+
    public int antiBunnyHopFactor = 1;
+
 
+
     private Vector3 moveDirection = Vector3.zero;
+
    private bool grounded = false;
+
    private CharacterController controller;
+
    private Transform myTransform;
+
    private float speed;
+
    private RaycastHit hit;
+
    private float fallStartLevel;
+
    private bool falling;
+
    private float slideLimit;
+
    private float rayDistance;
+
    private Vector3 contactPoint;
+
    private bool playerControl = false;
+
    private int jumpTimer;
+
 
+
    void Start() {
+
        controller = GetComponent<CharacterController>();
+
        myTransform = transform;
+
        speed = walkSpeed;
+
        rayDistance = controller.height * .5f + controller.radius;
+
        slideLimit = controller.slopeLimit - .1f;
+
        jumpTimer = antiBunnyHopFactor;
+
     }
+
 
+
    void FixedUpdate() {
+
 
         float inputX = Input.GetAxis("Horizontal");
 
         float inputX = Input.GetAxis("Horizontal");
 
         float inputY = Input.GetAxis("Vertical");
 
         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
 
         // 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 && limitDiagonalSpeed)? .7071f : 1.0f;
+
         float inputModifyFactor = (inputX != 0.0f && inputY != 0.0f && m_LimitDiagonalSpeed) ? .7071f : 1.0f;
     
+
 
         if (grounded) {
+
         if (m_Grounded)
 +
        {
 
             bool sliding = false;
 
             bool sliding = false;
 
             // See if surface immediately below should be slid down. We use this normally rather than a ControllerColliderHit point,
 
             // 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
 
             // because that interferes with step climbing amongst other annoyances
             if (Physics.Raycast(myTransform.position, -Vector3.up, out hit, rayDistance)) {
+
             if (Physics.Raycast(m_Transform.position, -Vector3.up, out m_Hit, m_RayDistance))
                 if (Vector3.Angle(hit.normal, Vector3.up) > slideLimit)
+
            {
 +
                 if (Vector3.Angle(m_Hit.normal, Vector3.up) > m_SlideLimit)
 +
                {
 
                     sliding = true;
 
                     sliding = true;
 +
                }
 
             }
 
             }
 
             // However, just raycasting straight down from the center can fail when on steep slopes
 
             // 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
 
             // So if the above raycast didn't catch anything, raycast down from the stored ControllerColliderHit point instead
             else {
+
             else
                 Physics.Raycast(contactPoint + Vector3.up, -Vector3.up, out hit);
+
            {
                 if (Vector3.Angle(hit.normal, Vector3.up) > slideLimit)
+
                 Physics.Raycast(m_ContactPoint + Vector3.up, -Vector3.up, out m_Hit);
 +
                 if (Vector3.Angle(m_Hit.normal, Vector3.up) > m_SlideLimit)
 +
                {
 
                     sliding = true;
 
                     sliding = true;
 +
                }
 
             }
 
             }
  
 
             // If we were falling, and we fell a vertical distance greater than the threshold, run a falling damage routine
 
             // If we were falling, and we fell a vertical distance greater than the threshold, run a falling damage routine
             if (falling) {
+
             if (m_Falling)
                 falling = false;
+
            {
                 if (myTransform.position.y < fallStartLevel - fallingDamageThreshold)
+
                 m_Falling = false;
                     FallingDamageAlert (fallStartLevel - myTransform.position.y);
+
                 if (m_Transform.position.y < m_FallStartLevel - m_FallingThreshold)
 +
                {
 +
                     OnFell(m_FallStartLevel - m_Transform.position.y);
 +
                }
 
             }
 
             }
         
+
 
 
             // If running isn't on a toggle, then use the appropriate speed depending on whether the run button is down
 
             // If running isn't on a toggle, then use the appropriate speed depending on whether the run button is down
             if (!toggleRun)
+
             if (!m_ToggleRun)
                 speed = Input.GetButton("Run")? runSpeed : walkSpeed;
+
            {
 +
                 m_Speed = Input.GetKey(KeyCode.LeftShift) ? m_RunSpeed : m_WalkSpeed;
 +
            }
  
 
             // 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 (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 && slideWhenOverSlopeLimit) || (slideOnTaggedObjects && hit.collider.tag == "Slide") ) {
+
             if ((sliding && m_SlideWhenOverSlopeLimit) || (m_SlideOnTaggedObjects && m_Hit.collider.tag == "Slide"))
                 Vector3 hitNormal = hit.normal;
+
            {
                 moveDirection = new Vector3(hitNormal.x, -hitNormal.y, hitNormal.z);
+
                 Vector3 hitNormal = m_Hit.normal;
                 Vector3.OrthoNormalize (ref hitNormal, ref moveDirection);
+
                 m_MoveDirection = new Vector3(hitNormal.x, -hitNormal.y, hitNormal.z);
                 moveDirection *= slideSpeed;
+
                 Vector3.OrthoNormalize(ref hitNormal, ref m_MoveDirection);
                 playerControl = false;
+
                 m_MoveDirection *= m_SlideSpeed;
 +
                 m_PlayerControl = false;
 
             }
 
             }
 
             // Otherwise recalculate moveDirection directly from axes, adding a bit of -y to avoid bumping down inclines
 
             // Otherwise recalculate moveDirection directly from axes, adding a bit of -y to avoid bumping down inclines
             else {
+
             else
                 moveDirection = new Vector3(inputX * inputModifyFactor, -antiBumpFactor, inputY * inputModifyFactor);
+
            {
                 moveDirection = myTransform.TransformDirection(moveDirection) * speed;
+
                 m_MoveDirection = new Vector3(inputX * inputModifyFactor, -m_AntiBumpFactor, inputY * inputModifyFactor);
                 playerControl = true;
+
                 m_MoveDirection = m_Transform.TransformDirection(m_MoveDirection) * m_Speed;
 +
                 m_PlayerControl = true;
 
             }
 
             }
  
 
             // Jump! But only if the jump button has been released and player has been grounded for a given number of frames
 
             // 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"))
 
             if (!Input.GetButton("Jump"))
                 jumpTimer++;
+
            {
             else if (jumpTimer >= antiBunnyHopFactor) {
+
                 m_JumpTimer++;
                 moveDirection.y = jumpSpeed;
+
            }
                 jumpTimer = 0;
+
             else if (m_JumpTimer >= m_AntiBunnyHopFactor)
 +
            {
 +
                 m_MoveDirection.y = m_JumpSpeed;
 +
                 m_JumpTimer = 0;
 
             }
 
             }
 
         }
 
         }
         else {
+
         else
 +
        {
 
             // If we stepped over a cliff or something, set the height at which we started falling
 
             // If we stepped over a cliff or something, set the height at which we started falling
             if (!falling) {
+
             if (!m_Falling)
                 falling = true;
+
            {
                 fallStartLevel = myTransform.position.y;
+
                 m_Falling = true;
 +
                 m_FallStartLevel = m_Transform.position.y;
 
             }
 
             }
         
+
 
 
             // If air control is allowed, check movement but don't touch the y component
 
             // If air control is allowed, check movement but don't touch the y component
             if (airControl && playerControl) {
+
             if (m_AirControl && m_PlayerControl)
                 moveDirection.x = inputX * speed * inputModifyFactor;
+
            {
                 moveDirection.z = inputY * speed * inputModifyFactor;
+
                 m_MoveDirection.x = inputX * m_Speed * inputModifyFactor;
                 moveDirection = myTransform.TransformDirection(moveDirection);
+
                 m_MoveDirection.z = inputY * m_Speed * inputModifyFactor;
 +
                 m_MoveDirection = m_Transform.TransformDirection(m_MoveDirection);
 
             }
 
             }
 
         }
 
         }
  
 
         // Apply gravity
 
         // Apply gravity
         moveDirection.y -= gravity * Time.deltaTime;
+
         m_MoveDirection.y -= m_Gravity * Time.deltaTime;
  
 
         // Move the controller, and set grounded true or false depending on whether we're standing on something
 
         // Move the controller, and set grounded true or false depending on whether we're standing on something
         grounded = (controller.Move(moveDirection * Time.deltaTime) & CollisionFlags.Below) != 0;
+
         m_Grounded = (m_Controller.Move(m_MoveDirection * Time.deltaTime) & CollisionFlags.Below) != 0;
 
     }
 
     }
  
    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 (toggleRun && grounded && Input.GetButtonDown("Run"))
 
            speed = (speed == walkSpeed? runSpeed : walkSpeed);
 
    }
 
  
 
     // Store point that we're in contact with for use in FixedUpdate if needed
 
     // Store point that we're in contact with for use in FixedUpdate if needed
     void OnControllerColliderHit (ControllerColliderHit hit) {
+
     private void OnControllerColliderHit(ControllerColliderHit hit)
         contactPoint = hit.point;
+
    {
 +
         m_ContactPoint = hit.point;
 
     }
 
     }
  
     // If falling damage occured, this is the place to do something about it. You can make the player
+
 
    // have hitpoints and remove some of them based on the distance fallen, add sound effects, etc.
+
     // This is the place to apply things like fall damage. You can give the player hitpoints and remove some
     void FallingDamageAlert (float fallDistance) {
+
    // of them based on the distance fallen, play sound effects, etc.
         print ("Ouch! Fell " + fallDistance + " units!");  
+
     private void OnFell(float fallDistance)
 +
    {
 +
         print("Ouch! Fell " + fallDistance + " units!");
 
     }
 
     }
}</syntaxhighlight>
+
}</syntaxhighlight>  
  
== Boo Version ==
 
<boo>
 
 
import UnityEngine
 
 
[RequireComponent(CharacterController)]
 
 
class FPSWalkerEnhanced (MonoBehaviour):
 
 
public walkSpeed = 6f
 
public runSpeed = 11f
 
 
# If limitDiagonalSpeed is true, diagonal speed (when strafing + moving forward or back)
 
# can't exceed normal move speed; otherwise it's about 1.4 times faster
 
 
public limitDiagonalSpeed = true
 
 
# If checked, the run key toggles between running and walking. Otherwise player runs if the key is held down and walks otherwise
 
# There must be a button set up in the Input Manager called "Run"
 
toggleRun = false;
 
 
jumpSpeed = 8.0;
 
gravity = 20.0;
 
 
# Units that player can fall before a falling damage function is run. To disable, type "infinity" in the inspector
 
fallingDamageThreshold = 10.0;
 
 
# 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
 
slideWhenOverSlopeLimit = false
 
 
# If checked and the player is on an object tagged "Slide", he will slide down it regardless of the slope limit
 
slideOnTaggedObjects = false
 
 
slideSpeed = 12.0
 
 
# If checked, then the player can change direction while in the air
 
airControl = false
 
 
# Small amounts of this results in bumping when walking down slopes, but large amounts results in falling too fast
 
antiBumpFactor = .75
 
 
# Player must be grounded for at least this many physics frames before being able to jump again; set to 0 to allow bunny hopping
 
antiBunnyHopFactor = 1
 
 
private moveDirection = Vector3.zero
 
private grounded = false
 
private controller as CharacterController
 
private myTransform as Transform
 
private speed as single
 
private hit as RaycastHit
 
private fallStartLevel as single
 
private falling = false
 
private slideLimit as single
 
private rayDistance as single
 
private contactPoint as Vector3
 
private playerControl = false
 
private jumpTimer as int
 
 
def Start ():
 
    controller = GetComponent(CharacterController)
 
    myTransform = transform
 
    speed = walkSpeed
 
    rayDistance = controller.height * .5 + controller.radius
 
    slideLimit = controller.slopeLimit - .1
 
    jumpTimer = antiBunnyHopFactor
 
    oldPos = transform.position
 
 
def FixedUpdate():
 
    inputX = Input.GetAxis("Horizontal");
 
    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
 
    inputModifyFactor = (.7071 if (inputX != 0.0 and inputY != 0.0 and limitDiagonalSpeed) else 1.0)
 
 
    if grounded:
 
        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(myTransform.position, -Vector3.up, hit, rayDistance):
 
            if Vector3.Angle(hit.normal, Vector3.up) > slideLimit:
 
                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(contactPoint + Vector3.up, -Vector3.up, hit)
 
            if Vector3.Angle(hit.normal, Vector3.up) > slideLimit:
 
                sliding = true
 
       
 
 
        # If we were falling, and we fell a vertical distance greater than the threshold, run a falling damage routine
 
        if falling:
 
            falling = false
 
            if myTransform.position.y < fallStartLevel - fallingDamageThreshold:
 
                FallingDamageAlert(fallStartLevel - myTransform.position.y)
 
 
        # If running isn't on a toggle, then use the appropriate speed depending on whether the run button is down
 
        if not toggleRun:
 
            speed = (runSpeed if Input.GetButton("Run") else walkSpeed)
 
 
        # 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 and slideWhenOverSlopeLimit) or (slideOnTaggedObjects and hit.collider.tag == "Slide"):
 
            hitNormal = hit.normal
 
            moveDirection = Vector3(hitNormal.x, -hitNormal.y, hitNormal.z)
 
            Vector3.OrthoNormalize (hitNormal, moveDirection)
 
            moveDirection *= slideSpeed
 
            playerControl = false
 
       
 
        # Otherwise recalculate moveDirection directly from axes, adding a bit of -y to avoid bumping down inclines
 
        else:
 
            moveDirection = Vector3(inputX * inputModifyFactor, -antiBumpFactor, inputY * inputModifyFactor)
 
            moveDirection = myTransform.TransformDirection(moveDirection) * speed
 
            playerControl = true
 
       
 
 
        # Jump! But only if the jump button has been released and player has been grounded for a given number of frames
 
        if not Input.GetButton("Jump"):
 
            jumpTimer++
 
        elif jumpTimer >= antiBunnyHopFactor:
 
            moveDirection.y = jumpSpeed;
 
            jumpTimer = 0;
 
       
 
   
 
    else:
 
        # If we stepped over a cliff or something, set the height at which we started falling
 
        if not falling:
 
            falling = true;
 
            fallStartLevel = myTransform.position.y;
 
       
 
 
        # If air control is allowed, check movement but don't touch the y component
 
        if airControl and playerControl:
 
            moveDirection.x = inputX * speed * inputModifyFactor
 
            moveDirection.z = inputY * speed * inputModifyFactor
 
            moveDirection = myTransform.TransformDirection(moveDirection)
 
   
 
 
    # Apply gravity
 
    moveDirection.y -= gravity * Time.deltaTime;
 
 
    # Move the controller, and set grounded true or false depending on whether we're standing on something
 
    grounded = ((controller.Move(moveDirection * Time.deltaTime) & CollisionFlags.Below) != 0)
 
 
 
def 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 toggleRun and grounded and Input.GetButtonDown("Run"):
 
        speed = (runSpeed if speed == walkSpeed else walkSpeed)
 
 
 
# Store point that we're in contact with for use in FixedUpdate if needed
 
def OnControllerColliderHit (hit as ControllerColliderHit):
 
    contactPoint = hit.point
 
 
 
# If falling damage occured, this is the place to do something about it. You can make the player
 
# have hitpoints and remove some of them based on the distance fallen, add sound effects, etc.
 
def FallingDamageAlert (fallDistance as single):
 
    Debug.Log ("Ouch! Fell " + fallDistance + " units!")
 
</boo>
 
 
 
[[Category: FPS]]
 
[[Category: JavaScript]]
 
[[Category: Boo]]
 
 
[[Category: C Sharp]]
 
[[Category: C Sharp]]
 
[[Category: MonoBehaviour]]
 
[[Category: MonoBehaviour]]
[[Category: Character Control Scripts]]
+
[[Category:CharacterController]]
 +
[[Category: FPS]]

Latest revision as of 00:53, 8 November 2018

Contents

[edit] Description

This is an enhanced C# port of the original FPSWalker script found in the Unity 2.x Standard Assets. It adds walking and running, sliding down slopes (both those above the Slope Limit and/or specific tagged objects), air control, anti-bunny hopping, the ability to check fall distances, and special anti-bump code that eliminates that irritating "bumpity bumpity bumpa bump" you get when trying to walk down moderately inclined slopes when using the default script.

Original author: Eric Haines (Eric5h5)

[edit] Anti Bump Factor

An amount used to reduce or eliminate the "bumping" that can occur when walking down slopes, which is a result of the player constantly toggling between walking and falling small distances. The default of .75 is sufficient for most cases, although a little bit can still occur on steep slopes, assuming sliding isn't enabled. Larger amounts will stop this from ever happening, but too much can result in excessive falling speeds when stepping over an edge. Very small amounts will enable bumping, if that's desired for some reason.

[edit] Slide When Over Slope Limit

If checked, the player will slide down slopes that are greater than the Slope Limit as defined by the CharacterController. Attempting to jump up such slopes will also fail. The player has no control until resting on a surface that is under the Slope Limit.

[edit] Slide On Tagged Objects

If checked, the player will slide down any objects tagged "Slide" when standing on them, regardless of how much or little they are sloped. (The tag "Slide" must be added to the Tag Manager.) This can be used to create chutes, icy surfaces, etc. Note that tagged objects with zero slope will cause sliding in an undefined direction.

[edit] Anti Bunny Hop Factor

Bunny hopping is repeated jumping by virtue of continuously holding down the jump button. Often considered annoying and silly, especially in multiplayer games. If the anti-hop value is at least 1, the player must release the jump button (and the release-and-hold-in-the-air trick is ineffective), and be grounded for the specified number of physics frames before being able to jump again. If the value is 0, then bunny hopping is allowed. If using noticeably large values, implementing some kind of visual indicator of jump availability is recommended to avoid player frustration.

[edit] Falling Threshold

How many units the player can fall before triggering the OnFell function. The script as-is merely prints the total number of units that the player fell if this happens but the function can be changed to do anything the programmer desires. The "fallDistance" variable in this function contains the number of vertical units between the initial point of "catching air" and the final impact point. If falling function is irrelevant simply change the number to "Infinity" in the inspector, and the function will never be called.

[edit] FPSWalkerEnhanced.cs

using UnityEngine;
 
[RequireComponent(typeof(CharacterController))]
public class FPSWalkerEnhanced : MonoBehaviour
{
    [Tooltip("How fast the player moves when walking (default move speed).")]
    [SerializeField]
    private float m_WalkSpeed = 6.0f;
 
    [Tooltip("How fast the player moves when running.")]
    [SerializeField]
    private float m_RunSpeed = 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 m_LimitDiagonalSpeed = true;
 
    [Tooltip("If checked, the run key toggles between running and walking. Otherwise player runs if the key is held down.")]
    [SerializeField]
    private bool m_ToggleRun = false;
 
    [Tooltip("How high the player jumps when hitting the jump button.")]
    [SerializeField]
    private float m_JumpSpeed = 8.0f;
 
    [Tooltip("How fast the player falls when not standing on anything.")]
    [SerializeField]
    private float m_Gravity = 20.0f;
 
    [Tooltip("Units that player can fall before a falling function is run. To disable, type \"infinity\" in the inspector.")]
    [SerializeField]
    private float m_FallingThreshold = 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 m_SlideWhenOverSlopeLimit = 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 m_SlideOnTaggedObjects = false;
 
    [Tooltip("How fast the player slides when on slopes as defined above.")]
    [SerializeField]
    private float m_SlideSpeed = 12.0f;
 
    [Tooltip("If checked, then the player can change direction while in the air.")]
    [SerializeField]
    private bool m_AirControl = false;
 
    [Tooltip("Small amounts of this results in bumping when walking down slopes, but large amounts results in falling too fast.")]
    [SerializeField]
    private float m_AntiBumpFactor = .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 m_AntiBunnyHopFactor = 1;
 
    private Vector3 m_MoveDirection = Vector3.zero;
    private bool m_Grounded = false;
    private CharacterController m_Controller;
    private Transform m_Transform;
    private float m_Speed;
    private RaycastHit m_Hit;
    private float m_FallStartLevel;
    private bool m_Falling;
    private float m_SlideLimit;
    private float m_RayDistance;
    private Vector3 m_ContactPoint;
    private bool m_PlayerControl = false;
    private int m_JumpTimer;
 
 
    private void Start()
    {
        // Saving component references to improve performance.
        m_Transform = GetComponent<Transform>();
        m_Controller = GetComponent<CharacterController>();
 
        // Setting initial values.
        m_Speed = m_WalkSpeed;
        m_RayDistance = m_Controller.height * .5f + m_Controller.radius;
        m_SlideLimit = m_Controller.slopeLimit - .1f;
        m_JumpTimer = m_AntiBunnyHopFactor;
    }
 
 
    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 (m_ToggleRun && m_Grounded && Input.GetButtonDown("Run"))
        {
            m_Speed = (m_Speed == m_WalkSpeed ? m_RunSpeed : m_WalkSpeed);
        }
    }
 
 
    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 && m_LimitDiagonalSpeed) ? .7071f : 1.0f;
 
        if (m_Grounded)
        {
            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(m_Transform.position, -Vector3.up, out m_Hit, m_RayDistance))
            {
                if (Vector3.Angle(m_Hit.normal, Vector3.up) > m_SlideLimit)
                {
                    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(m_ContactPoint + Vector3.up, -Vector3.up, out m_Hit);
                if (Vector3.Angle(m_Hit.normal, Vector3.up) > m_SlideLimit)
                {
                    sliding = true;
                }
            }
 
            // If we were falling, and we fell a vertical distance greater than the threshold, run a falling damage routine
            if (m_Falling)
            {
                m_Falling = false;
                if (m_Transform.position.y < m_FallStartLevel - m_FallingThreshold)
                {
                    OnFell(m_FallStartLevel - m_Transform.position.y);
                }
            }
 
            // If running isn't on a toggle, then use the appropriate speed depending on whether the run button is down
            if (!m_ToggleRun)
            {
                m_Speed = Input.GetKey(KeyCode.LeftShift) ? m_RunSpeed : m_WalkSpeed;
            }
 
            // 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 && m_SlideWhenOverSlopeLimit) || (m_SlideOnTaggedObjects && m_Hit.collider.tag == "Slide"))
            {
                Vector3 hitNormal = m_Hit.normal;
                m_MoveDirection = new Vector3(hitNormal.x, -hitNormal.y, hitNormal.z);
                Vector3.OrthoNormalize(ref hitNormal, ref m_MoveDirection);
                m_MoveDirection *= m_SlideSpeed;
                m_PlayerControl = false;
            }
            // Otherwise recalculate moveDirection directly from axes, adding a bit of -y to avoid bumping down inclines
            else
            {
                m_MoveDirection = new Vector3(inputX * inputModifyFactor, -m_AntiBumpFactor, inputY * inputModifyFactor);
                m_MoveDirection = m_Transform.TransformDirection(m_MoveDirection) * m_Speed;
                m_PlayerControl = 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"))
            {
                m_JumpTimer++;
            }
            else if (m_JumpTimer >= m_AntiBunnyHopFactor)
            {
                m_MoveDirection.y = m_JumpSpeed;
                m_JumpTimer = 0;
            }
        }
        else
        {
            // If we stepped over a cliff or something, set the height at which we started falling
            if (!m_Falling)
            {
                m_Falling = true;
                m_FallStartLevel = m_Transform.position.y;
            }
 
            // If air control is allowed, check movement but don't touch the y component
            if (m_AirControl && m_PlayerControl)
            {
                m_MoveDirection.x = inputX * m_Speed * inputModifyFactor;
                m_MoveDirection.z = inputY * m_Speed * inputModifyFactor;
                m_MoveDirection = m_Transform.TransformDirection(m_MoveDirection);
            }
        }
 
        // Apply gravity
        m_MoveDirection.y -= m_Gravity * Time.deltaTime;
 
        // Move the controller, and set grounded true or false depending on whether we're standing on something
        m_Grounded = (m_Controller.Move(m_MoveDirection * Time.deltaTime) & CollisionFlags.Below) != 0;
    }
 
 
    // Store point that we're in contact with for use in FixedUpdate if needed
    private void OnControllerColliderHit(ControllerColliderHit hit)
    {
        m_ContactPoint = 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!");
    }
}
Personal tools
Namespaces

Variants
Actions
Navigation
Extras
Toolbox