itch.io is community of indie game creators and players

Devlogs

Unbroken v0.4.136 - Sounds Good

Unbroken - Mankind United
A downloadable game

Dev Diary: Update 4

Previous Update | Next Update

“Hey there, pilot!

I’m Liam from Recon. Good news! The whole fleet has been equipped with an all new next-gen sound system. From now on, your bantering with mission control will be crystal clear and the beloved “incoming missile” alert will be sweeter than ever…

Just kidding. You didn’t believe that, did you? Of course, management doesn’t care for your sound experience and went for the low-end budget versions.

Back to business, though. My team scouted loads of new alien ships charging in towards earth. Lots of little blips on the long-range sensors, but also a big one at the far end. Seems like the aliens employ Frigate-class ships now as well. Should be a tough battle. But you can handle this! You can, right?”

Liam Epps, Head of Reconnaissance, Unbroken Organization

Release Notes

Gameplay

  • 5 alien waves (including a boss at the end)
  • ships take damage when hit by a projectile (according to the projectile’s damage value)
  • ships take damage when hit by another ship (according to the other ship’s remaining HP)
  • player receives score point for killing/evading alien ships (according to the alien ship category)

Sound Effects

  • mission background music
  • explosion sounds
  • weapon fire sounds

Content Update

  • new weapon: AutoCannon (for player ship)
  • new ship class: Corsair Frigate (boss with lots of HP)

Tech Talk

Unity Scripting: MonoBehavior vs. ScriptableObject

Unity provides two types of scripts: MonoBehavior and ScriptableObject. By default, all scripts in Unity inherit from Unity’s MonoBehavior class.

A MonoBehavior script must always be attached to a GameObject within the Scene. As the name suggests, MonoBehavior is suited for defining the behavior of its parent GameObject. With a MonoBehavior script, you can easily specify logic for initialization via the Start() method or logic to be executed each frame via the Update method().

All of our controller classes inherit from MonoBehavior. This is because they handle the behavior of our ships, weapons and projectiles which potentially requires several activities to be processed each frame.

For example, each ship which is spawned during the game has its own instance of a ShipController, which in turn receives a ShipClass, a Weapon, movement behavior and fire behavior for the ship it is attached to and uses all of this information in order to maneuver the ship, fire its weapons, check for collisions with other ships/projectiles and calculate damage tanked by its hull.

In constrast, a ScriptableObject script serves as a blueprint, specifying all relevant information required to describe a certain entity. By instantiating that blueprint, we can create an arbitrary number of asset files, which do not depend on any GameObject or Scene, but can in fact be shared across multiple GameObjects, Scenes or even Game Sessions. As a result, ScriptableObjects are perfect data containers.

All of our data classes like ShipClassSO, WeaponClassSO or ProjectileClassSO inherit from ScriptableObject. That way we do not have to save data within code, but have everything available in the form of asset files, which are easier to manage and can quickly be replaced whenever we need to.

As an example, the ShipClassSO functions as a blueprint for ship classes, specifying the name, the speed, acceleration and agility and many more parameters.

With that blueprint in hand, we can design as many ship classes as we like. By instantiating the ShipClassSO 3 times, we create 3 ShipClass assets.

For each of these ShipClass assets, we define the values specified in the ShipClassSO script. For the ExcaliburFigherShip, this looks as follows in the Unity Inspector.

Spawning Enemies in Waves

Since we separated data classes (i.e. ShipClass) and behavior classes (i.e. ShipController) earlier, spawning different types of ships with different weapons, movement and fire behaviors at different coordinates and with different flight speeds is now actually pretty straightforward.

Besides spawning alien ships, we might even use the SpawnManager to spawn other kinds of events (i.e. stopping the camera flight for a boss fight). In anticipation of this feature, we create a ScriptableObject called “WaveEventSO”. This abstract class defines a Spawn() method, which doesn’t do anything yet but waiting for a certain delay. All events that we could think of in the future will inherit the ability to wait a specified amount of time from this class.

The first event type, we need to implement is the “SpawnShipEventSO”. This ScriptableObject exposes variables in order to collect all data required to spawn a ship, inlcuding ship class, weapon class, movement and fire behavior, spawn position and initial vertical speed. It will override the Spawn() method of its superclass. After calling the base function (which waits for the specified spawn delay), it builds a new alien ship according to its exposed parameters, then spawns it at the desired location and finally notifies the spawn manager of the fact that this spawn event has finished processing.

I also created ScriptableObjects for spawning formations of ships of the same type, that follow a leading ship along the same path at the same speed. There are already a set of formations available, which can be easily extended in the future.

Using these building blocks, we can create many assets that spawn ships in different ways. The following example immediately (without delay) spawns a ship of the Excalibur class, with a plasma cannon, a path that slightly moves to the left and a speed of 1.5 units per second.

In order to spawn a whole wave of ships, we define another ScriptableObject called “WaveSO”. It is defined by a number and a list of WaveEvents.

Using the SpawnShipEvents and SpawnFormationEvents, we can now build our waves like LEGO. The wave in the example spawns 3 formations of ships, each with a 3 second delay.

Finally, we create a “SpawnManager” script (which inherits from MonoBehavior) and encapsulates the general logic for spawning wave events. This script needs a public “waves” variable of type List<WaveSO>, which we can populate with our pre-built waves via the Inspector. It also needs a waveIdx to keep track of which wave is currently ongoing.

The logic of the Update() method (which is executed for every frame) is trivial: If the game is still running AND there are no spawns pending AND there are no alien ships left, THEN either spawn the next wave or (if it was the last wave), show the “Mission Completed” screen.

The SpawnNextWave() function increases the wave index, extracts the respective wave from the list of waves and triggers each of the wave’s events.

That is all we needed. Our wave spawning system is finished.

Sound Effects

We have implemented nice explosion effects, when ships or projectiles are being destroyed. But that doesn’t feel right, as long as everything is silent. We need weapon sounds, explosion sounds and a soundtrack playing in the background, while we play the mission.

First, let’s hunt for some suitable sound assets online:

Next, we need to assign these sounds to the GameObjects, that are supposed to make the respective sound. Unity provides the AudioClip class as a container for sound assets. Therefore, our ScriptableObjects for Ships, Weapons and Projectiles now each expose a new variable holding an AudioClip.

  • WeaponClassSO receives a new “Fire Sound” variable
  • ProjectileClassSO receives a new “Explosion Sound” variable
  • ShipClassSO receives a new “Explosion Sound” variable as well

In the Unity Inspector, we assign our sound assets to the newly created variables. As an example, the alien boss ship class “Corsair Frigate Ship” now looks like this (notice the new Explosion Sound at the bottom):

Now, our weapons know how they should sound when firing and our ships/projectiles know how they should sound, when they explode. But there is no system in place yet, that actually knows how to play the right sound sound at the right time. Let’s change this and build a SoundManager.

Unity provides an AudioSource class, that can play the sound of AudioClips. Our SoundManager will create 3 AudioSources during its initialization. We could of course go with just a single AudioSource and play different AudioClips with it. But if every type of Sound Effect has its own source, we can be sure that all of these sounds can theoretically play in parallel at the same time.

Additionally, we create 3 public methods for playing AudioClips which allow other scripts to trigger each type of sound. This is important, because the SoundManager cannot know, WHEN it is the right time to play a sound. It just knows HOW to play a specific AudioClip.

But that’s fine, since our controllers know exactly WHEN to play sounds. For the WeaponController, it is whenever the Weapon is fired. For the ProjectileController and the ShipController it is exactly when the projectile or ship explodes. Let’s have a look at our ShipController and trigger the sound from within there.

We already had a function that deals with the explosion. We can just extend its functionality to call the SoundManager and play the explosion sound that was assigned to the ship class, this controller deals with.

Regarding the background music, we can extend our SoundManager and add an extra AudioSource. Since the music plays continuously during the whole mission, we don’t even need an external trigger but can just play and loop the AudioClip when the game starts. As a little extra, I also added some options to balance the volume for each kind of sound, another benefit of using separate audio sources.

That’s it for today. See you next time!

Previous Update | Next Update

Download Unbroken - Mankind United
Leave a comment