Adding a boss battle

GameDev Dustin
12 min readFeb 2, 2022

--

Here comes the big bad boss. The first real challenge to the player in our game.
What kind of boss should we make?

A big dumb bullet sponge? That’s the easy way, but usually not much fun for the player.

A super smart AI that will beat the player at chess, backgammon, and the game while they are at it?

How about somewhere in between?

What’s the plan?

So, it will be a lot easier putting this together if we have some idea what we intend to end up with.

The sprite I’ve chosen to use for my boss is a spaceship with 2 arms on either side.

I’ll be using sprites generated with “Animation Baking Studio” from 3D assets in the “The Big Spaceship Builder” and “Modular Sci-Fi Weapons” packages, all of which are offered on the Unity Asset Store.

Basic Attack

I’m thinking for a basic attack, a laser will fire from the end of each arm simultaneously.

I think I’ll have a randomized interval where most of the time it slow fires these 2 lasers, but sometimes fire them 3 times in rapid succession or something like that.

Secondary Attack

The middle of my boss sprite has a lot of area, I’m thinking I could spawn a player seeking mine or three underneath the main body of the boss sprite on a set interval.

They’d then slowly try to track down and ram into the player causing an explosion.

Boss Attack

For the boss attack, I want to do a modified ram the player attack.

Unlike our normal enemies which when in a certain proximity will seek to ram the player, our boss will suddenly rotate towards the player’s position.
Then some sort of graphical representation of “charging up” or “preparing to ram” will occur so the player can clearly tell a boss attack is incoming.

When the charge up timer ends, the boss will ram in the direction where the player was when it began the attack, regardless if the player is still there or not.

It will then return to its default position at the top middle of the player screen.

Prep work

I’ve generated several versions of 2D mines with various light emission captured and even left/right versions.

I then created quite a few prefabs using these sprites.

If I had a more clear idea of exactly what mines I will be implementing, I could have just created and prefabbed those but I’m still in the brainstorming stage on mines.

With all of these options I have here I should be able to do whatever I need.

Defining the Boss’ Secondary Attack

Our Boss’ secondary attack will utilize these mine prefabs I’ve generated or at least some of them.

Now I want to make some decisions on the mine behavior so that when I go to build this out in code I’ll have a clear idea of my goals.

First of all, I want to randomize between different mine deployments when the boss calls it’s secondary attack.

1) 3 player seeking mines are deployed to hunt down the player and explode on contact.

2) Multiple mines deployed that move in a set half circle pattern, moving straight off of the player screen, but exploding on player contact.

3) 2 player seeking mines that stun the player on contact.

Timing is Everything

To keep our boss from doing too many things at once, I’ll need to keep track of the timing very carefully.

I’ll steel from our SpawnManager script the method we’ve used to spawn waves based on timing.

Instead of new waves of enemies, we’ll initiate new boss actions.

Tentatively I’ll time these actions as follows:

1) Every 10 seconds, aim and fire lasers at player

2) Every 30 seconds, deploy a random mine attack

3) Every 45 seconds, attempt to ram the player (disable all other attacks temporarily)

It isn’t just our overall timing of attacks that have to be timed though.
The ram player attack will have its own set of timings.

First, we will need to aim at the player.

Second, delay of 3 to 5 seconds (playtesting required).
Third, ram attempt.

Fourth, return to home position.

From blueprint to building

Now that we have our so-called “blueprint”, let’s build this thing.

I’ve created a prefab based on the sprite I generated for our boss enemy.

I’ve also created an empty script, “Boss1” and added it to our Boss_1 prefab.

We have to add a collider, and in this case I’ve decided to go with something more computationally intensive since this will be only 1 object on screen and I want the player to see the hits on the enemy ship appropriately and not just in a big circle or square around the ship.

I’ve also added a rigidbody2D and Audio Source.

For organizational purposes, I’ve created and empty “Bosses” game object childed to the SpawnManager game object.

When we instantiate our Boss_1 prefab, we’ll parent it to this Bosses game object.

Modifying our SpawnManager script

I’ve added variables to the SpawnManager script that replicate how we have triggered waves 2 through 5.

We also need a reference to our “Bosses” game object for parenting purposes and the Boss1 prefab for instantiation.

To make it so that the enemy spawn routine no longer triggers during the boss wave, I’ve added a _bossWaveActive Boolean.

Adding a _victoryGameOver Boolean allows us to reset the waves on a new game.

We also need to assign our “Bosses” container game object to the SpawnManager script component.

Unlike the other waves which use coroutines to spawn enemies our powerups at certain intervals of times repeatedly, I only want to spawn our Boss1 once.

So a normal method rather than a routine is used.

In our Update() method I modify or if else statement to check for _startBoss1WaveActive = true and that the _spawnBoss1WaveDelayTime has been met.

We display an appropriate message, run our new SpawnBoss1() method, and make sure to set our wave active status to false so we didn’t spawn another Boss1 enemy.

We also want to make sure we don’t spawn any more enemies so we set _bossWaveActive = true.

We make use of our new _bossWaveActive Boolean variable to prevent enemies from spawning when the boss wave is active.

Our StartSpawningWave1() method is triggered by the Asteroid explosion at the beginning of the game and immediately sets the appropriate wave times through SetWaveTimes().

We add our time stamp for the Boss1 wave to the SetWaves() method and call the StartNextWave() routine to start the boss wave at the appropriate time by setting our start Boolean to true.

At this point, our Boss should spawn in at the appropriate time and enemy waves will stop spawning.

Now its time to work on getting our Boss to do its thing.

Modifying Boss1 script

As usual, we’ll need to add some variables to our script.

There are a lot of variables here, and truth be told, there don’t have to be.
Mainly most of the mine related code could have been moved onto the Mine script.
This would require telling the mine script what type of mine it is and what movement type it is to do.

Very doable, but I’ve kept all of the boss related attack logic and movement in boss1 script.

I’ll be diving into the variables for each attack type in separate articles covering each boss attack, so let’s briefly go over the general variables.

First of all I have variables that need to be assigned in the Unity editor or assigned in the Start() method so that I can interact directly with our SpawnManager and Player scripts as needed.

Next I’ve got all of my necessary prefab Game objects for different mine types and the laser prefabs.

Under the general boss variables section I’ve got _playerHasDied to use in null checks to avoid unnecessary errors.

I’ve also got boss health, Booleans for rotating the boss ship towards the player or towards a target, and targetPosition for use with aiming at a specific target.

After that, there is a section for each type of boss attack’s variables, and then a general boss action section.
In that last section we use these Boolean variables to keep our boss from attempting to do every attack all at once and keep everything timed appropriately.

The Start() method

In our Start() method we do our on the fly assignments for the SpawnManager and Player game objects and scripts.

I then call my now customary DoNullChecks() method which will Debug.Log anything that is null that shouldn’t be.

Testing Purposes

It is a lot easier to test out each boss attack individually than to try and test them at creation with the overall boss timing logic that triggers each boss attack.

So, these Coroutine method calls are only used when I’m trying to track down an issue or verify that a feature is working.

In doing so, the boss will spawn in as usual, move down to the middle of the screen, and then run whichever attack I’ve called here and only that attack.

For my testing code to work, I have to disable the boss attack timing logic in the Update() method as shown above.

The coroutine methods called in our Start() method for testing uses these 3 coroutines to limit which boss attack will occur.

Since these fire off in the Start() method and our boss ship needs time to move down from top of screen to the start position, we start with a 4 second delay before initiating our test attacks.

Further, as seen in StartMineDeployment(), since there are multiple mine attacks we can control specifically which mine attack to test.
The same goes for the StartLaserAttack() method.

For Ram player there is nothing further needed to refine down to what I’m testing.

General methods for future use

The OnTriggerEnter2D() method is still a work in progress, the values for damage are especially likely to change.

This also need to trigger the player LoseLife() method.

There are so many times that the boss will need to either rotate towards the player or rotate towards a specific target location such that I’ve created their own methods to avoid an overabundance of duplicate code.

This script is already pretty big!

Right now the OnDeath() method is incomplete, but I do know it will need to destroy the boss game object and tell the ScriptManager that the Victory condition has been achieved.

Starting the Boss wave

Once spawn manager starts the boss wave, it instantiates our boss game object above the screen.

The Update() method will check the alive status of mines every frame update as this is necessary for moving them around and determining when they have all been destroyed.

The first thing I want my Boss enemy to do is move down to 0, 5, 0 on the player screen.

This will also be necessary after the boss has moved out of position, such as when it does the ram player attack.

Everything is wrapped in a player null check to avoid the game continuing on after the player has already died.

After that, I need to know if the boss is above the screen or at least above the starting position which is checked with an if statement.

If it is, I don’t want the boss to take any actions, so _disableBossActions is set to true.

And we move the ship down this frame modified by Time.deltaTime.

If the boss is not in the process of moving down from the top of the screen, I need to set a few flag variables to allow boss actions to occur when appropriate.

After verifying the boss is in the start position, we check that several different flag variables are false and thus ready to start a new boss action.

The first thing is to set our _bossActionInProgress flag variable to true to prevent another boss action from firing off in the next frame update.

I’ve used Random.Range to randomize what attack the boss will do.
This allows me to “weight” the odds of each attack as well.

Laser attacks will occur roughly 30% of the time, Mine attacks 40% of the time, and Ram player attacks 30% of the time.

Additionally, as will be covered in future attack specific articles, there is more randomization for which mine attack and which laser attack will occur.

This creates a very unpredictable sequence of events which will hopefully be more engaging for players.

After we have initiated an attack or determined an attack is in progress, our Update() method moves around any game objects that require it.

This includes moving mines either in a player seeking manner (more on that later), in a pattern, or moving the Boss ship to ram the player, or rotating the boss ship towards the player or a specific target.

All of this must be done in the Update() method since movement is a continuous operation.

EndBossAction() method

One last method to cover in this article is the EndBossAction() method.

It can be difficult to track when a mine attack is over since the player can either kill the mines or just keep outrunning them.

Additionally, I don’t want the boss to immediately start another attack if the mines are immediately destroyed.

Using this method I can set a delay in seconds that either matches the known time allotment for a mine attack, such as our 2nd mine attack.

If player seeking mines are active, this will allows us to set a certain amount of time that even if mines are alive I want the boss to move on to it’s next action.

That’s it for this article, I’ll cover the Ram Player attack in the next one!

--

--

No responses yet