SakeTami
The Kuloran Players
The Kuloran Players

patreon


Empowering designers to compose behavior without Touching Code

The first attempt

So this all began when I was working on our inventory item system. Initially, I thought items could be classes, with configurable fields that will be exposed via the in-game editor to allow non-programmer designers to make variations. But I quickly realized that this has gotten out of control and we’re not even 2 days in. OOP, and this model in general, is just way too inflexible. We quickly ended up with giant god classes, because something like ConsumableItem needed to have fields to allow designers to configure it for every possible way you might want a consumable item for—remember, our goal is to give designers some independence from having to wait for a scripter to add something every time. We quickly had something like this

    public bool HealsHealth;

    public int HealthAmount;

    public bool HealsMana;

    public int ManaAmount;

    public bool GivesTemporaryBuff;

    public BuffType BuffType;

    public float BuffDuration;

    public bool RequiresCombat;

    public bool RequiresOutOfCombat;

    public bool PlaysSoundEffect;

    public string SoundEffectPath;

    public bool HasCooldown;

    public float CooldownSeconds;

    public bool HasCharges;

    public int MaxCharges;

    public bool CanUseInDungeon;

    public bool CanUseInPvP;

    public bool RequiresTargeting;

    public TargetType TargetType;

    // ... Much more fields…

Worse, the same patterns were repeated across other types. EquippableItem had its own god class. QuestItem had another. BuildingMaterial…. another massive class with dozens of fields. This is not only for items. Skills, spells, weapons… I quickly realized how unmaintainable this is shaping up to become. So I deleted days worth of work, and went back to the drawing board.

The Second attempt

As much as I realize all the flaws of OOP, deep down, I’m still a big OOP fan. So of course my first instinct was to use inheritance to break down the complexity. Create a base `ConsumableItem` class and inherit from it:

    public class HealthPotion : ConsumableItem

    public class ManaPotion : ConsumableItem 

    public class BuffPotion : ConsumableItem

    public class TargetedHealPotion : ConsumableItem

This worked for simple cases, but broke down immediately when designers wanted combinations. What do you do when they want a potion that heals health AND mana? Do you create a `HealthAndManaPotion` class? What about one that heals, gives a buff, AND has a cooldown?

The combinatorial explosion was worse than the original god class. We'd have needed hundreds of specific subclasses to cover all the combinations designers wanted. Back to the drawing board once again.

Third attempt

Next, I tried a component-based approach. Instead of one massive item class, items would have multiple components. Each component does one atomic action, and components have their own configuration fields in the editor. Nice separation of concerns. We’re getting somewhere! A health potion could have a `HealthHealComponent` and a `SoundEffectComponent`. A complex potion could combine multiple components. Designers can mix and match these behavior components just like they do normal, entity components when they work with our established ECS for npc’s and mobs and players and such, without touching code!

But the execution order problem killed it. Components needed to run in specific sequences. The sound effect should play AFTER the healing, not before. Some components needed to check conditions before others could run. Components needed to pass data between each other.

I tried adding priority systems, dependency injection, event passing between components. The complexity exploded again, and I’m getting quite fed up. Who said programming is fun!

Fourth time’s the charm

So, items should not be “Things with properties”. They are “Things With behavior”. They might have basic properties like name and description, but they are mainly composed from behavior. A consumable item isn't a thing that has healing properties and sound properties and cooldown properties. It's a thing that executes a sequence of actions when used. Play sound, Check conditions, Heal user, Apply buff, Start cooldown.

That sequence is a flow. And flows can be represented as trees of connected actions.

And then I realized. Trees of connected actions for behavior. Behavior trees…

Behavior trees is a very elegant, simple pattern for modeling AI for game entities and robotics. The rest of this post is not going to make a lot of sense if you’re not familiar with the pattern, so it might be a good time to read about behavior trees a little bit.

So we built a simpler system called execution trees. Behavior trees are designed for constantly running AI and they’re meant to be periodically ticked. We’re not doing this for AI. We have events that happen (item used, item equipped, etc), which need to execute a tree for behavior, once. Execution trees are behavior trees that are stateless and designed for single pass execution.

Remember our original goal? Letting designers create complex behaviors without touching code? This is where this system really shines. We built a UI tree editor where designers can use it to add nodes, connect them together, and configure their properties. Want to create a complex potion that only works in dungeons, requires the player to be out of combat, heals for a percentage of max health, plays a success sound, and then starts a 30-second cooldown? No problem! insert the appropriate nodes, a heal action, a play sound action, and a cooldown action Under sequence. Configure the properties through the editor. Done!

Tree views are very intuitive for most screen reader users (that’s us!). As cool as it is to have drag and drop and wires to visually connect nodes for data to flow through, like unreal engine’s blueprints, that’s just painful for keyboard use. The way we make data flow through execution is via ports and the execution context.

Ports are slots for configuration for nodes. A play sound node has a port for the file path for the sound to play. When you insert that node into your tree, you can configure that port by typing in a path to fill the port with, or with context expressions. Nodes can output data via output ports, which will take an expression that specifies a name to store that value.

So we could have something like a GetActorHealth node that gets the health of the user. It needs to store that value somewhere so we can operate on it, so it has an output port. You can type something like “$health”, and the health will be stored under that name. You can connect that to the input of another node by typing the same name, “$health”, in the field for that port.

That is a bit less intuitive than if we had visual connections with wires you could trace, but alas as blind users we don’t have that luxury so we have to improvise. Combine that with your typical flow control nodes like sequence and fallback, and you’ve got a highly composable, powerful, no-code system for defining behavior for designers to use without needing to wait for scripters for every tiny thing they want to do!

One last concern we had was performance. Execution trees involve a lot of small objects, virtual method calls, and dynamic evaluation. But computers are fast, and the way they are intended to be used makes that not a worry. Players aren't using hundreds of items or casting that much spells every frame. They use one thing evry some time, it executes its tree (which takes microseconds on a bad day), and then they go back to playing.

Finally, I’d like to mention the interesting implications this has for productivity. Once we build up a large node library, we get this perfect three-tier system. The engine developers work on the core infrastructure, performance optimizations, all the unappreciated tedious work… Our scripters work at the gameplay layer: they're building rich, reusable components, nodes, and other things that encapsulate all the MMO-specific logic. "Check if player is in guild", "Award experience points", "Trigger server-wide event", "Apply reputation changes". These become building blocks that designers can just mix and match. And the designers and content creators are using all of that to put out new items, quests, and events independently!

This is exactly what you need in an MMO environment! Content velocity is everything. Players burn through content faster than you can possibly create it with traditional "become a programmer or ask a programmer to implement every little thing" approaches. But with this system, once we have that rich node library built up, all three layers can work in complete parallel.

The end

Well, that was a wild ride! The failed attempts were well worth the time and frustration, because we’ve reached a very good place trying to solve these problems. It’s not every day that you face the unique requirements of MMO development, so solutions that might seem obvious now took much blood sweat and tears just to think about.

Thanks for reading! If you want to chat, please comment or discord!


More Creators