AI Rework – Behavior Tree Level 3.
Something a little different this time: rather than extending the tree, I’m taking measures to make it more versatile.
Last time we were here, we had this:
That’s all well and good for finding the closest tree, but what if we want to find the closest creature? We end up writing 5 more routines simply to do something we’ve already done.
That’s a waste of code, and as everyone knows, code is extremely expensive. It’s like printer ink. That’s why programmers get paid so much, cause of all the code they go through.
3 Kilobytes to the Euro
Seriously though, “find the closest X” is a routine I expect to be using a lot. “Find the closest genetically-compatible creature”, “find the closest corpse”, “find the closest prey”, “find the closest rocket-propelled chainsaw”, “find the closest chainsaw-propelled rocket” and so on. You get the idea.
So with that in mind, it’d be cool if I didn’t have to rewrite 5 routines for ever variation on a simple premise. Good code is reusable code, and right now the Behavior Tree system is making it a lot harder to write good code.
So, the obvious thing to do would be change this:
… to this
Rather than hard coding ClosestTree, I’m feeding it in as a parameter. Each of the dependent methods store it and act on it when their Act() method is called.
This is a fairly elegant solution, with only one minor drawback: the fact that it will not work.
The reason it won’t work is a bit complex, but I’ll try to summarise it with an example. Suppose we’re trying to define a routine that will get the creature to walk towards the “ClosestCorpse“. When the creature is born, it’s AI is initialised and Context.ClosestCorpse is fed into the MoveTo routine:
The creature then Moves during it’s life. As it moves, the game updates the Context.ClosestCorpse object.
But the MoveTo routine was set back when the AI was initialised: it still references the original Corpse A! If the creature is told to approach the closest corpse now, it’ll wander off in the wrong direction…
This is where Lazy Evaluation comes in. We don’t want a reference to whatever corpse was fed in when the AI was initialised, we want a
reference to whatever corpse is currently stored in Context.ClosestCorpse. Luckily C# provides a fairly neat way of doing this: rather than
providing a Corpse to the MoveTo routine, we provide a Function That Returns A Corpse (Func).
Then there’s a neat syntax to generate exactly that:
And we’re done! MoveToClosestCorpse() can now be changed to the much more generic MoveTo() command, and can use any variable we want to feed into it.
Except, we’re not quite done. This system works exceptionally for *getting* a variable, but what if we want to set one?
Back to our original example. Some fiddling with the above syntax means we can now generate a FindClosestMatch() method, which identifies the closest Creature or Tree that meets a given criteria. For example, this:
new FindClosestCreatureMatch(parent, new Inverter(new IsAlive(() => parent.Context.NextCreature)), new Inverter(new IsEntityBehindFence(() => parent.Context.NextCreature)) )
… identifies a creature that is a) dead, and b) inside the map boundaries, without me having to create a whole new FindClosestCorpse() routine.
But this result doesn’t go into the “ClosestCorpse” variable, because FindClosestMatch() can’t just set any variable it wants. It’s a predefined routine: it stores it’s variable into a generic “ClosestMatch” variable.
This is still somewhat workable, so long as we only need the ClosestMatch variable for one thing, within the routine in which it is defined. It just means that the value can’t be retained for later use without custom routines.
But can we do better? Can we use a similar syntactic trick to lazy evaluation, but for a getter? As it turns out, yes! But… it looks like this…
Can you read that? I can’t read that. I don’t even know how it’s supposed to be read. Great Cthulhu that’s a hideous pile of mess.
Sadly, this is a case where reusability and readability are diametrically opposed requirements. If I were to refactor for reusability using this trick, the code would be rendered unreadable.
So, I think in this case I shall err on the side of readability. While I do have a SetVariable routine in case I’m ever that desperate to reuse code, I think I’m better off creating custom “SetClosestCorpse” style routines for storing variables and using the temporary “ClosestMatch” variable wherever possible.
Next Time On Somebody Help He’s Forcing Me To Write His Blog Posts: Hunting and Fleeing! Plus: we find out what happens when 250 identical Primum specium become predators who prefer live prey. (hint: they die. A lot)