So I have a level editor for Exon. It’s still a bit sketchy around the edges, but it does the job: I can place decorations and modify terrain and live happily ever after.
But in a singleplayer RPG, decorations and terrain are only half the battle. These things have no life but the life I build into them, and that means I need a level scripting system.
Can you see where this is going? Oh yes.
Programming in Unity is done in C#. This code is then compiled into components you can add to objects to make them do things. That’s quite handy for building systems, for core engine functionality that’s relevant everywhere. That’s not so handy for level-specific noodly bits that need to directly interact with pre-placed things in the level that compiled scripts can’t easily know about. I already have about seventy triggers in my working level and it’s far from done — so that would be seventy discrete, pre-compiled components that can each be used in exactly one place. Not very convenient.
For this sort of thing, one traditionally turns to an intermediate scripting language — something quicker and dirtier than whatever you’ve been writing your engine in, probably only interpreted rather than compiled. This makes it easier to iterate and adjust things that happen in a level alongside fiddling with the objects and characters and terrain that they depend on. Apparently Lua is great for this, and is the scripting language of choice for a lot of games. (I’ve never actually used it, except a couple of copied lines to cheat in Baldur’s Gate when I was a lad.)
In theory, it’s not a difficult system to implement. Each “trigger” in WC3’s editor is just a function, each “action” directly mapping to an API call. When an event is fired, the trigger function is called and it all runs through all the calls as you’d expect if you’d written it by hand. Indeed, if you want to get fancy in WC3, you can go straight to scripting in that intermediate language and bypass the friendly user interface — which unlocks a measure of additional power, but also brings the attendant risk of… well, having more than enough rope to hang yourself.
I don’t want an intermediate scripting language, though. I don’t want to do any typing, and I don’t want to spend ages making a compiler that turns my nice friendly blocks into somebody else’s code either. I want to go straight to slotting it all together with the blocks, completely inside the Unity editor (which has been transformed into my Exon World Editor).
How hard could it be?
Since it has to exist inside Unity, and specifically inside scenes, the basis of my trigger system is MonoBehaviour components added to game objects — no different than anything else one builds on top of Unity. This immediately gives me all the good stuff that components can do, such as serialised references to link things together and, indeed, saving and loading of the triggers themselves as intrinsic parts of a scene.
A trigger object has a single trigger component, and any number of action components. Since the order of actions is important and the order of components in the inspector isn’t necessarily reliable, I ensure they’re referenced by an array which defines their true order.
Each action descends from the abstract TriggerAction base class, which demands a single method that takes in the TriggerParameters. When a trigger is executed, the array of actions is traversed and the parameters passed in for use as appropriate. For example, I have a “bark” action that could make the “triggering unit” say something witty. I also have many actions that ignore the parameters, such as the Destroy Destructible action which… well, destroys a specific destructible, as picked in a serialised field as normal.
So while each action must be implemented in C# and compiled into the engine, the actual triggers that control the flow of a level can be constructed in-engine by slotting a load of action components together in the editor. That’s the first box thoroughly ticked, and actually without all that much effort.
I also have conditions, which are pretty much the same as actions — they’re components that implement a single method to determine if they are satisfied or not, and only if all the conditions attached to the trigger object are happy will the trigger actually spin through its action payload. Should something only happen to the player hero rather than any unit? Easy peasy.
And then there are events. Which are… perhaps a little bit more messy.
The entire trigger system hinges on a single master object that lives alongside the level controller — there can be only one in each level. This is statically accessible because it needs to be told whenever anything that is an event happens — which can be a very wide variety of places. A unit or destructible died? A switch was pulled? A unit entered a region? It’s simple enough, but it’s got a huge blast radius because anything that I might want to use to shape a living level needs a hook added. I am still working on a single map of the prologue and I already have rather a lot of hooks.
The other side of the equation is subscribing to these from the triggers themselves. Once again, events are just components added to the trigger game object, but they differ in that they have no real active code. On level load, the trigger master does a sweep for all event components in the scene and asks them to subscribe — but after that point, the event components are completely meaningless and pointless. The master object is the one that actually deals with executing triggers when their events happen.
It does feel rather wasteful to do this, especially when some events don’t even require serialised properties (e.g. “any unit” died vs. “specific unit 1337” died), but I’m not sure if it’s worth culling them after they’ve subscribed or trying to find another place to store them that’s a bit “lighter” than a MonoBehaviour. For all my posturing, I suspect level trigger logic is going to be a microscopic fraction of all the processing Exon demands while in motion.
To round off the system, I have implemented a number of conveniences for common patterns.
In a singleplayer adventure, most triggers must fire once and once only (to prevent repetition of important narrative events and whatnot), so I’ve added a “one shot” tickbox, and it if this ticked, the trigger will immediately turn itself off after being invoked. To get this effect in WC3, I had to make the first action of pretty much every trigger I ever wrote be “Turn Off (this trigger)”, which is not the worst thing in the world but got awfully boring and could lead to great fun if I accidentally missed it off one.
I’ve also added an execution counter so if, for example, you need to kill 3 enemies to make a thing happen, then that can be an intrinsic part of the trigger, rather than something jemmied together with if statements and variable mathematics inside its body (get ready to collect 10 bear asses, yo).
The one thing that’s kind of missing is a “wait” system — because actions are single synchronous function calls, there’s no way to delay between one and the next like WC3 could do. Instead, I’ve added a possible delay to the trigger itself, so to do a delayed action I invoke a second trigger that has a delayed execution. It’s not the most convenient but I don’t think I’ll need waits enough for it to be a pain. (After all, those were useful for in-game cinematics and Exon is more driven by its dialogue engine than WC3-style cinematics.)
The final part of this whole escapade is the Trigger Editor window. While my triggers are built of components added to game objects and that means everything can be orchestrated in the inspector, that’s not the most convenient way to work.
After all, while Warcraft III‘s GUI is just a thin layer over actual code, it’s ultimately a friendly and approachable and powerful-enough layer without which I would never have got into programming at all (I didn’t “get” VB6 at school until I started following WC3 mapping tutorials). I have never cared for graph-based visual scripting as an alternative to actual programming; all I needed was WC3 using triggers to convert scary function names into neat English sentences with variable values inserted as placeholders that got me over the hump.
I haven’t written nice English sentences for each action, but there’s enough custom control rendering code here that, you know, maybe I could… But that would be quite a bit more effort so I’ll pass for now.