Here's a short writeup on how we're doing the enemy AI for our most recent game, Slinki. I recommend you read up on
C# delegates and
Unity coroutines if you're not familiar with either.
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
Things like these are really easy to implement using easy AI techniques such as state machines.
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
Disadvantages
- 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.