Compromise for Maximum Overdrive
This blog post will introduce you to all the coding magic that's done behind the scenes to enable thousands of objects to exist with a Construct 2 game with little to no slowdown, and how to design your games to boost performance with these tricks in mind.
Starting with these tricks in mind from the very beginning will make every game loop you code marginally faster, without changing the design of your games to compromise for the limitations of an engine or any kind of code, and expand what you can achieve on even weaker platforms that you develop for.
Machine Minds, being a game built specifically for Mobile has needed to make compromises that don't have a negative impact on the player. These were compromises you might have seen in another project that I've worked on known as "Jell.io", which had a similar goal in mind, but went through development hell before I designed these tricks. But, now that I have, you might get the pleasure of learning them before you feel the hardships of not knowing them.
Starting with these tricks in mind from the very beginning will make every game loop you code marginally faster, without changing the design of your games to compromise for the limitations of an engine or any kind of code, and expand what you can achieve on even weaker platforms that you develop for.
Machine Minds, being a game built specifically for Mobile has needed to make compromises that don't have a negative impact on the player. These were compromises you might have seen in another project that I've worked on known as "Jell.io", which had a similar goal in mind, but went through development hell before I designed these tricks. But, now that I have, you might get the pleasure of learning them before you feel the hardships of not knowing them.
"Game Loop Segmentation"
What I call "Game Loop Segmentation" (for lack of a better name), is a method of planning specific blocks of code to run on separate ticks, giving precise control over the performance of any given project. This is just one core feature of any game engine I make nowadays, because this separation (which doesn't include functions involving input and physics) is negligible by most players, and boosts performance by *s, "s" here being the actual count of segments that exist.
Game Loop Segmentation is the first part of any code that I write, which is done within the main project code (the game.cpp's main() function if programmed in C++). I always have a tick counter (the amount of ticks since the game started) that is modulated by a tick segment count integer.
In the Main method/function, I do this:
tickcount += 1;
tick(tickcount%tickSegments);
This does only one thing: calls the tick function for every included script file in the project. If, in those included scripts, the parameter passed is equal to the tick we need to call the functions of that class, or file, we call those functions.
In Machine Minds, I actually have 5 core ticks. They're all listed below.
The line here separates a different kind of tick that I'll discuss further in a moment. The four on top are of the same nature that I've been talking about. They're segmented Game Loops. As you can see, I call 4 unique ticks:
- "game.action_0
- "game.render_0"
- "game.action_1"
- "game.render_1"
You might be thinking at this point: if each loop contains different functions, the duration of each loop might be marginally different than another. You'd be right. That being said, this method requires a good amount planning. You must know the bottleneck of the functions being called for each loop, so as to separate them all as equally as possible. In my case, each loop calls the same amount and similar kinds of functions. That being said, I'm somewhat aware of the duration for which each loop will take to process.
Keeping with the name of "Game Loop Segments", I'm going to demonstrate the heaviest blocks of codes within each tick respectively.
Now, looking at this code you might have noticed that one other tick has cropped up that I haven't yet discussed. This tick, being the "wave tick" is a tick that calls every second (or the variable duration) and performs actions for the towers and enemies. This "wave tick" is the slowest of them all, and the game does experience some hanging when hitting it right now, because all of this functionality is done on that tick for the sake of simplicity. Moving these to the action ticks would work fine, but I'll need to also add checks to determine whether actions have been called on a specific tick at a specific time, allowing control over the amount of times actions are done on a tick. For my design's case, this amount of control is necessary.
Despite all that, the implementation is easy. You can see that I'm calling really big loops (including a large amount of moving objects) without any slowdown.
Do Not Disturb
One piece of advice when handling a large amount of objects is shared in many places: Do NOT create a lot of objects at any given moment in time, instead, create the amount you know you'll need+a little more, and move their positions and change their appearance as you see fit.
In the case of Machine Minds, I never delete Tile sprites, for existence. And that little minimap at the top left is looped through to determine where tile sprites should be and what frame within the animation they should be. While you can't see it, there are actually a few tilemaps up there for operational use, and I'm just taking advantage of their 1 pixel per tile placement nature to make a visible tilemap with a viewport box.
These kinds of loops are used for Enemies and Towers too, but it's slightly different for the two. Tower sprites are created when the tower itself is created by the player, and never leave. Enemy sprites are created upon initialization of each wave, and are deleted after the enemy is destroyed.
The phases "Build" and "Battle" within my game are used as short pauses that call "init" functionality for the waves, and within those init's complicated code is being called that include filling arrays, spawning enemies, and managing events that'll take place later on in the wave.
The Damage Time Optimization
Another optimization that was made was the total exclusion of complicated "if enemy hit by X bullet" calculations. The tilemap based nature of the game's design allows me to tell the game to apply damage on each Wave Tick based on the amount of damage that exists within a "Damage Tilemap" that you can see has two orange tiles at the top left because the towers are shooting at those given positions. (Yes, I know there are some Z-index ordering issues here)
The tilemap itself actually has 8 tiles, and each index within it determines the amount of damage to apply if the enemy walks onto that tile.
The tilemap itself actually has 8 tiles, and each index within it determines the amount of damage to apply if the enemy walks onto that tile.
So... We Need a Stress Test
I'm going to run the game in debug mode within Construct 2 with a WAVE_POOL (the enemy count) of 10000 enemies.
Jarring Pause
Okay, so, 1000 objects on my low spec system results in 30FPS. This was a test done after seperating ai_action function calls by a groupcount, and incremented by a group integer to determine which group of AI's to perform functions on at any given moment in time. This works because gameplay in Machine Minds isn't fully updated until "wave.tick" is called. "wave.tick" is commonly called every second. If we have 30-60 FPS, we can create AI groups that are the size of the smalled Framerate we've gotten in the last couple of seconds, so in this instance, we could create 30 groups... I'm going to set it to this value and set WAVE_Pool (the amount of enemies) to 1000.
(BTW, that object count is the amount of objects within the game, not the amount of enemies. There are 500 enemies in the instance above, and 20 groups)
(BTW, that object count is the amount of objects within the game, not the amount of enemies. There are 500 enemies in the instance above, and 20 groups)
You're watching this in semi-realtime, folks. 1339 objects, an average of 30 frames.
Now the great thing about Construct 2 is that we have the handy profiler. This tells us where the slowdown is really coming from, so let's look at that.
Now the great thing about Construct 2 is that we have the handy profiler. This tells us where the slowdown is really coming from, so let's look at that.
So, looking at this, we might deduce two things: GAME is where we include our main ticks. That is, as it should be, creating a bottleneck for the game. But what I'm interested in isn't that. It's the amount of processing time it takes for [render] main to complete. And in that, I found a bit of a technical flaw in the code. It's a loop that loops through every 2.5D object in the game to determine z-order.
I'm going to disable that for just a minute. And rerun the debugger with a WAVE_Pool 10 times the size of the last test. Keep in mind, every enemy has movement and damage calculations. I'll create another loop for ordering that doesn't conflict with the current rendering system later, but just for fun, I want to see these numbers.
Okay, so 10000, 10 FPS. Not great.
But at 1000, the performance went back up to 60FPS. Seems good. ;)
While there is still some rendering glitches, and performances hacks that can be done later, this is a good note to end on for today.
While there is still some rendering glitches, and performances hacks that can be done later, this is a good note to end on for today.
Ultimately
These are things I'd advise all developers use when developing a game, even in a lower level language like C++. C++ might be faster, but boosting the performance of your game where you can, without adding much time to development, is always something to strive for. That being said, you shouldn't try to work these tricks into a game that's already in development (unless the game is really that slow), because a lot of these should be things you code from the very beginning. Rewriting code to fit these practices can see you spending many more months coding, especially for a larger game. Especially for "Game Loop Segmentation", because it is basically creating multiple game loops. If you code with one game loop in mind, you're coding everything WITHOUT the game loop in mind. You don't write "Do it on this loop" code, because you only have one Game Loop.