I wrote recently of how I intend to tackle scenario-level logic in Exon; not systems like equipment handling or projectile weapon firing, but random bits of hyper-specific “bloke talks to you when you enter his house” or “quest updates when you acquire item” type stuff. This system works but has a few quirks that I’m not entirely comfortable with, so I’ve taken my life in my hands and started to look at a Unity feature that is quite fresh but seems to alleviate those concerns…
I have a few requirements for my trigger system:
- I want to use references to objects in the scene, so I can take advantage of the object pickers and whatnot for easy click ‘n’ drag management; no magic string variable names that can break
- I want triggers to live in the scenes to which they belong; no screeds of level-specific ScriptableObjects cluttering up the wider project
- I want to be able to easily add, update and delete events, actions and conditions; because I will be building the rest of the fucking game using this system
- I want to be able to easily reorder actions, because the sequence in which actions are invoked is often important
I had been solving these using an abstract MonoBehaviour, with each action a subclass implementing the one method — when a trigger is executed, it walks through the actions in its list and fires this method, and each action uses its serialised fields to do its actual work. Each trigger GameObject thus has a component attached for each trigger action.
This requires a complex custom editor window to be convenient. Because actions are themselves totally independent objects, the trigger itself doesn’t “contain” them — they are merely linked to it by an array, with this array defining the order of execution at play time. My custom window melded the array and the actions using a whole heap of nasty, nasty editor UI logic, including a public-but-undocumented bit of the editor API… with predictable results.
I also always had a concern about using MonoBehaviours for this system. MonoBehaviours are heavyweight things designed for objects in the game world that have a position; they’re very “physical”. But though trigger actions may refer to physical things like units, items and buildings, they are not physical at all; they’re ephemeral, narrative. They should be completely plain classes, but couldn’t be without sacrificing the editor integrations I wanted — the drag-and-drop serialised fields that allowed them to link with all those physical objects in the scene.
… Until now.
I was futzing around one day when I remembered that Unity recently added “polymorphic serialisation” to the editor. I initially ignored this because I didn’t have a use for it — although it lets plain, non-MonoBehaviour classes be serialised inside a component, it couldn’t let them be referenced outside there so its utility seemed negligible.
Then I realised that that is exactly what I need for my triggers. Each trigger does own its set of actions and conditions, and nothing else in the universe should ever be messing with them.
So if you add [SerializeReference] to an array of some abstract base type, Unity automatically makes them editable in the inspector inside the containing list — so you get all the usual advantages of a reorderable list and the standard scene saving for free, but they’re still appropriately lightweight C# objects.
Well, not entirely. Pressing the little “+” button to add an element to an abstract list like this adds a null entry and, without any other input, that’s all it can do. It makes sense; since you’re serialising a reference to a base type, Unity has no way of knowing what sub-type it should plop in during that addition. Luckily I already had a little type selector window for the old system, which worked for this situation with a few minor modifications. You press my new button, pick what type of action you want to add, and it does the work behind the scenes. It’s a bit dirty that I can’t supplant or hide the list’s own “+” button, but I can live with that.
There are other fun things I can do, which were off limits before. Something Unity likes to do with list items that are structs or objects is to give them a title, using the first String property it encounters. The thing that really got me into programming in Warcraft III was how trigger actions were converted into human-readable English sentences — not the uncanny valley of Visual Basic’s mangle of real words in the structure of code, but genuine formatted descriptions. So if I plop a little Description field onto my trigger actions and refresh it when a parameter is changed…
The other issue I’d been having is that I also want to use my trigger conditions in the conversation system, to determine what responses are available or what nodes come up initially based on things you’ve done (or not). Once again, the fact that MonoBehaviours are open for anyone to access makes it very difficult to select a condition for a conversation without accidentally taking one from a random trigger instead; or, indeed, just having a few random conditions floating around on stand-alone objects or packed in with the conversation nodes.
But, with the new polymorphic list, individual dialogue nodes and responses can keep their own self-contained conditions just as easily as the triggers do and that’s one less thing to worry about.
Of course with any system like this there’s the fear that an errant upgrade to Unity might obliterate the whole lot, so we’ll find out soon enough if I have just preordered a whole new universe of regrets.