CaddieOpen in CaddieDownload for Mac

How I Coded the Perks in my Roguelike

Shared by Caddie Library · Updated
Game Maker's Toolkit12:30Open on YouTube
Chapters show their main points

The bottom line

Giving each modifier its own small script that inherits from one shared base class lets a game handle over 100 perks with a single clean loop. It makes every perk easy to find, change, test and add, and the same pattern works for enemies, guns and upgrades.

Key takeaways

  • Checking every modifier with if statements inside one score script does not scale. 2:30
    • Each new modifier makes the script longer, so finding and fixing things gets harder.
    • Changing how modifiers work in general could mean editing the same code in 100 different if statements.
  • Each modifier should be its own tiny script with a function the main score script can call. 4:55
    • Trigger order then follows the player's modifier list, not the order of the code, which mattered for Balatro-style play.
    • The score script only needs a list of owned modifiers and one loop.
  • A shared base class makes an arbitrary list of scripts safe to loop through. 5:27
    • The abstract ModifierClass has a virtual onWordScored that returns -1 by default, meaning "nothing happened, move on".
    • The vowel modifier inherits from it and overrides the function to return the score times 1.5 when the first letter is a vowel.
  • The same pattern extends to every trigger in the game. 9:32
    • A base-class bool OnUpgradeUsed returns false by default, so modifiers that ignore upgrades do nothing and still work with the rest.
    • Triggers cover selling a modifier, losing a life, rerolling perks, reaching a new round and more, and upgrades use an OnTileDropped function in the same way.
  • Inheritance suits any game with many things that share core behavior but differ in details. 24:24
    • Enemies could share movement, health and death code, with a unique override for each attack pattern.
    • Guns could share aiming and reloading code, with unique special abilities.

So this is the power of inheritance, a really useful way of working with classes.

Mark Brown

Chapter summaries

0:00 Word Play's perks and why the system matters

Modifiers are one of three perk types, and the game needs a system that scales to 120 of them.

  • Modifiers are passive perks that pay off under the right conditions. 0:58
    • Word Play is a vocabulary roguelike where players spell words to score points.
    • Gifts add letters to the bag and upgrades improve tiles.
    • Modifiers work like Slay the Spire relics or Balatro Jokers.
    • An example is multiply the Final Score by 1.5 if the first letter is a vowel.
    • Modifiers also touch refreshes, bonus rounds, special tiles and the basic rules of spelling.
    • The examples use Unity, but the idea should apply to pretty much any engine.

2:41 The first version: inline checks

Per-modifier scripts beat one script full of if statements
Inline if statementsOne script per modifier
Where logic livesOne ever-growing score scriptA tiny script per modifier
Tweak one modifierHunt through all the codeOpen its own script
Change how all workEdit ~100 if statementsEdit the base class once
Trigger orderOrder in the codePlayer's modifier list
Score scriptSpaghetti of ifsOne short loop
The game jam prototype worked, but it couldn't scale to 120 modifiers or respect the player's modifier order.

The simplest way to score a modifier is to test for it directly inside the scoring code.

  • The prototype checked ownership and the condition in one place. 3:12
    • The code first adds up the points on the submitted tiles.
    • Each modifier is a separate GameObject, and a list records which ones the player owns.
    • If the player owns "VowelFirst" and the first letter is a vowel, the score is multiplied by 1.5.
    • This is exactly how the scrappy game jam prototype worked, and it did the job.
    • Changing it to the LAST letter means hunting through all the code to find it.
    • Order barely matters in Word Play, but Balatro made it a requirement for this system.

4:55 One script per modifier, and the need for a shared type

You can only loop over things that share a type
Unity scripts already share MonoBehaviour, and the same idea gives every modifier one guaranteed parent.

A list of scripts can only be looped through if they all share one class.

  • Each modifier script takes the word and its score and returns a new score. 5:00
    • It checks its own condition and multiplies the score if the condition is met.
    • It passes the result back to the main score script.
    • The main script loops through the owned scripts, calling onWordScore on each.
    • Asking for a list of ten items and then saying eat them fails if one item is a motorbike.
  • Every item in the list must be the same type of class. 5:55
    • Every Unity script already inherits from MonoBehaviour.
    • That inheritance gives scripts handy functions like OnCollisionExit, OnMouseOver and OnDestroy.
    • The same idea lets all modifiers share one parent class.

6:15 Building the base class and the vowel modifier

One abstract parent lets every modifier share one function
Because scores are never negative in Word Play, -1 is free to mean 'skip', so modifiers that don't care about a trigger need no code for it.

A custom abstract base class gives every modifier the same function, so one list can hold them all.

  • The base class sets a safe default that means "do nothing". 6:42
    • The shared function returns -1 unless a modifier overrides it.
    • Scores are never negative in Word Play, so -1 is free to mean "move on".
    • The function takes in the word and the current word score.
    • It must be marked virtual so child scripts can override it.
  • Each modifier overrides only the behavior that makes it unique. 7:14
    • mVowelFirst inherits from ModifierClass and checks whether the first letter is A, E, I, O or U.
    • If it is a vowel, it returns the word score times 1.5. Otherwise it returns -1.
    • Without an override, it would just use the base version and return -1.
  • The score script becomes one short loop. 8:16
    • Every script in the list is guaranteed to have OnWordScored, so the loop is safe.
    • Any result other than -1 updates the score, and can trigger a ping animation and sound.
    • The loop runs in the player's modifier order until the calculation ends.
    • The result is one loop instead of 100 if statements, with each modifier in its own script.

9:32 Extending the pattern to the rest of the game

The same base class handles every trigger and upgrade, and it suits any game with many similar things.

  • New triggers are added as extra functions on the base class. 9:42
    • OnUpgradeUsed returns false by default, so modifiers that ignore it stay untouched.
    • The "use upgrade" code loops through owned modifiers and calls OnUpgradeUsed on each.
    • One example override raises its bonus points whenever an upgrade is used.
    • Other triggers include losing a life, game over, opening the perk screen, refreshing tiles and adding a tile to the letter bag.
  • Each modifier script holds both its logic and its data. 10:44
    • It has a few lines of code plus its icon, description, rarity and special numbers.
    • Because the data is in the Unity inspector, changing numbers is easy.
  • Upgrades use the same inheritance idea. 10:59
    • Each upgrade overrides OnTileDropped from its base class.
    • Overrides can destroy the tile, turn it into an emerald tile or raise its score.
    • The system let the developer add, remove, debug and balance more than 150 perks in Word Play.

In the growth library

Mark Brown, creator of Game Maker's Toolkit and developer of Word Play. Shows how to build a roguelike perk system so that each of over 100 passive modifiers is its own small script built on a shared base class, instead of one giant block of if statements. The worked example is Word Play's 'first letter is a vowel, multiply score by 1.5' modifier, which grows into a system of triggers across the whole game.

More from Game Maker's Toolkit · The whole library, and ask it anything

Transcript156 lines
0:00

Hey, it's Mark. Now, I don't usually do a lot of programming related content on this channel, despite the fact that programming is of course An essential part of any game maker's toolkit. But that's just because I am far from an expert on this subject. I mean, the code bases for my two video games, Mind Over Magnet and Word Play, are basically held together with sticks and the glue. But there is one aspect of Word Play's programming that I'm actually really proud of, and that's the way I implemented the game's modifiers.

0:36

So - for context. Word Play is a vocabulary roguelike where you spell words to score points. And then you can unlock perks that let you bend the rules and score even bigger. These perks come in three flavours. Gifts typically add more letters to your bag. Upgrades let you change and improve your tiles. And modifiers are the passive perks that give big benefits under the right conditions. Like... this one that multiplies your score by 1.5 if the first letter is a vowel.

1:10

So they're basically the relics from Slay the Spire or the Jokers from Balatro. Now when I first started making the game I had no idea how I would implement these modifiers. There wasn't an obvious and easy way to do it. But, ultimately, I came up with a system that I'm really happy with. One that fits the requirements for the system, can be easily scaled to fit all 120 modifiers in the game, and makes it effortless to test, change, and debug the lot of them.

1:46

So I'm hoping this might prove interesting or helpful or inspirational to, let's say, a beginner or intermediate level programmer. to see what it takes to implement a somewhat complex system into a real life video game that's currently available on Steam. If that sounds good, then let's jump in. A quick note that I'll be talking about it in the context of making the game in Unity, because that's what I used. But the basic idea should apply to pretty much any engine.

2:23

So the modifiers in Word Play touch so many aspects of the game, with modifiers that fiddle with refreshes and bonus rounds and special tiles and the basic rules of spelling. And I'll come to that - but for now, let's make things simple by looking at the ones that impact the way your word is scored. Like - that modifier I showed earlier: "If the first letter is a Vowel, multiply the Final Score by 1.5". How would we implement that?

2:57

Well, first I'll write some code to tally up the word's initial score by adding up the points on all of the submitted tiles. And then... well, in Word Play all of the modifiers are separate GameObjects. And I have a list of which ones the player currently owns. So we could check if the players owns the modifier "VowelFirst". And, also, if the first letter is a vowel. If both are true... multiply the word's score by 1.5.

3:29

And that would work. That would work fine. In fact, that's exactly how I did it for the scrappy game jam prototype of Word Play. And it did the job, But when it came time to turn this into a real video game, I knew I needed a better system. Because... well, a couple reasons. The big one: every time we add a modifier, this script gets longer and longer. A massive spaghetti mess of if statements. And that makes it harder to find things, change things, and fix bugs and problems.

4:07

Like, what if I actually want it to multiply the score if the LAST letter is a vowel? Now I've got to hunt through all this code to find it. And what if I want to change something about how modifiers work in general - I might end up having to change the same code in 100 different if statements. Yikes. And another problem is that the order these modifiers trigger is defined by the order they show up in the code, not the order they are in your modifier list.

4:38

Now this didn't actually end up being a big problem for Word Play - you almost never need to worry about the order of your modifiers. But it's a huge deal in Balatro, and so - with that game serving as my initial inspiration - it was a requirement when I was first coding up this system. Okay. So my plan was to give each modifier its own super short script, with a function for calculating word score. I could pass in the word and its score. Then check if the condition is met.

5:08

If so, multiply the word score by the desired amount and pass it back to the main score script. Now the main score script just needs a list of the player's modifiers, and loop through the associated scripts one by one - testing that onWordScore function. The only problem is... well, you can't just have an arbitrary list of scripts, and try and run a function on all of them. I guess think of it like this. I ask you to bring me a list of ten items, and then I say, "eat them".

5:43

You'd be like, "wait, what? One of these is a motorbike". And I'd be like, "eat them". It wouldn't work, right? The list needs to be more specific. They all need to be edible for starters. They all need to be of the same type or of the same Class. Ah! You see, every script you write in Unity is based on the same class - MonoBehavior. This lets you inherit and use all sorts of handy functions like OnCollisionExit, OnMouseOver, and OnDestroy.

6:15

But you can actually write your OWN class - and then make scripts that inherit all of THAT class's functions. Ehhh... it's probably easier if I just show you. So it's pretty easy to do. We can just make a new class called say ModifierClass and all we need to do is set it to be an abstract class and now we can build other classes on top of this. And then in this base class, we can make, say, a public float called onWordScored, which brings in the word and the current word score.

6:55

And by default, we'll just return minus one. Because in Word Play, you can never have minus numbers in your score. So I can use minus one as a shorthand to say "Nothing happened, move on, do nothing". We just need to turn it into a virtual float so it can be overridden. Then we can make a new class called say mVowelFirst and set it to inherit from the ModifierClass and now we can use the onWordScored, and it sets it into a public override.

7:31

So by default, it's just returning what is in the base version of this class, which would be minus one. But for this one we want to say if the word substring zero is equal to A or E or I or O or U, aka a vowel, then return the word score multiplied by 1.5. Otherwise, return minus one. And so it basically takes in the word, checks if the first letter is a vowel. If so, multiply the word score by 1.5 and pass it back to the score check script.

8:10

Otherwise, return minus 1, which says there was no vowel. Nothing to see here. Move on. And now I actually CAN make a list of scripts - specifically, a list of ModifierClass scripts, because now Unity knows that every script will definitely have an OnWordScored function. Either the base function that just returns -1, or an override that may return a higher number. And so back to our wordScore script, we now can loop through that list of owned modifiers, run the OnWordScored function on all of them...

8:46

and if they don't return -1, we can update the word score. And maybe, like, I dunno, make the modifier do a little ping animation and sound effect. And now I can just make a whole bunch of these tiny scripts, with OnWordScored overrides... And the score calculator will loop through them, in the correct order, until the calculation is finished. And ultimately that's that! We've gone from the script having 100 if statements in a big unwieldy chunk, to having the script just do one elegant loop.

9:23

And all of the score functionality lives in separate scripts - which makes it easy to find them, change them, and, of course, add new ones. This system also let me make the modifiers work in other areas of the game. For instance, there are modifiers that do things when you use an upgrade. So I add a bool to the base class, called OnUpgradeUsed, that returns false by default. Then I can make a new script with an override that, say, increases its bonus points when you use an upgrade.

9:55

And then finally, in the "use upgrade" code, I just loop through the owned modifiers - and trigger the function OnUpgradeUsed on each of them. And the nice thing is - this is completely compatible with the other modifiers. Like, the "is the first letter a vowel" modifier doesn't have an override for OnUpgradeUsed, so it will simply use the code on the base class - which is to do nothing and return false. And so with that, I can have these triggers all throughout Word Play.

10:26

Like, when you sell a modifier. Score each letter. When you lose a life. Get a game over. Open the perk screen. Re-roll the perks. Reach a new round. Refresh the tiles. Add a tile to the letter bag. And so on and so on and so on. Each one is a tiny script, simply containing a few lines of code needed for the modifier's functionality - plus details like the modifier's icon, description, rarity, and special numbers... so it's easy to change them from the Unity inspector.

10:59

I also used this same system for the upgrades, which are the yellow perks that change a tile when you drag it on. Each upgrade inherits from a base class with an OnTileDropped function - and then each upgrade's script has a unique override for that function, depending on whether it destroys the tile, turns it into an emerald tile, increases the tiles's score, and so on. So, this is the power of inheritance, a really useful way of working with classes.

11:31

And you can imagine all kinds of ways to use this in your game. Perhaps a base class for enemies that all share the same basic movement, health system, and death functions - but with unique code for each of their attack patterns. Or a base class for guns with the same code for aiming and reloading, but unique functions for their special abilities. The possibilities are endless. But this system ended up providing the perfect architecture for my game - letting me add, remove, debug, balance, and change the more than 150 perks in Word Play.

12:08

Thank you so much for watching. I hope you found it interesting. If you like the look of the game, Word Play is currently available on Steam and it's coming very soon to iPhone and iPad. So look out for an announcement on that. Thanks very much and see you around. Bye!