Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

A Basic First Person Character Controller for prototyping

Discussion in 'Scripting' started by AlucardJay, Sep 13, 2021.

  1. AlucardJay

    AlucardJay

    Joined:
    May 28, 2012
    Posts:
    328


    Gist Link

    After some interest in an old post here , I spent the weekend frankensteining snippets of code to make a single script FPCC that could be used like the FPCC from Unity 4~5, including the crouch and run from Aldo, and I also added farcry sliding. There is a Menu Item function to easily create and configure a new FPCC object. Otherwise very easy to set up, it only requires a layer to exclude itself from sphere/raycasts. There is a check to see if there is enough headroom to stand from crouching (it hasn't pushed through the floor yet...). It also has the basic function to affect non-kinematic rigidbodies.

    Made in Unity 2018.4, it's been a long time since I even opened Unity, so I don't know what the standard assets are like these days, or what else is out there. Let me know what you think!

    Smooth Look
    Jump
    Crouch
    Run
    Slide
    Slip down steep angles
    Ground Check
    Ceiling Check

    Setup : the menu item does everything below, except for creating the player layer. By default (for easy use in new projects), it assigns the Ignore Raycast layer.

    The Player gameobject needs it's own layer, then the Culling Mask has to be assigned to ignore that layer, so the raycasts can detect the world and not the player.

    • Create an Empty GameObject. Name it Player
    • Create and assign a Layer for the Player gameobject. Click on the button next to Layer > Add Layer... > Type Player in an empty user layer. Click on the Player gameobject and confirm the Layer is assigned as Player
    • Attach the BasicFPCC script. This will also add the CharacterController component. Change the Center variable on the CharacterController component to X 0, Y 1, Z 0
    • Drag the Main Camera as a child of the Player gameobject. Set the Transform Position to X 0, Y 1.7, Z 0 (Rotation 0, Scale 1)
    • (optional player graphic object) Create a cylinder as a child of the Player gameobject. Remove the collider component. Set the Transform Position to X 0, Y 1, Z 0 (Rotation 0, Scale 1)
    • Select the Player gameobject. In the Inspector, drag in the Camera (and GFX if created). Under Grounded Settings, set Casting Mask to Everything, then uncheck the Player layer (it then shows mixed, you can click to confirm)
    • Under Debug, check the box to show where the grounded spherecast starts and ends (starts inside the base of the capsule and then up for the default skin width, set with the groundCheckY variable). When in play mode it also shows a blue line as the slope direction, and a green line as the player gravity and when slipping down steep slopes. The Slope Limit variable is in the CharacterController component.
    F to slide, while running (Left Shift)
    Toggle lock cursor with the ` console key [~]

    Gist Link

    Code (csharp):
    1. // ------------------------------------------
    2. // BasicFPCC.cs
    3. // a basic first person character controller
    4. // with jump, crouch, run, slide
    5. // 2020-10-04 Alucard Jay Kay
    6. // ------------------------------------------
    7.  
    8. // source :
    9. // https://forum.unity.com/threads/a-basic-first-person-character-controller-for-prototyping.1169491/
    10. // Brackeys FPS controller base :
    11. // https://www.youtube.com/watch?v=_QajrabyTJc
    12. // smooth mouse look :
    13. // https://forum.unity.com/threads/need-help-smoothing-out-my-mouse-look-solved.543416/#post-3583643
    14. // ground check : (added isGrounded)
    15. // https://gist.github.com/jawinn/f466b237c0cdc5f92d96
    16. // run, crouch, slide : (added check for headroom before un-crouching)
    17. // https://answers.unity.com/questions/374157/character-controller-slide-action-script.html
    18. // interact with rigidbodies :
    19. // https://docs.unity3d.com/2018.4/Documentation/ScriptReference/CharacterController.OnControllerColliderHit.html
    20.  
    21. // ** SETUP **
    22. // Assign the BasicFPCC object to its own Layer
    23. // Assign the Layer Mask to ignore the BasicFPCC object Layer
    24. // CharacterController (component) : Center => X 0, Y 1, Z 0
    25. // Main Camera (as child) : Transform : Position => X 0, Y 1.7, Z 0
    26. // (optional GFX) Capsule primitive without collider (as child) : Transform : Position => X 0, Y 1, Z 0
    27. // alternatively :
    28. // at the end of this script is a Menu Item function to create and auto-configure a BasicFPCC object
    29. // GameObject -> 3D Object -> BasicFPCC
    30.  
    31. using System.Collections;
    32. using System.Collections.Generic;
    33. using UnityEngine;
    34.  
    35. #if UNITY_EDITOR // only required if using the Menu Item function at the end of this script
    36. using UnityEditor;
    37. #endif
    38.  
    39. [RequireComponent(typeof(CharacterController))]
    40. public class BasicFPCC : MonoBehaviour
    41. {
    42.     [Header("Layer Mask")]
    43.     [Tooltip("Layer Mask for sphere/raycasts. Assign the Player object to a Layer, then Ignore that layer here.")]
    44.     public LayerMask castingMask;                              // Layer mask for casts. You'll want to ignore the player.
    45.  
    46.     // - Components -
    47.     private CharacterController controller;                    // CharacterController component
    48.     private Transform playerTx;                                // this player object
    49.  
    50.     [Header("Main Camera")]
    51.     [Tooltip("Drag the FPC Camera here")]
    52.     public Transform cameraTx;                                 // Main Camera, as child of BasicFPCC object
    53.  
    54.     [Header("Optional Player Graphic")]
    55.     [Tooltip("optional capsule to visualize player in scene view")]
    56.     public Transform playerGFX;                                // optional capsule graphic object
    57.    
    58.     [Header("Inputs")]
    59.     [Tooltip("Disable if sending inputs from an external script")]
    60.     public bool useLocalInputs = true;
    61.     [Space(5)]
    62.     public string axisLookHorzizontal = "Mouse X";             // Mouse to Look
    63.     public string axisLookVertical    = "Mouse Y";             //
    64.     public string axisMoveHorzizontal = "Horizontal";          // WASD to Move
    65.     public string axisMoveVertical    = "Vertical";            //
    66.     public KeyCode keyRun             = KeyCode.LeftShift;     // Left Shift to Run
    67.     public KeyCode keyCrouch          = KeyCode.LeftControl;   // Left Control to Crouch
    68.     public KeyCode keyJump            = KeyCode.Space;         // Space to Jump
    69.     public KeyCode keySlide           = KeyCode.F;             // F to Slide (only when running)
    70.     public KeyCode keyToggleCursor    = KeyCode.BackQuote;     // ` to toggle lock cursor (aka [~] console key)
    71.  
    72.     // Input Variables that can be assigned externally
    73.     // the cursor can also be manually locked or freed by calling the public void SetLockCursor( bool doLock )
    74.     [HideInInspector] public float inputLookX        = 0;      //
    75.     [HideInInspector] public float inputLookY        = 0;      //
    76.     [HideInInspector] public float inputMoveX        = 0;      // range -1f to +1f
    77.     [HideInInspector] public float inputMoveY        = 0;      // range -1f to +1f
    78.     [HideInInspector] public bool inputKeyRun        = false;  // is key Held
    79.     [HideInInspector] public bool inputKeyCrouch     = false;  // is key Held
    80.     [HideInInspector] public bool inputKeyDownJump   = false;  // is key Pressed
    81.     [HideInInspector] public bool inputKeyDownSlide  = false;  // is key Pressed
    82.     [HideInInspector] public bool inputKeyDownCursor = false;  // is key Pressed
    83.    
    84.     [Header("Look Settings")]
    85.     public float mouseSensitivityX = 2f;             // speed factor of look X
    86.     public float mouseSensitivityY = 2f;             // speed factor of look Y
    87.     [Tooltip("larger values for less filtering, more responsiveness")]
    88.     public float mouseSnappiness = 20f;              // default was 10f; larger values of this cause less filtering, more responsiveness
    89.     public bool invertLookY = false;                 // toggle invert look Y
    90.     public float clampLookY = 90f;                   // maximum look up/down angle
    91.    
    92.     [Header("Move Settings")]
    93.     public float crouchSpeed = 3f;                   // crouching movement speed
    94.     public float walkSpeed = 7f;                     // regular movement speed
    95.     public float runSpeed = 12f;                     // run movement speed
    96.     public float slideSpeed = 14f;                   // slide movement speed
    97.     public float slideDuration = 2.2f;               // duration of slide
    98.     public float gravity = -9.81f;                   // gravity / fall rate
    99.     public float jumpHeight = 2.5f;                  // jump height
    100.  
    101.     [Header("Grounded Settings")]
    102.     [Tooltip("The starting position of the isGrounded spherecast. Set to the sphereCastRadius plus the CC Skin Width. Enable showGizmos to visualize.")]
    103.     // this should be just above the base of the cc, in the amount of the skin width (in case the cc sinks in)
    104.     //public float startDistanceFromBottom = 0.2f;
    105.     public float groundCheckY = 0.33f;               // 0.25 + 0.08 (sphereCastRadius + CC skin width)
    106.     [Tooltip("The position of the ceiling checksphere. Set to the height minus sphereCastRadius plus the CC Skin Width. Enable showGizmos to visualize.")]
    107.     // this should extend above the cc (by approx skin width) so player can still move when not at full height (not crouching, trying to stand up),
    108.     // otherwise if it's below the top then the cc gets stuck
    109.     public float ceilingCheckY = 1.83f;              // 2.00 - 0.25 + 0.08 (height - sphereCastRadius + CC skin width)
    110.     [Space(5)]
    111.     public float sphereCastRadius = 0.25f;           // radius of area to detect for ground
    112.     public float sphereCastDistance = 0.75f;         // How far spherecast moves down from origin point
    113.     [Space(5)]
    114.     public float raycastLength = 0.75f;              // secondary raycasts (match to sphereCastDistance)
    115.     public Vector3 rayOriginOffset1 = new Vector3(-0.2f, 0f, 0.16f);
    116.     public Vector3 rayOriginOffset2 = new Vector3(0.2f, 0f, -0.16f);
    117.  
    118.     [Header("Debug Gizmos")]
    119.     [Tooltip("Show debug gizmos and lines")]
    120.     public bool showGizmos = false;                  // Show debug gizmos and lines
    121.  
    122.     // - private reference variables -
    123.     private float defaultHeight = 0;                 // reference to scale player crouch
    124.     private float cameraStartY = 0;                  // reference to move camera with crouch
    125.  
    126.     [Header("- reference variables -")]
    127.     public float xRotation = 0f;                     // the up/down angle the player is looking
    128.     private float lastSpeed = 0;                     // reference for calculating speed
    129.     private Vector3 fauxGravity = Vector3.zero;      // calculated gravity
    130.     private float accMouseX = 0;                     // reference for mouse look smoothing
    131.     private float accMouseY = 0;                     // reference for mouse look smoothing
    132.     private Vector3 lastPos = Vector3.zero;          // reference for player velocity
    133.     [Space(5)]
    134.     public bool isGrounded = false;
    135.     public float groundSlopeAngle = 0f;              // Angle of the slope in degrees
    136.     public Vector3 groundSlopeDir = Vector3.zero;    // The calculated slope as a vector
    137.     private float groundOffsetY = 0;                 // calculated offset relative to height
    138.     public bool isSlipping = false;
    139.     [Space(5)]
    140.     public bool isSliding = false;
    141.     public float slideTimer = 0;                     // current slide duration
    142.     public Vector3 slideForward = Vector3.zero;      // direction of the slide
    143.     [Space(5)]
    144.     public bool isCeiling = false;
    145.     private float ceilingOffsetY = 0;                // calculated offset relative to height
    146.     [Space(5)]
    147.     public bool cursorActive = false;                // cursor state
    148.  
    149.    
    150.     void Start()
    151.     {
    152.         Initialize();
    153.     }
    154.  
    155.     void Update()
    156.     {
    157.         ProcessInputs();
    158.         ProcessLook();
    159.         ProcessMovement();
    160.     }
    161.    
    162.     void Initialize()
    163.     {
    164.         if ( !cameraTx ) { Debug.LogError( "* " + gameObject.name + ": BasicFPCC has NO CAMERA ASSIGNED in the Inspector *" ); }
    165.        
    166.         controller = GetComponent< CharacterController >();
    167.        
    168.         playerTx = transform;
    169.         defaultHeight = controller.height;
    170.         lastSpeed = 0;
    171.         fauxGravity = Vector3.up * gravity;
    172.         lastPos = playerTx.position;
    173.         cameraStartY = cameraTx.localPosition.y;
    174.         groundOffsetY = groundCheckY;
    175.         ceilingOffsetY = ceilingCheckY;
    176.  
    177.         RefreshCursor();
    178.     }
    179.  
    180.     void ProcessInputs()
    181.     {
    182.         if ( useLocalInputs )
    183.         {
    184.             inputLookX = Input.GetAxis( axisLookHorzizontal );
    185.             inputLookY = Input.GetAxis( axisLookVertical );
    186.  
    187.             inputMoveX = Input.GetAxis( axisMoveHorzizontal );
    188.             inputMoveY = Input.GetAxis( axisMoveVertical );
    189.  
    190.             inputKeyRun        = Input.GetKey( keyRun );
    191.             inputKeyCrouch     = Input.GetKey( keyCrouch );
    192.  
    193.             inputKeyDownJump   = Input.GetKeyDown( keyJump );
    194.             inputKeyDownSlide  = Input.GetKeyDown( keySlide );
    195.             inputKeyDownCursor = Input.GetKeyDown( keyToggleCursor );
    196.         }
    197.  
    198.         if ( inputKeyDownCursor )
    199.         {
    200.             ToggleLockCursor();
    201.         }
    202.     }
    203.  
    204.     void ProcessLook()
    205.     {
    206.         accMouseX = Mathf.Lerp( accMouseX, inputLookX, mouseSnappiness * Time.deltaTime );
    207.         accMouseY = Mathf.Lerp( accMouseY, inputLookY, mouseSnappiness * Time.deltaTime );
    208.  
    209.         float mouseX = accMouseX * mouseSensitivityX * 100f * Time.deltaTime;
    210.         float mouseY = accMouseY * mouseSensitivityY * 100f * Time.deltaTime;
    211.  
    212.         // rotate camera X
    213.         xRotation += ( invertLookY == true ? mouseY : -mouseY );
    214.         xRotation = Mathf.Clamp( xRotation, -clampLookY, clampLookY );
    215.  
    216.         cameraTx.localRotation = Quaternion.Euler( xRotation, 0f, 0f );
    217.        
    218.         // rotate player Y
    219.         playerTx.Rotate( Vector3.up * mouseX );
    220.     }
    221.  
    222.     void ProcessMovement()
    223.     {
    224.         // - variables -
    225.         float vScale = 1f; // for calculating GFX scale (optional)
    226.         float h = defaultHeight;
    227.         float nextSpeed = walkSpeed;
    228.         Vector3 calc; // used for calculations
    229.         Vector3 move; // direction calculation
    230.        
    231.         // player current speed
    232.         float currSpeed = ( playerTx.position - lastPos ).magnitude / Time.deltaTime;
    233.         currSpeed = ( currSpeed < 0 ? 0 - currSpeed : currSpeed ); // abs value
    234.  
    235.         // - Check if Grounded -
    236.         GroundCheck();
    237.  
    238.         isSlipping = ( groundSlopeAngle > controller.slopeLimit ? true : false );
    239.  
    240.         // - Check Ceiling above for Head Room -
    241.         CeilingCheck();
    242.  
    243.         // - Run and Crouch -
    244.  
    245.         // if grounded, and not stuck on ceiling
    246.         if ( isGrounded && !isCeiling && inputKeyRun )
    247.         {
    248.             nextSpeed = runSpeed; // to run speed
    249.         }
    250.  
    251.         if ( inputKeyCrouch ) // crouch
    252.         {
    253.             vScale = 0.5f;
    254.             h = 0.5f * defaultHeight;
    255.             nextSpeed = crouchSpeed; // slow down when crouching
    256.         }  
    257.  
    258.         // - Slide -
    259.  
    260.         // if not sliding, and not stuck on ceiling, and is running
    261.         if ( !isSliding && !isCeiling && inputKeyRun && inputKeyDownSlide ) // slide
    262.         {
    263.             // check velocity is faster than walkSpeed
    264.             if ( currSpeed > walkSpeed )
    265.             {
    266.                 slideTimer = 0; // start slide timer
    267.                 isSliding = true;
    268.                 slideForward = ( playerTx.position - lastPos ).normalized;
    269.             }
    270.         }
    271.         lastPos = playerTx.position; // update reference
    272.  
    273.         // check slider timer and velocity
    274.         if ( isSliding )
    275.         {
    276.             nextSpeed = currSpeed; // default to current speed
    277.             move = slideForward; // set input to direction of slide
    278.  
    279.             slideTimer += Time.deltaTime; // slide timer
    280.            
    281.             // if timer max, or isSliding and not moving, then stop sliding
    282.             if ( slideTimer > slideDuration || currSpeed < crouchSpeed )
    283.             {
    284.                 isSliding = false;
    285.             }
    286.             else // confirmed player is sliding
    287.             {
    288.                 vScale = 0.5f;            // gfx scale
    289.                 h = 0.5f * defaultHeight; // height is crouch height
    290.                 nextSpeed = slideSpeed;   // to slide speed
    291.             }
    292.         }
    293.         else // - Player Move Input -
    294.         {
    295.             move = ( playerTx.right * inputMoveX ) + ( playerTx.forward * inputMoveY );
    296.  
    297.             if ( move.magnitude > 1f )
    298.             {
    299.                 move = move.normalized;
    300.             }
    301.         }
    302.  
    303.         // - Height -
    304.  
    305.         // crouch/stand up smoothly
    306.         float lastHeight = controller.height;
    307.         float nextHeight = Mathf.Lerp( controller.height, h, 5f * Time.deltaTime );
    308.  
    309.         // if crouching, or only stand if there is no ceiling
    310.         if ( nextHeight < lastHeight || !isCeiling )
    311.         {
    312.             controller.height = Mathf.Lerp( controller.height, h, 5f * Time.deltaTime );
    313.  
    314.             // fix vertical position
    315.             calc = playerTx.position;
    316.             calc.y += ( controller.height - lastHeight ) / 2f;
    317.             playerTx.position = calc;
    318.  
    319.             // offset camera
    320.             calc = cameraTx.localPosition;
    321.             calc.y = ( controller.height / defaultHeight ) + cameraStartY - ( defaultHeight * 0.5f );
    322.             cameraTx.localPosition = calc;
    323.  
    324.             // calculate offset
    325.             float heightFactor = ( defaultHeight - controller.height ) * 0.5f;
    326.            
    327.             // offset ground check
    328.             groundOffsetY = heightFactor + groundCheckY;
    329.            
    330.             // offset ceiling check
    331.             ceilingOffsetY = heightFactor + controller.height - ( defaultHeight - ceilingCheckY );
    332.  
    333.             // scale gfx (optional)
    334.             if ( playerGFX )
    335.             {
    336.                 calc = playerGFX.localScale;
    337.                 calc.y = Mathf.Lerp( calc.y, vScale, 5f * Time.deltaTime );
    338.                 playerGFX.localScale = calc;
    339.             }
    340.         }
    341.  
    342.         // - Slipping Jumping Gravity -
    343.  
    344.         // smooth speed
    345.         float speed;
    346.        
    347.         if ( isGrounded )
    348.         {
    349.             if ( isSlipping ) // slip down slope
    350.             {
    351.                 // movement left/right while slipping down
    352.                 // player rotation to slope
    353.                 Vector3 slopeRight = Quaternion.LookRotation( Vector3.right ) * groundSlopeDir;
    354.                 float dot = Vector3.Dot( slopeRight, playerTx.right );
    355.                 // move on X axis, with Y rotation relative to slopeDir
    356.                 move = slopeRight * ( dot > 0 ? inputMoveX : -inputMoveX );
    357.  
    358.                 // speed
    359.                 nextSpeed = Mathf.Lerp( currSpeed, runSpeed, 5f * Time.deltaTime );
    360.  
    361.                 // increase angular gravity
    362.                 float mag = fauxGravity.magnitude;
    363.                 calc = Vector3.Slerp( fauxGravity, groundSlopeDir * runSpeed, 4f * Time.deltaTime );
    364.                 fauxGravity = calc.normalized * mag;
    365.             }
    366.             else
    367.             {
    368.                 // reset angular fauxGravity movement
    369.                 fauxGravity.x = 0;
    370.                 fauxGravity.z = 0;
    371.  
    372.                 if ( fauxGravity.y < 0 ) // constant grounded gravity
    373.                 {
    374.                     //fauxGravity.y = -1f;
    375.                     fauxGravity.y = Mathf.Lerp( fauxGravity.y, -1f, 4f * Time.deltaTime );
    376.                 }
    377.             }
    378.  
    379.             // - Jump -
    380.             if ( !isSliding && !isCeiling && inputKeyDownJump ) // jump
    381.             {
    382.                 fauxGravity.y = Mathf.Sqrt( jumpHeight * -2f * gravity );
    383.             }
    384.  
    385.             // --
    386.  
    387.             // - smooth speed -
    388.             // take less time to slow down, more time speed up
    389.             float lerpFactor = ( lastSpeed > nextSpeed ? 4f : 2f );
    390.             speed = Mathf.Lerp( lastSpeed, nextSpeed, lerpFactor * Time.deltaTime );
    391.         }
    392.         else // no friction, speed changes slower
    393.         {
    394.             speed = Mathf.Lerp( lastSpeed, nextSpeed, 0.125f * Time.deltaTime );
    395.         }
    396.  
    397.         // prevent floating if jumping into a ceiling
    398.         if ( isCeiling )
    399.         {
    400.             speed = crouchSpeed; // clamp speed to crouched
    401.  
    402.             if ( fauxGravity.y > 0 )
    403.             {
    404.                 fauxGravity.y = -1f; // 0;
    405.             }
    406.         }
    407.  
    408.         lastSpeed = speed; // update reference
    409.  
    410.         // - Add Gravity -
    411.  
    412.         fauxGravity.y += gravity * Time.deltaTime;
    413.        
    414.         // - Move -
    415.  
    416.         calc = move * speed * Time.deltaTime;
    417.         calc += fauxGravity * Time.deltaTime;
    418.  
    419.         controller.Move( calc );
    420.  
    421.         // - DEBUG -
    422.        
    423.         #if UNITY_EDITOR
    424.         // slope angle and fauxGravity debug info
    425.         if ( showGizmos )
    426.         {
    427.             calc = playerTx.position;
    428.             calc.y += groundOffsetY;
    429.             Debug.DrawRay( calc, groundSlopeDir.normalized * 5f, Color.blue );
    430.             Debug.DrawRay( calc, fauxGravity, Color.green );
    431.         }
    432.         #endif
    433.     }
    434.  
    435.     // lock/hide or show/unlock cursor
    436.     public void SetLockCursor( bool doLock )
    437.     {
    438.         cursorActive = doLock;
    439.         RefreshCursor();
    440.     }
    441.  
    442.     void ToggleLockCursor()
    443.     {
    444.         cursorActive = !cursorActive;
    445.         RefreshCursor();
    446.     }
    447.  
    448.     void RefreshCursor()
    449.     {
    450.         if ( !cursorActive && Cursor.lockState != CursorLockMode.Locked )   { Cursor.lockState = CursorLockMode.Locked; }
    451.         if (  cursorActive && Cursor.lockState != CursorLockMode.None   )   { Cursor.lockState = CursorLockMode.None;   }
    452.     }
    453.    
    454.     // check the area above, for standing from crouch
    455.     void CeilingCheck()
    456.     {
    457.         Vector3 origin = new Vector3( playerTx.position.x, playerTx.position.y + ceilingOffsetY, playerTx.position.z );
    458.  
    459.         isCeiling = Physics.CheckSphere( origin, sphereCastRadius, castingMask );
    460.     }
    461.    
    462.     // find if isGrounded, slope angle and directional vector
    463.     void GroundCheck()
    464.     {
    465.         //Vector3 origin = new Vector3( transform.position.x, transform.position.y - (controller.height / 2) + startDistanceFromBottom, transform.position.z );
    466.         Vector3 origin = new Vector3( playerTx.position.x, playerTx.position.y + groundOffsetY, playerTx.position.z );
    467.  
    468.         // Out hit point from our cast(s)
    469.         RaycastHit hit;
    470.  
    471.         // SPHERECAST
    472.         // "Casts a sphere along a ray and returns detailed information on what was hit."
    473.         if (Physics.SphereCast(origin, sphereCastRadius, Vector3.down, out hit, sphereCastDistance, castingMask))
    474.         {
    475.             // Angle of our slope (between these two vectors).
    476.             // A hit normal is at a 90 degree angle from the surface that is collided with (at the point of collision).
    477.             // e.g. On a flat surface, both vectors are facing straight up, so the angle is 0.
    478.             groundSlopeAngle = Vector3.Angle(hit.normal, Vector3.up);
    479.  
    480.             // Find the vector that represents our slope as well.
    481.             //  temp: basically, finds vector moving across hit surface
    482.             Vector3 temp = Vector3.Cross(hit.normal, Vector3.down);
    483.             //  Now use this vector and the hit normal, to find the other vector moving up and down the hit surface
    484.             groundSlopeDir = Vector3.Cross(temp, hit.normal);
    485.  
    486.             // --
    487.             isGrounded = true;
    488.         }
    489.         else
    490.         {
    491.             isGrounded = false;
    492.         }   // --
    493.  
    494.         // Now that's all fine and dandy, but on edges, corners, etc, we get angle values that we don't want.
    495.         // To correct for this, let's do some raycasts. You could do more raycasts, and check for more
    496.         // edge cases here. There are lots of situations that could pop up, so test and see what gives you trouble.
    497.         RaycastHit slopeHit1;
    498.         RaycastHit slopeHit2;
    499.  
    500.         // FIRST RAYCAST
    501.         if (Physics.Raycast(origin + rayOriginOffset1, Vector3.down, out slopeHit1, raycastLength))
    502.         {
    503.             // Debug line to first hit point
    504.             #if UNITY_EDITOR
    505.             if (showGizmos) { Debug.DrawLine(origin + rayOriginOffset1, slopeHit1.point, Color.red); }
    506.             #endif
    507.             // Get angle of slope on hit normal
    508.             float angleOne = Vector3.Angle(slopeHit1.normal, Vector3.up);
    509.  
    510.             // 2ND RAYCAST
    511.             if (Physics.Raycast(origin + rayOriginOffset2, Vector3.down, out slopeHit2, raycastLength))
    512.             {
    513.                 // Debug line to second hit point
    514.                 #if UNITY_EDITOR
    515.                 if (showGizmos) { Debug.DrawLine(origin + rayOriginOffset2, slopeHit2.point, Color.red); }
    516.                 #endif
    517.                 // Get angle of slope of these two hit points.
    518.                 float angleTwo = Vector3.Angle(slopeHit2.normal, Vector3.up);
    519.                 // 3 collision points: Take the MEDIAN by sorting array and grabbing middle.
    520.                 float[] tempArray = new float[] { groundSlopeAngle, angleOne, angleTwo };
    521.                 System.Array.Sort(tempArray);
    522.                 groundSlopeAngle = tempArray[1];
    523.             }
    524.             else
    525.             {
    526.                 // 2 collision points (sphere and first raycast): AVERAGE the two
    527.                 float average = (groundSlopeAngle + angleOne) / 2;
    528.                 groundSlopeAngle = average;
    529.             }
    530.         }
    531.     }
    532.    
    533.     // this script pushes all rigidbodies that the character touches
    534.     void OnControllerColliderHit( ControllerColliderHit hit )
    535.     {
    536.         Rigidbody body = hit.collider.attachedRigidbody;
    537.  
    538.         // no rigidbody
    539.         if ( body == null || body.isKinematic )
    540.         {
    541.             return;
    542.         }
    543.  
    544.         // We dont want to push objects below us
    545.         if ( hit.moveDirection.y < -0.3f )
    546.         {
    547.             return;
    548.         }
    549.  
    550.         // If you know how fast your character is trying to move,
    551.         // then you can also multiply the push velocity by that.
    552.         body.velocity = hit.moveDirection * lastSpeed;
    553.     }
    554.  
    555.     // Debug Gizmos
    556.     #if UNITY_EDITOR
    557.     void OnDrawGizmosSelected()
    558.     {
    559.         if ( showGizmos )
    560.         {
    561.             if ( !Application.isPlaying )
    562.             {
    563.                 groundOffsetY = groundCheckY;
    564.                 ceilingOffsetY = ceilingCheckY;
    565.             }
    566.  
    567.             Vector3 startPoint = new Vector3( transform.position.x, transform.position.y + groundOffsetY, transform.position.z );
    568.             Vector3 endPoint = startPoint + new Vector3( 0, -sphereCastDistance, 0 );
    569.             Vector3 ceilingPoint = new Vector3( transform.position.x, transform.position.y + ceilingOffsetY, transform.position.z );
    570.  
    571.             Gizmos.color = ( isGrounded == true ? Color.green : Color.white );
    572.             Gizmos.DrawWireSphere( startPoint, sphereCastRadius );
    573.  
    574.             Gizmos.color = Color.gray;
    575.             Gizmos.DrawWireSphere( endPoint, sphereCastRadius );
    576.  
    577.             Gizmos.DrawLine( startPoint, endPoint );
    578.  
    579.             Gizmos.color = ( isCeiling == true ? Color.red : Color.white );
    580.             Gizmos.DrawWireSphere( ceilingPoint, sphereCastRadius );
    581.         }
    582.     }
    583.     #endif
    584. }
    585.  
    586.  
    587. // =======================================================================================================================================
    588.  
    589. // ** DELETE from here down, if menu item and auto configuration is NOT Required **
    590.  
    591. // this section adds create BasicFPCC object to the menu : New -> GameObject -> 3D Object
    592. // then configures the gameobject
    593. // demo layer used : Ignore Raycast
    594. // also finds the main camera, attaches and sets position
    595. // and creates capsule gfx object (for visual while editing)
    596.  
    597. // A using clause must precede all other elements defined in the namespace except extern alias declarations
    598. //#if UNITY_EDITOR
    599. //using UnityEditor;
    600. //#endif
    601.  
    602. public class BasicFPCC_Setup : MonoBehaviour
    603. {
    604.     #if UNITY_EDITOR
    605.  
    606.     private static int playerLayer = 2; // default to the Ignore Raycast Layer (to demonstrate configuration)
    607.  
    608.     [MenuItem("GameObject/3D Object/BasicFPCC", false, 0)]
    609.     public static void CreateBasicFPCC()
    610.     {
    611.         GameObject go = new GameObject( "Player" );
    612.  
    613.         CharacterController controller = go.AddComponent< CharacterController >();
    614.         controller.center = new Vector3( 0, 1, 0 );
    615.  
    616.         BasicFPCC basicFPCC = go.AddComponent< BasicFPCC >();
    617.  
    618.         // Layer Mask
    619.         go.layer = playerLayer;
    620.         basicFPCC.castingMask = ~(1 << playerLayer);
    621.         Debug.LogError( "** SET the LAYER of the PLAYER Object, and the LAYERMASK of the BasicFPCC castingMask **" );
    622.         Debug.LogWarning(
    623.             "Assign the BasicFPCC Player object to its own Layer, then assign the Layer Mask to ignore the BasicFPCC Player object Layer. Currently using layer "
    624.             + playerLayer.ToString() + ": " + LayerMask.LayerToName( playerLayer )
    625.         );
    626.  
    627.         // Main Camera
    628.         GameObject mainCamObject = GameObject.Find( "Main Camera" );
    629.         if ( mainCamObject )
    630.         {
    631.             mainCamObject.transform.parent = go.transform;
    632.             mainCamObject.transform.localPosition = new Vector3( 0, 1.7f, 0 );
    633.             mainCamObject.transform.localRotation = Quaternion.identity;
    634.  
    635.             basicFPCC.cameraTx = mainCamObject.transform;
    636.         }
    637.         else // create example camera
    638.         {
    639.             Debug.LogError( "** Main Camera NOT FOUND ** \nA new Camera has been created and assigned. Please replace this with the Main Camera (and associated AudioListener)." );
    640.  
    641.             GameObject camGo = new GameObject( "BasicFPCC Camera" );
    642.             camGo.AddComponent< Camera >();
    643.            
    644.             camGo.transform.parent = go.transform;
    645.             camGo.transform.localPosition = new Vector3( 0, 1.7f, 0 );
    646.             camGo.transform.localRotation = Quaternion.identity;
    647.  
    648.             basicFPCC.cameraTx = camGo.transform;
    649.         }
    650.  
    651.         // GFX
    652.         GameObject gfx = GameObject.CreatePrimitive( PrimitiveType.Capsule );
    653.         Collider cc = gfx.GetComponent< Collider >();
    654.         DestroyImmediate( cc );
    655.         gfx.transform.parent = go.transform;
    656.         gfx.transform.localPosition = new Vector3( 0, 1, 0 );
    657.         gfx.name = "GFX";
    658.         gfx.layer = playerLayer;
    659.         basicFPCC.playerGFX = gfx.transform;
    660.     }
    661.     #endif
    662. }
    663.  
    664. // =======================================================================================================================================
     
    Last edited: Oct 8, 2021
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,519
    Seems pretty legit! I like your setup menu item too... nice touch. The whole point of this is no-muss no-fuss and this is definitely so.

    Two suggestions for the menu item:

    - complain bitterly if there is no camera, or even create one afresh and stick it in place

    - make a stronger call to action for setting up the custom Player layer: I saw it set up Ignore Raycast so I just left it at that and my jumps didn't work. I didn't even notice the nice yellow warning until the second or third try.

    Finally, I know it's just nitpicking, but I am actually able to crouch/uncrouch while in the air, and that seems ... wrong. Although technically IRL there's nothing to keep you from crouching midair... hm.

    BUT! As a consequence of crouching midair, I was able to fall through a 1m-thick X/Z-stretched cube floor ONCE by uncrouching right as I landed. I tried it like a zillion more times and it never happened again, but there you go, repro rate of 1 / zillion... that's better than most Windows software . :)
     
    AlucardJay likes this.
  3. WerewoofPrime

    WerewoofPrime

    Joined:
    Mar 16, 2020
    Posts:
    3
    this is AMAZING! this is just the thing I needed! thank u so much. :D
    however I did notice that I cannot seem to jump and sprinting allows me to slide but does not make me go any faster, and when I stop moving my character will slowly slide in one direction. I am not sure if this is me not setting something up right or the script. I love it tho everything else is amazing and buttery smooth!
     
  4. AlucardJay

    AlucardJay

    Joined:
    May 28, 2012
    Posts:
    328
    Thanks, hopefully we can get it working properly for you. I'm not sure about the stationary sliding, this should only happen on slopes that are steeper than the value in the CharacterController component. Try the latest script in the first post, make sure you place it in the scene at least 0.251 units above the ground so it initializes properly (until I can work it out!), and post your progress.

    Thank you for the feedback. And your mouse smoothing made a huge improvement to the Brackeys controller.

    The initial script has been updated, changes are :
    Exposed the Inputs in the inspector.
    Set the capsule gfx layer also, just in case.
    Changed the layer message to an error and warning message, to hopefully attract attention.
    Added an error message for the Main Camera not found, then creates example camera.

    The jump crouch was left intentionally, after many years of using this Source engine mechanic in Team Fortress 2 :)

    I'm currently working through some of the logic, so the initial post will get a few edits e.g just fixed sliding or sprinting while wedged under a ceiling, now adding a velocity check so the player has to be moving before sliding.

    More testing is definitely still required, to try and find any cases of falling through the ground.
    I'm hoping most can be fixed by raising the groundCheckY and increasing sphere & raycast distance.

    I found it strange that it wouldn't work using the Ignore Raycast layer, because that's pretty much the same as ignoring the layer it's assigned anyway. So I did some testing.

    There was a strange problem. In a new scene, creating a cube floor, then menu-creating the object, it wouldn't jump until it crouched first.
    The bool flags showed it was not grounded, but it was hitting a ceiling?? Crouching fixes it.

    I found the minimum height from ground start value where it would work to be 0.251, slightly greater than the spherecast radius(possibly just coincidence). So there is definitely something strange I cannot work out.

    The fixes seem to be position start +0.251 in the air, or to crouch when first entering play mode.

    Thanks again for the testing and feedback !

    Edit:
    Added velocity check to sprint, now object must be moving faster than walkSpeed for slide to work.
    isCeiling gizmo changes colour.
    Fix fauxGravity to stop the object floating when jumping into a ceiling.

    There is a behaviour I cannot figure out. When crouching, then jumping into a ceiling: first two times the object falls normally, after that the object floats every jump, until crouch is released? Some strange combination the isCeiling check fails, is this associated with the initialization fail if the object is grounded?
     
    Last edited: Sep 14, 2021
  5. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,519
    I was fiddling around a bit with it and I think perhaps the issue is starting the spherecast downwards with part of the sphere already in the collider. That's why 0.251 is required, as the default spherecast radius is 0.25.

    Here was my test: breakpointed the spherecast code, it was spherecasting from (0,0,0) downwards, and was missing the ground, which was at y == 0.
     
  6. AlucardJay

    AlucardJay

    Joined:
    May 28, 2012
    Posts:
    328
    Thank you so much for testing and helping Kurt, I found the problem, your feedback about the breakpoint cast starting at 0,0,0 is what clued me in. Problem was a simple one, in both ceiling and ground OffsetY variables.

    groundCheckY is what the user set, but the script used groundOffsetY. First problem, I hadn't initially assigned groundOffsetY.
    Second problem, I was hiding this error in the gizmos with : groundOffsetY == 0 ? groundCheckY : groundOffsetY ...
    which I originally wrote because of groundOffsetY not being assigned when not in play mode.

    The fix was to (of course!) assign them at start; and in the gizmos, set the ground/ceiling OffsetY if not playing.
    Both ground and ceiling gizmos are now correctly positioned with the casts, and change colour based on state.

    The character now seems to work as expected when first entering play mode, regardless if it is placed on the ground or in the air. (the original post is updated with the latest script).

    Only problem I see currently (which for the moment I'm classing as a feature), is the crouch-jump into a ceiling. Still for me, if the player remains crouched, the first 2 jumps into a ceiling are normal, then after that the isCeiling doesn't detect and the player 'floats' with the fauxGravity being set to jump (and not reset by isCeiling). I shall update as I learn more. Thank you for all the help, much appreciated, am very happy with this project result!

    Edit: cleaned up into functions, and exposed the inputs so they can be controlled by an external script. I'll leave it alone now :D until further feedback, or if I find anything else.
     
    Last edited: Sep 15, 2021
    Kurt-Dekker likes this.
  7. Hi_ImTemmy

    Hi_ImTemmy

    Joined:
    Jul 8, 2015
    Posts:
    174
    Great stuff. I find the camera incredibly sluggish to move and rotate using the mouse though. Even if I crank up the mouse sensitivity it gets to the point where rotating left and right is quick, but up and down is super slow.
     
  8. AlucardJay

    AlucardJay

    Joined:
    May 28, 2012
    Posts:
    328
    I was unable to replicate the problem. I tried a different mouse (Logitech M185 1k DPI) and increased the mouseSensitivity to 350, and it worked as expected, the camera moved the same speed as with my normal 3.5k DPI mouse.

    My guess is you have another script on the camera, that is why the camera moves at different speeds for each axis, and increasing the mouseSensitivity only affects one axis. Check the Main Camera for any old scripts that are still attached.
     
  9. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,519
    Are you playing on super-wide? I found this if I had like 2:1 aspect ratio or greater... I think it is basically because the mouse sensitivity is a scalar that applies to both X and Y equally, but affects them as a percentage or something. I didn't dig too deep.

    You could just dupe that field, make the first apply to horizontal, the second apply to vertical, and then tweak them independently.

    PS - I also found 100 waaaay slow, so I jacked mine to 500 :)
     
  10. AlucardJay

    AlucardJay

    Joined:
    May 28, 2012
    Posts:
    328
    That's interesting, thanks. Only having one monitor, I never experienced this behaviour. I shall do some tests for different widescreen aspects later today, and if it is a factor then make some changes, possibly even auto-scale the mouse X based on the widescreen scale. (I'd rather not split the sensitivity unless it's unavoidable).
    I'll also multiply the mouse sensitivity by 100 internally, so the sensitivity variable seems more relative (e.g. 1= 1x speed, 5 = 5x speed). But I'll wait for Temmys reply with any new information before editing the post.
     
  11. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,519
    I would opt away from this complexity: if you DO change it, make it 2 values; some people really want varying H/V sensitivity, especially based on game types.

    In other news, I'm now linking people to this BasicFPCC thread when they ask about issues with the Unity CC example code.

    ALSO: have you considered throwing the above in a public gist on github? It makes it a LOT easier to edit and also to copy down.

    If you don't have that set up, with your permission i will happily host it for you over on my gists:

    https://gist.github.com/kurtdekker
     
  12. AlucardJay

    AlucardJay

    Joined:
    May 28, 2012
    Posts:
    328
    Makes sense, I shall take your advice, and expose everything for the user.

    And wow, thank you for linking to this thread, made my day to hear! Am very happy with the idea this script could help others. Yes, it's about time I utilized github :D Thanks for the offer, but I'll create a gist later today when I get home, as I have other scripts that still get comments (terrain tools, proc gen).
     
    Kurt-Dekker likes this.
  13. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,519
    Please do... as you steadily build stuff up it feels pretty cool to be able to solve folks easy problems with tiny examples.
     
  14. Hi_ImTemmy

    Hi_ImTemmy

    Joined:
    Jul 8, 2015
    Posts:
    174
    I'm using a monitor running 2560 x 1440 at 144Hz but yeah, I find myself cranking the mouse sensitivity up to 400 - 500. I'm using this mouse: https://steelseries.com/gaming-mice/rival-600

    There are no other scripts on the camera. It's a blank project setup using the steps in the original post.
     
  15. Hi_ImTemmy

    Hi_ImTemmy

    Joined:
    Jul 8, 2015
    Posts:
    174
    P.S. @AlucardJay I'd like to congratulate you on a setup that returns grounded = true going down slopes. This makes me very happy.
     
    Kurt-Dekker likes this.
  16. AlucardJay

    AlucardJay

    Joined:
    May 28, 2012
    Posts:
    328
    Thanks! That's due to using a spherecast rather than the cc collider. Some cases might need tweaking of the sphereCastDistance and raycastLength.

    Thanks again to Kurt for identifying the mouse scalar vs different aspect ratios. I modified the script with separate mouse X and Y sensitivity variables, and scaled them to use more reasonable values (between 1 and 10 should cover all cases).

    The gist is up now too. Thanks to everyone for all the feedback.
     
    Last edited: Sep 22, 2021
    Kurt-Dekker likes this.
  17. AlucardJay

    AlucardJay

    Joined:
    May 28, 2012
    Posts:
    328
    Big Update :
    • Fixed the initial fall gravity bug. isSlipping calculation was missing a Time.deltaTime factor.
    • Added smoothing to speed when changing states. No more sudden change in motion when pressing sprint, crouch, jump; in air velocity maintained.
    Original post and gist are updated (version date 2020-09-22).
     
    Kurt-Dekker likes this.
  18. AlucardJay

    AlucardJay

    Joined:
    May 28, 2012
    Posts:
    328
    Update 2021-09-27

    • added a check, if the player isSliding but is not actually moving (stopped by an object), then cancel isSliding (override slide timer)
    • fixed the movement speed, where if the player cannot stand up (isCeiling) but is not pressing the crouch key, can only move at crouch speed
    • added another lerp, to smooth fauxGravity when becoming grounded
    • rearranged the inspector, to prioritize the layer mask and main camera
    • made a short video to show the setup and functionality
    There should be no more major updates for a while; after a couple of weeks of testing, that's all the major quirks I've found while working on some add-ons... ;)

    Edit: Update 2021-10-04 - changed the behaviour when isSlipping. Player can now move on the local X while slipping down. Player can now jump while slipping. (changed to Tomb Raider mechanics)
     

    Attached Files:

    Last edited: Oct 8, 2021
    Kurt-Dekker likes this.
  19. ramehnii

    ramehnii

    Joined:
    Mar 5, 2022
    Posts:
    4
    Hey! I have a query about editing the code, i was wondering if anyone would have information on how to add wall running. If anyone knows how to add it to the code and/or has a code that works with it, please let me know, i would greatly appreciate it.
     
  20. TheDuke4

    TheDuke4

    Joined:
    Dec 23, 2022
    Posts:
    1
    How do i disable the Crouch Jump Functoin?
     
  21. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,519
    Do you mean inhibit jump input when you are crouched?

    It will look something like:

    - gather jump intent
    - if crouched then clear that intent
    - act on jump intent

    How you do it will affect the ultimate feel.

    The simplest solution would be to replace this line:

    Code (csharp):
    1.             inputKeyDownJump   = Input.GetKeyDown( keyJump );
    with these lines:

    Code (csharp):
    1.             inputKeyDownJump = false;
    2.             // only allow jump if NOT holding crouch
    3.             if (!inputKeyCrouch)
    4.             {
    5.                 inputKeyDownJump   = Input.GetKeyDown( keyJump );
    6.             }
     
  22. midnight622

    midnight622

    Joined:
    Dec 20, 2021
    Posts:
    6
    how do you change it so when you're pushing W you're actually going forward not backwards?