Alright this isn't exactly programming 101, but I really want to share this because it's by far one of the most powerful and useful programming patterns I've been using in my games since... 2017 or so. Google says this is called the "Strategy" pattern, which I don't know is entirely accurate to what I'm doing, but we'll go with that.
With this pattern, you can (for example)
* Create custom monster behaviors completely decoupled from any monster classes, and allow designers to swap behaviors without touching a text file
* Create a single status effect, then give ten different magical items ten different implementations of that status effect, all without duplicating code or data
* Write new code in a completely clean, modular way that can be safely edited without breaking anything else in your game
* Inject completely batshit and wild behaviors and functions basically anywhere in your code path, without affecting the complexity, length, or readability of any core functions
PART 1 - BASIC OVERVIEW
The simplest way to summarize Strategy is boiling it down to saving a function NAME in a string (such as in a game data .txt file) then using some wizardry called ✨Reflection✨ to match that string up with an actual function in the game code, and finally running it. We can of course save function arguments as well.
Say you're making some kind of deckbuilder. You define your cards in data files. You write a block of text like this (the actual format doesn't matter).
cardname=Fireball
damage=12
visual_function=ShootFireballAtTarget
visual_args=Numfireballs:5
When the game is actually running, you set up card execution logic like this (pseudocode):
void PlayCard(Card cardObject)
{
... // gameplay logic here
if (cardObject.HasVisualFunction())
{
cardObject.visualFunction.Run(cardObject.visual_args);
}
}
The PlayCard function has absolutely no idea what visual function is going to run, and it doesn't care. You write the code for that function somewhere else, like in CardVisualFunctions.
void ShootFireballAtTarget(Dictionary<string,string> args)
{
// Make the card spin around
// Spawn fire particles
// Shoow fire particles at the target
// Repeat the above args["Numfireball"] times
}
Now, C# and other languages have concepts like "delegates", and if you're familiar with those you might be thinking Strategy seems pretty similar. It is!
A "delegate" is basically using a function as a parameter, something we can Invoke, which allows us to assign behaviors in a clean way. The delegate version of the above would be something like this:
public class CardObject()
{
public Action<Dictionary<string,string>> VisualFunction
}
...
void PlayCard(Card cardObject)
{
if (cardObject.VisualFunction != null)
{
cardObject.VisualFunction.Invoke();
}
}
The amazing thing about the Strategy pattern however is that you can use it entirely using text strings - which is perfect if you want to store your game data in external data files for easy designer access and player modding.
PART 2 - WHAT PROBLEMS DOES THIS SOLVE?
I could write a book on how useful this is and all the many headaches it has solved for me, but let's just dive in to more examples. Say you have StatusEffects in your game, and each StatusEffect does something based on a trigger. That trigger could be something like swinging your weapon, missing, getting hit, blocking an attack, moving, etc. So you start defining different battle triggers with an enum, something like...
public enum BattleTriggers { BASIC_ATTACK_SWUNG, DODGED, BLOCKED, CRITICALLY_HIT, TOOK_STEP, PARRIED, MOVED }
You can then set up 'hooks' elsewhere in your code like this:
void OnAttack(Fighter target)
{
foreach(StatusEffect se in myStatusEffects)
{
if (!se.CheckForTrigger(BattleTriggers.BASIC_ATTACK_SWUNG)) continue;
se.OnTrigger(BattleTriggers.BASIC_ATTACK_SWUNG);
}
}
Then your StatusEffect class simply has a definition for its effect, and a BattleTriggers field for what makes it fire. Great!
But then you think about it a little more. What if you want the status effect to trigger on swinging a dagger, but not a sword? What if you want it to trigger if you dodge a boss monster, but not a regular monster? What if you want it to trigger every three swings? If you move every 15 meters, instead of just moving at all? If you block a fire attack, but not any other element?
Well... now your code isn't looking so hot. With the above approach, you'd have to make way more enums, and start junking up your code with lots of conditionals. Not good. Bad!
So instead, let's give StatusEffect a couple new fields:
public class StatusEffect()
{
public Effect myEffect;
public BattleTrigger trigger;
public string requirementsFunction;
public Dictionary<string, string> requirementsFunctionArgs;
}
We can keep our list of BattleTriggers somewhat shorter and more reasonable. Let's say we're making two StatusEffects from the above list:
* "Dagger Boost" triggers on BASIC_ATTACK_SWUNG, but only if you have a dagger
* "Triple Trouble" triggers on BASIC_ATTACK_SWUNG, but only every 3 swings
In our data files, we write something like this.
status="Dagger Boost"
trigger=BASIC_ATTACK_SWUNG
effect=[whatever]
requirementsFunction=CheckForWeaponType
requirementsFunctionArgs=DAGGER
stauts="Triple Trouble"
trigger=BASIC_ATTACK_SWUNG
effect=[whatever]
requirementsFunction=TriggerPerSwings
requirementsFunctionArgs=3
The shape of this should be starting to crystallize in your mind now. When we write OnTrigger for StatusEffect, we do this:
void OnTrigger(BattleTriggers theTrigger)
{
if (HasRequirementsFunction())
{
bool validResult = RunRequirementsFunction(requirementsFunctionArgs);
if (!validResult) return;
}
// ... do effect as normal
}
Now of course the logic for CheckForWeaponType and TriggerPerSwings has to live somewhere. But they can now go in separate, clean files. I don't need to write them out; the exact logic isn't important. The key thing is that they can live elsewhere in the codebase, decoupled.
If a designer wants to change the number of required swings in TriggerPerSwings, they can easily do that. If you want to make a duplicate of Dagger Boost, but for weapon type SPEAR, you can do that too. Zero code required for either of those changes.
This is just one small example. You can use this all over your codebase to great effect.
You want to reach for this pattern if you find yourself coming up with all sorts of 'edge case' logic that would otherwise involve a lot of shoehorning, hard coding, and clunky conditionals in an otherwise-clean code path.
PART 3 - NERD STUFF (HOW TO ACTUALLY DO IT)
So far I've been using pseudocode, let's get into the mechanics now. In C#, you reate a dictionary to 'cache' function lookups first (for performance reasons):
public static Dictionary<Type, Dictionary<string, MethodInfo>> dictUnboxedMethods;
Then, use a helper function in some utility class to get the method based on the string. This is a bit long so I've put it on hastebin here. Once you write it, you never need to touch it again.
Next, when you need to use the strategy pattern, you have to first check if the string is not empty, that the method exists, and then run it safely in try/catch. Here's a specific example from my codebase:
// customRequirementsFunction is a string
if (!string.IsNullOrEmpty(template.customRequirementsFunction))
{
// AbilityRequirementsScripts is a static class with the function logic
MethodInfo runscript = CustomAlgorithms.TryGetMethod(typeof(AbilityRequirementsScripts), template.customRequirementsFunction);
if (runscript != null)
{
// 3 is the arbitrary number of parameters I want to pass in: the user of this ability, the ability itself, and my arguments
object[] paramList = new object[3];
paramList[0] = fighterOwner;
paramList[1] = abil;
paramList[2] = template.customRequirementsFunctionArgs;
// In this case, I know that the result will be a bool, so I have to cast for it
try
{
bool usable = (bool)runscript.Invoke(null, paramList);
if (!usable) return false;
}
catch(Exception e)
{
Debug.Log("Failed to run customRequirementsFunction because: " + e);
}
}
}
PART 4 - FAQ (I GUESS? Nobody asked anything yet)
Q. Is this performant?
A. In my experience, yes. The "unboxing" of a method is the biggest hit here, but you only need to do it once ever. After that, it's fine. Now would I run it tens of thousands of times per second? Probably not. But in the context of using it for gameplay events that aren't in a hot path, it's fine. I've never had any problems with it, even on low spec hardware like Switch.
Q. Are there any downsides?
A. The main one is that you lose compiler name-checking. If a designer types in "TriggersPerSwings" when the function is actually called "TriggerPerSwings", that would get caught if you were writing an actual function name in C#, but in this case you only see it at runtime. You have to do your own validation.
Q. Surely there are other downsides?
If there are, I'm struggling to think of any. I've been using this across 3 games and I've never had any issues with it. This is just a really awesome pattern. In my current game Tangledeep 2, I use this across ~41 different method types and counting. It rules.
Q. This really is just delegates, right?
A. Again, kind of - it's the same idea of storing function/methods as a variables, and invoking them later. But the magic of it is that you can change functions and arguments strictly by editing text. I try to keep as much of my game's data in external data files as possible, mainly for modding purposes, but also so I can change stuff without recompiling the game, or without opening Visual Studio, or whatever. Plus, designers can change stuff far more easily if they don't know C#.
Q. Doesn't this have security issues letting a modder execute arbitrary code?
A. Not at all. If you look at the above examples carefully, the game will only try to run a function if it already exists in the codebase and that function has to be within a pre-selected class. So even if you have a function in the code that is somewhat more dangerous, a player won't be able to access it by changing what StatusEffectRequirementFunction is being used.
Q. Does this work in [XYZ] other language?
A. I only write in C# with Unity so I have no idea. I'm sure it works in C++. In something like GDSCript or GML I have no idea.
... and that's it! Thanks for bearing with me as I procrastinate just a little longer. Happy to answer any questions anyone might have about this.