Lesson 44 - Adding Scoring Mechanics and Enhancements

Tutorial Series: Introduction to Unity with C# Series

Previous Article  |  Next Article


In this lesson, we're going to do a bunch of things. We're going to keep score and output it to a scoreboard.We'll alert us with an air horn signal before the next enemy wave comes. We'll also play and modify the musi. Sort of make it more uptempo after a certain wave of enemy is reached. We'll also give out and announce a bonus if a certain score level is reached. We'll contain almost all of this in a new script called WorldManager. First, in SphereController, let's set some public fields we'll need to keep track of close calls. We get close to an enemy, we'll sort of give out points based on that. Also use an enemy wave count as a multiplier for that scoring mechanic.

In SphereController, let's keep track of those elements with these two fields; WaveCount and int CloseCall. We'll initialize those and start with CloseCall = 0, and WaveCount = 0. Now, we'll simply increase the WaveCount whenever we respawn, so that's in SpawnEnemy() method. Right after instantiation, we could add WaveCount ++.. For the CloseCall, which will be our point score basically, the way we give out points we can. Whenever we're close to an enemy, which we already have this CoolCrowd functionality in the audio playing the crowd noise, we'll just put underneath here. CloseCall += 2 * WaveCount. The higher the WaveCount, the more scoring is going to be which means the more enemies on the screen, the more challenging things are getting. That's when the real scoring happens. That's all done with this quick and simple little calculation. Now let's create that WorldManager script and attach it to the Main Camera GameObject. Call it WorldManager and I might just move the script down, just for organization sake. Then just add the WorldManager script. In the WorldManager script, what we want to do is get the total score by accessing each Sphere's individual CloseCall score. What we'll do is store each Sphere in a list which we'll talk about in a second.

Here, we'll create a field. It's going to be a List of type GameObject, so here again we need a namespace reference. It's going to be System dot. Holding down control and hitting period. It's going to be System.Collections.Generic. Add that to your using directives. List<GameObject> and call it AllSpheres. We'll just create the list right here, initialize it as a empty list at the moment. We'll populate it later. We'll also need another field, we'll say public static int GrandTotalScore. We’re making it public because we're going want to output it through the HUD OnGUI() method later on, OnGUI(). for the HUD script. Private void GetCloseCallScore() So I don't forget, call it immediately here in Update() and for this method what we'll do is we'll create a foreach to go through all the Sphere GameObjects. Right now we'll do it like this: foreach (sphere in GameObject.FindGameObjectsWithTag()) This one returns a GameObject, whereas this one returns an array of GameObjects, which is what we know it's going to be. It's going to be an array. GameObjectsWithTag() we have to pass in the string for that and that string is based on the tag right here. Right now it's untagged. We use this tagging property for exactly this scenario, when you need to keep track of GameObjects, need to reference them, you don't know what their instance names are going to be because Unity is handling them in the background. You simply tag it with a name and right now I'm going to give it the name 'Enemy'. I created a tag, it's in my tag list now.

There you go. I tagged it as Enemy, and back in the script we just reference that tag. All the Spheres are going to have that tag. It's going to return an array of all those Spheres. Excuse me. I don't know why I put a termination there. This is how we're going to add each Sphere that we grab from that array to our list of type GameObject called ‘AllSpheres'. First we check if the List<AllSpheres> already contains the Sphere. If it already exists in the list, so if (!AllSpheres.Contains()) it's a method that figures that out. Then the particular sphere at a particular index in the loop. If the list already contains that Sphere or if doesn't already contain the sphere, we add it. We then access the Add() method, which is available to Lists, which AllSpheres is, which we'll again talk about in a moment. We'll add the sphere. Alright. As you can see, the List is a lot like an array. In most ways, you can think of it like an array but it's actually a generic class as you can see signified by those angled brackets after the class name. It's an array-like structure which is actually what's called a collection, which is the broader terminology for array-like objects in C#, which thEre are several, but the generic List is probably the most common.

Why use a List rather than an array? An array is a lot simpler computationally, so it takes up less resources but there are some damning limitations that make arrays unsuitable for certain tasks. Most notably, arrays can't be resized. They are fixed at instantiation. If we use an array here, which we could do, we would have to make a new array every time a new item is needed to be added. Why not use a List instead, which has the handy Add() method as we saw, and it just adds that item to that existing List. Alright, so now that we have a list of Sphere GameObjects, let's iterate though the list just as we would an array to pull the scores from each sphere and then add them up and get the GrandTotalScore. Have a temp total called int temptotal to start off with at 0, and this will just grab the score from each sphere and add it to the temptotal before it gets out to the GrandTotalScore, and we'll do another foreach. Once again, this is how Lists are similar to arrays. You can think of them almost the same way foreach (sphere in AllSpheres) We want to grab the CloseCallScore that each sphere has, whenever we just graze the enemy. Have a local reference to each sphere's SphereController. Call it eachSphereScript. We’ll run GetComponent() to get that, and for the TempTotal we'll just += whatever each sphere's CloseCall number is. EachSphereScript.CloseCall, we're going to need to make CloseCall public. WaveCount should be public too because I think we're going to need to reference that. Make them both public.

Now we'll see CloseCall available and end that foreach, we'll add the TempTotal to the GrandTotalScore. On this frame, we get the total for each sphere through this foreach, add it to the TempTotal and then we just assign the existing totals for each sphere's CloseCall score to the GrandTotalScore. One of the most useful things I'm showing here, is how to use a List in order to keep track and hold references to these outside GameObjects that you need to reference in your script here. It's a very common usage for the List in your projects. In this case, you actually don't have to use the List. You can actually do away with it entirely, and you may have spotted it yourself and just have it all contained in a single iteration, just like this. Alright so this iteration that's creating the List, we can just get rid of that so long as no other method in the class needs access to the references to these spheres. You just get rid of it and take this, just put it right here. It returns an array, and we just can iterate right through that array that we're returning. Right in this one foreach so that accomplishes everything we need. We don't need this List unless, of course as I said, another method in the script needs to keep track of these and do some process on each enemy.

That might be a useful sort of side project for you that you might want to look in to. Go through this list of spheres in a method, call it in Update() and in that method have it apply some process to each sphere or each third sphere or whatever you want to do as we looked at with iterations in a previous lesson. You can apply what you learned there. We'll retain the List just so you can reference this and go back and remember how to do it. Now that we have GrandTotalScore, all we have to do is reference the static field in the HeadsUpDisplay. Copy this and modify it slightly, we want to be somewhere around the top middle of the screen.. That's usually where the scores are displayed. The X, we'll put at 300 and we'll say “SCORE”. For this argument we'll pass in WorldManager.GrandTotalScore and we'll ToString() it just to keep consistent. Quickly run this just to make sure it's working right. We'll keep an eye on the score as we're grazing our Sphere enemies. I should probably change the color here. Let's make it magenta. That'll be a little bit easier to see. Alright.

The next thing we'll want the WorldManager to do is sound the airhorn to signal the next enemy wave is coming. We should make some changes to SpawnEnemy() the method in the SphereController first to make it easier to reference when to sound the AirHorn in the WorldManager. Let's set the spawn interval in SphereController here, it's every five hundred ticks. Let's set that as a public field that we can reference in SpawnEnemy() method and as well as the WorldManager. Public int SpawnInterval, and so it's always going to be 500, unless we change it here. We'll reference it here. We should also make the Timer in the SphereController reset, rather than keep going, as we'll have to do some sort of calculation in WorldManager did line up everything as the Timer increases, Let's just default the Timer to zero. We'll just say if (Timer > SpawnInterval) Timer = 0, it just resets. Goes all the way up to 500 and then resets to 0. Just going to be a little bit easier for us to line this up with the pre spawn AirHorn that we're going to have in the. WorldManager. Then here in the WorldManager, we'll make a field that's a SphereController so we can keep track of. We can get the Timer and the SpawnInterval in a minute here. Also need an AudioSource, actually an array, to grab all the audio that we have in our GameObject that we previously set up.

Here in Start() we'll say SphereScript,. that field equals GameObject.Find(“Sphere”).GetComponent<SphereController>() Then Audio = GetComponents() for this GameObject of type AudioSource. It's getting all the different audio Components we've attached here to this GameObject., We'll want a method, we'll call PreSpawnAirhorn. Say private void PresSpawnAirHorn. If(SphereScript.Timer) we'll get that SphereScript reference, and we'll get the Timer from it. If the Timer == SphereScript.SpawnInterval. We want that as well -150, so 150 ticks before 500, which is 350. Every time it reaches 350, that's where we do the resetting. Just makes this a lot easier. We'll play the AirHorn, which I think is in the first or in the second. The [1] index for the audio clips that are going to be returned. Just make sure that's the case. Should be this one. The [1] index. [0] index is CrowdNoise. Remember how we had those music loops? The second loop is sort of a more high energy music loop that will kick in after a certain enemy wave has been reached. We can achieve this by muting one loop and unmuting the other. In order to make sure everything stays in sync, we have to start both loops at the same time.

Let's do this. Both of those clips are going to loop through at the same time, but we only want one to be unmuted at any given time. First let's get the clips to start. Guess we can do Play On Awake, but I want to do it in a method here for no particular reason. Let's create a method called private void MusicStart() All that does is start those loops. It's going to be Audio [2] the second index and Audio [3] the third index, set them both to pay at the same time. Then we just call that method in Start() Now here is what we'll do, we'll modify the music with another method. Private void MusicModifier() we'll call it. Then here we'll just have a simple conditional to make sure only one loop is playing at any one given moment. If (SphereScript.WaveCount < 3) we'll say .. Let me just copy and paste this. Mute = false, because we want the first music loop to play. If we haven't reached the third wave yet, and Audio [3] .mute = true. I'm going to grab that, just flip it for the else if (WaveCount == 3) from this point forward, we'll flip it. We'll mute the first one and the second, more high energy loop, will kick into effect. Of course, we have to call it.

Here we'll call the MusicModifier and actually here is another modification we can do so as not to have this be so static, but rather changes with each increasing wave after the .. Let's say the fifth WaveCount. We'll just increase the pitch of the loop, which will have the effect of spinning it up, and making it a little bit more uptempo and aggressive. Say Audio [3] this is only going to be the fifth WaveCount, we know it's only going to be the Audio at this index is going to be playing. Just pitch that up by accessing its pitch property and we'll just Lerp. It's a float, so we'll return a float from Mathf.Lerp() method and Lerp from the current pitch to 1.2. 20% faster if you look at it that way. We'll give it a little percentage of how that fast happens. Else if (WaveCount > 5) At this point, you survived and the screen's probably filled with a lot of enemies. Let's just actually copy and paste this. It's only going to be a slight modification. Alright. Now to round out the function of our WorldManager, we'll have a bonus effect after certain score levels have been reached, and play some audio clips when that bonus has been reached to announce it.

Let's add those clips to the main camera. For audio it's going to be a couple more voice overs so import, grab this one. BoomShakalaka and OnFire. There we go, and OnFire. We'll just add those AudioSources. Make sure they don't play on a wake. We'll need a bool to handle this properly, so create a field called BonusSwitch. We'll set it to true arbitrarily to start with, and let's have another private method here that handles just this task for the bonus announcing called BonusAnnounce(). We'll have a conditional if (GrandTotalScore > 1000) is greater than a thousand, and if the GrandTotalScore 3000, and BonusSwitch is true, which it will be, we'll play the audio clip at the fourth index, and let's give a multiplier bonus to existing PowerUpMeter. Let's say float powerUpBonus, local variable to store the PowerUpManager.PowerUpMeter. We'll say PowerUpBonus *= 1.5, use a multiplier of one point five. So the higher your PowerUpMeter at that point, if you haven't used it too much, the higher the bonus essentially.

Since the PowerUpMeter is actually an int, so the multiplier only the works properly on floats so having the PowerupMeter as an int, we'll have to cast that powerUpBonus and assign it to the PowerUpMeter. And so cast it to (int)powerUpBonus. We want to switch the BonusSwitch to the opposite value. It's going to be false. On the next frame, if we're greater than 1,000, the score, it doesn't play this again. It doesn't keep playing it over and over again on each frame, until you reach greater than 3,000. It only plays it once. That's what the functionality usage of the bool is. Here we'll say, else if (GrandTotalScore > 3000) and we expect the switch to be false, because it had already been switched here. We take all this and we'll play the other audio at the fifth index and we'll give a lesser, only basically a 25% boost or bonus to the PowerUpMeter. You don't want to overly encourage people not to use their PowerUpMeter. We'll flip this BonusSwitch again so it doesn't play it again on the next frame. Here we'll say BonusAnnounce() call that. Let's test that out. Let's try to play long enough to get our score to get past 1,000 and past 3,000. Okay. That was just for testing. We should be a little bit less lenient here for the bonus threshold, so let's say 25,000 and 50,000. Of course 50,000 here as well. All right. That covers quite a bit. I'll end the video here and we'll continue on in the next lesson.

Related Articles in this Tutorial:

Lesson 1 - Who This Course is For

Lesson 2 - What to Expect from this Course

Lesson 3 - Installation and Getting Started

Lesson 4 - Starting the First Project

Lesson 5 - Prototype Workflow

Lesson 6 - Basic Code Review

Lesson 7 - Game Loop Primer

Lesson 8 - Prototyping Continued

Lesson 9 - C# Fundamentals and Hello World

Lesson 10 - Variables and Operations

Lesson 11 - Variables and Operations Continued

Lesson 12 - Floats, Bools and Casting

Lesson 13 - If Statement Conditionals

Lesson 14 - If Statements Continued

Lesson 15 - Complex Evaluations and States

Lesson 16 - Code Syntax vs. Style

Lesson 17 - Variable Scope

Lesson 18 - Object-Oriented Programming Intro

Lesson 19 - OOP, Access Modifiers, Instantiation

Lesson 20 - Object Containment and Method Returns

Lesson 21 - "Has-A" Object Containment

Lesson 22 - "Is-A" Inheritance Containment

Lesson 23 - Static Fields and Methods

Lesson 24 - Method Inputs and Returns

Lesson 25 - Reference vs. Value Types

Lesson 26 - Introduction to Polymorphism

Lesson 27 - Navigating the Unity API

Lesson 28 - Applying What You've Learned and Refactoring

Lesson 29 - Constructors, Local Variables in the Update Method

Lesson 30 - Collecting Collectibles, Items and Powerups

Lesson 31 - Spawning and Managing Prefab Powerups

Lesson 32 - Implementing Powerup State Logic

Lesson 33 - Displaying Text, OnGUI, Method Overloading

Lesson 34 - Referencing Instantiated GameObjects, Parenting

Lesson 35 - Understanding the Lerp Method

Lesson 36 - Creating Pseudo Animations in Code

Lesson 37 - Understanding Generic Classes and Methods

Lesson 38 - Animations Using SpriteSheets and Animator

Lesson 39 - Working with Arrays and Loops

Lesson 40 - Debugging Unity Projects with Visual Studio

Lesson 41 - Camera Movement and LateUpdate

Lesson 42 - Playing Audio Clips

Lesson 43 - Routing Audio, Mixers and Effects

Lesson 44 - Adding Scoring Mechanics and Enhancements

Lesson 45 - Scene Loading and Game Over Manager

Lesson 46 - Understanding Properties

Lesson 47 - Controller Mapping and Input Manager

Lesson 48 - Understanding Enums

Lesson 49 - Dealing with Null References

Lesson 50 - Handling Variable Framerates with time.DeltaTime

Lesson 51 - Preparing the Project for Final Build

Lesson 52 - Final Build and Project Settings

Lesson 53 - Introduction to the Unity Physics Engine

Lesson 54 - Understanding FixedUpdate vs. Update

Lesson 55 - Movement Using Physics

Lesson 56 - Attack Script and Collision Events with OnCollisionEnter2D

Lesson 57 - Projectiles and Stomping Attack

Lesson 58 - Parallax Background and Scrolling Camera

Lesson 59 - Infinitely Tiling Background Sprites

Lesson 60 - OOP Enemy Classes

Lesson 61 - OOP Enemy Classes Continued

Lesson 62 - Trigger Colliders and Causing Damage

Lesson 63 - Multi-Dimensional Arrays and Procedural Platforms

Lesson 64 - Finishing Touches

Lesson 65 - Series Wrap


Please login or register to add a comment