Going About Testing in Unity ECS
With the Conversion workflow which has been recommended by Unity the method I suggested of instantiating a prefab will likely be deprecated. Archived items will still be available for reference’s sake.
Questions or comments I hear quite often when I mention I write unit tests for games are:
Why do you need to write tests when games are visual and change over time?
Can’t you just ask someone to help you test your game so you can catch bugs?
Those questions are quite true to a degree and there are certainly times when I think about not writing tests and just being able to move on to the next feature instead. But, more often than not, some of the pain points I had with scaling a game are:
- it’s easy to spot bugs with your eyes but it’s difficult to pin point/debug where the issue is
- with a small team and when we’re under crunch time, not everyone has the time to regressively play test features to ensure they work
You won’t completely solve those issues with unit testing but you can certainly alleviate the pain of those common scenarios by writing smart unit tests.
Game Background
So before I get into how my team and I go about unit testing, it’s worth explaining what our game, Project Arena (name pending), is basically about.
Project Arena is a 2D arena local multiplayer game where players choose cards between each round to defeat their opponent in battle. The game is played with a common local server, like your laptop, and smart phones as client controllers and secondary screens for each player.
I’ll certainly post more about this during the upcoming weeks/months.
Because we’re dealing with network code, we actually have to write tests to reason about our logic to ensure that we expect things to happen correctly despite the network connection. But in this blog, I’ll actually walk you through one of our test which tests our character’s visual effect and gameplay mechanic.
Note: From here on out, I’ll refer to visual effect as VFX.
We build our game using Hybrid Entity Component System (ECS), which is an architecture that allows us to separate data away from logic providing us ease of scale and performance benefits.
For a brief overview, Arturo Nereu of Unity has a nice comic strip which explains ECS in a digestible manner.
So much thanks and credits to him!
The Game Mechanic
The playable character is very similar to the mechanics of Megaman/X/Zero. The player can move, jump, shoot projectiles and use cards. Unlike Megaman, our player actually charges automatically since holding down a button on a phone is very uncomfortable while moving and performing other simultaneous actions.
Understanding the Data Behind Charging a Shot
Even though we use the Entity Component System, we still have dependencies on MonoBehaviour such as the Particle System.
In our case, we treat MonoBehaviours as simply objects with properties, with limited functionalities. Below is a table which shows the minimum amount of data we need to get the effect shown in the gif:
Implementing the Charge Shot Logic
So the Charge Shot behaviour is pretty simply, when the ChargeShotTimer’s Current time is greater than or equal to the max Wait time, the larger particle in the center (the parent) will play, otherwise it disappears. The child particle will always play when the Current time is greater than epsilon.
Epsilon denotes the smallest value, in our case we treat epsilon as effectively 0.
When shooting, the Current time is effectively 0, thus both particle systems will stop playing. This ensures that visually, our player does not look like they are getting a more powerful shot when shooting.
Below is the logic for our ChargeFXIndicatorSystem
, which handles what was stated above.
Since we don’t actually set up our VFX to be a child of Ellen, we internally map each Particle System by its player’s ID. This keeps our character prefab light with only the data we need and keeps our VFX as a separate entity.
But, what tests are we actually writing for the VFX?
The tests are quite simple with function names that are quite verbose:
- ChargingAttackShowsChildIndicator — if we charge a shot, we show our VFX’s child Particle System
- FullyChargedAttackShowsBothIndicators — when we fully charge a shot, we show our VFX’s parent and child Particle System
- ReleasedChargedAttackShowsNoIndicator — when we shoot, we show neither the parent VFX or child VFX
[ARCHIVED] Prepping Up Our Tests
One of the benefits of testing in Unity is that we can test with real prefabs we use in our game.
To use these assets in our tests, we built a utility function to grab a copy of the assets from the project. This is the code version of dragging a prefab into the scene!
Test Fixture Set Up
Now that we have the assets, we need to set up a clean environment to perform our tests. To do this, we create a text fixture, which has the bare minimum implementation of setting up and tearing down a clean environment for each test to run in.
This helps isolate our logic from the rest of the game/systems so we ensure that the logic works correctly on its own. Below is our minimal implementation of our test fixture.
All we do in our SetUp
function is we create a new test world and cache the previous, one so we can restore it in our Teardown
function after a test finishes.
To quickly recap we want to test the following scenarios:
- if we charge a shot, we show our VFX’s child Particle System
- when we fully charge a shot, we show our VFX’s parent and child Particle System
- when we shoot, we show neither the parent VFX or child VFX
Here’s our ChargeFXIndicatorSystemTests:
For each test we just have to set our ChargeShotTimer and ensure that the Current time is either greater than, equal to, or less than our Wait time.
- ChargingAttackShowsChildIndicator — we can reason that the Current time has to be larger than 0 but less than our Wait time, so we just set the Current time to be 1 second.
- We only expect there to be 1 entity available after the ChargeFXIndicatorSystem updates, otherwise our system is doing something odd and creating more entities than we need!
- As for our VFX, we should expect that only the child VFX plays and not the parent, otherwise our system’s logic does not operate on our data correctly.
The rests of the tests follow the same structure but just test other values of the Current time.
Some Closing Notes
So that is the general gist of how my team and I go about testing. We usually plan our systems and tend to ask questions regarding the game design and the data we need first. This enables to really understand our direction before we start writing any code.
We also don’t go about writing tests for every single case possible. We’re a small team, so we write tests for our primary essential features. This helps us iterate quickly on our infrastructure and be able to call out poor infrastructure choices we’ve made while we’re developing our game.
Unit testing certainly takes time and patience to do and by no means replaces manual QA testing. You’ll certainly need design feedback on your game, which unit tests can’t provide.
Some additional resources I read about unit testing that I found useful are: