Welcome back to our Unity 4.3 2D Tutorial series!
Yes, Unity 4.5 was recently released, but this series is about Unity’s 2D features, which were first introduced in version 4.3. Some bugs have been fixed in 4.5 and a few GUI elements have changed slightly. So, keep that in mind if you’re using a newer version of Unity and you see slight discrepancies between your editor and the screenshots here.
Throughout the first, second and third parts of this series, you learned most of what you need to begin working with Unity’s 2D tools, including how to import and animate your sprites.
In the fourth part of the series, you were introduced to Unity’s 2D physics engine and learned one way to deal with different screen sizes and aspect ratios.
By the end of this, the final part of this series, you’ll have cats dancing in a conga line and your player will be able to win or lose the game. You’ll even throw in some music and sound effects just for fun.
This last tutorial is the longest of the series, but it seemed better to post it as one huge chunk rather than to make you wait even one extra day for the second half.
This tutorial picks up where the fourth part of the series left off. If you don’t already have the project from that tutorial, download it here.
Unzip the file (if you needed to download it) and open your scene by double-clicking ZombieConga/Assets/Scenes/CongaScene.unity.
You’ve got most of Zombie Conga’s pieces in place, so now it’s time to do what so many aspiring game developers have trouble doing: finish the game!
Getting Started
Zombie Conga is supposed to be a side-scrolling game, but so far you’re zombie has been stuck staring at one small section of beach. It’s high time he had someplace to go.
In order to scroll the scene to the left, you’ll move the camera within the game world to the right. That way, the beach, along with the cats, zombies and old ladies that hang out there, will scroll by naturally without you needing to modify their positions yourself.
Select Main Camera in the Hierarchy. Add a new C# script called CameraController. You’ve already created several scripts by this point in the tutorial series, so try it yourself. If you need a refresher, check out the following spoiler.
Open CameraController.cs in MonoDevelop and add the following instance variables:
public float speed = 1f; private Vector3 newPosition; |
You’ll use speed
to control how quickly the scene scrolls. You only need to update the x
component of the Camera
‘s position, but the individual components of a Transform
‘s position
are readonly. Rather than repeatedly creating new Vector3
objects every time you update the position, you’ll reuse newPosition
.
Because you’ll only be setting newPosition
‘s x value, you need to initialize the vector’s other components properly. To do so, add the following line inside Start:
newPosition = transform.position; |
This copies the camera’s initial position to newPosition
.
Now add the following code inside Update
:
newPosition.x += Time.deltaTime * speed; transform.position = newPosition; |
This simply adjusts the object’s position as if it were moving speed
units per second.
newPosition
accurately reflects the camera’s position. If you are one of said sticklers, feel free to replace that line withnewPosition.x = transform.position.x + Time.deltaTime * speed;
Save the file (File\Save) and switch back to Unity.
Play your scene and things start moving. The zombie and enemies seem to handle it fine, but they quickly run out of beach!
You need to handle the background similarly to how you handled the enemy. That is, when the enemy goes off screen, you’ll change its position so it reenters the scene from the other side of the screen.
Create a new C# script named BackgroundRepeater and add it to background. You’ve done this sort of thing several times now, so if you need a refresher, look back through the tutorial to find it.
Open BackgroundRepeater.cs in MonoDevelop and add the following instance variables:
private Transform cameraTransform; private float spriteWidth; |
You’ll store a reference to the camera’s Transform
in cameraTransform
. This isn’t absolutely necessary, but you’ll need to access it every time Update
runs, so rather than repeatedly finding the same component, you’ll simply find it once and keep using it.
You’ll also need to repeatedly access the sprite’s width, which you’ll cache in spriteWidth
because you know you aren’t changing the background’s sprite at runtime.
Initialize these variables by adding the following code in Start
:
//1 cameraTransform = Camera.main.transform; //2 SpriteRenderer spriteRenderer = renderer as SpriteRenderer; spriteWidth = spriteRenderer.sprite.bounds.size.x; |
The above code initializes the variables you added as follows:
- It finds the scene’s main
Camera
object (which is the only camera in Zombie Conga) and setscameraTransform
to point to the camera’sTransform
. - It casts the object’s built-in
renderer
property to aSpriteRenderer
in order to access itssprite
property, from which it gets the Sprite’sbounds
. TheBounds
object has asize
property whosex
component holds the object’s width, which it stores inspriteWidth
.
In order to determine when the background sprite is off screen, you could implement OnBecameInvisible
, like you did for the enemy. But you already learned about that, so this time you’ll check the object’s position directly.
In Zombie Conga, the camera’s position is always at the center of the screen. Likewise, when you imported the background sprite way back in Part 1 of this tutorial series, you set the origin to the sprite’s center.
Rather than calculate the x position of the left edge of the screen, you’ll estimate by assuming the background has scrolled off screen if it’s at least a full sprite’s width away from the camera. The following image shows how the background sprite is well offscreen when positioned exactly one-sprite’s width away from the camera’s position:
The left edge of the screen will be different on different devices, but this trick will work as long as the screen’s width is not larger than the width of the background sprite.
Add the following code to Update
:
if( (transform.position.x + spriteWidth) < cameraTransform.position.x) { Vector3 newPos = transform.position; newPos.x += 2.0f * spriteWidth; transform.position = newPos; } |
The if
check above checks to see if the object is sufficiently off screen, as described earlier. If so, it calculates a new position that is offset from the current position by twice the width of the sprite.
Why twice the width? By the time this logic determines that the background went offscreen, moving the sprite over by spriteWidth
would pop it into the area viewable by the camera, as shown below:
Save the file (File\Save) and switch back to Unity.
Play the scene and you’ll see that the background goes off screen and eventually comes back into view, as shown in the sped-up sequence below:
That works fine, but you probably don’t want those blue gaps that keep showing up. To fix it, you’ll simply add another background sprite to fill that space.
Right-click on background in the Hierarchy and select Duplicate from the popup menu that appears. Select the duplicated background (if it already isn’t selected) and set the x value of the Transform‘s Position to 20.48, as shown below:
Remember from Part 1 of this series that the background sprite is 2048 pixels wide and you imported it with a ratio of 100 pixels per unit. That means that setting one background sprite’s x position to 20.48 will place it immediately to the right of the other object, whose x position is zero.
You now have a much longer stretch of beach in your Scene view, as shown below:
Play the scene again and now your zombie can spend his entire apocalypse strolling along the beach, as shown in the following sped-up sequence. Don’t let the glitches in this low-quality GIF fool you – in the real game, the background scrolls seamlessly.
While playing the scene, one thing that probably stands out is how utterly devoid of cats that beach is. I don’t know about you, but whenever I go to the beach, I always bring my kitty.
Spawning Cats
You’ll want new cats to keep appearing on the beach until the player wins or loses the game. To handle this, you’ll create a new script and add it to an empty GameObject.
Create a new empty game object by choosing GameObject\Create Empty in Unity’s menu. Name the new object Kitten Factory.
Create a new C# script called KittyCreator and attach it to Kitten Factory. No more hints for creating new scripts – you can do it! (But if you can’t do it, look back through the earlier parts of the tutorial.)
Open KittyCreator.cs in MonoDevelop and replace its contents with the following code:
using UnityEngine; public class KittyCreator: MonoBehaviour { //1 public float minSpawnTime = 0.75f; public float maxSpawnTime = 2f; //2 void Start () { Invoke("SpawnCat",minSpawnTime); } //3 void SpawnCat() { Debug.Log("TODO: Birth a cat at " + Time.timeSinceLevelLoad); Invoke("SpawnCat", Random.Range(minSpawnTime, maxSpawnTime)); } } |
This code doesn’t actually spawn any cats, it simply lays the groundwork to do so. Here’s what it does:
-
minSpawnTime
andmaxSpawnTime
specify how often new cats appear. After a cat spawns,KittyCreator
will wait at leastminSpawnTime
seconds and at mostmaxSpawnTime
seconds before spawning another cat. You declared them public so you can tweak the spawn rate in the editor later if you’d like. - Unity’s
Invoke
method lets you call another method after a specified delay.Start
callsInvoke
, instructing it to waitminSpawnTime
seconds and then to callSpawnCat
. This adds a brief period after the scene starts during which no cats spawn. - For now,
SpawnCat
simply logs a message letting you know when it executes and then usesInvoke
to schedule another call toSpawnCat
. It waits a random amount of time betweenminSpawnTime
andmaxSpawnTime
, which keeps cats from appearing at predictable intervals.
Save the file (File\Save) and switch back to Unity.
Run the scene and you’ll start seeing logs like the following appear in the Console:
Now that you have your Kitten Factory working on schedule, you need to make it spit out some cats. For that, you’ll be using one of Unity’s most powerful features: Prefabs.
Prefabs
Prefabs reside in your Project rather than in your scene’s Hierarchy. You use a Prefab as a template to create objects in your scene.
However, these instances are not just copies of the original Prefab. Instead, the Prefab defines an object’s default values, and then you are free to modify any part of a specific instance in your scene without affecting any other objects created from the same Prefab.
In Zombie Conga, you want to create a cat Prefab and have Kitten Factory create instances of that Prefab at different locations throughout the scene. But don’t you already have a cat object in your scene, properly configured with all the animations, physics and scripts you’ve set up so far? It sure would be annoying if you had to redo that work to make a Prefab. Fortunately, you don’t have to!
To turn it into a Prefab, simply drag cat from the Hierarchy into the Project browser. You’ll see a new cat Prefab object created in the Project browser, but you should also see the word cat turn blue in the Hierarchy, as shown below:
While working on your own games, remember that objects with blue names in the Hierarchy are instances of Prefabs. When you select one, you will see the following buttons in the Inspector:
These buttons are useful while editing instances of a Prefab. They allow you to do the following:
- Select: This button selects in the Project browser the Prefab object used to create this instance.
- Revert: This button replaces any local changes you’ve made to this instance with the default values from the Prefab.
-
Apply: This button takes any local changes you’ve made to this instance and sets those values back onto the Prefab, making them the default for all Prefab instances. Any existing instances of the Prefab that have not set local overrides for these values will automatically have their values changed to the new defaults.
Important: Clicking Apply affects every GameObject that shares this object’s Prefab in every scene of your project, not just the current scene.
Now you need to get the Kitten Factory to stop polluting your Console with words and start polluting your beach with cats!
Go back to KittyCreator.cs in MonoDevelop and add the following variable to KittyCreator
:
public GameObject catPrefab; |
You’ll assign your cat Prefab to catPrefab
in Unity’s editor and then KittyCreator
will use it as a template when creating new cats. But before you do that, replace the Debug.Log
line in SpawnCat
with the following code:
// 1 Camera camera = Camera.main; Vector3 cameraPos = camera.transform.position; float xMax = camera.aspect * camera.orthographicSize; float xRange = camera.aspect * camera.orthographicSize * 1.75f; float yMax = camera.orthographicSize - 0.5f; // 2 Vector3 catPos = new Vector3(cameraPos.x + Random.Range(xMax - xRange, xMax), Random.Range(-yMax, yMax), catPrefab.transform.position.z); // 3 Instantiate(catPrefab, catPos, Quaternion.identity); |
The above code chooses a random position that’s visible to the camera and places a new cat there. Specifically:
- The camera’s current position and size define the visible part of the scene. You use this information to calculate the x and y limits within which you want to place a new cat. This calculation does not place cats too close to the top, bottom, or far left edges of the screen. See the image that follows to help visualize the math involved.
- You create a new position using
catPrefab
‘s z position (so all cats appear at the same z-depth), and random values for x and y. These random values are chosen within the area shown in the image that follows, which is slightly smaller than the visible area of the scene. -
You call
Instantiate
to create an instance ofcatPrefab
placed in the scene at the position defined bycatPos
. You passQuaternion.identity
as the new object’s rotation because you don’t want the new object to be rotated at all. Instead, the cat’s rotation will be set by the spawn animation you made in Part 2 of this tutorial series.Note: This makes all cats face the same direction when they spawn. If you wanted to mix it up, you could pass toInstantiate
a random rotation around the z axis instead of using the identity matrix. However, be advised that this won’t actually work until after you’ve made some changes you’ll read about later in this tutorial.
Save the file (File\Save) and switch back to Unity.
You no longer need the cat in the scene because your factory will create them at runtime. Right-click cat in the Hierarchy and choose Delete from the popup menu that appears, as shown below:
Select Kitten Factory in the Hierarchy. Inside the Inspector, click the small circle/target icon on the right of the Kitty Creator (Script) component’s Cat Prefab field, shown below:
Inside the Select GameObject dialog that appears, choose cat from the Assets tab, as shown in the following image:
Kitten Factory now looks like this in the Inspector:
Don’t worry if your Kitten Factory doesn’t have the same Transform values as those shown here. Kitten Factory only exists to hold the Kitty Creator script component. It has no visual component and as such, it’s Transform values are meaningless.
Run the scene again and watch as everywhere you look, the very beach itself appears to be coughing up adorable fur balls.
However, there’s a problem. As you play, notice how a massive list of cats slowly builds up in the Hierarchy, shown below:
This won’t do. If your game lasts long enough, this sort of logic will bring it crashing to a halt. You’ll need to remove cats as they go off screen.
Open CatController.cs in MonoDevelop and add the following method to CatController
:
void OnBecameInvisible() { Destroy( gameObject ); } |
This simply calls Destroy
to destroy gameObject
. All MonoBehaviour
scripts, such as CatController
, have access to gameObject
, which points to the GameObject
that holds the script. Although this method doesn’t show it, it is safe to execute other code in a method after calling Destroy
because Unity doesn’t actually destroy the object right away.
GrantCatTheSweetReleaseOfDeath
, the other method in CatController
, uses DestroyObject
for the same purpose as you are now using Destroy
. What gives?
To be honest, I’m not sure if there is any difference. Unity’s documentation includes Destroy
but not DestroyObject
, but they both seem to have the same effect. I probably just type whichever one I happen to type and since the compiler doesn’t complain, I’ve never thought anything of it.
If you know of a difference or why one should be preferred over the other, please mention it in the Comments section. Thanks!
Save the file (File\Save) and switch back to Unity.
Run the scene again. As was mentioned in Part 3, OnBecameInvisible
only gets called once an object is out of sight of all cameras, so be sure the Scene view is not visible while testing this bit.
Now, no matter how long you play, the Hierarchy never contains more than a few cats. Specifically, it contains the same number of objects as there are cats visible in the scene, as shown below:
Note: Creating and destroying objects is fairly expensive in Unity’s runtime environment. If you’re making a game even only slightly more complicated than Zombie Conga, it would probably be worthwhile to reuse objects when possible.
For example, rather than destroying a cat when it exits the screen, you could reuse that object the next time you needed to spawn a new cat. You already do this for the enemy, but for the cats you would need to handle keeping a list of reusable objects and remembering to reset the cat to an initial animation state prior to spawning it.
This technique is known as object pooling and you can find out a bit more about it in this training session from Unity.
Ok, you’ve got a beach filling up with cats and a zombie walking around looking to party. I think you know what time it is.
Conga Time!
If you’ve been following along with this tutorial series since Part 1, you’ve probably started wondering why the heck this game is even called Zombie Conga.
It is time.
When the zombie collides with a cat, you’ll add that cat to the conga line. However, you’ll want to handle enemy collisions differently. In order to tell the difference, you’ll assign specific tags to each of them.
Using Tags to Identify Objects
Unity allows you to assign a string to any GameObject, called a tag. Newly created projects include a few default tags, like MainCamera and Player, but you are free to add any tags that you’d like.
In Zombie Conga, you could get away with only one tag, because there are only two types of objects with which the zombie can collide. For example, you could add a tag to the cats and then assume if the zombie collides with an object that is missing that tag, it must be an enemy. However, shortcuts like that are a good way to cause bugs when you later decide to change something about your game.
To make your code easier to understand and more maintainable, you’ll create two tags: cat and enemy.
Choose Edit\Project Settings\Tags and Layers from Unity’s menu. The Inspector now shows the Tags & Layers editor. If it’s not already open, expand the Tags list by clicking the triangle to the left of its name, as shown in the following image:
Type cat in the field labeled Element 0. As soon as you start typing, Unity adds a new tag field labeled Element 1. Your Inspector now looks like this:
Select cat in the Project browser and choose cat from the combo box labeled Tag in the Inspector, like this:
When you were adding the cat tag, you could have added the enemy tag, too. However, I wanted to show you another way to create a tag.
Many times you’ll decide you want to tag an object, only to check the Tag combo box in the Inspector and realize the tag you want doesn’t exist yet. Rather than go through Unity’s Editor menu, you can open the Tags and Layers editor directly from the Tags combo box.
Select enemy in the Hierarchy. In the Inspector, choose Add Tag… from the Tag combo box, as shown below:
Once again, the Inspector now shows the Tags & Layers editor. Inside the Tags section, Type enemy in the field labeled Element 1. The Inspector now looks like the image below:
With the new tag created, select enemy in the Hierarchy and set its Tag to enemy, as shown below:
Now that your objects are tagged, you can identify them in your scripts. To see how, open ZombieController.cs in MonoDevelop and replace the contents of OnTriggerEnter2D
with the following code:
if(other.CompareTag("cat")) { Debug.Log ("Oops. Stepped on a cat."); } else if (other.CompareTag("enemy")) { Debug.Log ("Pardon me, ma'am."); } |
You call CompareTag
to check if a particular GameObject has the given tag. Only GameObjects can have tags, but calling this method on a Component – like you’re doing here – tests the tag on the Component’s GameObject.
Save the file (File\Save) and switch back to Unity.
Run the scene and you should see the appropriate messages appear in the Console whenever the zombie touches a cat or an enemy.
Now that you know your collisions are set up properly, it’s time to actually make them do something.
Triggering Animations From Scripts
Remember all those animations you made in Parts two and three of this series? The cat starts out bobbing happily, like this:
When the zombie collides with a cat, you want the cat to turn into a zombie cat. The following image shows how you accomplished this in the earlier tutorial by setting the cat’s InConga
parameter to true
in the Animator window.
Now you want to do the same thing, but from within code. To do that, switch back to CatController.cs in MonoDevelop and add the following method to CatController
:
public void JoinConga() { collider2D.enabled = false; GetComponent<Animator>().SetBool( "InConga", true ); } |
The first line disables the cat’s collider. This will keep Unity from sending more than one collision event when the zombie collides with a cat. (Later you’ll solve this problem in a different way for collisions with the enemy.)
The second line sets InConga
to true
on the cat’s Animator
Component. By doing so, you trigger a state transition from the CatWiggle Animation Clip to the CatZombify Animation Clip. You set up this transition using the Animator window in Part 3 of this series.
By the way, notice that you declared JoinConga
as public. This lets you call it from other scripts, which is what you’ll do right now.
Save CatController.cs (File\Save) and switch to ZombieController.cs, still in MonoDevelop.
Inside ZombieController
, find the following line in OnTriggerEnter2D
:
Debug.Log ("Oops. Stepped on a cat."); |
And replace it with this line:
other.GetComponent<CatController>().JoinConga(); |
Now whenever the zombie collides with a cat, it calls JoinConga
on the cat’s CatController
component.
Save the file (File\Save) and switch back to Unity.
Play the scene and as the zombie walks into the cats, they turn green and start hopping in place. So far, so good.
Nobody wants a bunch of zombie cats scattered across a beach. What you want is for them to join your zombie’s eternal dance, and for that, you need to teach them how to play follow the leader.
Conga Motion
You’ll use a List
to keep track of which cats are in the conga line.
Go back to ZombieController.cs in MonoDevelop.
First, add the following at the top of the file with the other using
statements.
using System.Collections.Generic; |
This using
statement is similar to an #import
statement in Objective-C. It simply gives this script access to the specified namespace and the types it contains. In this case, you need access to the Generic
namespace to declare a List
with a specific data type.
Add the following private variable to ZombieController
:
private List<Transform> congaLine = new List<Transform>(); |
congaLine
will store Transform
objects for the cats in the conga line. You’re storing Transform
s instead of GameObject
s because you’ll be dealing mostly with the cat’s positions, and if you ever need access to anything else you can get to any part of a GameObject
from its Transform
, anyway.
Each time the zombie touches a cat, you’ll append the cat’s Transform
to congaLine
. This means that the first Transform
in congaLine
will represent the cat right behind the zombie, the second Transform
in congaLine
will represent the cat behind the first, and so forth.
To add cats to the conga line, add the following line to OnTriggerEnter2D
in ZombieController
, just after the line that calls JoinConga
:
congaLine.Add( other.transform ); |
This line simply adds the cat’s Transform
to congaLine
.
If you were to run the scene right now, you wouldn’t see any difference from before. You’re maintaining a list of cats, but you haven’t written any code to move the cats from their initial positions when they join the conga line. As conga lines go, this one isn’t very festive.
To fix this, open CatController.cs in MonoDevelop.
The code for moving the cats will be similar to what you wrote to move the zombie in
Part 1. Start out by adding the following instance variables to CatController
:
private Transform followTarget; private float moveSpeed; private float turnSpeed; private bool isZombie; |
You’ll use moveSpeed
and turnSpeed
to control the cat’s rate of motion, the same way you did for the zombie. You only want the cat to move after it becomes a zombie, so you’ll keep track of that with isZombie
. Finally followTarget
will hold a reference to the character (cat or zombie) in front of this cat in the conga line. You’ll use this to calculate a position toward which to move.
The above variables are all private, so you may be wondering how you’ll set them. For the conga line to move convincingly, you’ll base the movement of the cats on the zombie’s movement and turn speeds. As such, you’re going to have the zombie pass this information to each cat during the zombification process.
Inside CatController.cs, replace your implementation of JoinConga
with the following code:
//1 public void JoinConga( Transform followTarget, float moveSpeed, float turnSpeed ) { //2 this.followTarget = followTarget; this.moveSpeed = moveSpeed; this.turnSpeed = turnSpeed; //3 isZombie = true; //4 collider2D.enabled = false; GetComponent<Animator>().SetBool( "InConga", true ); } |
Here’s a break down of this new version of JoinConga
:
- The method signature now requires a target Transform, a movement speed and a turn speed. Later you’ll change
ZombieController
to callJoinConga
with the appropriate values. - These lines store the values passed into the method. Notice the use of
this.
to differentiate between the cat’s variables and the method’s parameters of the same names. - This flags the cat as a zombie. You’ll see why this is important soon.
- The last two lines are the same ones you had in the previous version of
JoinConga
.
Now add the following implementation of Update
to CatController
:
void Update () { //1 if(isZombie) { //2 Vector3 currentPosition = transform.position; Vector3 moveDirection = followTarget.position - currentPosition; //3 float targetAngle = Mathf.Atan2(moveDirection.y, moveDirection.x) * Mathf.Rad2Deg; transform.rotation = Quaternion.Slerp( transform.rotation, Quaternion.Euler(0, 0, targetAngle), turnSpeed * Time.deltaTime ); //4 float distanceToTarget = moveDirection.magnitude; if (distanceToTarget > 0) { //5 if ( distanceToTarget > moveSpeed ) distanceToTarget = moveSpeed; //6 moveDirection.Normalize(); Vector3 target = moveDirection * distanceToTarget + currentPosition; transform.position = Vector3.Lerp(currentPosition, target, moveSpeed * Time.deltaTime); } } } |
That may look a bit complicated, but most of it is actually the same as what you wrote to move the zombie. Here’s what it does:
- You don’t want the cat to move until it joins the conga line, but Unity calls
Update
during every frame that the cat is active in the scene. This check ensures the cat doesn’t move until it’s supposed to. - If the cat is in the conga line, this method gets the cat’s current position and calculates the vector from its current position to
followTarget
‘s position. - This code rotates the cat to point in the direction its moving. This is the same code you wrote in ZombieController.cs back in Part 1 of this series.
- It then checks
moveDirection
‘smagnitude
– which is the vector’s length, for the non-mathies out there – and checks to see if the cat is not currently at the target. - This check makes sure that the cat doesn’t move more than
moveSpeed
per second. -
Finally, it moves the cat the appropriate distance based on
Time.deltaTime
. This is basically the same code you wrote in ZombieController.cs in Part 1 of this series.
You’re done with CatController.cs for now, so save the file (File\Save).
Because you changed JoinConga
‘s method signature, you need to change the line that calls this method in ZombieController
. Switch back to ZombieController.cs in MonoDevelop.
Inside OnTriggerEnter2D
, replace the call to JoinConga
with the following code:
Transform followTarget = congaLine.Count == 0 ? transform : congaLine[congaLine.Count-1]; other.GetComponent<CatController>().JoinConga( followTarget, moveSpeed, turnSpeed ); |
That first, tricky-looking line figures out what object should be in front of this cat in the conga line. If congaLine
is empty, it assigns the zombie‘s Transform to followTarget
. Otherwise, it uses the last item stored in congaLine
.
The next line calls JoinConga
, this time passing to it the target to follow along with the zombie’s movement and turn speeds.
Save the file (File\Save) and switch back to Unity.
Run the scene and your conga line is finally in place. Sort of. But not really.
When you played the scene, you may have noted the following problems:
- If any cat in the conga line goes off screen, it gets removed and then Unity starts printing the following exception to the console:
- The motion is too perfect. It’s smooth like a snake, but the animation you defined in CatConga was meant to resemble happy hopping zombie cats, not slippery snake cats. You do know what happy hopping zombie cats look like, don’t you?
- The cats always point straight to the right, no matter what direction they’re moving. Zombie cats are one thing, but that’s downright spooky.
These issues happen to be listed in the order of effort required to fix them. The first fix is simple, so start with that.
Go back to CatController.cs inside MonoDevelop.
You already added isZombie
to keep track of when the cat is a zombie. Add the following line at the beginning of OnBecameVisible
to avoid deleting the cat while it’s getting its groove on:
if ( !isZombie ) |
Save the file (File\Save) and switch back to Unity.
Run the scene again, and now cats in the conga line can safely go off screen and later dance right back into view.
Fixing the Conga Animation
To make it look like the cats are hopping along enjoying their undeath, you’ll need to change the logic slightly. Rather than calculating the target position every frame, each cat will choose a point and then hop to it over the course of one CatConga
animation cycle. Then the cat will choose another point and hop to it, and so on.
Switch back to CatController.cs in MonoDevelop and add the following variable to CatController
:
private Vector3 targetPosition; |
This will store the cat’s current target position. The cat will move until it reaches this position, and then find a new target.
Initialize targetPosition
by adding this line to JoinConga
:
targetPosition = followTarget.position; |
Here you set targetPosition
to followTarget
‘s current position. This ensures the cat has someplace to move as soon as it joins the conga line.
Replace the line that declares moveDirection
in Update
with this line:
Vector3 moveDirection = targetPosition - currentPosition; |
This simply calculates moveDirection
using the stored targetPosition
instead of followTarget
‘s current position.
Save the file (File\Save) and switch back to Unity.
Run again and bump into some kitty cats. Hmm. There seems to be a problem.
Whenever the zombie hits a cat, that cat heads straight to wherever the last member of the conga line happens to be at the moment of the collision. It then stays right there. Forever.
The problem is that you assign targetPosition
when the cat joins the conga line, but you never update it after that! Silly you.
Switch back to CatController.cs in MonoDevelop and add the following method:
void UpdateTargetPosition() { targetPosition = followTarget.position; } |
This method simply updates targetPosition
with followTarget
‘s current position. Update
already looks at targetPosition
, so you don’t need to write any other code to send the cat toward the new location.
Save the file (File\Save) and switch back to Unity.
Recall from Part 3 of this tutorial series that Animation Clips can trigger events. You’ll add an event that calls UpdateTargetPosition
during the first frame of CatConga, allowing the cats to calculate their next target position before each hop.
However, you may also recall from that tutorial that you can only edit animations for a GameObject in your scene rather than a Prefab in your project. So to create the animation event, you first need to temporarily add a cat back into the scene.
Drag the cat Prefab from the Project browser to the Hierarchy.
Select cat in the Hierarchy and switch to the Animation view (Window\Animation).
Choose CatConga from the clips drop-down menu in the Animation window’s control bar.
Press the Animation view’s Record button to enter recording mode and move the scrubber to frame 0, as shown below:
Click the Add Event button shown below:
Choose UpdateTargetPosition() from the Function combo box in the Edit Animation Event dialog that appears, as shown in the following image, and then close the dialog.
With that set up, your cats will update their target in sync with their animation.
Run the scene again, and now the cats hop along from point to point, as you can see in the sped-up animation below:
This works, but the cats are spread out a bit too much. Have these cats ever even been in a conga line?
Switch back to CatController.cs in MonoDevelop.
Inside JoinConga
, replace the line that sets this.moveSpeed
with the following code:
this.moveSpeed = moveSpeed * 2f; |
Here you set the cat’s speed to twice that of the zombie. This will produce a tighter conga line.
Save the file (File\Save) and switch back to Unity.
Run the scene again and you’ll see the conga line looks a little friendlier, as the following sped-up sequence demonstrates:
If you’d like, experiment with different conga styles by multiplying the zombie’s speed by values other than two. The larger the number, the more quickly the cat gets to its target, giving it a more jumpy feeling.
The cats are moving along nicely, except that they refuse to look where they’re going. What gives? Well, that’s just how Unity works and there’s no way around it. Sorry about that. Tutorial done.
Aaaah. I’m just messing with you. There’s an explanation for what’s going on, and a solution!
Making Animations and Scripts Play Nicely Together
Why won’t animated GameObjects respect the changes made to them via scripts? This is a common question, so it’s worth spending some time here to work through the solution.
First, what’s going on? Remember that while the cat hops along in the conga line, it’s playing the CatConga Animation Clip. As you can see in the following image, CatConga adjusts the Scale property in the cat’s Transform:
Important: If you remember only one thing today, make it this next paragraph.
It turns out that if an Animation Clip modifies any aspect of an object’s Transform, it is actually modifying the entire Transform. The cat was pointing to the right when you set up CatConga, so CatConga now ensures that the cat continues to point to the right. Thanks, Unity?
There is a way around this problem, but it’s going to require some refactoring. Basically, you need to make the cat a child of another GameObject. Then, you’ll run the animations on the child, but adjust the parent’s position and rotation.
You’ll need to make a few changes to your code in order to keep it working after you’ve rearranged your objects. Here you’ll go through the process in much the same way you might if you had just encountered this problem in your own project.
First, you need to move the cat Prefab into a parent object.
Create a new empty game object by choosing GameObject\Create Empty in Unity’s menu. Name the new object Cat Carrier.
Inside the Hierarchy, drag cat and release it onto Cat Carrier. I bet that was the least effort you’ve ever expended putting a cat into its carrier. ;]
You’re Hierarchy now looks like this:
When you made the enemy spawn point a child of Main Camera in Unity 4.3 2D Tutorial: Physics and Screen Sizes, you learned that the child’s position defines an offset from its parent’s position.
In the case of the cat, you want the child to be centered on the parent, so setting the parent’s position to (X,Y,Z) essentially places the child at (X,Y,Z).
Therefore, select cat in the Hierarchy and ensure its Transform‘s Position is (0, 0, 0), as shown below:
Likewise, select Cat Carrier in the Hierarchy and ensure its Transform‘s Position is (0, 0, 0) as well. In reality, only its z position matters, but it’s always nice to keep things tidy. (I swear I had no intention of making a Tidy Cat pun right there.)
In order to limit the number of changes you need to make to your code, you’ll move CatController from cat to Cat Carrier.
Select cat in the Hierarchy. Inside the Inspector, click the gear icon in the upper-right of the Cat Controller (Script) component. Select Remove Component from the popup menu that appears, as shown below:
Click Apply at the top of the Inspector to ensure this change makes it back to the Prefab, as shown in the following image:
Select Cat Carrier in the Hierarchy. In the Inspector, click Add Component and choose Scripts\Cat Controller from the menu that appears, as demonstrated below:
Now drag Cat Carrier from the Hierarchy into the Project browser to turn it into a Prefab. Just like when you created the cat Prefab, Cat Carrier’s name turns blue in the Hierarchy to indicate it is now an instance of a Prefab, as shown below:
Select Cat Carrier in the Hierarchy and delete it by choosing Edit\Delete from Unity’s menu.
The Hierarchy now looks like this:
Inside the Project browser, you now have a cat Prefab and a Cat Carrier Prefab, which itself contains a cat Prefab, as shown below:
The two cat Prefabs do not refer to the same asset, and you no longer need the un-parented one. To avoid confusion later, right-click the un-parented cat Prefab and choose Delete from the popup menu, then click Delete in the confirmation dialog that appears, as shown below:
Finally, select Kitten Factory in the Hierarchy. As you can see in the following image, the Kitty Creator (Script) component’s Cat Prefab field now says “Missing (GameObject)”:
That’s because Cat Prefab had been set to the asset you just deleted.
Change the Cat Prefab field in the Kitty Creator (Script) component to use Cat Carrier instead of cat. If you don’t remember how to do that, check out the following spoiler.
Run the scene. At this point, you’ll see exceptions similar to the following in the Console whenever the zombie collides with a cat.
Double-click one of these exceptions inside the Console and you’ll arrive at the relevant line, highlighted in MonoDevelop, as shown below:
These exceptions occur because ZombieController
looks for a CatController
component on the GameObject with which it collides, but that component now resides on the cat’s parent, Cat Carrier, rather than the cat itself.
Replace the line highlighted in the image above with the following:
other.transform.parent.GetComponent<CatController>().JoinConga( followTarget, moveSpeed, turnSpeed ); |
You now use the cat’s Transform
to access its parent, which is the Cat Carrier. From there, the rest of the line remains unchanged from what you already had.
JoinConga
method that simply passes its parameters to JoinConga
in its parent’s CatController
component. It really only depends on how you like to organize your code and how much you want different objects to know about each other.Save the file (File\Save) and switch back to Unity.
Run the scene. Once again, you see exceptions in the Console when the zombie collides with a cat. This time they complain of a missing component, like this:
Double-click one of these exceptions in the Console to arrive at the relevant line in MonoDevelop. As you can see, this time the problem is in CatController.cs:
Inside JoinConga
, you attempt to access the object’s Animator
component. This no longer works because you moved the script onto Cat Carrier but the Animator
is still attached to cat.
You don’t want to move the Animator, so instead you’ll change the code.
Inside CatController.cs, find the following two lines of code in JoinConga
:
collider2D.enabled = false; GetComponent<Animator>().SetBool( "InConga", true ); |
Replace those lines with the following code:
Transform cat = transform.GetChild(0); cat.collider2D.enabled = false; cat.GetComponent<Animator>().SetBool( "InConga", true ); |
This code simply uses Cat Carrier’s Transform
to find its first child – indexed from zero. You know Cat Carrier only has one child, which is cat, so this finds the cat. The code then accesses the cat’s Collider2D
and Animator
components in otherwise the same way you did before.
GetChild(0);
:
Transform cat = transform.FindChild("cat"); |
However, that solution relies on you knowing the name of the child you need. Better, but maybe not ideal.
The best solution might be to avoid looking up the object at runtime altogether. To do that, you could add a Transform
variable to CatController
and assign the cat Prefab to it in Unity’s editor. Such choice!
Save the file (File\Save) and switch back to Unity.
Run the scene and now when the zombie collides with a cat…you get this error:
This problem is in your Animation Clip, CatConga. Earlier, you added an event at frame zero that would call the cat’s UpdateTargetPosition
. However, you’ve moved CatController.cs onto a different object, so this error is telling you that you’re trying to call a method that doesn’t exist on the target object.
Select Cat Carrier in the Project browser and then open the Animation view (Window\Animation). What’s this? There are no Animation Clips!
This actually makes sense. Remember, you added the Animation Clips to cat, not Cat Carrier. In fact, the whole reason you added Cat Carrier was because Unity’s animation system was interfering with your GameObject’s Transform.
Expand Cat Carrier in the Project browser and select cat, then choose CatConga from the clip drop-down menu in the Animation view’s control bar. Mouse-over the animation event marker in the timeline and you’ll see it says Error!:
Double click the animation event marker and…nothing happens. Pop quiz! Why? Check the spoiler below for the answer.
Once you’ve corrected the situation, double click the animation event marker again and the following dialog appears, indicating that UpdateTargetPosition is not supported:
Part 4 of this tutorial series alluded to this problem. Animation Events can only access methods on scripts attached to the object associated with the clip. That means you’ll need to add a new script to cat.
Select cat in the Hierarchy and add a new C# script named CatUpdater.cs.
Open CatUpdater.cs in MonoDevelop and replace its contents with the following code:
using UnityEngine; public class CatUpdater : MonoBehaviour { private CatController catController; // Use this for initialization void Start () { catController = transform.parent.GetComponent<CatController>(); } void UpdateTargetPosition() { catController.UpdateTargetPosition(); } } |
This script includes a method named UpdateTargetPosition
that simply calls the identically named method on the CatController
component in the cat’s parent. To avoid repeatedly getting the CatController
component, the script finds the component in Start
and stores a reference to it in catController
.
Save the file (File\Save). However, instead of switching back to Unity, open CatController.cs
in MonoDevelop
.
You called CatController
‘s UpdateTargetPosition
from CatUpdater
, but UpdateTargetPosition
is not a public method. If you went back to Unity now you’d get an error claiming the method is ‘inaccessible due to its protection level’.
Inside CatController.cs, add public
to the beginning of UpdateTargetPosition
‘s declaration, as shown below:
public void UpdateTargetPosition() |
Save the file (File\Save) and switch back to Unity.
Before moving on, you should verify that your animation events are set up correctly. Select cat in the Project browser and choose CatConga from the clip drop-down menu in the Animation view’s control bar. Mouse-over the animation event marker in the timeline and you’ll see it says UpdateTargetPosition():
With cat still selected in the Hierarchy, click Apply in the Inspector to make sure the Prefab includes the script you just added. Then delete Cat Carrier from the scene by right-clicking it in the Hierarchy and choosing Delete from the popup menu.
Run the scene and you, the zombie and the cats can all finally have a dance party.
Now, the zombie can collect cats in his conga line, but the old ladies have no way to defend against this undead uprising. Time to give those ladies a fighting chance!
Handling Enemy Contact
In Zombie Conga, the player’s goal is to gather a certain number of cats into its conga line before colliding with some number of enemies. Or, that will be the goal once you’ve finished this tutorial.
To make it harder to build the conga line, you’ll remove some cats from the line every time an enemy touches the zombie.
To do so, first open CatController.cs in MonoDevelop and add the following method to the class:
public void ExitConga() { Vector3 cameraPos = Camera.main.transform.position; targetPosition = new Vector3(cameraPos.x + Random.Range(-1.5f,1.5f), cameraPos.y + Random.Range(-1.5f,1.5f), followTarget.position.z); Transform cat = transform.GetChild(0); cat.GetComponent<Animator>().SetBool("InConga", false); } |
The first two lines above assign targetPosition
a random position in the vicinity of the camera’s position, which is the center of the screen. The code you already added to Update
will automatically move the cat toward this new position.
The next two lines get the cat from inside the Cat Carrier and disable its Animator
‘s InConga
flag. Remember from Unity 4.3 2D Tutorial: Animation Controllers, that you need to set InConga
to false
in the Animator
in order to move the animation out of the CatConga state. Doing so will trigger the cat to play the CatDisappear animation clip.
Save the file (File\Save).
You maintain the conga line in ZombieController
, so that’s where you’ll add a call to ExitConga
. Open ZombieController.cs in MonoDevelop now.
Inside the class, find the following line in OnTriggerEnter2D
:
Debug.Log ("Pardon me, ma'am."); |
And replace it with this code:
for( int i = 0; i < 2 && congaLine.Count > 0; i++ ) { int lastIdx = congaLine.Count-1; Transform cat = congaLine[ lastIdx ]; congaLine.RemoveAt(lastIdx); cat.parent.GetComponent<CatController>().ExitConga(); } |
This for
loop may look a little strange, but it’s really not doing much. If there are any cats in the conga line, this loop removes the last two of them, or the last one if there is only one cat in the line.
After removing the cat’s Transform
from congaLine
, it calls ExitConga
, which you just added to CatController
.
Save the file (File\Save) and switch back to Unity.
Run the scene and get some cats in your conga line, then crash into an old lady and see what happens!
Unfortunately, when you crashed into the old lady, you crashed right into two more problems.
First, if the conga line had more than two cats when the zombie collided with the enemy, you probably saw every cat spin out of the line. You can see that in the previous animation.
The second problem is yet another exception in the Console:
No receiver, eh? Before fixing the first problem, try debugging the exception yourself. You’ve already solved an identical problem earlier in this tutorial. If you get stuck, check out the following spoiler.
With the exception fixed, it’s time to figure out how to keep the enemy from destroying your entire conga line with just one hit.
First, what’s going on? As you saw in Unity 4.3 2D Tutorial: Physics and Screen Sizes, Unity is reporting quite a few collisions as the zombie walks through the enemy.
For the cats, you solved this problem by disabling the cat’s collider when handling the first event. To eliminate redundant enemy collisions, you’ll do something a bit fancier.
You’re going to add a period of immunity after the initial collision. This is common in many games, where contacting an enemy reduces health or points and then blinks the player’s sprite for a second or two, during which time the player can take no damage. And yes, you’re going to make the zombie blink, too!
Open ZombieController.cs in MonoDevelop and add the following variables to the class:
private bool isInvincible = false; private float timeSpentInvincible; |
As their names imply, you’ll use isInvincible
to indicate when the zombie is invincible, and timeSpentInvincible
to keep track of for how long the zombie has been invincible.
Inside OnTriggerEnter2D
, find the following line:
else if(other.CompareTag("enemy")) { |
and replace it with this code:
else if(!isInvincible && other.CompareTag("enemy")) { isInvincible = true; timeSpentInvincible = 0; |
This change to the if
condition causes the zombie to ignore enemy collisions while the zombie is invincible. If a collision occurs while the zombie is not invincible, it sets isInvincible
to true
and resets timeSpentInvincible
to zero.
To let the player know they have a moment of invincibility, as well as to indicate that they touched an enemy, you’ll blink the zombie sprite.
Add the following code to the end of Update
:
//1 if (isInvincible) { //2 timeSpentInvincible += Time.deltaTime; //3 if (timeSpentInvincible < 3f) { float remainder = timeSpentInvincible % .3f; renderer.enabled = remainder > .15f; } //4 else { renderer.enabled = true; isInvincible = false; } } |
Here’s what this code does:
- The first
if
check verifies that the zombie is currently invincible, because that’s the only time you want to execute the rest of this logic. - If so, it adds
Time.deltaTime
totimeSpentInvincible
to keep track of the total time the zombie has been invincible. Remember that you resettimeSpentInvincible
to zero when the collision first occurs. - It then checks if the collision occurred less than three seconds ago. If so, it enables or disables the zombie’s renderer based on the value of
timeSpentInvincible
. This bit of math will blink the zombie on and off about three times per second. - Finally, if it’s been at least three seconds since the collision, the code enables the zombie’s renderer and sets
isInvincible
tofalse
. You enable the renderer here to ensure the zombie doesn’t accidentally stay invisible.
Save the file (File\Save) and switch back to Unity.
Run now and the conga line grows and shrinks as it should.
Ok, the conga line works, but without a way to win or lose, it’s still not a game. (That’s right, I said it. If you can’t win or lose, it’s not a game!) Time to fix that.
Winning and Losing
Players of Zombie Conga win the game when they build a long enough conga line. You maintain the conga in ZombieController.cs, so open that file in MonoDevelop.
Add the following code to OnTriggerEnter2D
, inside the block that handles cat collisions, just after the line that adds other.transform
to congaLine
:
if (congaLine.Count >= 5) { Debug.Log("You won!"); Application.LoadLevel("CongaScene"); } |
This code checks if the conga line contains at least five cats. If so, it logs a win message to the Console and then calls Application.LoadLevel
to reload the current scene, named CongaScene. While it includes “level” in its name, LoadLevel
actually loads Unity scenes. See the Application
class documentation to find out more about what this class has to offer.
Don’t worry – reloading CongaScene is only for testing. You’ll change this later to show a win screen instead.
5
in the if
check to any number you’d like.Save the file (File\Save) and switch back to Unity.
Play the scene. Once you get five cats in your conga line, you’ll see “You won!” in the Console and the scene will reset to its start state.
Winning isn’t as satisfying if there’s no way to lose, so take care of that now.
Switch back to ZombieController.cs in MonoDevelop and add the following variable to the class:
private int lives = 3; |
This value keeps track of how many lives the zombie has remaining. When this reaches zero, it’s Game Over.
Add the following code to OnTriggerEnter2D
, inside but at the end of the block of code that handles collisions with enemy objects:
if (--lives <= 0) { Debug.Log("You lost!"); Application.LoadLevel("CongaScene"); } |
This code subtracts one from lives
and then checks to see if there are any lives left. If not, it logs a message to the Console and then calls Application.LoadLevel
to reload the current scene. Once again, this is only for testing – you’ll change it later to show a lose screen.
Save the file (File\Save) and switch back to Unity.
Play the scene now and hit three old ladies. No, don’t do that. Play the game, and in the game, let three old ladies hit you. You’ll see “You lost!” in the Console and the scene will reset to its start state.
And that’s it! Zombie Conga works, even if it is a bit unpolished. In the remainder of this tutorial, you’ll add a few finishing touches, including additional scenes, some background music and a sound effect or two.
Additional Scenes
To finish up the game, you’ll add the following three screens to Zombie Conga:
So just draw those images and when you’re done, come back and learn how to add them to the game. Shouldn’t take but a minute.
Ok, I really don’t have time to wait for you to do your doodles. Just download and unzip these resources so we can get going.
The file you downloaded includes two folders: Audio and Backgrounds. Ignore Audio for now and look at Backgrounds, which contains a few images created by Mike Berg. You’ll use these images as backgrounds for three new scenes.
You first need to import these new images as Sprites. You learned how to do this way back in Part 1 of this series, so this would be a good time to see how much you remember.
Try creating Sprite assets from the images in Backgrounds. To keep things organized, add these new assets in your project’s Sprites folder. Also, remember to tweak their settings if necessary to ensure they look good!
You should now have three new Sprites in the Project browser, named StartUp, YouWin and YouLose, as shown below:
Before creating your new scenes, make sure you don’t lose anything in the current scene. Save CongaScene by choosing File\Save Scene in Unity’s menu.
Choose File\New Scene to create a new scene. This brings up an empty scene with a Main Camera.
Choose File\Save Scene as…, name the new scene LaunchScene and save it inside Assets\Scenes.
Add a StartUp Sprite to the scene, positioned at (0,0,0). You should have no problem doing this yourself, but the following spoiler will help if you’ve forgotten how.
With the background in the scene, see if you can set up LaunchScene‘s camera yourself. When you’re finished, your Game view should show the entire StartUp image, like this:
If you need any help, check the following spoiler.
At this point, you’ve set up LaunchScene. Play the scene and you should see the following:
Be honest: how long did you stare at it waiting for something to happen?
You want Zombie Conga to start out showing this screen, but then load CongaScene so the user can actually play. To do that, you’ll add a simple script that waits a few seconds and then loads the next scene.
Create a new C# script named StartGame and add it to Main Camera.
Open StartGame.cs in MonoDevelop and replace its contents with the following code:
using UnityEngine; public class StartGame : MonoBehaviour { // Use this for initialization void Start () { Invoke("LoadLevel", 3f); } void LoadLevel() { Application.LoadLevel("CongaScene"); } } |
This script uses two techniques you saw earlier. Inside Start
, it calls Invoke
to execute LoadLevel
after a three second delay. In LoadLevel
, it calls Application.LoadLevel
to load CongaScene.
Save the file (File\Save) and switch back to Unity.
Run the scene. After three seconds, you’ll see the following exception in the Console.
This exception occurs because Unity doesn’t know about your other scene. Why not? It’s right there in the Project browser, isn’t it?
Yes, it’s there, but Unity doesn’t assume that you want to include everything in your project in your final build. This is a good thing, because you’ll surely create many more assets than you ever use in your final game.
In order to tell Unity which scenes are part of the game, you need to add them to the build.
Inside Unity’s menu, choose File\Build Settings… to bring up the Build Settings dialog, shown below:
The lower left of the dialog includes the different platforms for which you can build. Don’t worry if your list doesn’t look the same as the above image.
The current platform for which you’ve been building – most likely, PC, Mac & Linux Standalone – should be highlighted and include a Unity logo to indicate it’s selected.
To add scenes to the build, simply drag them from the Project browser into the upper area of Build Settings, labeled Scenes In Build. Add both LaunchScene and CongaScene to the build, as shown below:
As you can see in the following image, levels in the Scenes In Build list are numbered from zero. You can drag levels to rearrange their order, and when running your game outside of Unity, your player starts at level zero. You can also use index numbers rather than scene names when calling LoadLevel
.
Close the dialog and run the scene. This time, the startup screen appears and then the game play starts after three seconds.
You should now create and add to your game two more scenes: WinScene and LoseScene. These should each display the appropriate background image – YouWin and YouLose, respectively. After three seconds, they should reload CongaScene.
Simply repeat the steps you took to create LaunchScene. The difference is that for these two scenes, you can reuse StartGame.cs rather than creating a new script. Or, check out the following spoiler if you’d like a shortcut.
After creating your new scenes, add them to the build. Your Build Settings should now look similar to this, although the order of your scenes after LaunchScene really doesn’t matter.
Once these scenes are in place, you need to change your code to launch them rather than print messages to the Console.
Open ZombieController.cs in MonoDevelop.
Inside OnTriggerEnter2D
, find the following lines:
Debug.Log("You won!"); Application.LoadLevel("CongaScene"); |
And replace them with this line:
Application.LoadLevel("WinScene"); |
This will load WinScene instead of just reloading CongaScene.
Now fix OnTriggerEnter2D
so it loads LoseScene at the appropriate time.
Save the file (File\Save) and switch back to Unity.
At this point, you can play the game in its entirety. For the best experience, switch to LaunchScene before playing. After start up, play a few rounds, making sure you win some and you lose some. Hmm. That’s sounds pretty cool – I should trademark it.
With all your scenes in place, it’s time to get some tunes up in this tut!
Audio
Find the folder named Audio in the resources you downloaded earlier. This folder contains music and sound effects made by Vinnie Prabhu for our book, iOS Games by Tutorials.
Add all five files to your project by dragging Audio directly into the Project browser.
Open the Audio folder in the Project browser to reveal your new sound assets, as shown below:
Select congaMusic in the Project browser to reveal the sound’s Import Settings in the Inspector, shown in the following image:
Notice in the image above that Audio Format is disabled. That’s because Unity will not let you choose the format when importing compressed audio clips.
Unity can import .aif, .wav, .mp3 and .ogg files. For .aif and .wav files, Unity lets you choose between using the native format or compressing into an appropriate format for the build target. However, Unity automatically re-encodes .mp3 and .ogg files if necessary to better suit the destination. For example, .ogg files are re-encoded as .mp3 files for iOS.
There is a slight loss of sound quality if Unity needs to convert from one compressed format to another. For that reason, Unity’s documentation recommends that you import audio files in lossless formats like .aif and .wav and let Unity encode them to .mp3 or .ogg as needed. You’re using an .mp3 file here because I didn’t have a lossless version and this one sounds good enough.
For each of the five audio files you imported, you’ll leave most settings with their default values. However, you won’t be placing your sounds in 3D space, so uncheck 3D Sound, as shown below, and then click Apply:
When you hit Apply, Unity reimports the sound clip. If this takes a while, you’ll see a dialog that shows the encoding progress, as shown below:
Disable 3D sound for each of the other four sounds files: hitCat, hitEnemy, loseMusic and winMusic.
With your sound files imported properly, you’ll first add sounds to CongaScene. Save the current scene if necessary and open CongaScene.
To play a sound in Unity, you need to add an Audio Source component to a GameObject. You can add such a component to any GameObject, but you’ll use the camera for Zombie Conga’s background music.
Select Main Camera in the Hierarchy. Add an audio source from Unity’s menu by choosing Component\Audio\Audio Source. The Inspector now displays the Audio Source settings shown below:
Just like how you’ve set assets in fields before, click the small circle/target icon on the right of the Audio Source component’s Audio Clip field to bring up the Select AudioClip dialog. Select congaMusic from the Assets tab, as shown in the following image:
Note that Play On Awake is already checked in the Audio Source component. This instructs Unity to begin playing this audio clip immediately when the scene loads.
This background music should continue to play until the player wins or loses, so check the box labeled Loop, shown below:
This instructs Unity to restart the audio clip when the clip reaches its end.
Play the scene and you’ll finally hear what the cats have been dancing to all this time.
Before you worry about the win and lose scenes, you’ll spice up the gameplay with a few collision sound effects.
Open ZombieController.cs in MonoDevelop and add the following variables to ZombieController
:
public AudioClip enemyContactSound; public AudioClip catContactSound; |
These variables store the AudioClip
s you’ll play during specific collisions. You’ll assign them later in the editor.
In OnTriggerEnter2D
, add the following line inside the block of code that runs when the zombie collides with a cat:
audio.PlayOneShot(catContactSound); |
This calls PlayOneShot
on audio
to play the audio clip stored in catContactSound
. But where did audio
come from?
Every MonoBehaviour
has access to certain built-in fields, like the transform
field you’ve been accessing throughout this tutorial series. If a GameObject contains an AudioSource
component, you can access it through the built-in audio
field.
Now add the following line to OnTriggerEnter2D
, inside the block of code that runs when the zombie collides with an enemy:
audio.PlayOneShot(enemyContactSound); |
This code plays enemyContactSound
when the zombie collides with an enemy.
Save the file (File\Save) and switch back to Unity.
Select zombie in the Hierarchy. The Zombie Controller (Script) component now contains two new fields in the Inspector:
Set Enemy Contact Sound to the hitEnemy sound asset. Then set Cat Contact Sound to hitCat. If you don’t remember how to set these audio clips, review the steps you used earlier to set congaMusic in the camera’s Audio Source.
Play the scene now and run the zombie into an enemy or a cat. Oops. Unity prints out the following exception each time the zombie collides with someone, letting you know there’s a component missing:
The exception points out the problem and helpfully suggests the solution. ZombieController
tried to access the zombie’s AudioSource
via its audio
field, but zombie doesn’t currently have an Audio Source.
Correct this now by adding an Audio Source component to zombie. Select zombie in the Hierarchy and choose Component\Audio\Audio Source in Unity’s menu.
The Audio Source’s default settings are fine. You won’t set an Audio Clip on it because ZombieController
provides the clips when it plays them.
Play the scene again and listen as the beach comes to life with Realistic Sound Effects Technology!
Now add some background music to WinScene and LoseScene on your own. Make WinScene play winMusic and make LoseScene play loseMusic. In both cases, make the sound play as soon as the scene starts and do not let it loop.
And that’s it! To get the full Zombie Conga experience, play LaunchScene and then enjoy the music as it kicks in when the gameplay starts. If you win, you’ll be rewarded with WinScene‘s fun image and music, but if you lose you’ll see a sad zombie listening to a sad tune. Enjoy!
Where to Go From Here?
If you’ve stuck it out through this entire series, congratulations! You’ve made a simple game in Unity, and hopefully along the way you’ve learned a lot about Unity’s new 2D features.
You can download the complete Zombie Conga project here.
To learn more about working with Unity, 2D or otherwise, I recommend taking a look through Unity’s Live Training Archive. Also, take a look through Unity’s documentation, which was recently updated with the release of Unity 4.5.
I hope you enjoyed this series. As usual, please leave any feedback or ask questions in the Comments sections. Or contact me on Twitter.
Now go play some Zombie Conga. And when you’re done playing, go make a game!
Unity 4.3 2D Tutorial: Scrolling, Scenes and Sounds is a post from: Ray Wenderlich
The post Unity 4.3 2D Tutorial: Scrolling, Scenes and Sounds appeared first on Ray Wenderlich.