Audio design and C# scripting

Hello lovely chaps!

Today I want to discuss some Audio design choices through scripting in C#. I recently finished a course on video game audio production using Wwise. It was hosted by Berklee, it was awesome and you can check it out here!

One of the projects we did was AngryBots. If you haven’t played it, the game consists of hacking terminals to get doors open. At a point in the game you’ll obviously face the final door. I thought it would be a good idea at that point to emphasise the importance of the moment, and music by itself wasn’t just cutting it, as it would have been changing only after entering the final door. So I decided to tie the Final_Door_Opening event in Wwise to a custom transition for the playing music, so that it will stop but not abruptly. This left some space for the door sound (which is quite dramatic) and a nice tremolo strings pad (to build some tension). Now, that alone wasn’t enough. It was just a tremolo string pad going on with no change whatsoever and it was feeling repetitive after a bit. So the idea was to tie the volume of that tremolo strings to a RTPC (Real Time Parameter Control). RTPC are basically values that the engine (Unity3D in this case) sends to Wwise. The choice on what would have controlled the volume change obviously fell onto the distance that separates the player from the door. But there was no script to tell me how much that distance amounted to. Even worse, there was no script that would send such an information to Wwise. So I decided to get my hands dirty and do it myself, and here I’ll show the code I used and I’ll explain it bit by bit and I’ll assume you have no programming knowledge whatsoever.

So, we start with the header:


using UnityEngine;
using System.Collections;

This is code that goes before all the other code. It is important because basically it tells us (and the compiler) what are the libraries that we are going to use. Imagine you’re a cook. The header tells you were you’ll find the ingredients for your recipe. It’s like saying: “using Fridge;” or “using Cupboard.SecondShelf;”.

Then we declare the name of the class.


public class DistancePlayerDoor : MonoBehaviour {

We have a few privacy options here, but for simplicity and for easiness while dealing with Unity we’ll go with public. This means that all other classes can see and use material from this class. It’s like a library. Some rooms are locked, some aren’t. You have access to the books in the unlocked rooms, and we are basically saying that people will have access to this room. Also, in this case, this class inherits the properties from another class which is called MonoBehaviour. This basically means that our room will have the same furniture disposition as the MonoBehaviour room, and that we will move around this room just as we would in the other. This also means that if MonoBehaviour is a silent room, so will be our DistancePlayerDoor.

We can now proceed in declaring the variables that we’ll need:


private float distance;
public int MAXDIST = 30; // this is set to public to tweak within Unity3D inspector
private bool isDoorActive;
private Vector3 doorPosition;
private Vector3 playerPosition;
private FinalDoorUnlocked finalDoorUnlocked;

As you can see I set all of those variables to private but one. That public variable MAXDIST is my weighting factor and I’ll be able to tweak it from Unity to define the area in which the volume change will happen.

All the other variable names are quite self-explanatory. So distance is the distance between the player and the door, which is what we are looking for. isDoorActive tells me wether the Final Door can be opened. If you remember, to open the doors, the player has to hack some terminals. This doors requires two terminals to be hacked for it to open. So we’ll just check wether this is open or close and store the answer in this variable. doorPosition and playerPosition are the co-ordinates in the 3-dimensional space for the player and the door. We’ll need it to calculate the distance. Finally, finalDoorUnlocked is an instance of the FinalDoorUnlocked class. This basically means that we are referencing another script. In simple words (the library example) we set up a camera to see what’s going on in the room called FinalDoorUnlocked and we can see it on our smartphone through an app that we called finalDoorUnlocked. Take this Watchdogs!

Now it’s time for the Awake function.


void Awake (){
     finalDoorUnlocked = GetComponent<FinalDoorUnlocked> ();
     isDoorActive = finalDoorUnlocked.isDoorActive;
     doorPosition = GameObject.Find ("LockedDoorFromHellDouble_3").transform.localPosition;
}


The Awake function, is a bit of code that gets called whenever an object is made active for the first time. Often this is at the start of the game, but that is not always the case. In the Awake function you assign a value to the variables you defined earlier on. So it’s basically like filling up the library for the first time and deciding where to put each section: “Narrative” goes here, “User’s guides” goes there, “Comic books” over there, and so on. They can be rearranged in the future, but you’ve got to have a starting point. (Do you see the point in setting variables as private now? People won’t just take your books and put them back in the wrong place!). So, the first thing that we are doing is to take our smartphone, open our finalDoorUnlocked app and turn on the camera. This is done through the GetComponent method, which does exactly what it says. It gets a component from a gameobject. In this case our component is another script, which is called “FinalDoorUnlocked”. One of the properties of this script is isDoorActive. Guess what it tells us?! Exactly! So we store that value (which is true or false) into our variable called… isDoorActive. Beware that this second variable is relative to our script and it’s different from the other isDoorActive! It’s like copying the settings of your friend’s smartphone. Same model, same brand, same settings, different phone. The last thing we do is to find the co-ordinates of the door. As it will not be moving during the game it is alright to do it here and call it a day.

Obviously when playing a game things changes rapidly, and we want our scripts to be up to date with what’s going on in the game all the time! Lucky we have a method to do that. Guess how it’s called?! Yes, the Update method.


void Update(){
     if (isDoorActive != finalDoorUnlocked.isDoorActive) {
          isDoorActive = finalDoorUnlocked.isDoorActive;
     }
     // if the door is activated
     if (isDoorActive == true) {
          playerPosition = GameObject.FindGameObjectWithTag ("Player").transform.localPosition;
          Debug.Log ("player position is: " + playerPosition.x + playerPosition.y + playerPosition.z);
          // call player distance from door calculator
          distance = DistanceCalc (playerPosition, doorPosition);

// distance handler: puts it in the correct range if it's out if (distance > MAXDIST) { distance = 100; } else { distance = distance * 100 / MAXDIST; } // call function for rtpc "Distance_Player_FinalDoor" to Wwise AkSoundEngine.SetRTPCValue("Distance_Player_FinalDoor", distance); } }

Now this is juicy stuff!!! Woah woah woah, let’s slow down a bit and tackle this bit by bit. We’ll actually start from the end of the code to understand what’s going on. So: why are we writing this code in the first place? To send a value to Wwise under RTPC form. So let’s examine this bit of code:


AkSoundEngine.SetRTPCValue("Distance_Player_FinalDoor", distance);

This is a function that is found in a script provided by AudioKinectic (Wwise’s developers) that allow us to send a RTPC value to Wwise. As you can see it takes two arguments: the name of the RTPC as set up in Wwise, and the value that we want to pass, which in our case will be stored in the distance variable.

But the thing here is when will we need for this parameter to be passed? Do we need it always? Or just sometimes? Of course we want this to happen when the Final Door has been unlocked. And so, just on top we write the conditional statement:


if (isDoorActive == true) {

The problem that arises now is that we don’t have any value for the distance between the door and the player yet! Come to think about it, where is the player exactly?! Let’s track her down!!


playerPosition = GameObject.FindGameObjectWithTag ("Player").transform.localPosition;

This makes use of the “Tag” feature in Unity. It’s like having a handy GPS that always tells us where the player is. In this way we can then ask for the informations contained in the transform component, in particular we’d like to know the localPosition, i.e. the player co-ordinates. Ok, now that we know where the player is let’s calculate the distance. We’ll write a method that’ll do that for us, but we’ll do that later. We’ll call it DistanceCalc, and it will take in 2 arguments, namely the player position and the door position:


distance = DistanceCalc (playerPosition, doorPosition);

Great! we have now have the distance! We also know that our RTPC will have a value from 0 to 100, so what we’d like to do is to handle the distance value (which will be in game units) and make it a percentage. So obviously the closest the player can be to the door is 0. The furthest will be MAXDIST (look who’s showing up here, hello!). So, things can work out in 2 ways. The player is farther than the MAXDIST, or the player is in between MAXDIST and 0.


if (distance > MAXDIST) {
     distance = 100;
}  else {
     distance = distance * 100 / MAXDIST;
}

In the first case we just set the distance to be the maximum possible in Wwise. In the second one we use the MAXDIST value as a weighting factor to express the current distance as a percentage of the maximum distance (and make Wwise happy).

This is all great, but all of this won’t ever get executed for the time being, because for how thing stands the door will never be active. We didn’t check wether the door is open or not. Yes, I know we used the code to say that IF the door is open then execute the code, but the value of isDoorActive, will always be false! “Why is that?” Thanks for asking me. You see, in the Awake function we checked wether the door was open or not (and it surely wasn’t as we were at the beginning of the game), and we wrote down this fact on a piece of paper near us. The problem is that the IF statement in the Update method is actually checking that little piece of paper, and not the actual door. So we need to remember to check that door from time to time (60 times a second to be precise). We do this by adding another IF statement that will wrap everything together:


if (isDoorActive != finalDoorUnlocked.isDoorActive) {
     isDoorActive = finalDoorUnlocked.isDoorActive;
}

As you can see we are checking wether our isDoorActive value differs from the real one. If that is the case than things have changed and we need to update our info. So we’ll quickly scrap that “false” we wrote before and write a “true” down. This is more efficient than asking the system to re-assign the value each frame. It just checks wether things are equal or not, if they’re not then it bothers writing it down. Otherwise we’ll have tons of wasted paper.

Just brilliant! Everything is looking good and it should work! So you open Unity, click the play button, play it to the Final Door and… and Unity slaps you right in the face! “What exactly is DistanceCalc?” *slap*

Ah! We didn’t calculate the distance. But we’ll do it now, it’s alright! Let’s go:

Do we need anybody else to access this function? Well, it is quite generic, but in this case we don’t really need it. Just set it to private and call it a day.

What kind of value will this function return? Oh, it’s going to return a distance so a float value will be the best choice (a float is a number with decimals)

Will this function need any kind of input when called? Sure, it’ll need the co-ordinates of the things to calculate the distance between.


private float DistanceCalc(Vector3 playerCoordinates, Vector3 doorCoordinates){
     float result;
     Vector3 delta;
     delta = doorCoordinates - playerCoordinates;
     result = delta.magnitude;
     Debug.Log ("distance is: " + result);
     return result;
}

So here we are. result is obviously the result of the operation we are about to perform and it’s what we want our function to spit out. Just like the distance it is a float. delta is a 3-dimensional vector. Now, get your algebra book and look under the vector section. The distance between two points in space is the magnitude of the vector obtained by their difference. So delta is the vector obtained by subtracting the player coordinates from the door coordinates. It tells us how far is the player from the door and what direction in the space she is facing. We don’t need the info about the direction in space, so we’ll just write down the magnitude of the delta vector. And because that is what we were looking for, we will store it into the result variable. The result variable will then be returned (spit-out) and assigned to the distance variable in the Update method.

This is it! We are done. Now you can go to the Unity inspector and with your Wwise session hooked up you can tweak the MAXDIST value to better define the boundaries of this effect! If you want to check out how this sounds feel free to check out my showreel on the home page.

Well, I hope this has been helpful and gave you some food for thoughts! Feel free to leave a comment and let me know what you think about the post, the topic, life, the universe and everything!

Take care!

Author: Lorenzo Salvadori

a video game audio specialist with a B.Sc. in Physics (specialisation: Astronomy), and with an extensive background in music. He is always researching and opening his mind to the new frontiers of technology. Bringing together all of the technical and audio skills he is constantly looking for new people to work with and new challenges to thrive on.

2 thoughts on “Audio design and C# scripting

  1. Jon Pascone says:

    Hey Lorenzo!

    This is SUCH a wonderfully composed post – thank you so much for putting this together! I got a kick out of the tone and the content helped me understand a lot of helpful concepts I was struggling with!

    Reply
    1. Lorenzo Salvadori says:

      Thanks a lot for your kind words Jonathan. I’m glad it was helpful 🙂

      Reply

Leave a Reply

Your email address will not be published. Required fields are marked *