Today, let’s rework some of the game mechanics to fix & improve our RTS!
This article is also available on Medium.
It’s been nearly a year since I’ve begun this RTS programming tutorial series and, in all this time, we’ve worked on lots of mechanics: we’ve made units with a basic AI, we’ve designed a resource management system, we’ve serialised data in various ways, we’ve prepared lots of UI elements and dynamics, we’ve talked about sounds, and of importing external models…
Although there are still a few things I want to implement in the upcoming weeks, in this article, I first want to make the project more robust. So we’ll take care of some fixes for bugs I saw along the way, or that readers kindly pointed out, and even of refactors that could make the game a bit better.
Ready? Then let’s go! 🙂
Fix: all units can build… and units can enter an already placed building!
A few weeks ago, we improved the build mechanics so that, instead of instantly placing buildings on the ground via a simple click, we have some unit characters, the Workers, construct buildings over a small amount of time:
The feature works well overall, but the other day, as I was playing around with the RTS project, I discovered two annoying things:
- my Soldier units can actually “enter” the construction site… even though they have no build power at all (I set
buildPower = 0in their data Scriptable Object)!
- units can apparently re-enter a building after it’s already been completed: if you right-click an active building, then the character unit’s renderer is toggled off and you get some smoke VFX as if you had an in-progress construction site
These two bugs come from some missing tests in our character behaviour tree nodes… and even a missing sub-branch! Let me explain 🙂
First, let’s make sure that we can’t re-enter the build mode if the building is already alive. This is pretty straight-forward, since we already have an
IsAlive getter for our units – we’ll update our
TaskFollow node classes:
If you re-run the game, you’ll see that now the units can’t try to restart the construction of a finished building (but, of course, a Worker can still create a new construction site).
For the other bug, it’s a bit more complex. Remember that, a long time ago, when we worked on our character behaviour trees, we made some diagrams to preview and design our units’ behaviours, like this one:
I later updated this character behaviour tree when I added the building construction process – I said that if the unit had a non-null
buildPower, it would get an additional branch in its tree:
And that’s my mistake! At the time, it worked fine visually because I didn’t have any animation, so I didn’t catch the error; but, in truth, this means that a unit with a null
buildPower won’t be able to exit the “Follow” mode and clear its target if I ask it to go towards a construction site. Instead, it will stay in this “ready to build” state infinitely and keep on “following” the building target.
In truth, we do want this branch to exist even in the behaviour tree of non-Worker units… it’s just the actual timer and “Build” task that we’ll want to remove! We still need to check if the unit is in build range so that we can reset it to idle mode when it reaches the target.
This just requires a few updates in:
After those little fixes, you see that now my Soldier unit (that cannot build) will simply run towards the construction site and stop once it’s there, reverting to its idle state.
Fix: the minimap indicator…
To be honest, the minimap indicator feature is a big thorn in my side, since I feel like it’s not as robust as the other ones. Changing from the 3D to the 2D context requires some tricky manipulations, and it’s clear that it’s not top-notch yet.
Accepting the system’s limitations
At the moment, it’s quite shaky and doesn’t work well with all the maps I prepared… roughly put, the system we have makes it virtually impossible to handle maps with very wide ranges of altitudes. If you have big peaks and deep valleys, then the overall map wrapper collider will sometimes go above the orthographic camera and completely freak out the minimap indicator. The indicator will be offset by a strange amount and you’ll get inconsistent results between what the main (orthographic) camera is pointed at and what is highlighted in the minimap.
I thought I’d done something really wrong with the system and wondered where the bug could be…
But then, I re-opened the old Warcraft 3 editor and realised something. This editor doesn’t allow for very wide altitude ranges. It expects your levels to stick to the -2 (deep water) to +3 (high mountain) range, and it has a fixed 0-altitude reference.
The thing is that this limitations make a lot sense for RTS games: high peaks and low valleys could quickly result in sharp reliefs where you don’t actually see what’s going on, and since we decided we can’t rotate the camera, we’d be stuck with part of our maps being useless!
That’s why I decided to go with Warcraft 3’s mechanics and accept these rules.
We can fix our minimap system a bit so that it works for maps that respect the following criteria:
- the spawnpoints are at the reference altitude level – I put mine at
y = 30so that I have some extra depth both in the negative and positive directions
- there aren’t too-high or too-deep “sharp” areas: I will stick with somewhat “middle-range” altitudes
Assuming those rules, we can place our minimap collider at the reference altitude and make it very thin, so it doesn’t ever “gobble up” the camera; I’ll thus modify my
GameManager to initialise the collider as an almost-flat box at the given “mid” altitude:
Then, I’ll also update my util
GetCameraWorldBounds() function so that its raycast is longer and indeed reaches our new collider, even if it’s lower:
Finally, all that’s left to do is update my maps so that they do follow my rules, and I will get okay results for my minimap indicator!
But also: really fixing the camera “teleports” and limits!
That’s already a big improvement since it looks like these rules make for a more consistent and robust behaviour of the minimap indicator. There is, however, another problem that is still crippling this minimap: the click on the minimap to teleport the camera and the camera bounds when you move it with the arrow keys or by hovering the screen edges are currently off.
Those new bugs come from recent additions to our game and they are a good example of what’s called code regression: it’s when by implementing a new feature in a project, or by fixing a bug, you (in)directly introduce one or more other bugs. This is obviously quite tiring since it means sometimes you’re stepping backwards… There are various ways to mitigate these regression issues, the most common solution being to add sanity checks and unit tests to your project, for example.
Note: I won’t go into the details on how to do that here, but if you want to learn more about unit testing in Unity and project deployment, you can check out some of my other tutorials and articles like this one, or this one 🙂
The very first issue is that, now that we have several scenes loaded at the same time with multiple canvases, the
canvasScaleFactor value we store in our
GameManager instance isn’t correct anymore – Unity might get confused and try to get this factor from the wrong Canvas instance. Instead of looking for a “Canvas” game object, it’s safer to just add a direct reference to it in our
Note: of course, don’t forget to assign it in the “GameScene” Unity scene 😉
The second problem also comes from our new workflow: it’s because of the loading process we implemented recently. Since reloading a game session can potentially re-place the camera to its last saved position, we need to make sure that we wait until this initialisation is finished before we compute the camera bounds, else they might be off.
So, instead of doing our bounds computation in the
Start() method of the
CameraManager, let’s extract it to a separate public function:
And then let’s simply call this method at the end of the data loading process, i.e. when our
DataHandler class finishes its
Note: remember though that it can exit early if there is no data to load – so we also need to call our new
InitializeBounds() camera method if it does!
Finally, I needed to fix the UI ratio scaling in the
With that various modifications (and our set of constraints on the maps…), I believe we now have a working minimap indicator system! 🙂
Improvement: limiting building placement to discovered areas
At the moment, our fog of war system is pretty cool, and it does hide the enemy units plus the unexplored parts of the map as intended. But, quite often, these “explored VS unexplored” areas also determine where you can or can’t place new buildings for your base.
For now, we could ask a worker to go and make a new building in a dark unexplored areas:
This is a bit strange, and it would be better to enforce some boundaries for our base… which is actually quite easy to do with our current fog of war/FOV system! 🙂
Remember that our FOVs are defined using actual game objects in our scene, we have meshes anchored to our units that determine which areas we’ve currently explored:
In other words, when we move our “phantom” building and check if its placement is valid (in the
BuildingManager class), in addition to looking at the ground flatness, we just need to add a second condition and insure that it is also within a FOV thanks to a raycast:
Here, I’m basically offsetting my “phantom” building position along the vertical Y axis and then casting a ray down to completely get rid of any altitude delta issues; I also optimise my query to only look for my “FOV” layer, that is defined in my
Globals script along with the others:
We also need to make sure that these FOV objects aren’t counted as blocking collisions, so let’s exclude them for the collisions count in the
OnTriggerExit() methods of our
And finally, to really make all these collisions possible, let’s add a “SphereCollider” component on our FOV prefab:
If you run the game again, you’ll see that you can’t ask your Workers to go and build Houses or Towers in the unexplored areas of the map anymore 🙂
Fix: re-placing the camera ground target to get contextual sounds back!
Another bug I found while playing around with the RTS is that, at point, when we refactored our camera system, we forgot to re-integrate the update of the “ground target” position.
Remember, the “ground target” object is an empty anchor that is “hooked” to our camera and moves around with it to always stay in the middle of the screen, but projected on the terrain. This object has our “AudioListener” component, and it’s what allows us to hear some sounds “in 3D”, i.e. depending on our distance to the source (for example, the ambiance sounds of our buildings).
Well, at the moment, this anchor is just stuck at the exact same position as the camera, miles away from the terrain, so we can’t really hear those 3D sounds anymore!
The fix is luckily quite easy and quick to do:
- first, in our
CameraManager, we’ll add a private method to check and optionally move the anchor to the proper position (the middle-of-the-screen-projected point):
- then, in the GameScene, we’ll just drag the “GroundTarget” object into the newly created slot on our
And the issue is now fixed! 🙂
Refactor: picking better event names
Disclaimer: this was smartly noticed by Maxime – many thanks to him for suggesting this improvement!
We’ve seen throughout the tutorial that events are a very powerful way of having our various game systems interact without being too tightly coupled. They allow us to quickly trigger multiple effects from one common cause across the codebase, but they don’t impose the scene structure as much as Singletons or global references do.
However, a good event system requires a good naming of your events. And, right now, there are a few events that could be named better. In particular, some that are related to our UI like “UpdateResourceTexts” or “UpdateConstructionMenu”.
Those names don’t feel like we are reacting to a specific event; rather, they look like direct instructions for the receiver(s). It would be better to have something that describes the situation, such as “UpdatedResources”, and then have the receiver(s) handle the event their own way.
Also, the usual convention is to name events in the past tense, as in “this thing just happened” – so we should use “UpdatedResources” instead of “UpdateResources” 😉
Of course, this is an optional refactor, and it does imply quite a lot of renaming – but it makes for a cleaner codebase and this is always interesting in the long run…
If you want to improve the codebase, you can therefore do the following refactors, like me (remember to change the event name both when you trigger it and you add or remove the listener on it!):
- UpdateResourceTexts => UpdatedResources
- UpdateConstructionMenu => UpdatedConstructors
- UpdatePlacedBuildingProduction => UpdatedPlacedBuildingPosition
- HoverSkillButton => HoveredSkillButton
- UnhoverSkillButton => UnhoveredSkillButton
- SelectUnit => SelectedUnit
- DeselectUnit => DeselectedUnit
- UpdateUnitFormationType => UpdatedUnitFormationType
- PauseGame => PausedGame
- ResumeGame => ResumedGame
- MoveCamera => ClickedMinimap
To be consistent, I’ve also renamed the callback functions associated with those events, so for example in my
UIManager I renamed the function
Note that I still keep some events more in the “instruction” format for some very specific cases, like playing a sound, because I find it more intuitive – but you can refactor those as well if you prefer!
Improvement: properly timing the scene transition fades
A final improvement I want to make is on our scene transitions. We currently use a UI black panel to temporarily hide the screen and have a fade out/fade in effect – it’s good, but it’s not timed perfectly right!
There is no real issue with our main menu, because the delay between the moment the Unity scene is loaded and the moment we’ve prepared all of our objects is negligible, so we don’t see anything strange.
But now that we’ve made our game scene a bit more complex and that we handle reloading data, it can happen that the fade screen stats to disappear before we’ve completely initialised our scene (simply because the initialisation phase takes longer than before and can now be seen by the human eye)!
This sort of defeats the point of the transition screen: we see the fades but they don’t completely hide the load process – we definitely need to fix this 🙂
For now, we’ve told our scene switch logic to wait for the Unity scene load before doing the fade in into the new scene, with this bit of code (in our
The problem is that we should be waiting a bit longer now – we should wait until the entire scene has been loaded but also initialised. And the solution is once again to rely on some events.
First of all, I will remove my “EventManager” from the game scene “GAME” object and instead add it to the “Booter” object in my Core scene: this way, it will be available at any point in my game but I will still have only one instance at a time.
Then, I’ll update my
CoreBooter to look at a little boolean flag before starting the fade in, and I will register a new event, “LoadedScene”, to toggle this flag on:
Finally, I’ll simply trigger this new event:
- in my main menu, at the end of the
- in my game scene, from the
DataHandler, when I’m done reloading my data (like before when we initialised our camera bounds, we need to trigger it at the end of the full loading process or when we abort early):
And we now have smooth transitions again, that wait until the scene is fully loaded rather than showing a half-baked state 😉
There are still plenty of improvements we could think of to make our RTS more player-friendly: not deselecting the Worker if we click on an invalid area with a “phantom” building, adding some logic to go back to the menu from the game scene…
But this tutorial aims at describing a nice skeleton for a RTS and not a full production-ready product, so I’ll leave it up to your curiosity to explore those additional ideas 😉
Next time, we’ll talk about some optimisation to make our game more efficient and we’ll see some Unity tips and tricks!