Trimming the Fat With Playmaker
Not a major update, but I wanted to talk about an experiment I ran last night in Project Universe.
One of the aspects that I have in place right now is NPC routing. I had a need for some life-like activity within a sector, represented by merchants, police, miners, and pirates, so that a player doesn’t feel that she’s the only person in the game world. Merchants seemed like the easiest critters to create: they would spawn at a random jumpgate, head to a random station where they’d lay over for a few minutes, and would then re-appear and head to another random jumpgate and vanish from the scene.
I had spent a long time developing these amateur-level AIs for three of the four types (pirates involve combat, which is a system I’ve not approached yet) using a combination of code on the objects themselves, as well as on the scene controller for traffic management. When I say long time, I mean a few weeks. I had struggled with getting the NPCs to aim at their destination, to make sure they picked the proper destination types, and to make sure they didn’t overshoot, and that they took appropriate action when they reached the trigger of their waypoint nodes. The system isn’t clean by any stretch, but it works; my merchants jump in, dock, and jump out; my police patrol between public elements like stations and gates; the miners will hang out at asteroid belts and will occasionally return to stations to drop off their goods.
What I wanted to try was to mimic these behaviors with Playmaker. For those who didn’t catch my old posts from my defunct GameDev website, Playmaker is a visual state machine add-on for Unity. It allows a developer to define “states of being” for an object — idle, moving, jumping, docking — and to transition between those states based on criteria like player input or variable assignments. It obfuscates code by presenting states in a visual flow-chart style which sees the state nodes connected by event flows. I’ve been a long time fan of Playmaker, but had opted to try creating these AI guys by hand just to see if I could.
Last night I had set up a new FSM (finite state machine) in Playmaker for the “merchant” object in a new scene in my working copy of Project Universe. I removed all existing NPC scripts and created a new, single script that had only a few responsibilities:
- Set the movement plan: Determine which nodes the merchant is going to visit
- Select the next leg of the movement plan: Where should it go next?
- Movement: Getting from the current node to the next node
- Jumping: When the ship enters a jumpgate trigger, destroy the object
- Docking: When the ship enters a station trigger, pause, and start a timer
- Undocking: When the docking timer reaches zero, set bits that allows the NPC to move to the next node
That’s pretty much what the original NPC movement script did, but whereas the original script had 445 lines of code, this new version only has 102, and is nowhere near as dense, and doesn’t contain anywhere near the cross-script dependencies that the all-code version does.
Playmaker’s role in all of this is mostly to call the methods of the script based on conditions. You can see the FSM in the screen shot below:
- Idle: calls the method for setting the movement plan (choosing the nodes to visit) and aligns the NPC with the first node.
- Move: Calls the method that sets the “IsMoving” boolean on the NPC. When this is set to true, the Unity engine’s “Update” statement moves the NPC ahead towards the destination.
- Jumpgate: If the Move state detects that the NPC has entered a jumpgate trigger, this state calls a method that destroys the NPC. Later, I’ll have to update it to “jump” the NPC, because I use object pooling for NPCs and don’t want to destroy it. Later later, I want NPCs to “live” and actually travel through the game, so I really don’t want to destroy them.
- Station: If the Move state detects that the NPC has entered a station trigger, this state calls a method that sets “IsMoving” to false and “IsDocked” to true. Unity’s “Update” statement has a section for “IsDocked” being true, which is to check a time difference between “now” and the time the NPC docked. When that difference reaches 10 seconds (arbitrary value), the “IsDocked” is set to false and “IsMoving” is set to true. This is an incomplete system, since the NPC should be hidden while docked, and in order to do that I had to disable the NPC. Doing that removes my ability to address the object, meaning Playmaker can’t talk to it to tell it when to re-appear. So control of docking has to be done outside of the NPC itself.
- Select Next: before the NPC gets underway, this state calls a method which aligns the facing to the location stored at “Current Node + 1” in the route dictionary array so the NPC can move to it’s next destination. Once that happens, the event cycles back to the Move state so the NPC can actually move. If the next node is greater than the array limit, we’ve got a problem unless the NPC has the “IsLooper” set to true. In that case, it’ll just cycle back to Node, it’s origin, and start all over. That’s for the police; for a merchant, Node is a jumpgate, which will currently destroy the NPC. I have to handle Node+1 scenarios for merchants at a later time.
As pleased as I am that I was able to replicate the same functionality with less code and in less time than I had originally spent with the long-hand script, committing to a tool like Playmaker is not something to be taken lightly. Once you start using it, you have to keep using it, lest you end up with some long hand code and some Playmaker code, confounding debugging attempts and making manageability a veritable nightmare of “where the hell does X do Y?!”. To be honest, I’m leaning towards being OK with relying on Playmaker, since I’ve gotten the simple AI working long-hand and know what it’s supposed to look like; this is kind of just a refactoring of the code for efficiency and cleanliness. And considering I’ve got some rather complicated systems that I want to try and work into the game, having a more visual representation of those systems might not be a bad idea, so I don’t end up confusing my future self when I have to test and debug.