Production
The decision to implement a state machine proved pivotal for maintaining a modular and organized character state management system. This choice enhanced scalability, eased maintenance, and debugging compared to a large script.
Walk States
Jumping / Falling States
Climbing State
Biking State
Jetpack State
State Runner
To implement the state machine, I had to familiarize myself with abstract classes and how to inherit them in the character controller.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
namespace StateMachine
{
// StateRunner is a MonoBehaviour that controls the lifecycle and transitions of various states.
// This is a generic class that can handle any state for a given MonoBehaviour.
public abstract class StateRunner<T> : MonoBehaviour where T : MonoBehaviour
{
// List of available states for this runner.
[SerializeField]
private List<State<T>> _states;
// Dictionary mapping each state's type to its instance for fast access.
private readonly Dictionary<Type, State<T>> _stateByType = new();
// The currently active state this runner is in.
private State<T> _activeState;
// On Awake, we initialize our state dictionary and set the first state as the active state.
protected virtual void Awake()
{
// Populate the dictionary with each state's type as the key and the state itself as the value.
_states.ForEach(s => _stateByType.Add(s.GetType(), s));
// Set the initial state to the first state in our list.
SetState(_states[0].GetType());
}
// Method to transition to a new state using the state's type.
public void SetState(Type newStateType)
{
// If there's an active state, call its Exit method to handle any state-exit logic.
if (_activeState != null)
{
_activeState.Exit();
}
// Set the new state as the active state and initialize it.
_activeState = _stateByType[newStateType];
_activeState.Init(GetComponent<T>());
}
// Unity's Update method where we handle input and any update logic for the active state.
private void Update()
{
_activeState.CaptureInput();
_activeState.Update();
}
// Unity's FixedUpdate method where we potentially change states and handle any fixed update logic.
private void FixedUpdate()
{
_activeState.ChangeState();
_activeState.FixedUpdate();
}
}
}
State Runner that CharacterCtrl inherits from
using UnityEngine;
using UnityEngine.InputSystem;
namespace StateMachine
{
public class CharacterCtrl : StateRunner<CharacterCtrl>
{
[Header("Reference Other Scripts")]
public PlayerMovementBigActionMap PMBA;
public GameManager GM;
public InputHandler IH { get; private set; }
[SerializeField] AmIGrounded _AIG;
public IsFacingWall _IFW;
public Jumping _jumpingSC;
[Header("GameObjects")]
[SerializeField] private GameObject playerprefab;
[SerializeField] private GameObject ClimbingPrefab;
[Header("Components")]
[SerializeField] public Rigidbody playerRb;
[SerializeField] Animator playerAnim;
[SerializeField] Animator _JetPackAnim;
private PlayerInput playerinput;
[SerializeField] private CapsuleCollider playercollider;
[Header("Animation")]
int moveAnimationID;
int moveWithBagId;
public bool _isGrounded;
[Header("Floats")]
[SerializeField] private float onGroundMoveForceSlow;
[SerializeField] private float onGroundMoveForceNormal;
[SerializeField] private float OnGroundRun;
[SerializeField] private float _rotationSpeed;
[Header("Jumping")]
public float jumpForce = 10f;
public float maxJumpTime = 1f;
public float fallMultiplier = 2.5f;
public float jumpTimer = 0.2f;
public int maxJumps = 2;
public int jumpCount = 0;
public bool isJumpingPressed;
public bool jump;
[Header("Bike")]
[Header("Climbing")]
[SerializeField] public bool readyToClimb;
[SerializeField] private float climbspeed;
[SerializeField] private float wallDistanceOffset;
[SerializeField] private float sideRaycastOffset;
[Header("Transforms")]
[SerializeField] private Transform orientation;
[SerializeField] private Transform _thisObject;
[SerializeField] private Transform _cameraPlayer;
[SerializeField] private Transform groundCheck;
[Header("Vector3")]
//private Vector3 movement;
private Vector3 movementBike;
private Vector3 upDirection = Vector3.up;
[Header("Layers")]
[SerializeField] private LayerMask groundLayer;
[SerializeField] private LayerMask ClimbLayer;
[Header("Slope Handling")]
[SerializeField] private float maxSlopeAngle;
private RaycastHit slopeHit;
//Bike
[SerializeField] private TurnBikeWheels _tB;
[SerializeField] private Transform _bikeOrientation;
[SerializeField] private GameObject _bike;
[SerializeField] private GameObject _fork;
[SerializeField] private float _forkRotationSpeed;
[SerializeField] private float _bikespeed;
[SerializeField] private float _rotationSpeedBike;
[SerializeField] private float _minRotateValueSpeedBike;
[SerializeField] private float _maxRotateValueSpeedBike;
[SerializeField] private float _minWheelTurn;
[SerializeField] private float _maxWheelTurn;
[SerializeField] private float _minValueSpeedBike;
[SerializeField] private float _maxValueSpeedBike;
//JetPack
[SerializeField] private GameObject _JetPack;
// Hand And Feet Transforms
[Header("Transforms")]
[Header("Trackers Bike")]
[SerializeField] private Transform _leftHandBike, _rightHandBike, _leftFootBike, _rightFootBike;
[Header("trackers Hands and Feet")]
[SerializeField] private Transform _leftHand, _rightHand, _leftFoot, _rightFoot;
[Header("Trackers Climbing")]
[SerializeField] private Transform _lefthandClimb, _rightHandClimb, _leftFootClimb, _rightFootClimb;
// Getters And Setters
public Animator PlayerAnimator { get { return playerAnim; } set { playerAnim = value; } }
public Animator JetPackAnimator { get { return _JetPackAnim; } set { _JetPackAnim = value; } }
public CapsuleCollider PlayerCollider { get { return playercollider; } set { playercollider = value; } }
public Rigidbody PlayerRB { get { return playerRb; } set { playerRb = value; } }
public TurnBikeWheels TB { get { return _tB; } set { _tB = value; } }
//public GameManager GM { get { return _GM; } set { _GM = value; } }
public AmIGrounded AIG { get { return _AIG; } }
//Getters And Setters Bools
public bool ISGrounded { get { return _isGrounded; } }
// Getters And Setters GameObjects
public GameObject Bike { get { return _bike; } set { _bike = value; } }
public GameObject Fork { get { return _fork; } set { _fork = value; } }
public GameObject JetPack { get { return _JetPack; } set { _JetPack = value; } }
// Getters And Setters Floats
public float BikeSpeed { get { return _bikespeed; } set { _bikespeed = value; } }
public float ForkRotationSpeed { get { return _forkRotationSpeed; } set { _forkRotationSpeed = value; } }
public float RotationSpeedBike { get { return _rotationSpeedBike; } set { _rotationSpeedBike = value; } }
public float RotationSpeed { get { return _rotationSpeed; } }
public float MinRotateValueSpeedBike { get { return _minRotateValueSpeedBike; } }
public float MaxRotateValueSpeedBike { get { return _maxRotateValueSpeedBike; } }
public float MinWheelTurn { get { return _minWheelTurn; } }
public float MaxWheelTurn { get { return _maxWheelTurn; } }
public float MinValueSpeedBike { get { return _minValueSpeedBike; } }
public float MaxValueSpeedBike { get { return _maxValueSpeedBike; } }
public float OnGroundMoveForceNormal { get { return onGroundMoveForceNormal; } }
public float OnGroundMoveForceSlow { get { return onGroundMoveForceSlow; } }
public float OnGroundRunning { get { return OnGroundRun; } }
public float JumpTimer { get { return jumpTimer; } set { jumpTimer = value; } }
public float JumpForce { get { return jumpForce; } set { jumpForce = value; } }
public float MaxJumpTime { get { return maxJumpTime; } set { maxJumpTime = value; } }
public float FallMultiplier { get { return fallMultiplier; } set { fallMultiplier = value; } }
// Getters And Setters Animation
public int MoveAnimationID { get { return moveAnimationID; } set { moveAnimationID = value; } }
public int MoveWithBagID { get { return moveWithBagId; } set { moveWithBagId = value; } }
public int JumpCount { get { return jumpCount; } set { jumpCount = value; } }
public int MaxJumps { get { return maxJumps; } set { maxJumps = value; } }
// Getters And Setters Transforms General
public Transform CameraPlayer { get { return _cameraPlayer; } }
public Transform ThisObject { get { return _thisObject; } set { _thisObject = value; } }
public Transform BikeOrientation { get { return _bikeOrientation; } }
public Transform Orientation { get { return orientation; } }
//Getters and Setters Hand And Feet Transforms Bikes;
public Transform LeftHandBike { get { return _leftHandBike; } }
public Transform LeftFootBike { get { return _leftFootBike; } }
public Transform RightHandBike { get { return _rightHandBike; } }
public Transform RightFootBike { get { return _rightFootBike; } }
//Getters And Setters Hand And Feet Transforms
public Transform _LeftHand { get { return _leftHand; } set { _leftHand = value; } }
public Transform _LeftFoot { get { return _leftFoot; } set { _leftFoot = value; } }
public Transform _RightHand { get { return _rightHand; } set { _rightHand = value; } }
public Transform _RightFoot { get { return _rightFoot; } set { _rightHand = value; } }
protected override void Awake()
{
base.Awake();
IH = GetComponent<InputHandler>();
moveAnimationID = Animator.StringToHash("Move");
moveWithBagId = Animator.StringToHash("moveWithBag");
_IFW = GetComponent<IsFacingWall>();
_jumpingSC = GetComponent<Jumping>();
GM = FindObjectOfType<GameManager>();
}
}
}
Character Controller
Scriptable Objects
I used scriptable objects for the states, organizing them in a list. This approach allowed for real-time adjustments during playtesting, making the development process smoother.
using UnityEngine;
using UnityEngine.InputSystem;
namespace StateMachine
{
// The abstract class State is a ScriptableObject that can represent a state for a MonoBehaviour.
// It's generic, which means it can be used for any MonoBehaviour (indicated by the <T>).
public abstract class State<T> : ScriptableObject where T : MonoBehaviour
{
// _runner is a reference to the MonoBehaviour that is running this state.
protected T _runner;
// Initialize the state with a reference to its parent MonoBehaviour.
public virtual void Init(T parent)
{
_runner = parent;
}
// Abstract method to capture any required input.
// Subclasses must provide an implementation for this.
public abstract void CaptureInput();
// Abstract method for update logic.
// Subclasses must provide an implementation for this.
public abstract void Update();
// Abstract method for fixed update logic.
// Subclasses must provide an implementation for this.
public abstract void FixedUpdate();
// Abstract method to change to another state.
// Subclasses must provide an implementation for this.
public abstract void ChangeState();
// Abstract method for logic when exiting the state.
// Subclasses must provide an implementation for this.
public abstract void Exit();
}
}
State class all states inherits from
using UnityEngine;
using UnityEngine.InputSystem;
namespace StateMachine
{
[CreateAssetMenu(menuName = "States/Player/Vehicle/Bike")]
public class BikeState : State<CharacterCtrl>
{
// References to various components and managers
CharacterCtrl _parent;
IsFacingWall _IFW;
AmIGrounded _AIG;
GameManager _GM;
//collider
private CapsuleCollider _playercollider;
// Animator
private Animator _playerAnim;
//Generic Transforms
private Transform _bikeOrientation;
private Transform _cameraPlayer;
private Transform _thisObject;
//Rigidbody off the player
private Rigidbody _playerRB;
//Wheel script
private TurnBikeWheels _TB;
// GameObjects
public GameObject _bike;
public GameObject _fork;
// Transforms on bike for hands and feet
private Transform _leftFootBike;
private Transform _rightFootBike;
private Transform _leftHandBike;
private Transform _rightHandBike;
//Transforms on player Hands And Feet
private Transform _leftHand;
private Transform _leftFoot;
private Transform _rightHand;
private Transform _rightFoot;
//Vectors
private Vector2 _inputVectorOnBike;
private Vector2 _inputVectorOnBikeSpeed;
private Vector2 _bikeMove;
private Vector3 _movementBike;
private Vector2 previousJoystickValue;
// Floats
private float _bikeSpeed;
private float _forkRotationSpeed;
private float _rotationSpeedBike;
private float _rotationSpeed;
private float _minRotateValueSpeedBike;
private float _maxRotateValueSpeedBike;
private float _minWheelTurn;
private float _maxWheelTurn;
private float _minValueSpeedBike;
private float _maxValueSpeedBike;
[SerializeField] private float RunAccelRate;
[SerializeField] private float RunDecelRate;
[SerializeField] private float _timeChangeV;
[SerializeField] private float _timeLeft;
//Bools
private bool _changeVehicle;
public override void Init(CharacterCtrl parent)
{
// Initialization logic when the state is first created
base.Init(parent);
_parent = parent;
_playercollider = parent.PlayerCollider;
_playerAnim = parent.PlayerAnimator;
_playerRB = parent.PlayerRB;
_TB = parent.TB;
_bikeOrientation = parent.BikeOrientation;
_cameraPlayer = parent.CameraPlayer;
_thisObject = parent.ThisObject;
_bike = parent.Bike;
_fork = parent.Fork;
_leftFootBike = parent.LeftFootBike;
_rightFootBike = parent.RightFootBike;
_leftHandBike = parent.LeftHandBike;
_rightHandBike = parent.RightHandBike;
_leftFoot = parent._LeftFoot;
_rightFoot = parent._RightFoot;
_leftHand = parent._LeftHand;
_rightHand = parent._RightHand;
_timeLeft = _timeChangeV;
_bikeSpeed = parent.BikeSpeed;
_forkRotationSpeed = parent.ForkRotationSpeed;
_rotationSpeedBike = parent.RotationSpeedBike;
_rotationSpeed = parent.RotationSpeed;
_minRotateValueSpeedBike = parent.MinRotateValueSpeedBike;
_maxRotateValueSpeedBike = parent.MaxRotateValueSpeedBike;
_minWheelTurn = parent.MinWheelTurn;
_maxWheelTurn = parent.MaxWheelTurn;
_minValueSpeedBike = parent.MinValueSpeedBike;
_maxValueSpeedBike = parent.MaxValueSpeedBike;
}
public override void CaptureInput()
{
}
public override void Update()
{
}
public override void FixedUpdate()
{
// Physics-based update logic
_changeVehicle = _parent.IH.SwitchVehicle1;
// setting the Inputvector of the movement
_inputVectorOnBike = _parent.IH.InputVectorOnBike;
_inputVectorOnBikeSpeed = _parent.IH.InputVectorOnBikeSpeed;
_timeLeft -= Time.deltaTime;
_AIG = _parent.AIG;
_GM = _parent.GM;
//Setting the animation for the bike state
_playerAnim.SetBool("OnBike", true);
// Assigning different body parts off the player to the bike
_leftFoot.transform.position = _leftFootBike.transform.position;
_rightFoot.transform.position = _rightFootBike.transform.position;
_leftHand.transform.position = _leftHandBike.transform.position;
_rightHand.transform.position = _rightHandBike.transform.position;
// turning on the Bike GameObject
_bike.SetActive(true);
// Turning off the Collider on the Player
_playercollider.enabled = false;
//Bike Moves
if (_inputVectorOnBike.magnitude >= .1f)
{
_IFW = _parent._IFW;
float forward = _inputVectorOnBike.y * _bikeSpeed;
float right = _inputVectorOnBike.x * _bikeSpeed;
Vector3 targetSpeed = (!_IFW._isFacingWall() ? _bikeOrientation.forward : Vector3.zero) * forward + _bikeOrientation.right * right;
Vector3 velocity = _playerRB.velocity;
velocity.y = 0;
Vector3 speedDiff = targetSpeed - velocity;
float accelRate = (Mathf.Abs(targetSpeed.magnitude) >= 0.5f) ? RunAccelRate : RunDecelRate;
Vector3 movement = speedDiff * accelRate;
_playerRB.AddForce(movement, ForceMode.Force);
//Rotates bike
Quaternion targetRotation = _thisObject.transform.rotation;
_thisObject.transform.rotation = targetRotation;
float targetAngle = Mathf.Atan2(movement.x, movement.z) * Mathf.Rad2Deg;
targetRotation = Quaternion.Euler(0, targetAngle, 0);
_thisObject.transform.rotation = Quaternion.Slerp(_thisObject.transform.rotation, targetRotation, _rotationSpeedBike * Time.deltaTime);
//Rotates fork of bike
float forkTargetAngle = Mathf.Atan2(movement.x, movement.z) * Mathf.Rad2Deg;
Quaternion forkTargetRotation = Quaternion.Euler(-90, forkTargetAngle - 180, 0);
_fork.transform.rotation = Quaternion.Slerp(_fork.transform.rotation, forkTargetRotation, _forkRotationSpeed * Time.deltaTime);
}
//controlls speed of bike and rotations on pedals and wheels
Vector2 joystickvalue = _inputVectorOnBikeSpeed;
// Calculate the rotation angle based on input
float rotationAngle = (joystickvalue.x + previousJoystickValue.x) * _rotationSpeed * Time.deltaTime;
float rotationAngleBike = (joystickvalue.x + previousJoystickValue.x) * _rotationSpeed * Time.deltaTime;
float rotationAngleTurnWheels = (joystickvalue.x + previousJoystickValue.x) * _rotationSpeed * Time.deltaTime;
// Update the current value based on the rotation angle
_bikeSpeed += rotationAngle;
_rotationSpeedBike += rotationAngleBike;
_TB.RotationSpeed += rotationAngleTurnWheels;
// Clamp the current value within the specified range
_bikeSpeed = Mathf.Clamp(_bikeSpeed, _minValueSpeedBike, _maxValueSpeedBike);
_rotationSpeedBike = Mathf.Clamp(_rotationSpeedBike, _minRotateValueSpeedBike, _maxRotateValueSpeedBike);
_TB.RotationSpeed = Mathf.Clamp(_TB.RotationSpeed, _minWheelTurn, _maxWheelTurn);
// Update the previous joystick value for the next frame
previousJoystickValue = joystickvalue;
}
public override void ChangeState()
{
if (_changeVehicle && !_GM._hasJetPack && _timeLeft <= 0f)
{
_runner.SetState(typeof(IdleState));
}
if (_changeVehicle && _GM._hasJetPack && _timeLeft <= 0f)
{
_runner.SetState(typeof(JetPackState));
}
if (_GM._CamIsActive)
{
_runner.SetState(typeof(PauseState));
}
}
public override void Exit()
{
}
}
}
Bike state