Let’s continue our RTS and implement an upgrade system for our units!
This article is also available on Medium.
The last episodes were quite dense – we learnt about a big programming pattern, the behaviour tree, and we applied it to our units to redesign their AI. Today, let’s go back to something a bit simpler: we’ll continue working on our units and focus on the upgrade, or level up, system.
The system in itself is not too hard to code up, but I will introduce various concepts to properly implement all of the features – so, once again, I’ll split it in two parts to make it more digestible 🙂
Today, we’ll introduce the ins-and-outs of our level up system, setup the overall process and start modifying our UI. Then, next time, we’ll see how to define custom evolution functions, we’ll finish up our UI, add some “level up sound” and even some VFX using shaders!
Designing our upgrade system
As usual, before diving into the implementation, let’s first discuss a bit what it is we want to implement; saying that we want an “update” level can mean lots of things and, in the particular case of a RTS game, it might not be as clear as for an RPG for example, where you only follow one character at a time.
So the system I will present and write up here works as follows:
- units can upgrade and increase their level – to upgrade a unit, you need to buy the upgrade using in-game resources
- units start at level 1
- when it levels up, a unit will improve its resource production (if it has one) and/or its attack damage and range (if it can attack)
- a unit requires the same type of resources for upgrade as it does for the initial creation; so, for example, a unit that costs gold and wood upon instantiation will cost gold and wood for its upgrades, while a unit that only requires gold to be created will only required gold to be upgraded
- upgrades will get more and more costly as we progress through the levels, but not necessarily in a linear fashion: we will try to define our own evolution function to properly control how the level up costs increase
- and finally, of course: we’ll want to show some nice “power up” visual effect on the upgraded unit and to play a little “level up” sound! 🙂
Ok – that’s a lot to take in, so let’s try and decompose this in several steps.
Preparing the level up workflow
Before we actually work on complex evolution logic for our unit upgrade cost, let’s first go through the entire level up process with easy well-defined functions and values.
Basically, we want to add all the methods and variables that are required to level up a unit, and then we’ll replace those with some more advanced objects later on to make it easier to tweak and configure.
Trying it out with a simple test setup
Leveling up will be eventually done via our UI. For now, we’ll just simulate it by pressing a key on our keyboard: so if we have a unit that is selected and we press “L”, it will level up.
You might remember that we actually already prepared some useful variables and methods in our
Unit script a long time ago, namely
What we will do for now is simply write some comments to remember what we need to implement and add a little debug to print “Level up to level X!” in the console:
Then, in our
UnitManager, let’s add our little test-with-keyboard logic:
Now, if you select a unit and you press “L” on your keyboard, you should get debugs that show you it’s levelling up consistently 🙂
Modifying the unit’s attributes (for production, attack…)
Of course, we want the upgrade system to actually impact our unit and make it better with each level! So let’s see how to modify:
- its production efficiency
- its attack variables
We will not change either the production or the attack rates, but the rest of the unit’s production and attack attributes will be slightly increased.
Changing the production amounts is the easiest because we simply need to update the values in our
_production dictionary and, next time the unit’s
ProduceResources() method is called, it will go through these updated values:
Note: the little
+0.1f offset is to avoid getting stuck at zero if some integer rounding unfortunately loses too much info 😉
For the attack damage and range, though, we can’t do exactly the same, because up til now we were referring to the values in our associated
UnitData instance. This means that, for example, all of our Soldier units had the same attack damage: it was not specific to a unit but to a unit type.
To be able to handle and modify these values at the unit level, we need to add two variables in our
Unit script that store the unit-specific amounts (and that are initialised with the unit type data, but can then evolve independently on each unit instance):
Then, we’ll need to update our
CheckEnemyInAttackRange classes to use those instead of the unit type-specific attributes:
Thanks to those changes, we can now access and modify these
_attackRange variable inside of our
LevelUp() function and make our unit more powerful as it upgrades!
Figuring out the cost of the upgrade and updating our resources
The last core part of our levelling up process is actually computing how much this upgrade costs (remember that it has to increase with each level), checking if we can afford the upgrade and subtracting this amount from our current game resources if we do level up.
For this “level up cost”, I went with the following idea: I attribute an experience cost (“XP cost”) to my level up and I then convert this to a given amount of gold, stone and wood thanks to a little arbitrary-defined conversion table.
So, for example – let’s start with a simple case where upgrading to level 2 costs 2 XP, level 3 costs 3 XP, level 4 costs 4 XP… and so on. Then, I will say that 1 XP point converts to 100 gold, or 80 wood, or 40 stone (because stones are rarer and harder to get, so they’re more precious).
This means that upgrading to level 2, which costs to 2 XP, can be converted to a price of 200 gold (= 2 XP points), or 160 wood (= 2 XP points), or 80 stone (= 2 XP points); or even a mix, like 100 gold (= 1 XP point) and 40 stone (= 1 XP point). But the question is: how do you choose between those alternatives? Which one is “the best” one?
A first criterion is that, as I said before, I want my unit’s upgrade to be converted to the same types of resources as the ones that were used for the initial creation. So, for example, in my project, a House costs only gold and wood; so the upgrade can only be a mix of gold and wood.
However, that still leaves several possibilities:
|Gold (= 100/XP point)||Wood (= 80/XP point)|
But, of course, I don’t want to pick it at random: every time I level up a House from level 1 to level 2, I expect it to cost the exact same amount of gold and wood!
So: how can we get an algorithm that scales to our various use cases (i.e. different XP costs and different resource types) but is deterministic (i.e. it always outputs the same result for the same inputs)?
I decided to make something quite “balanced” and attribute one XP point to each resource type, cycling again and again, always starting with the most costly one, and only looking at the allowed resource types.
For this basic example of “levelling up a House to level 2”, this gives me the following routine:
- compute the total XP cost: we upgrade to level 2, so
xpCost = 2
- get the allowed resource types: we are upgrading a House, that is initially built with gold and wood; so the resource types that are allowed for the upgrade are gold and wood (not stone)
- sort these resources by “experience cost”: I want gold to be “cheaper” than wood, meaning that 1 XP point converts to more gold than wood; wood is the most costly resource in terms of XP because, in my conversion table, 1 XP point translates to less of that resource
- while I have XP points left to spare, add 1 “point” of each resource type going from most costly to less costly and cycling through
- I add 1 point of wood, and I have 1 XP point left to spend
- I go to the next resource in my list of allowed resources: that’s gold
- I add 1 point of gold and I have 0 XP points left
- so I exit my loop
In the end, levelling up my House to level 2 costs 1 point = 80 wood and 1 point = 100 gold (for a total of 2 XP points, as asked originally). The nice thing with that algorithm is that it’s perfectly reproducible (there is no randomness anywhere) and it easily scales to a larger amount of XP points and/or more resource types.
We can return this cost as a list of
ResourceValue, just like our initial unit cost: this will help us factorise our code. We’ll do all of this in our
Global.cs script (because I’ll define it one for the whole game):
Note: depending on your project and your team structure, you might want to make this data easier to edit. In particular if you want your game designers to work on these values and tweak the parameters to find the best one for a nice game flow, you might want to make those values more easily editable than in a script… 😉
While we’re at it, let’s also add a little function in our
Globals class that allows us to check if we have enough resources to buy something at a given cost:
We can now use all of this in our
Unit scripts to compute the cost of the upgrade and update our current amount of resources afterwards:
Plus, in our
UnitManager, we can check if we can actually afford the upgrade (or else we’ll debug a little error saying we can’t buy the upgrade):
Now, if you try this again, you’ll get various debugs: either the level you upgraded to or errors to tell you you’re missing some resources. Also, if you did manage to upgrade, you will see that the UI at the top of the screen updates to show your new amount of in-game resources (thanks to the “UpdateResourceTexts” even we emitted).
Checking for “maxed out” units
There is an additional feature that I wanted to code up for my upgrade system: the idea of a max level for units, so that when units reach this level, then they have “maxed out” and they can’t level up anymore.
Limiting the amount of levels is not always relevant, and some games do have infinite levels – for example, in Diablo III, you can raise your Paragon level over and over again without it ever reaching a limit.
Choosing whether you have a max level or not is very important, and it will definitely impact your game design and the objectives you can set for your players. It is deeply linked to the system of rewards in your game (both the intrinsic and extrinsic ones) – if you want to learn more about that, you can check out this nice 2017 GDC talk by Travis Day on rewards in video games (and in particular in Blizzard video games) 🙂
In my RTS, I want to have a limited level for units because the point is not to make one specific entity evolve. The point is to manage a whole tribe, a whole town of units and characters. So I do want the player to care a bit for the units on screen (and upgrading your units will indirectly give them value to your own eyes), but this progression will be capped so that, at one point, you’re forced to make more units to get a stronger army. This will keep the primary goal of growing a population and avoid the player from simply focusing all of their efforts on a single unit.
This info will be stored in our
GameGlobalParameters.cs file, in a static function called
For now, it will simply return an arbitrary integer value, but we’ll see in the next episode on how to deduce it from our custom cost evolution functions.
And we’ll use this threshold in our
Unit script to check whether the unit has maxed out or not, once it’s levelled up:
If you implement this limit and you run your game again, you’ll see that once the unit has reached this max level, it cannot run its
LevelUp() function anymore (even if you have enough resources) 😉
Updating our UI
Ok, debugging in the console is cool but let’s be honest: eventually, we want all of this info to update in our UI.
There are, in fact, quite a lot of places where we need to make sure the UI is up-to-date:
- the top-bar with the current amount of resources: that’s already taken care of, thanks to our event
- the selected unit panel: the upgrade button should be only clickable if you have enough resources to afford the upgrade; also, its text should switch from “Upgrade” to “Maxed out” if the unit has reached the maximum level
- when you hover the upgrade button, you should get a little info panel that tells you what level you’ll upgrade the unit to and how much it will cost (with texts in red for missing resources that update when you produce new resources)
All of this is pretty straight-forward, but we have to be careful not to forget anything.
A quick refactoring of our
And actually, before we go into our new checks and updates, let’s go back to our
UIManager script and look at our
SetInfoPanel() method. You can see that, for now, it expects a unit and it displays its name, its description and its cost in in-game resources.
But in truth: this is pretty similar to the info we need to output for our upgrades as well! If we hover the “level up” button, we’ll expect to have a little info panel that tells us what level this would upgrade the unit to and how much this upgrade costs. So, yeah – pretty much the same thing.
This means that it’s a good idea to factorise those two cases into one by rewriting a bit the prototype of our
SetInfoPanel() function. More precisely, we’re going to add a second prototype to replace our current one, and change the current one to another prototype that accepts the title, description and cost values as separate values, rather than them being packed into a
I haven’t changed anything in the logic itself, I’ve just renamed some input parameters and added a new prototype.
Adding hover/unhover/click events on the “upgrade” button
Now, I’ll be able to easily call this function for my upgrade system, too, whenever I hover my “level up” button. To do this, we’ll define two public methods in the
Then, we’ll simply assign them to our button in the Inspector. We first add a new “Event Trigger” component, then add the “Pointer Enter” and “Pointer Exit” listeners and finally assign the two functions as callbacks:
To actually show up our info panel, let’s replace my comment with a call to a little util method,
Note that I cache the cost of the upgrade in a variable,
_selectedUnitNextLevelCost, when I first select my unit because it won’t change until I either upgrade the unit or change my selection, so it’s more efficient to compute it once and then re-use the value rather than to recalculate it every time the UI is updated.
And now, if we select a unit and we hover the “upgrade” button in the selected unit panel on the right, we get a little info panel that tells us what the next level would be and how much it costs! 😉
Now, we can also add the actual click event to our button to make it easy to level up our unit via the UI. We’ll define the callback in a
ClickLevelUpButton() method that simply calls the
LevelUp() function on the selected unit, updates the info in the right panel and checks to see if the unit has maxed out:
Don’t forget to assign it to the button in the Inspector 🙂
Warning the user if they can’t afford a unit or an upgrade
It’s always nice to help the player better understand why some action is not currently available. In our case, since we’re disabling the “level up” button conditionally depending on whether or not you can afford the upgrade, it would be cool to show what resources are missing for the upgrade (just like for buying our units, with red texts).
Let’s actually extract this check logic to its own function,
Note: the final part that updates the text colors could clearly be optimised by caching some variables when we create the info panel, because remember that accessing components with
GetComponent() is always costly…!
Note 2: for this code to work, you have to make sure that the name of your resource sprites are exactly the same as the name of your resource in the
InGameResource enum (casing counts) 🙂
And now, we’ll simply call this function from different spots in our
UIManager class to make sure the UI always reflects the current state of our resources:
By the way – now that we can access our upgrade system from the UI, we can remove the little bit of testing logic we added in the
Update() method of our
Today, we’ve implemented the first version of our upgrade system and we’ve updated the UI to properly inform the player. Here is a little demo of what we’ve achieved in this tutorial 🙂
Next time, we’ll continue working on the level up logic: we’ll work on defining some custom functions for the evolution of our modifiers and of our upgrade cost, we’ll finish the UI to better tell the player the impact of the upgrade on the unit, and we’ll even see how to add some sounds and visual effects using shaders!