From an earlier post: I love animals, especially doggos. I’d like to come up with ways to make intelligent animals, methods I can use across Unity games. An animal stack, if you will.
I think I have a viable candidate for that now. I may be wrong, but it seems to work so far.
The stack
Here’s what I came up with.
At the bottom are an animator, a rigid body, a nav mesh agent, and other standard components. The other layers use these components.
The layer above that controls the animal’s body movement, like running, walking, crouching, and so on. For me, this is the most complex layer. To make an animal run from one place to another, it needs to start moving its limbs slowly, speed them up, slow down as it nears the destination, and stop. It has to turn realistically, not just rotate on the Y axis. That means bending the spine, turning the head, getting the limbs to start, with the limbs on the outside of the turn moving a little faster than the limbs on the inside. When the animal is facing the right way, unbend the spine, rotate the head back, and even up the stride. This has to work for direction changes of 10º, 220º, whatevs. Oh, and change the animal’s pitch and roll as it runs over uneven ground.
Blah!
MalberS’ animal controller component is good for controlling body movement. I have yet to learn how to use it well, but it seems like it would work, as far as realistic enough body movement goes.
The layer above body control handles navigation. That means deciding how to get from one place to another, moving around obstacles in between. MalberS’ AI Controller can do that.
The layering is helpful. I can give the AI controller layer a destination, and it works with the animation controller to work out the body movements to get there.
I need one more layer to control the animal’s tactics. Like, go around some waypoints, and if you see an enemy, alert your friends, and attack. If your health gets too low, run away.
I could use MalberS’ animal brain component for that, but I’m not sure if it’s general enough to create all the behavior I want. I also find it difficult to visualize overall behavior, since the brain uses lots of little objects that have to work together.
There’s buzz about behavior trees (BTs), so I thought I’d give them a try. They’re supposed to be easier to scale and change, compared to finite state machines.
After trying a couple of BT implementations, I settled on nodeCanvas from ParadoxNotion. Key to getting the stack to work is the interface between the layers. The AI controller ⇆ animal controller interface has been worked out by MalberS, since they wrote them both.
I have to write the behavior tree ⇆ AI controller interface. The nodeCanvas docs explain how to do that in a way I understood. I’m not an expert, so good docs are essential.
Bloog and the cube
As before, I started with a simple task: get Bloog to move to the cube.
Bloog is a Read Deer doggo, using MalberS’ animal controller and AI controller components. The animal controller and AI controller are as they were in earlier posts. Let’s focus on the top layer and its interface to lower layers.
I had a hard time understanding behavior trees. Not the basic tech, like leaf, composite and decorator nodes, succeed, running or failed status, that stuff. But I wasn’t sure how to use BTs to get behavior. The article Behavior trees for AI: How they work by Chris Simpson helped. There are more articles linked at the bottom of the nodeCanvas behavior tree overview docs.
I’m not going to explain BTs. Check the links above.
I changed the task a little. I wanted Bloog to start in an idle state, wait a few seconds, then go to the cube.
Here’s a BT for that:
(Notice how small the font for the comments is. Over the last coupla years, a few people on Discord have asked for a setting for that, but ParadoxNotion haven’t fixed it.)
The BT first sets Bloog to an idle state. It does that once. It waits five seconds. The BT then tells the animal controller to set Bloog’s speed. In MalberS’ animal controller, this affects both:
- The body. Doggos have different gaits at different speeds.
- Movement rate.
Bloog doesn’t start moving, though. She doesn’t know where she needs to move to. The set-AI-target task does that.
The last leaf doesn’t mean what you think. Bloog doesn’t keep running. The node does. It keeps returning a status of Running.
nodeCanvas comes with a bazillion task and action types, like wait a number of seconds. It doesn’t know how to use the animal or AI controller components, though. I have to write code for that. The code forms the interface between the BT and MalberS’ components.
Here’s code for the idle animal action. I didn’t write it from scratch. nodeCanvas has a wizard that makes an action class template for you.
public class IdleAnimal : ActionTask {
private const int IdleStateIndex = 1;
private MAnimal _animal;
protected override string OnInit()
{
_animal = agent.gameObject.GetComponent();
return null;
}
protected override void OnExecute() {
_animal.State_Activate(IdleStateIndex);
EndAction(true);
}
}
Idling is something Bloog’s body does. It doesn’t involve navigation. So, it makes sense to use the animal controller (AC), rather than the AI controller. Line 8 grabs the AC for the object the BT is attached to.
OnExecute() is called when the BT checks the node. It uses AC’s API to set Bloog’s body to idle. The method returns success.
Here’s code for an action setting the AC’s speed:
public class SetMalbersAnimalSpeed : ActionTask {
private MAnimal _animal;
public int speedId = 1;
protected override string OnInit() {
_animal = agent.gameObject.GetComponent();
return null;
}
protected override void OnExecute()
{
_animal.Speed_CurrentIndex_Set(speedId);
EndAction(true);
}
}
Like the last one, it calls a method on AC. However, SetMalbersAnimalSpeed has one public field. It shows up in an inspector, as you would expect:
Rather than using the magic number, you can make a dropdown in the usual way:
public enum Speed
{
Walk, Trot, Run
}
public Speed speed;
The last task sets the AI controller’s target:
public class SetMalbersAnimalAITarget : ActionTask {
public MAnimalAIControl animalAIControl;
public GameObject target;
private MAnimal _animal;
private bool _isArrived;
protected override string OnInit() {
_animal = agent.gameObject.GetComponent();
animalAIControl.OnTargetArrived.AddListener(OnAnimalArrived);
return null;
}
private void OnAnimalArrived(Transform arg0)
{
_isArrived = true;
}
protected override void OnExecute()
{
animalAIControl.SetTarget(target);
}
protected override void OnUpdate() {
if (_isArrived)
{
EndAction(true);
}
}
}
OnUpdate() keeps being run, doing nothing until _isArrived is true. It implicitly returns the status Running until then.
Conclusion
The animal stack worked nicely, albeit for a simple task. The glue code was not difficult to write. With it, I don’t have to worry about all the lower-level deets when I make behavior trees.
I’ve still a way to go. I need to learn:
- How to tune the animal controller for different beasts, so starting, stopping, and turning are realistic (ish).
- How to make BTs for complex behavior.
- How to keep complex BT logic organized, so it’s easier to think about.
I’m sure things will get messy, but I have a good start.