We decided early on that the enemies should have really simple behaviours. For instance, here's an example of how the Shorg works on the newest version:
- Wait until Slinki comes close
- Appear and jump out once he's close enough
- Wait for 2 seconds
- Jump towards slinki a certain small distance
- Go to 3
The method
Create a new MonoBehaviour for your AI. It should look like this://this is like a template for all our states public delegate IEnumerator State(); //the current state private State stateFunc; //seconds between AI updates public float AIupdateFrequency = 0.1f; void OnEnable() { //Sets the first State stateFunc = StateWait; StartCoroutine (Action()); } public IEnumerator Action() { while (true) { //Waits untill stateFunc is done before proceeding yield return StartCoroutine(stateFunc()); //Waits for AIupdateFrequency so we don't busy-run our AI logic yield return new WaitForSeconds(AIupdateFrequency); } }
Here's an example of a possible state method. This one just makes Shorg stand still for 2 seconds before changing to the "Hop" state.
IEnumerator StateWait() { yield return new WaitForSeconds(2); //next state stateFunc = StateHop; yield return null; }
Here's another state method, this one applies gravity while Shorg is airborne. Once he lands he goes back to waiting.
IEnumerator StateAirborne() { kinController.jumpSpeed = -kinController.gravity * AIupdateFrequency; if (kinController.isGrounded) { stateFunc = StateWait; } yield return null; }
Guidelines
The "rules" for writting new states are simple: Following the delegate - they should be functions that return IEnumerator , they should end in yield return null, and they need to have one or more conditions for changing the stateFunc variable (i.e. change to another state).Summary
Advantages of this method:
- Simple
- Easy to create new states
- Very easy to debug just using prints
- Self destructs cleanly if the GameObject is destroyed or disabled
- Can get complicated as the AI gets more complex
- No way to enforce the guidelines due to the way Unity and C# work
- Coroutines can get a bit funky to use for an inexperienced programmer
- State is lost if the gameObject is temporarily disabled
Special thanks to David Craft for this amazing hack that allows code syntax highlighting on blogger
Note: If you need to temporarily run the AI logic more often than AIupdateFrequency will allow, all you need to do is yield inside a cycle. This probably sounds confusing so here's another version of the Wait state. In this one Shorg turns towards the player before waiting:
IEnumerator StateWait() { //Facing wrong way? while (Vector3.Dot(transform.forward, slinkiDirection.x * Vector3.right) < 0) { transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), Time.deltaTime * 100); Debug.Log("Turning around"); yield return null; } //Debug.Log("Waiting for 1 second"); yield return new WaitForSeconds(1); stateFunc = StateHop; }
That while cycle will make the coroutine run every frame update. Without it, Shorg would only try to face the player once every 0.1 seconds, producing a very jerky animation.