Implementing State Manager in Unity
In this blog post, we delve into our state manager's workings. While it may not be perfect, it's efficient enough for our current needs, allowing us to focus on other crucial aspects of development. For those interested in the evolution and learning process of our state manager, check out this blog post.
Today, we're focusing on our stack-based state machine implementation and the technical details behind it. We've covered the basic concepts in a previous post, and we suggest you get familiar with it for better context.
How does it work?
Each state must implement the BaseState
abstract class, to provide unique behaviours for lifecycle events like Start()
, Pause()
, Resume()
, and Finish()
. Start and Resume return an IEnumerator
, enabling coroutine creation if necessary. This feature allows simple initialization as well as actions with time-dependent effects. The State Manager is responsible for invoking the appropriate methods for each managed state.
Base State class
In our case, the BaseState is a standard abstract class. The Start()
and Resume()
functions return an IEnumerator
, allowing the state to execute over time using coroutines if necessary. We'll provide specific examples later in the post.
State Manager
Here is our current State Manager implementation:
Example States
Let's explore some of our existing states in more detail.
Stunned State
To represent a stunned character, we designed a class as shown below. A noteworthy feature here is the handling of stun effect pausing and resuming - it's not about removing the stun effect, but transitioning from the running to the paused state.
public class StunnedState : BaseState
{
public float Duration { get; private set; }
public float StartTime { get; private set; }
// private readonly Character character; // Reference to the character object
public StunnedState(Character character, float duration)
{
Duration = duration;
// this.character = character; // We do not really use it here in sample code
}
public override IEnumerator Start()
{
StartTime = Time.time;
yield return new WaitForSeconds(Duration);
Finish();
}
public override void Pause()
{
// Nothing really
}
public override IEnumerator Resume()
{
yield return new WaitForSeconds(Duration - (Time.time - StartTime));
Finish(); // Signals that the state is done and should be removed
}
}
Hero Moving State
This state represents the player's character's "default" state. It's primarily about moving and responding to inputs. An interesting aspect here is the use of coroutine and yield return new WaitForFixedUpdate()
to mimic the MonoBehaviour's standard FixedUpdate()
method behavior.
[Serializable]
public class HeroMovingStateStats
{
[SerializeField]
private float moveSpeed = 5f;
public float MoveSpeed => moveSpeed;
[SerializeField]
private float maxRotationAngle = 420f;
public float MaxRotationAngle => maxRotationAngle;
}
public abstract class HeroMovingState : BaseState
{
// Common Components
protected readonly CharacterController characterController;
protected readonly PlayerInputBuffer inputBuffer;
protected readonly HeroMovingStateStats stats;
protected HeroMovingState(HeroCharacter character, HeroMovingStateStats stats)
{
characterController = character.GetComponent<CharacterController>();
inputBuffer = character.InputBuffer;
this.stats = stats;
}
protected IEnumerator Update()
{
while (true)
{
yield return new WaitForFixedUpdate();
MoveAndRotate(inputBuffer.MoveDirection);
}
}
// TODO: Add a gradual speed up and slow down on the start and end of the movement
protected virtual void MoveAndRotate(Vector3 moveDirection)
{
// Move the player character controller
characterController.SimpleMove(moveDirection * stats.MoveSpeed);
// Rotate the player
if (moveDirection != Vector3.zero)
{
Quaternion targetRotation = Quaternion.LookRotation(moveDirection);
var transform = characterController.transform;
if (Vector3.Angle(transform.forward, moveDirection) > 90)
{
transform.rotation = Quaternion.LookRotation(moveDirection);
}
transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRotation, stats.MaxRotationAngle * Time.deltaTime);
}
}
public override IEnumerator Start()
{
return Update();
}
public override void Pause() { }
public override IEnumerator Resume()
{
return Update();
}
}
Summary
Understanding the workings of our state manager and the different states helps streamline our development process. While our state manager may not be perfect, it's efficient, allowing us to focus on other critical aspects of development. Stay tuned for more insights and updates as we continue to evolve our tools and processes.