Let’s dive a little bit into Infinite Cosmos, our SHMUP for Android and iOS and how we handle instantiating random groups of asteroids, or seemingly random groups of asteroids in a cluster using just a single group of the exact same asteroid object.
Let’s take a quick look at what we hope to achieve before diving into the game logic that drives this randomness – here’s what one of our asteroid groups look like before they are instantiated into a running scene:
They all look exactly the same. Same sprites, same configuration, same count. Now let’s hit the play button to see what they look like after our logic takes over:
See how they all changed their sprites, configuration and count? Let’s stop and hit play again:
As you can see, the asteroid group changes every time logic begins to run. How are we doing this? We simply throw a few random calls to set a single sprite, collider size, if it lives, etc. Let’s break this down into our execution order with the first order of business – should it live?
The first thing we should do is decide whether or not any individual asteroid is going to live or die, doing this ensures that no other calls are completed before this action, saving precious (or not so) processing cycles for other things. Why bother swapping sprites, rotation, collider sizes, etc if we are just going to kill it?
We use our game’s internal difficulty level to dictate the chance that any one of these will be destroyed. You can set any chance dynamically with math or hard code it per difficulty. We chose dynamic as it allows us to set it and forget it. With each difficulty level increase, the chance that an asteroid will be destroyed is dropped slightly so the higher the difficulty the larger the clusters should be. When a group is instantiated each asteroid rolls a random value against this number and either passes or fails, destroying itself.
After this first phase is complete we randomize the sprite that will be used for each asteroid. This is simply done by another random roll using an INT and throwing that against our Sprite array. It looks something like this:
int x = Random.range(0, sprite.length);
_renderer.sprite = sprite[x];
What does this do? We get a random number from 0 to whatever our array length is. Since the high number is always exclusive and arrays start at 0, this is perfect. An array size of 7 will always return 0-6, perfect. We then assign the X that we just assigned to the array value of sprite. Why not just throw the Random inside of the sprite like:
_renderer.sprite = sprite[Random.range(0, sprite.length)];
Because we are not done using x just yet.
Now that we have x, we know what size our collider needs to be for that particular sprite that was just assigned. We don’t need pixel-perfect collision on these objects as they are purely secondary so we simply just use a circle collider and adjust it’s radius based on x. If x is larger than 4, increase the collider’s radius to y. Simple and effective. That’s life, sprite and collider set.
Let’s set them spinning off in a random rotation since if they just sit still it won’t look any kind of fun. We simply choose a random rotation value from (-z, z) and apply rotation. Now we have a minimum and maximum rotation speed that either rotates counter-clockwise or clockwise based on the returned value from the dice.
BUT WAIT, THERE’S MOAR!
We aren’t done yet. Let’s take this group functionality a little bit further by giving the player an extra bit of points based on the size of the cluster when they destroy all objects in that cluster before they move off screen. This, is also incredibly simple and there are a few ways to go about it: subscription, direct.
You know me, screw subscribing to things but I’ll tell you anyhow. In Unity3D, you can use Transform.childCount to return the number of GameObjects attached to any parent object. This is fine and good and well and OK. You’d simply check every frame if that count is empty, null, zero, less than 1, etc.
Let’s not do this. There’s no reason to count every frame the game is running if children still exist. We really should only be checking this when an asteroid is destroyed. So in our group controller we create a RemoveAsteroid() function which checks the number of asteroids left in that GameObject and if less than 1 (0, null, etc), we send the bonus points to the score controller and destroy the parent object.
Again, we can make this work better still. Why use Transform.childCount every time one is destroyed and recount? Rubbish. Let’s just create an int to hold data sent to it by the children asteroids. Now we go back to our asteroids – after all is said and done and we are sure the asteroid is alive and kicking off-screen, we simply have the asteroid tell the parent to increase its count int we just made by 1.
In our function to RemoveAsteroid() we simply remove 1 from the count and check to see if it is less than 1. This prevents checking every frame using Transform.childCount and prevents recounting when an asteroid is removed. We can also vary the amount of points given to the player based on how many asteroids were destroyed in the cluster when they are totally destroyed or a static amount. For the former we can always use simple multiplication or addition to add an X point value to a bonus point value whenever an asteroid is destroyed. When the cluster is gone and the parent is about to be destroyed, send that value to the score controller.
- Check to see if an object should live
- Increase the object count on the parent by 1
- Set the sprite
- Set the collider size
- Set random rotation
- On child destroy, remove 1 from the parent count
- On parent count 0, send points if we do that sort of thing and destroy parent
You can do this for enemy groups, as well! Spawning enemies into groups or however you like. It’s simple logic to randomly select a few parameters and then some small-time counting on the parent. It’s effective, simple to code, easy to read and understand and without subscriptions, recounting – it’s lightweight.