Updated: Aug 22, 2019
I cleverly avoided doing anything AI-related on my senior project, and before that, the most complex thing I had tackled was the wandering child state machine in Daycare Disaster. Now, I have COM-controlled cows and UFOs both, and I needed some kind of plan for handling that. I certainly could have written my own system, directing my cows to move in straight lines from one random point to the next, and rotating 180 degrees every time they bumped into something, but there were a few snags that I wasn't sure how to handle: making sure those destinations weren't in the middle of trees or other obstacles, being able to move in and out of barns, and interacting with each other, especially when they started being penned and were constantly in close quarters. My simple system couldn't handle those conditions.
Unity's semi-built-in NavMesh package (you have to download it separately, but it's located in a free GitHub repo) can do some things for you, and in fact it was plenty powerful for what senior project required. It's got basic obstacle avoidance, but a lot of the other things it tries to tout as features seem only half-implemented and utterly un-customizable under scrutiny. The worst is how the package claims to include support for agents avoiding each other (like I wanted), but they just...don't. At all. I'm not crazy; search the forums and you'll find a ton of "WTF?" posts and workarounds that weren't acceptable for my situation.
The Asset Store offered another solution, the A* Pathfinding Project. It also promised RVO (Reciprocal Velocity Obstacles) support, and proved it with a set of example videos. The reviews all said that this was the asset to get, and especially because the dev is amazing with supporting it. I got the package and found that it did what I wanted, and all I needed to do was throw a bunch of pre-written scripts onto the various objects in my scene: pathfinding Seekers on each cow, an RVOSimulator on my DarkMagician GameController, and an A* Inspector on the parent object of my dynamically generated environment. Oh yeah, it does NavMesh creation for dynamic maps much better than Unity's asset, including letting you specify tags and layers of objects to include or not. (This will come up again in my next post.) My cows were now able to avoid each other and do some more interesting things as they pathed around the world.
The base of my own code is a WanderWithinRadius() function that takes in a center point and a radius out from it. When called, the cow picks a new destination within radius of point, using an A* API call to make sure the chosen location is in fact on the generated NavMesh, not inside an obstacle or another un-walkable area. Typically, this method is either called with the wandering cow's current location and a general distance, or with the penned cow's pen's center and radius. So free cows can just sort of go where they want, within a given distance, while penned cows always stay in their pens.
Cows can also be interrupted from their wandering by A) being abducted, and B) being lured. Abduction was definitely the harder of the two, as it involved figuring out how to stop the cow from moving automatically, then moving it manually, including rotation and scaling, depending on interactions with the player's abduction cone. And what should happen if a player is interrupted mid-abduction, and a handful of other things that could never be anticipated until they broke the game during testing. When a cow falls back to the ground (regardless if it was dropped, is being scored, or was shot down), it checks to see if it passed through the special pen trigger, and then calls the WanderWithinRadius() function accordingly. Luring wasn't so bad, since it just involved setting the lure object as the cow's new destination...given, of course, that the lure hadn't already picked a different cow to target, that the current game mode didn't prevent luring, and...yeah, okay, but it was still much easier to interface with A*.
As a last special case, some of the game modes require cows to teleport away after they're scored with, either back to their designated homes or to another random point. I created a child class, the TeleportingSpaceCow, to handle these special interactions. Now, when the cow hits the ground, it checks additional variables, like if it still has an owner set, if it's penned, if it's already teleported this turn, etc. That OnCollisonEnter clause is one of the most complicated pieces of code in the game right now, because of all the combinations of conditions that are possible and how the cow should react to them. Tons of debug statements were life-saving when they helped me trace through the exact execution of each case, and all the comments that still remain show exactly when we should be where. In the end, there's a much higher volume of considerations when working with AI, but really it's just an extension of solving any coding problem: you lay out what you want to happen, under what conditions, and you create the logic statements to make that happen.
Next time: Adding UFOs.