- Home /
Basic idle/walk Movement FSM as a beginner experiment
I'm trying to implement a very basic idle/walk FSM over the default FPSInputController. I've worked with FSM some year ago but i feel like my patterns won't apply in this context. Here's my approach: i've 2 main abstract classes like these
public abstract class AbstractFSM {
protected List<AbstractFSMState> states;
protected AbstractFSMState currentState;
public abstract void Setup();
public void Run(FSMInput input) {
this.currentState.Enter();
int next = this.currentState.Execute(input);
this.currentState.Exit();
this.currentState = this.states[next];
}
}
public abstract class AbstractFSMState {
public void Enter() {
Debug.Log("Entering state " + this.getName());
}
public void Exit() {
//
}
public int Execute(FSMInput input) {
this.Enter();
int next = this.ExecuteImpl(input);
this.Exit();
return next;
}
public abstract string getName();
public abstract void EnterImpl();
protected abstract int ExecuteImpl(FSMInput input);
public abstract void ExitImpl();
}
Then their implementations
public class FSMMovement : AbstractFSM {
public static int STATE_IDLE = 0;
public static int STATE_WALK = 1;
public override void Setup () {
Debug.Log("FSMMovement is being setup.");
this.states = new List<AbstractFSMState>();
this.states.Add(new StateIdle());
this.states.Add(new StateWalk());
this.currentState = this.states[0];
Debug.Log("Init state is " + this.currentState.getName());
}
}
// The ExecuteImpl in StateIdle, omitted the rest
protected override int ExecuteImpl(FSMInput input) {
Vector3 directionVector = (Vector3)input.Get("direction");
if (directionVector != Vector3.zero) {
return FSMMovement.STATE_WALK;
} else {
return FSMMovement.STATE_IDLE;
}
}
// The ExecuteImpl in StateWalk, omitted the rest. Basically,
// it's the same code from default FPSInputController - except
// for the audio lines.
protected override int ExecuteImpl(FSMInput input) {
Vector3 directionVector = (Vector3)input.Get("direction");
AudioSource audio = (AudioSource)input.Get("audio");
CharacterMotor motor = (CharacterMotor)input.Get("motor");
if (directionVector != Vector3.zero) {
if (!audio.isPlaying) {
audio.Play();
}
// Get the length of the directon vector and then normalize it
// Dividing by the length is cheaper than normalizing when we already have the length anyway
float directionLength = directionVector.magnitude;
directionVector = directionVector / directionLength;
// Make sure the length is no bigger than 1
directionLength = Mathf.Min(1.0f, directionLength);
// Make the input vector more sensitive towards the extremes and less sensitive in the middle
// This makes it easier to control slow speeds when using analog sticks
directionLength = directionLength * directionLength;
// Multiply the normalized direction vector by the modified length
directionVector = directionVector * directionLength;
} else {
audio.Stop();
}
// Apply the direction to the CharacterMotor
motor.inputMoveDirection = transform.rotation * directionVector;
motor.inputJump = Input.GetButton("Jump");
}
Finally, i've changed the FPSInputController Update() like this
void Update() {
// Get the input vector from kayboard or analog stick
Vector3 directionVector = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
// Collect input
FSMInput input = new FSMInput();
input.Add("direction", directionVector);
input.Add("audio", audio);
input.Add("motor", motor);
// Execute
this.fsm.Run(input);
return;
}
I built this code in about an hour and suddenly stopped because i spot many issues, one coming to light for first.
As you can see any state will need a lot of informations from the game context (the controller input, the audio source, the motor script, etc.). I've tried to skip over this adding a FSMInput class -a mere collection built on C# Dictionary- but there's a lot of overhead passing all these data anytime.
On the other hand i won't like hard-coding anything in a single script with tons of switch/case lines.
Can you suggest an approach that better fits in this scenario? Maybe passing the GameObject as the only input value? Or am i doing this completely wrong?
Thanks for reading!