'How do I switch substates in a heirarchical state machine while maintaing the superstate/root state?
I am rewriting my player movement script to make use of hierarchical state machines (mostly to get the practice and learn how they work) as per this tutorial.
I am beginning to understand how the state machine works, but having some trouble making changes that I'd like to make. Specifically, I want to make the PlayerJumpState a substate that gets called if the PlayerAirborneState is switched to and the jump button was pressed. The problem is I can't seem to figure out a way to switch to the jump as a substate while maintaining the airborne state as the superstate.
I have a PlayerGroundedState which functions as it should - it initializes the necessary substates, and the player is switched to these substates when the right conditions are met, meanwhile, the superState they are in can be either grounded or airborne. The jump state I want to use should only be accessible by the airborne state, and it should maintain the airborne superstate when switched to.
Here's the PlayerGroundedState script:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerGroundedState : PlayerBaseState
{
public PlayerGroundedState(PlayerStateMachine currentContext, PlayerStateFactory playerStateFactory)
: base(currentContext, playerStateFactory)
{
IsRootState = true;
InitializeSubstate();
}
public override void EnterState()
{
Debug.Log("Grounded State Entered");
Ctx.CharacterController.stepOffset = Ctx.OrigStepOffset;
Ctx.Animator.SetBool("isGrounded", true);
Ctx.Animator.SetBool("isJumping", false);
Ctx.YSpeed = -1;
}
public override void UpdateState()
{
if (Ctx.IsJumpPressed)
{
Ctx.WasJumpPressed = true;
}
CheckSwitchStates();
}
public override void ExitState() { }
public override void InitializeSubstate()
{
if (!Ctx.IsMovementPressed)
{
SetSubState(Factory.Idle());
}
else if (Ctx.IsMovementPressed)
{
SetSubState(Factory.Walk());
}
}
public override void CheckSwitchStates()
{
if (Ctx.IsJumpPressed)
{
SwitchState(Factory.Airborne());
}
}
void OnAnimatorMove()
{
Vector3 velocity = Ctx.Animator.deltaPosition;
velocity.y = Ctx.YSpeed * Time.deltaTime;
Ctx.CharacterController.Move(velocity);
}
}
My PlayerAirborneState script is currently set up the same way as PlayerGroundedState, but obviously isn't even switching the substate like I was hoping. If I use the SwitchState function in CheckSwitchStates() it will switch to the jump state, but doing that does not maintain airborne as the superstate. I expect that some of this might have to do with the fact that in my PlayerStateMachine script, the grounded state is initialized as the first current state.
PlayerAirborneState:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerAirborneState : PlayerBaseState
{
public PlayerAirborneState(PlayerStateMachine currentContext, PlayerStateFactory playerStateFactory)
: base(currentContext, playerStateFactory)
{
IsRootState = true;
}
public override void EnterState()
{
Debug.Log("Airborne State Entered");
Ctx.Animator.SetBool("isJumping", true);
InitializeSubstate();
}
public override void UpdateState()
{
HandleAirMovement();
CheckSwitchStates();
}
public override void ExitState()
{
Debug.Log("Airborne State exited");
Ctx.Animator.SetBool("isJumping", false);
}
public override void InitializeSubstate()
{
if (Ctx.WasJumpPressed || Ctx.YSpeed > 0)
{
SetSubState(Factory.Jump());
}
else
{
SetSubState(Factory.Falling());
}
}
public override void CheckSwitchStates()
{
if (Time.time - Ctx.LastGroundedTime <= Ctx.JumpButtonGracePeriod)
{
SwitchState(Factory.Grounded());
}
//if (Ctx.WasJumpPressed || Ctx.YSpeed > 0)
//{
// SwitchState(Factory.Jump());
//}
//else
//{
// SwitchState(Factory.Falling());
//}
}
void HandleAirMovement()
{
Ctx.LastGroundedTime = null;
if (Ctx.WasJumpPressed)
{
Ctx.YSpeed = Ctx.JumpSpeed;
}
Vector3 airVelocity;
airVelocity = Ctx.BetterMoveVector.normalized * Ctx.InputMagnitude * Ctx.JumpHorizontalSpeed;
airVelocity.y = Ctx.YSpeed;
Ctx.CharacterController.Move(airVelocity * Time.deltaTime);
}
}
And my PlayerStateMachine script:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerStateMachine : MonoBehaviour
{
// input booleans (iHeartGameDev tutorial)
bool _isJumpPressed = false;
bool _isMovementPressed;
// constants (iHeartGameDev)
float _rotationFactorPerFrame = 15;
public ParticleSystem jetFire1;
public ParticleSystem jetFire2;
public PlayerInventory playerInventory;
[SerializeField] float _rotationSpeed;
[SerializeField] float _turnSmoothTime;
[SerializeField] float _jumpSpeed;
[SerializeField] float _jumpButtonGracePeriod;
[SerializeField] float _jetForce;
[SerializeField] float _jumpHorizontalSpeed;
[SerializeField] float _boostSpeed;
[SerializeField] float _boostTime;
[SerializeField] float _wallJumpBuffer;
[SerializeField] float _wallJumpForce;
[SerializeField] Transform _cameraTransform;
CharacterController _characterController;
Animator _animator;
CapsuleCollider _capsuleCollider;
float _ySpeed;
float _inputMagnitude;
float? _jumpButtonPressedTime;
float? _lastGroundedTime;
float _origStepOffset;
float _wallJumpStartTime;
bool _touchingWall;
bool _wallJumpStarted;
bool _raycastIsGrounded;
bool _wasJumpPressed;
Vector2 _moveVector2;
Vector3 _betterMoveVector;
Vector3 _moveDirection;
Vector3 _wallJumpNormal;
//Vector3 _airVelocity;
// state variables
PlayerBaseState _currentState;
PlayerStateFactory _states;
// getters and setters
public PlayerBaseState CurrentState { get { return _currentState; } set { _currentState = value; } }
public CharacterController CharacterController { get { return _characterController; } }
public Animator Animator { get { return _animator; } }
public CapsuleCollider CapsuleCollider { get { return _capsuleCollider; } }
public float JumpButtonGracePeriod { get { return _jumpButtonGracePeriod; } }
public float YSpeed { get { return _ySpeed; } set { _ySpeed = value; } }
public float JumpSpeed { get { return _jumpSpeed; } }
public float OrigStepOffset { get { return _origStepOffset; } }
public float InputMagnitude { get { return _inputMagnitude; } }
public float JumpHorizontalSpeed { get { return _jumpHorizontalSpeed; } }
public float? JumpButtonPressedTime { get { return _jumpButtonPressedTime; } set { _jumpButtonPressedTime = value; } }
public float? LastGroundedTime { get { return _lastGroundedTime; } set { _lastGroundedTime = value; } }
public bool IsJumpPressed { get { return _isJumpPressed; } }
public bool WasJumpPressed { get { return _wasJumpPressed; } set { _wasJumpPressed = value; } }
public bool IsMovementPressed { get { return _isMovementPressed; } }
public bool RaycastIsGrounded { get { return _raycastIsGrounded; } }
public Vector2 MoveVector2 { get { return _moveVector2; } set { _moveVector2 = value; } }
public Vector3 BetterMoveVector { get { return _betterMoveVector; } set { _betterMoveVector = value; } }
//public Vector3 AirVelocity { get { return _airVelocity; } set { _airVelocity = value; } }
private void Awake()
{
_characterController = GetComponent<CharacterController>();
_animator = GetComponent<Animator>();
_capsuleCollider = GetComponent<CapsuleCollider>();
_ySpeed = -1;
// setup state
_states = new PlayerStateFactory(this);
_currentState = _states.Grounded();
_currentState.EnterState();
}
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
_currentState.UpdateStates();
//_ySpeed += (Physics.gravity.y * Time.deltaTime);
RaycastGroundCheck();
_animator.SetFloat("Input Magnitude", _inputMagnitude, 0.05f, Time.deltaTime);
// set movement vector to inputs
_betterMoveVector.x = MoveVector2.x;
_betterMoveVector.z = MoveVector2.y;
HandleRotation();
}
void HandleRotation()
{
// move in direction of camera
_betterMoveVector = Quaternion.AngleAxis(_cameraTransform.rotation.eulerAngles.y, Vector3.up) * _betterMoveVector;
_betterMoveVector.Normalize();
// the current rotation of our character
Quaternion currentRotation = transform.rotation;
if (_isMovementPressed)
{
// creates a new rotation based on where the player is currently pressing
Quaternion targetRotation = Quaternion.LookRotation(_betterMoveVector, Vector3.up);
// rotate the character to face the positionToLookAt
transform.rotation = Quaternion.Slerp(currentRotation, targetRotation, _rotationFactorPerFrame * Time.deltaTime);
}
}
void RaycastGroundCheck()
{
float extraHeight = 0.1f;
if (Physics.Raycast(_capsuleCollider.bounds.center, Vector3.down, CapsuleCollider.bounds.extents.y + extraHeight))
{
_raycastIsGrounded = true;
}
else
{
_raycastIsGrounded = false;
}
}
//input functions
public void JumpFunction(InputAction.CallbackContext context)
{
_isJumpPressed = context.ReadValueAsButton();
_jumpButtonPressedTime = Time.time;
}
public void MoveFunction(InputAction.CallbackContext context)
{
if (context.performed || context.started)
{
Debug.Log("Movement!");
_moveVector2 = context.ReadValue<Vector2>();
//Debug.Log("_moveVector2: " + _moveVector2);
_inputMagnitude = _moveVector2.magnitude;
_isMovementPressed = _inputMagnitude != 0;
_moveVector2.Normalize();
}
else
{
Debug.Log("movement exited");
_moveVector2 = Vector2.zero;
_isMovementPressed = false;
}
}
public void JetUpFunction(InputAction.CallbackContext context)
{
if (context.performed)
{
Debug.Log("Jet Up performed");
}
}
public void JetBoostFunction(InputAction.CallbackContext context)
{
if (context.performed)
{
Debug.Log("Forward Boost performed");
}
}
public void DebugCurrentStates(InputAction.CallbackContext context)
{
if (context.performed)
{
Debug.Log("Current State: " + _currentState);
}
}
}
This is my first Unity project as well as my first question on SO, so apologies for my newbiness on both accounts.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|
