Monday 9 September 2013

Simple AI in Unity3D

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:
  1. Wait until Slinki comes close
  2. Appear and jump out once he's close enough
  3. Wait for 2 seconds
  4. Jump towards slinki a certain small distance
  5. 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.