Making a RTS game #27: Levelling up our units! 2/2 (Unity/C#)

Today, let’s finish up our unit upgrade system!

This article is also available on Medium.

Last time, we started working on the level-up system for our units. We explained that you can buy upgrades for your units using in-game resources, that upgrading a unit improves its production and/or attack parameters and that levels get more and more costly.

Today, we’ll implement custom evolution functions for the different variables of our upgrade system, finish up the UI and add some sound and visual effects using shaders!

Creating custom evolution functions easily: defining trends via curves

So far, we have hard-coded some values here in there for the influence of an upgrade on the unit’s production/attack system, or for the evolution of the upgrade prices. What would be nice is to have a user-friendly way of defining those values (and also, modifying the production/attack multiplier depending on the new level).

There are various ways you can do this. And, actually, you could simply implement a function with a little if or a switch to directly map the levels to some values:

This works and it is highly customisable, but it does require you to get your hands in the code and write down the result for all levels. What if you want a linear progression? Then you’d have to write that for level 1, the value is 1; for level 2, the value is 2; and so on…

In our case, there are two better solutions:

  • we could define a function rather than a simple list of values: it’s similar to what we did for the resource production of our units, for which we defined the woodProductionFunc and stoneProductionFunc, based on a delegate. The advantage is that it scales to any level value instantly (unless you define a discontinuous function but that would be messy…). On the other hand, it’s still coded up in your scripts and it might be hard to visualise.
  • or we could hijack Unity’s animation curves system to actually define those functions in the Inspector rather than in code!

Wait – “animation” curves?

This might sound weird, I know: why would we use animation curves?

Well, in fact, nothing forces you to use those curves for animation. It’s just the name that says so. In truth, those curves just map a given float (called “time”, but it’s your X value) to another (your Y value). It is very useful if you want to have some value in a component evolve throughout an animation, which is why this is the favourite use case, but you can absolutely use it in other ways!

And the nice things is that those curves automatically get a little curve editor in Unity’s Inspector 🙂

Here, for example, let’s say we add some variables to our global game parameters:

I’ve defined one curve for each variable of our upgrade system we want to change depending on the current level.

Now, in the Inspector, if I select my global game parameters Scriptable Object instance, I have 4 new slots where I can set the value:

And if I click on them, I get a little curve editor that allows me to easily setup a little trend for my variables:

The idea of these curves is that you define two or more keyframes (still terminology from the world of animation, but you’re not limited to this use case!) that define precise (x, y) pairs – i.e. at those specific X values, you have those specific Y values. Then, the rest of the curve is interpolated to automatically “blend” between those fixed points.

Depending on the interpolation type, you might get quite different curves, and so quite different values for the intermediary (interpolated) values:

Example of animation curve interpolations (constant, linear and bézier) from the Blender’s docs

Unity’s allows us to play around with the shape of our curve quite a lot, using the little pre-defined trends in the top-left corner or manually adjusting the handles on our keyframes; all in all, this lets you create visually a little function that maps a float to another.

The curve is then auto-filled after your last keyframe and you can right-click this keyframe to select which type of fill you want:

Now – how should we choose between the two solutions (functions in the code or animation curves)?

I think it depends on several parameters:

  • how complicated/specific you want your functions to be (if they’re simple, you might as well write them down instead of dragging handles all around)
  • whether you’ll want game designers (who are not used to navigating scripts) to tweak these values, in which case visual editors are probably better
  • most importantly, whether or not you have a max level: animation curves may have this auto-fill feature, but you’re still really in control only of the parts with keyframes. If you want your game to have infinitely many levels and to get some funky behaviour for these values in the higher ranges, you’ll probably need to define your function in code.

In my case, I know I want a max level of 4 for my units. So what I’ll do is define trend curves for my four variables in the 1 – 4 X-value range (even if, actually, the values for 1 won’t be used since we’re already at level 1…) and then, in my GameGlobalParameters.cs file, use one of those curves as reference to grab my max unit level:

I’m using the C# Linq tool to easily get the keyframe with the highest X value in my experience evolution curve, which corresponds to the max level for my units. Note that I chose this curve arbitrarily and that this requires you to have all of the curves define the same X-value range; but it’s still quite handy, so I don’t think it’s that big of a deal 😉

And thus now, if I decide to move my keyframe to the right or add a new one in this curve, the entire game will automatically adapt to increase the maximum level of the units!

Sampling the curves in our code

Of course, now, we need to sample these curves to actually grab values and use them in our code. To do this, we just need to call the Evaluate() method of our animation curves with the X value (in our case, the new level) as parameter:

If you want to try this out, what you can do is define a clearly exaggerated trend, like this one for example, for your unit’s production upgrade multiplier (i.e. the productionMultiplierCurve variable):

Then, start your game and upgrade your initial House building:

You see that its production just doubled suddenly, because the multiplier curve was sampled at x = 2 (since we were upgrading to level 2) and it got a value of 2 on our curve.

In my project, I eventually defined most of the trends to go from 1 to 2 but with various curve shapes, in order to avoid too-harsh increases; the experience cost, however, goes from 1 to 10! And, of course , this depends on your game 🙂

Little UI updates: showing up the attack parameters and the impact of the upgrade

Important note: besides the modifications I detail here, I’ve also modified my overall UI layout to use a screen-scaled canvas and better organise the various elements. You can see the full changelog in the corresponding commit on the Github repository for this project 🚀, but it basically meant doing one thing:

Get the scale of the canvas, store it in the GameManager instance and then divide all my screen-to-world converted vectors by this factor to properly place the healthbar, minimap indicator and soon-to-be-placed building resource production tooltip.

This is also why the Awake() function has changed (the objects hierarchy is a bit different so I had to update the Find() paths) and why there is no code to set the size of the selected unit panel subelements: it’s done via Unity’s UI Layout Element and other UI utilities 🙂


We’ve already improved our UIManager quite a lot in the previous episode to show the info panel for levelling up and call the upgrade system. But there are still two little features we can add to make the UI even more user-friendly:

  • for now, we aren’t displaying the attack range and damage of our units: we could add this to the selected unit info panel, beneath the production capacity of the unit
  • and it would be nice to be able to anticipate the impact of the upgrade: so we’ll also write the unit’s parameters if it levels up to let the player know if levelling up is interesting

Alright: let’s get to it!

Displaying the attack parameters

To make it easier to watch the current value of a unit’s attack damage or attack range, we should add those values to the UI.

Once again: I won’t go into all the details of my UI game objects setup in the scene. It’s basically a series of nested vertical layouts and layout elements that together show up the “content” (the top part of the menu, with the readonly info) and the “buttons” (the skills if there are any, the “upgrade” and the “destroy” buttons).

If you want to see exactly how it is set up, you can check out the project on Github 🚀

In terms of code, the idea is to do something similar to the production display, except that we’ll always be printing two lines, one for the attack damage and for the attack range.

This requires various variables and setups (we already have most of those but I’ll recall them so you see how similar it is for the new _selectedUnitAttackParametersParent reference):

Then, in our _SetSelectedUnitMenu(), we can display the values beneath the production info:

Now, if you run the game and select a unit, you will also see its attack damage and range 🙂

Showing up the impact of the upgrade

Once again, remember that it’s always better to cache the data as much as possible rather than recompute it everytime you refresh your UI. So, instead of our little _selectedUnitNextLevelCost variable, we’re actually going to need a simple data structure to hold all the relevant data:

  • the cost of the next level
  • the new production of the unit if it upgrades
  • the new attack damage of the unit if it upgrades
  • the new attack range of the unit if it upgrades

Thanks to this data structure, we’ll be able to compute all of this data once (when we update our production or when we level up) and then re-read it multiple times way more quickly!

We can define in our Unit.cs file, above the class:

And then, we’ll just add a variable and a method inside the Unit class to hold the data for upgrading the next level, and for actually computing this data:

I’ve basically extracted the middle of my LevelUp() function to a _GetLevelUpData() util method, and I then call this function in various places during my unit routine to make sure it is allows up-to-date and valid for the next level.

Now that our data is ready, let’s use it in our UIManager! First, let’s replace our _selectedUnitNextLevelCost variable: instead, we’ll directly access the LevelUpData.cost that is defined on our _selectedUnit instance:

And then, let’s update our _SetSelectedUnitMenu() function so that:

  • it accepts a new optional parameter, showUpgrade, that is false by default: if this parameter is active, then the panel will display the values of the upgrade unit; else, it will display the current values
  • it uses this parameter to change the display of the production and the attack parameters

We can even have the upgraded values be shown in light green (to clearly see it’s a specific display mode) using Unity’s rich text feature that lets us put HTML-like tags in the string to further customise how it’s rendered:

Here, I’m simply using the hexadecimal colour format to tell the UI to use a light green colour for the upgraded values.

Note: for this code to work properly, you need to make sure that the Text components on the prefabs you use all have the “Rick text” option enabled – otherwise, they won’t be able to interpret the <color> tag and it won’t display anything 😉

Now, we just need to pass in the flag in some specific cases, namely when we hover/unhover the “upgrade” button or when we click it and we still have some possible levels afterwards:

If you run the game, you’ll see that if you select a unit, the normal panel will show to display its current production and attack parameters. But if you hover the “upgrade” button, then the labels will turn green and show you the updated values 🙂

Last but not least: playing some SFX and adding some visual effects!

Ok – now that we’re finished setting up the actual logic and UI, it’s time to have some fun and just add a little bit of sound and VFX! 🙂

We’ll do all of this in our UnitManager and just create a new LevelUp() function:

And of course we’ll have to call this method from the LevelUp() function in our Unit script:

Adding a little sound

Now, playing a little sound upon upgrade is quite quick to implement thanks to how we’ve prepared it in our project. We just have to tell the audio source of the manager to play a specific audio clip! We’ll actually store this clip in our game global parameters so it’s shared between all units and consistent all throughout the scene:

Once again, I went to some open-source free sound libraries (in this case Mixkit) and got some nice short “level up” sound to play along with my VFX:

Showing up some visual animation!

That’s the real cool thing with video games: they are full of neat visual effects of all sorts! 🙂

Most of those effects are written thanks to shaders.

A (very) quick overview of shaders

Shaders are a complex thing. If you’re not very familiar with them, I recently started a new CG series called “Shader Journey” and the intro article has a small recap on what shaders are and how they work.

So make sure to check it out! 🙂

My unit level up shader

Here, I’ve decided to go for a “spiraly” shader that wraps around my unit as it levels up and animates for a second before disappearing:

If you’re curious, here is the shader that creates this visual effect:

As you can see, it has several input parameters (like the colours to use, how many loops there are, how smoothed out they are…). An important property is the _CurrentTime: this is a float that we will set from our C# script to make the shader animation run over time.

Other than that, this shader uses the additive blend mode and it is transparent, which is quite common for this type of VFX, and all the computation of masks and spirals is done in the fragment shader (the vertex shader simply passes through our data).

Preparing the right mesh

Something to note, though, is that this shader has to be applied on a mesh with a specific set of UVs: you need to have an outer cylinder “band” (i.e. without the top and bottom faces) where the entire band is UV mapped to a single rectangle.

This is different from the built-in Unity cylinder, for example, that separates the “band” in two rectangles. If I apply my shader to this objects, then I get a somewhat strange effect:

So I actually used Blender to prepare a little cylinder band properly UV mapped, exported it to the OBJ format and re-imported into my Unity project.

You can find this OBJ mesh it in the Resources/Imports asset folder in the Github repository 🙂

Using this shader in our UnitManager

To wrap this up, let’s see how to instantiate and use this shader in the LevelUp() function of our UnitManager. Before we can actually use the shader, though, we need to create a material that uses it:

You see that we have the various parameters we defined in our Properties block, and that the shader is located in the menu we set via the shader’s name.

Now, we can create a little prefab for our “level up effect” that takes the custom imported mesh and applies our new material on it:

And finally, we can update our UnitManager script. We’ll instantiate our prefab, cache some references and the use a basic coroutine to update our _CurrentTime parameter and “play” the shader animation. We also make sure to destroy any previously (unfinished) prefab, and we remove the object after its 1-second lifetime has expired.

To set our _CurrentTime, we can simply call the SetFloat() method on our material that sends this data to its shader 😉

Conclusion

Last week and with this episode, we implemented a basic level up system for our units! We can now buy upgrades to increase their production or attack parameters; we’ve seen how to define easy-to-tweak custom functions for the evolution of the modifiers for this system, and we’ve even added some sound and VFX using shaders!

Here is a little demo of what we’ve accomplished (x4 speed):

Next time, we will integrate some shortcuts in our game to access some actions more quickly!

Leave a Reply

Your email address will not be published. Required fields are marked *