- Home /
Rewriting a MonoBehaviour based action using a Finite State Machine
Hello, I am having a little difficulty trying to figure out how I can transform already written code (using Monobehaviour), which dictates a player action, to a Finite State Machine which uses States and pluggable Scriptable objects to perform the same action.
I have a marble, which is the player object, which has a Dash Ability. If it is in range of a Dash-Pad, a gamepad placed at the beginning of a pre-made Path, and the user presses SHIFT, the marble will dash along every waypoint in the Path, then return back to normal speed at the end. The code that I use is shown below
public class PathwayDash : MonoBehaviour
{
public SlowMotion slowmo;
private int pickup_length;
private int path_id = 0;
private int path_end = 0;
private int dash = 0; // start/end the dash
private int shift_press = 0; //when shift is pressed
private int entered = 0; //signifies when touching a dash pad
private float entry_speed;
private Rigidbody rb;
[Header("Pathing")]
public PickupConnect Path; // Attach to corres. path
public GameObject dashpad;
[HideInInspector]
public MarbleSmoothRoll marbleroll;
[HideInInspector]
//public MarbleUserControl marblecontrol;
[Header("Variables")]
public float rotation_speed = 5.0f;
public float reach_distance; //distance to start moving
//the minimum distance while traveling to a waypoint //the marble must be, before checking to go to next //waypoint
public float reach_min;
private float distance;
[Range(1f, 1000f)]
public float path_speed;
private Vector3 desired_velocity;
private Vector3 resultant_velocity;
private Vector3 current_velocity;
private Vector3 current_position;
private Vector3 local_vel;
// Use this for initialization
void Start ()
{
pickup_length = Path.pickups.Count - 1;
print("Pickup length is:" + pickup_length);
marbleroll = GetComponent<MarbleSmoothRoll>();
rb = GetComponent<Rigidbody>();
}
private void Update()
{
if (entered == 1) //it touched the dashpad
{
if (Input.GetKeyDown(KeyCode.LeftShift)) //shift engages the actual dash while on the dashpad
{
shift_press = 1;
dash = 1; //signify that the dash has started
marbleroll.enabled = false;
//marblecontrol.enabled = false;
entry_speed = rb.velocity.magnitude; //record the entry speed as the marble enters
slowmo.SlowMo();
}
else { }
}
else
{
shift_press = 0;
}
}
// Update is called once per frame
public void FixedUpdate ()
{
distance = Vector3.Distance(Path.pickups[path_id].position, transform.position); //Get distance
if (dash == 1) //if the dash has started
{
GoToNextPickup(); //move towards current set pickup of path_id
}
else { }
}
void GoToNextPickup()
{
//print("***TimeScale is at: " + Time.timeScale + "\n");
// print("the current velocity is: " + rb.velocity);
if (path_end == 1) //it is going towards last pickup
{
if(distance < reach_min)
{
dash = 0; //no longer dashing
print("Ending dash");
SetExitVelocity();
slowmo.SlowMo(); //call slow motion to toggle off slowmotion
}
else
{
VelocityChange();
}
}
else
{
VelocityChange(); //change velocity towards current pikcup
var rotation = Quaternion.LookRotation(Path.pickups[path_id].position - transform.position);
transform.rotation = Quaternion.Slerp(transform.rotation, rotation, Time.deltaTime * rotation_speed);
if (distance < reach_min) //If marble is now close to target pickup, make next pickup the target
{
path_id++; //used to identify next pickup in array
print("***Current path_id***: " + path_id + "\n");
if (path_id >= pickup_length)
{
print("End of the path with path_id:" + path_id);
path_end = 1; //signify end of path
}
}
}
}// End GoToNextPickup()
void SetExitVelocity() //used to find the vector of direction for exiting a path and then setting the velocity. Uses the positions of the last two pickups to determine vector.
{ //Exits with speed of entry velocity
distance = Vector3.Distance(Path.pickups[Path.pickups.Count - 1].position, Path.pickups[Path.pickups.Count - 2].position); //Get distance to target pickup
Vector3 direction = new Vector3(0,0,0);
direction = (Path.pickups[Path.pickups.Count-1].position) - (Path.pickups[Path.pickups.Count-2].position); //last two pickups make direction
direction = direction/distance;
//print("Direction:" + direction + "\n");
direction = direction * entry_speed ;
//direction.y = 0f;
//print("Entry speed: " + entry_speed);
//print("Setting Exit Velocity: " + direction + "\n");
//print("rb.velcoity is : " + rb.velocity + "\n");
resultant_velocity = direction - rb.velocity;
rb.AddForce(resultant_velocity, ForceMode.VelocityChange);
// print("Velocity of resultant is: " + resultant_velocity + "\n");
//print("Exit velocity actually is: " + rb.velocity + "\n");
path_id = 0;
path_end = 0;
} //End SetExitVelocity()
void VelocityChange() //finds vector to pickup, normalizes the vector then multiplies by a speed factor.
{
current_velocity = rb.velocity; //current velocity
print("Current velocity is: " + current_velocity + "\n");
desired_velocity = (((Path.pickups[path_id].position) - transform.position)/distance) * path_speed *
Time.unscaledDeltaTime/Time.timeScale; //wanted velocity, unscaledDeltaTime/timescale ensures same speed no matter the timescale
//desired_velocity.y = 0f; //don't want to go in y direction for now
print("Desired velocity is: " + desired_velocity + "\n");
resultant_velocity = desired_velocity - rb.velocity; //needed velocity to add to reach desired_velocity
//rb.velocity = desired_velocity;
rb.AddForce(resultant_velocity, ForceMode.VelocityChange); //change the velocity instantaneously
}//End Velocity Change
private void OnTriggerEnter(Collider other)
{
if (other.gameObject == dashpad) //if the marble is currently touching the dash pad
{
entered = 1;
//print("currently on Dashpad");
if (shift_press == 1) //only start if shift is pressed
{
print("Shift was pressed while on dashpad \n");
}
}
}
private void OnTriggerExit(Collider other)
{
if (other.gameObject == dashpad)
{
entered = 0;
}
}
}
It looks like too much, but it's pretty simple. While it isn't the end of the Dash, the code will continue to run GoToNextPickup(). In GoToNextPickup(), the function VelocityChange() simply changes the velocity to go towards the target pickup, if it gets close enough to that pickup, which is dictated by the variable reach_min, GoToNextPickup will increase the index of the pickup array so that the target pickup is now the next pickup.
Once it reaches the final pickup I have a SetExitVelocity code which will stop the dash and return back to the speed with which the marble entered the dash.
It may not be the cleanest way, but I've worked on it long enough to iron out any bug I encountered, and in the end it works well.
However, now I am trying to clean up the implementation of the Dash action. I've research Scriptable Objects and watched videos on creating Finite State Machine. I feel like this would be a very good fit to replace my original dash action. It will also give me the opportunity to easily create new actions. However I'm stuck on exactly how to transition this code over.
Following Richard Fine's example for Pluggable AI, I've created the following scripts
Dash Action Script
[CreateAssetMenu(menuName = "Pluggable/Actions/Dash")]
public class DashAction : Action
{
public override void Act(StateController controller)
{
}
private void Dash(StateController controller)
{
}
}
Action Script
public abstract class Action : ScriptableObject
{
public abstract void Act(StateController controller);
}
State Controller Script
public class StateController : MonoBehaviour
{
public State currentState;
private bool DashActive;
[HideInInspector] public Rigidbody Object; //Object that will be controlled
[HideInInspector] public List<Transform> wayPointList;
[HideInInspector] public int nextWayPoint;
private void Awake()
{
Object = GetComponent<Rigidbody>();
}
private void Update()
{
if (!DashActive) return;
currentState.UpdateState(this);
}
private void OnDrawGizmos()
{
if(currentState != null)
{
Gizmos.color = currentState.sceneGizmocolor;
Gizmos.DrawWireSphere(transform.position, 1f);
}
}
}
State Script
[CreateAssetMenu(menuName = "Pluggable/State")]
public class State : ScriptableObject
{
public Action[] actions;
public Color sceneGizmocolor = Color.grey;
public void UpdateState(StateController controller)
{
DoActions(controller);
}
public void DoActions(StateController controller)
{
for (int i= 0; i<actions.Length; i++)
{
actions[i].Act(controller);
}
}
}
I'm just trying to transfer the Dash action solely before adding in any transitions from state to state. I'm just a little confused how to even start rewritting the function in the DashAction scriptable object. I'm assuming that in the Dash() function of the Dash:Action script I should put my GoToNextPickup function, and the current State of my Marble will be the condition that decides if it continues dashing, but im still unsure of how to transfer the code and also unsure of how I'm supposed to implement the StateController contoller variable.
Any help or tips would be greatly appreciated