Hey y’all~! ^,,^
Well it’s been a busy heccin’ time for me! But most of it’s been within my personal life keeping me super dang occupied, so unfortunately not a huge amount to discuss here! I did start studying again in the (limited) free time I have, so that’s been interesting to say the least. Just small online certifications, but it’s been very fun to commit to learning again! I’ll be taking a pause from that until I’ve caught up on all my work though, particularly my work with PokéStudy and roll.
On the mention of roll however, I did manage to make a rather impressive update to the game! As such, how about another tech-update!
- World Generation – Prediction: I’ve mentioned before that roll is a game about racing through and exploring a near-endless labyrinth-like world. Previously, as the player would progress through the game, it would check the player’s location and decide whether it needed to render more surrounding territory. In terms of optimisation, I had mostly focused on the object instantiation functions, the process of spawning the actual objects into the game. For good reason I might add; spawning and activating objects consumes a LOT of processing in mobile games, and I have since gotten to a point where I am happy with how it was handled (I won’t go into detail here, you can read about it back in [Post 88]~). But there is always something to improve. 😉
Between the mad-rush of my work that was February, I had a brain-thought; what if the game knew every part of the world that needed to be spawned in? The game generates from a seed, there is very little in terms of world-generation that needs to be calculated during play-time. Honestly, this is a very standard idea; why calculate when the player is interacting when it could be done during load-time? I hadn’t considered this possibility because I was so focused on keeping content and data to a minimum, and as such pursued a path of “Only generate what we need, what we see.” It worked well mind-you, all it had to do was check the relevant seed value, grab the appropriate ID, and then instantiation would take it from there. Tiles generated like this were kept track of in a special list, however this meant that if the player had explored quite a bit of this world, they may end up with quite a substantial list size, which in-turn could make other simple checks slower, such as checking if a tile at X/Y location had been spawned in or not.
Okay, so it isn’t a huge issue, but my idea was still relevant; how can we predict the whole world at load-time instead of play-time. Well, for starters, we need to understand that regardless of our approach, we go from an ‘infinite’ world to a ‘finite’ world. Fortunately, the method I figured out gives me pretty good control over setting that limit to notably further than what should be physically possible. Additionally, having a hard-limit on the world size is a good thing for a mobile game; on the near-impossible chance someone did go insanely far into an infinite world, it is likely it would crash the game due to the limited capacity of a phone.
Before I explain the process; it needs to be mentioned that the world is calculated as an X/Y grid, with each segment of that world existing along set X/Y coordinates. So, if we then set a limit to the world, the grid size, we can then assume we are able to predict the full set of information within those limits! After reading through both MSDN and several pages on data performance, I wrote up a Quadratic Jagged Byte Array, which is me being fancy and saying I have 4 groups containing all the X/Y information in the world. What happens is at load-time, the game checks EVERY grid-coordinate and allocates a byte-value that represents the ID for that area. This means, during play-time, all the game has to do is say “What is at position X/Y?” and it gets the answer immediately, instead of having to calculate the world data during play-time.
Some important notes about this process:
- 4 Jagged Arrays: Firstly, a Jagged Array is essentially an ‘Array of Arrays’, so when we look for an X/Y coordinate, we ask the Array “What is at element X, and of that group what is at element Y?” Additionally, because Arrays are static (their size is set during creation and can not be altered) it is very easy for the program to look at a set spot. But, why FOUR? Well, because the world also has negative X and negative Y axis. I could definitely have done this in a way to have a single Array for everything, but it was much easier to simply say ‘Abs(-X)’ in the negative X Array instead of building the Array to account for negative elements, which in-turn would have meant always having to adjust the look-up value.
- Bytes: Why use bytes? Easy, bytes are small. C# generally likes to work with integers which are 32-bit numbers, whereas a byte is an 8-bit number. As such, there is an upside and a downside to using bytes over integers. As I said, the upside is space; if the grid-limit is set to 250, this means the world will have 250,000 entries. As bytes, this is 0.25Mb of processing just on holding this data! This isn’t heaps but it’s enough to think maybe we’re pushing it for something so simple. But what if these were integers instead? With integers being 4 times bigger, this takes those Arrays up to a whole 1Mb of processing, just to hold those values! Again, phones have gigabytes of RAM, why is this important? Because every bit counts, pun intended. In a mobile game that is already doing some crazy stuff generating fake lighting and a procedural world, storing a few simple numbers here and there needs to be as small as possible. So, whats the down-side? Well, a byte can only be a number from 0 to 255 (inclusive), so it means I can only have 256 possible combinations of information. This is made trickier because…
- Dual-Purpose: I said earlier, the game also needs to know if a coordinate has already been spawned in or not. Originally, there was a ‘Linked-List’ that the game would scan through; if it found the X/Y value, the coordinate already existed, otherwise nope. But this takes longer and longer to do the more of the world gets spawned. I could make another 4 Arrays, but that then DOUBLES the processing demand of what we already set-up! So… I rebuilt it to use the same array! Let’s assume there are 128 possible values for world information in a single byte in the Array. When the game looks at the info, it spawns it into the world, and then increases the byte-value by 128. Next time the game looks at the info, is sees the value is above 128, and as such, knows that coordinate already exists and ignores it! The downside to this additional implementation is it limits my possible values to 128, but honestly, this is very close to the number I was looking for already, so I’m not disappointed. 🙂 Plus, if it becomes an issue, I can rewrite the coordinate check into a Bit-Matrix instead, but for now this works perfectly~
Overall, this change simultaneously makes a very small, yet very large difference to the gameplay. While the optimisation improvement is, honestly in some cases negligible, it also maintains a more consistent performance even for slower devices, reducing the number of called functions per tick, and removes the possibility for generating lists that are too big for the device or game to handle. Yes it applies restrictions, but over the many years in game-dev, restriction in technology has often promoted innovation; how do we make the most of the limitations we are working with to make something bigger than it should be? When I started making this mobile game, I was making it with a PC dev’s mind, but slowly and surely I tweaked, optimised, changed, upgraded little-by-little. Is the game perfect? Hecc no. Is it ready? Months still to go. But have I learnt a lot, am I proud of what I am doing, and is it getting better and better? You bet.
That’s it, I gotta go to a meeting! Be wonderful and kind! Black, Asian, indigenous, and queer lives still, and always will, matter! Stay safe, and till next time, all the best~!