Doers of Stuff.org

Indie Game Dev & Simulation Engineer | Unity + C# Wizard | Building Worlds, One Line of Code at a Time

Say it With Data

A good friend and fellow programmer once accused me of trying to replace all my code with just data. He told me it just wasn’t right to use dynamic data to rewrite the code on the fly. I didn’t actually accomplish this using Oracle PL/SQL, but I would later learn you could pretty much do exactly that with Perl. But I’m getting ahead of myself…

We continue our refactoring efforts by revisiting our wave system. I blogged about this before, but knew as I wrote the code it would be shortly replaced. This might beg the question of why I wrote it in the first place. The answer? Experience. It’s one of those things where you just don’t know what you don’t know until you know you don’t know it. Know what I mean? I wrote it so I could see exactly what it didn’t do so I would better understand what I wanted to begin with.

I had already run across this phenomenon once already when I created the side moving enemy. Initially, I actually felt bad. Like I was cheating the lesson by making a “new” enemy that was such an easy [programmatic] change to our existing enemy. Then I play tested the game and got my butt handed to me straight out of the gates. Turns out, tracking enemy objects from multiple directions at once is harder than I thought.

In the same manner, I wanted to see just how difficult it would be to manage and fine tune a wave system using a blunt force code, copy, paste add more code, method would be.

Not surprisingly, especially with my recent success with my spawn manager data struct, I found myself leaning towards a data solution. Something I can expose in the Unity Inspector. Something with which I can monkey with the values, even during game play. Ideally, something a smaller (if possibly more cryptic) code function can loop through to instantiate and manage each wave.

Say hello to my little Wave Manager!

    [SerializeField] private WaveManager[] currentWave;

    [System.Serializable]
    public struct WaveManager
    {
        public float[] _spawnRateRange;
        public int     _numberOfEnemyToSpawn;
        public WaveManager ( int     numberOfEnemyToSpawn
                           , float[] spawnRateRange
                           )
        {
            _numberOfEnemyToSpawn = numberOfEnemyToSpawn;
            _spawnRateRange       = spawnRateRange;
        }
        public float[] SpawnRateRange       
          { get { return _spawnRateRange;       } 
            set { spawnRateRange       = value; } }
        public int     NumberOfEnemyToSpawn 
          { get { return _numberOfEnemyToSpawn; } 
            set { numberOfEnemyToSpawn = value; } }

     }

Currently it has only two attributes. The first is the number of total enemy (of any type) to spawn for that wave. Remember, the Spawnable data struct will provide a weighted selection system for determining which enemy types to spawn. The second attribute is a simple array with two float values. These represent the lower and upper limits of the cooldown time before the next object is spawned. It should be noted here the current system will randomly choose an enemy OR powerup after each cool down. Thus, it is theoretically possible to spawn several powerups before the next enemy ship appears. However, given the ability to set different weights and change up the timing, I am fairly certain we can bully our way out of that being a regular coincidence. If it happens on some rare occasion, then I say we just treat it as “bank error in your favor.”

In the Inspector, it looks like this:

I could wish for a better label than Element 0/1 for the lower and upper boundaries of the Spawn Rate Range, but for now I’m willing to put that on the Round Tuit list for later.

We now end up with this as our driving function:

    private IEnumerator SpawnRoutine()
    {
        gameManager.CurrentEnemyCount += currentWave[gameManager.CurrentWave -1].NumberOfEnemyToSpawn;
        gameManager.WaveOver = false;
        while (currentWave[gameManager.CurrentWave - 1].NumberOfEnemyToSpawn > 0 && gameManager.GameLive) 
        {
            var index = spawnWeight[Random.Range(0, spawnWeight.Length - 1)];
            GameObject newSpawnable = Instantiate(spawnableObjects[index].Prefab);
            newSpawnable.GetComponent<ISpawnable>().MySpeed = spawnableObjects[index].MovementSpeed;

            if (newSpawnable != null && spawnableObjects[index].Type.ToUpper() == "ENEMY") 
            { 
                newSpawnable.transform.parent = enemyContainer.transform;
                currentWave[gameManager.CurrentWave - 1].NumberOfEnemyToSpawn--;
            } else if (newSpawnable != null && spawnableObjects[index].Type.ToUpper() == "POWERUP") 
            {
                newSpawnable.transform.parent = powerUpContainer.transform;
            }

            yield return new WaitForSeconds(Random.Range( currentWave[gameManager.CurrentWave - 1].SpawnRateRange[0]
                                                        , currentWave[gameManager.CurrentWave - 1].SpawnRateRange[1]));
        }
    }

It’s a bit much, I’ll grant. This is partially due to the messiness have having to now dive into the depths of the data structure. It’s also partially due to the fact I recombined several methods I had previously broken out into separate methods to increase readability. Bringing them back together again makes for a pretty dense looking block of code.

The complex values with all the dots and brackets I can probably simplify by turning into properties of some sort. The exact same code will still exist, but it would be hidden away so this particular method would not look so scary. I will almost certainly experiment with this but will probably let the code sit as-is for a bit and see if my subconscious comes up with anything clever first.

The minor increase in complexity is primarily due to finally combining both the enemy and powerups into the same spawning routine. This means I do need to branch slightly in the code based on the type of game object being spawned. Now, I might be able to remove some of that by pushing more into the data structure and better “encapsulate” each object’s knowledge of itself.

It’s also possible, these structs would be better implemented as proper classes, or maybe interfaces. Time will tell. I did already implement my very first interface, the ISpawnable interface which both the Enemy.cs and PowerUp.cs classes are expected to implement.

All of this does bring us closer to deciding how our game will end. Will we define every wave we want, and declare “Game Over” when you get through them all? Or perhaps we define the first few to introduce the cast of characters, and then define a calculation on the various values thereafter and make the game endless? Still unsure at this point.

For now, I won’t spend tooooo much time tweaking the data as I already plan later to work specifically on that. I do still need to complete my complement of enemy types which includes one new weapon type. But there is a pretty good chance that won’t be very difficult with this new structure in place. So yea, I’m kinda stoked at the moment. I’m starting to like my code again…

Leave a Reply