Unity 4.3 2D Tutorial: Scrolling, Scenes and Sounds

Unity 4.3 2D Tutorial: Scrolling, Scenes and Sounds
Line up for some Zombie Conga!

Line up for some Zombie Conga!

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.

Solution Inside: Need help adding a new script? SelectShow
There are several ways you could create this script and add it to Main Camera. Here is just one:

  1. Click Add Component in the Inspector, and in the menu that appears, choose New Script.
  2. Enter CameraController for the name and choose CSharp for the language, then click Create and Add.

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.

Note: Sticklers for correctness might not like how the first line of code above relies on the assumption that newPosition accurately reflects the camera’s position. If you are one of said sticklers, feel free to replace that line with
newPosition.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!

broken_scrolling_no_repeats

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:

  1. It finds the scene’s main Camera object (which is the only camera in Zombie Conga) and sets cameraTransform to point to the camera’s Transform.
  2. It casts the object’s built-in renderer property to a SpriteRenderer in order to access its sprite property, from which it gets the Sprite’s bounds. The Bounds object has a size property whose x component holds the object’s width, which it stores in spriteWidth.

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:

bg_offscreen_detection

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:

bg_adjusted_1x

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:

broken_scrolling_gaps

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:

duplicate_bg_position

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:

long_beach_in_scene

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.

good_scrolling_bg

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.

Note: You could put this script on any GameObject that exists for the life of the scene, such as zombie or Main Camera. However, using a dedicated object lets you give it a descriptive name, and that makes it easier to find in the Hierarchy when you want to tweak settings.

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:

  1. minSpawnTime and maxSpawnTime specify how often new cats appear. After a cat spawns, KittyCreator will wait at least minSpawnTime seconds and at most maxSpawnTime seconds before spawning another cat. You declared them public so you can tweak the spawn rate in the editor later if you’d like.
  2. Unity’s Invoke method lets you call another method after a specified delay. Start calls Invoke, instructing it to wait minSpawnTime seconds and then to call SpawnCat. This adds a brief period after the scene starts during which no cats spawn.
  3. For now, SpawnCat simply logs a message letting you know when it executes and then uses Invoke to schedule another call to SpawnCat. It waits a random amount of time between minSpawnTime and maxSpawnTime, 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:

cat_spawn_msg

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:

create_cat_prefab

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:

prefab_inspector_buttons

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:

  1. 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.
  2. 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.
  3. You call Instantiate to create an instance of catPrefab placed in the scene at the position defined by catPos. You pass Quaternion.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 to Instantiate 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.

Cat spawn area calculation.

Cat spawn area calculation.

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:

delete_cat

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:

cat_prefab_field

Inside the Select GameObject dialog that appears, choose cat from the Assets tab, as shown in the following image:

cat_prefab_selection

Kitten Factory now looks like this in the Inspector:

kitten_factory_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.

Photo of spawning cats, because the GIFs weren't cooperating.

Photo of spawning cats, because the GIFs weren’t cooperating.

However, there’s a problem. As you play, notice how a massive list of cats slowly builds up in the Hierarchy, shown below:

crazy_cat_clones

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.

Note: You may have noticed that 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:

After running for a minute, Hierarchy still only contains the visible cats

After running for a minute, Hierarchy still only contains the visible cats

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.

wheres_the_conga2

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:

empty_tags_list

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:

cat_tag

Note: Unity insists there be at least one more field than you have defined tags, so it will add one whenever you type in the last available field. Even if you change the value in the Size field to match the number of tags you have, Unity will automatically change it back to a value one greater than the number of tags you have.

Select cat in the Project browser and choose cat from the combo box labeled Tag in the Inspector, like this:

setting_cat_tag

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:

add_tag_menu

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:

tags_in_list

With the new tag created, select enemy in the Hierarchy and set its Tag to enemy, as shown below:

enemy_tag_set

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.

collision_msgs

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:

cat_anim_wiggle

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.

cat_conga_working

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.

zombification_1

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 Transforms instead of GameObjects 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:

  1. The method signature now requires a target Transform, a movement speed and a turn speed. Later you’ll change ZombieController to call JoinConga with the appropriate values.
  2. 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.
  3. This flags the cat as a zombie. You’ll see why this is important soon.
  4. 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:

  1. 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.
  2. 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.
  3. 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.
  4. It then checks moveDirection‘s magnitude – 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.
  5. This check makes sure that the cat doesn’t move more than moveSpeed per second.
  6. 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.

bad_conga_line_2

When you played the scene, you may have noted the following problems:

  1. If any cat in the conga line goes off screen, it gets removed and then Unity starts printing the following exception to the console:
    exception_destroying_cats_in_conga
  2. 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?
  3. 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.

better_conga_line

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.

bad_follow_target

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:

animation_record_mode

Click the Add Event button shown below:

add_event_button

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.

edit_anim_event_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:

Difficult to see in this GIF, but trust me, these cats are hopping.

Difficult to see in this GIF, but trust me, these cats are hopping.

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:

conga_speed_2

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.

no_tutorial_surprise2

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:

catconga_scale

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:

cat_in_carrier

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:

cat_transform

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:

remove_component

Click Apply at the top of the Inspector to ensure this change makes it back to the Prefab, as shown in the following image:

prefab_apply_button

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:

add_script

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:

cat_carrier_in_hierarchy

Select Cat Carrier in the Hierarchy and delete it by choosing Edit\Delete from Unity’s menu.

The Hierarchy now looks like this:

no_cat_carrier_in_hierarchy

Inside the Project browser, you now have a cat Prefab and a Cat Carrier Prefab, which itself contains a cat Prefab, as shown below:

prefab_assets

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:

delete_prefab

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)”:

kitty_creator_missing_prefab

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.

Solution Inside: Need help setting the Cat Prefab field? SelectShow
To assign Cat Carrier to the Cat Prefab field, do the following:

  1. With Kitten Factory selected in the Hierarchy, click the small circle/target icon in the Inspector just to the right of the Kitty Creator (Script) component’s Cat Prefab field.
  2. Inside the Select GameObject dialog that appears, choose Cat Carrier from the Assets tab.

Run the scene. At this point, you’ll see exceptions similar to the following in the Console whenever the zombie collides with a cat.

null_ref_exception-Recovered

Double-click one of these exceptions inside the Console and you’ll arrive at the relevant line, highlighted in MonoDevelop, as shown below:

The code above was rearranged slightly to better fit the screenshot. Don't be alarmed.

The code above was rearranged slightly to better fit the screenshot. Don’t be alarmed.

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.

Note: You could have fixed this by adding a new script to the cat Prefab. In that script, you could add a 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:

missing_component_exception

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:

error_in_mono_2b

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.

Note: I don’t really like this sort of code because it relies on knowing that the cat is the first child of Cat Carrier. If you wanted to do something more robust, you could find the component with the following line instead of using 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:

anim_event_exception

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!

empty_animation_window

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!:

error_in_anim_event

Double click the animation event marker and…nothing happens. Pop quiz! Why? Check the spoiler below for the answer.

Solution Inside SelectShow
Remember, you cannot modify Animation Clips on a Prefab. Double-clicking the event marker should bring up the Edit Animation Event dialog, which you can’t access if you can’t edit the object.

To correct the situation, drag Cat Carrier from the Project browser into the Hierarchy. Then select its child, cat, in the Hierarchy.

Once you’ve corrected the situation, double click the animation event marker again and the following dialog appears, indicating that UpdateTargetPosition is not supported:

targetpos_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():

fixed_updatetarget

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.

conga_working

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!

bad_enemy_collisions

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:

anim_event_error_2

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.

Solution Inside: Cat not receiving your (function) calls? SelectShow
You can fix this in either of two ways.

Option 1: You could add a method like the following to CatUpdater.cs:

void GrantCatTheSweetReleaseOfDeath()
{
  catController.GrantCatTheSweetReleaseOfDeath();
}

However, for that to work, you need to change the declaration of GrantCatTheSweetReleaseOfDeath in CatController.cs so it is public, like this:

public void GrantCatTheSweetReleaseOfDeath()

Option 2: The easier way to handle this situation is to add a method like the following to CatUpdater.cs:

void GrantCatTheSweetReleaseOfDeath()
{
  Destroy( transform.parent.gameObject );
}

This simply tells the cat’s parent to remove itself, which in turn removes the cat.

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:

  1. 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.
  2. If so, it adds Time.deltaTime to timeSpentInvincible to keep track of the total time the zombie has been invincible. Remember that you reset timeSpentInvincible to zero when the collision first occurs.
  3. 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.
  4. Finally, if it’s been at least three seconds since the collision, the code enables the zombie’s renderer and sets isInvincible to false. 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.

good_enemy_collisions

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.

Note: In a real game, players should probably need more than five cats in their conga to win. However, testing the win state would take longer, and you’re a busy person with important things to do. Feel free to change the 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.

win_reload

you_won_log

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.

lose_reload

you_lose_log

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:

A splash screen to show when the game launches.

A splash screen to show when the game launches.

A win screen to reward the player for a job well done.

A win screen to reward the player for a job well done.

A lose screen to express your disappointment with the player's lack of skill.

A lose screen to express your disappointment with the player’s lack of skill.

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.

waiting_for_drawing

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!

Solution Inside: Need help creating Sprites? SelectShow
Creating Sprites was covered extensively in Unity 4.3 2D Tutorial: Getting Started, so there won’t be much detail here. If you need more help, review that tutorial again.

To create a Sprite asset, you first need to add the files to the project. The easiest way is to drag them into the Project browser from your Finder/Explorer.

If Unity is still in 2D mode, which you set up back in Part 1, then these images were turned into Sprites automatically. If not, you need to change each asset’s Texture Type to Sprite in the Inspector.

sprite_texture

Each of the images is 1136×640 pixels, which is too large for Unity’s default texture size of 1024×1024. To make them look their best, you should adjust each Sprite’s Max Size to 2048.

texture_size

Finally, while they all look fine with the default Format of Compressed, I prefer to set StartUp‘s Format to 16 bits.

texture_format

You should now have three new Sprites in the Project browser, named StartUp, YouWin and YouLose, as shown below:

all_sprites_in_project

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.

empty_scene

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.

Solution Inside: Need help adding a sprite? SelectShow
To add the Sprite, simply drag StartUp from the Project browser into the Hierarchy.

Select StartUp in the Hierarchy and make sure its Transform‘s Position is (0,0,0).

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:

start_screen

If you need any help, check the following spoiler.

Solution Inside: Need help setting up the scene? SelectShow
Just like you did with CongaScene, you’ll want LaunchScene‘s camera to have an orthographic projection with a size of 3.2.

Select Main Camera in the Hierarchy. In the Inspector, choose Orthographic for Projection and set Size to 3.2, as shown below:

camera_setup

At this point, you’ve set up LaunchScene. Play the scene and you should see the following:

start_screen

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.

Note: You’re adding the script to the camera, but you could have added it to StartUp or to an empty GameObject. It just needs to be on an active object in the scene.

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.

missing_level_exception

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:

build_settings

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:

adding_scenes

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.

scenes_in_build

Close the dialog and run the scene. This time, the startup screen appears and then the game play starts after three seconds.

starting_game

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.

Solution Inside: Want a shortcut for creating your scenes? SelectShow
Rather than make each new scene, simply duplicate the existing LaunchScene and replace the image.

To do so, first, save LaunchScene via File\Save Scene to ensure you don’t lose any of your work.

Then, save your scene again, but this time use File\Save Scene as… and name it WinScene.

Delete StartUp from the Hierarchy and replace it with YouWin from the Project browser.

That’s it. Save the scene (File\Save) and then repeat the process to create LoseScene.

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.

all_scenes

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.

Solution Inside: Not sure where to load the scene? SelectShow
Still in ZombieController.cs, find the following lines in OnTriggerEnter2D:

Debug.Log("You lost!");
Application.LoadLevel("CongaScene");

And replace them with this line:

Application.LoadLevel("LoseScene");

Now, when players lose, they’ll know it.

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.

full_game

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:

audio_files

Select congaMusic in the Project browser to reveal the sound’s Import Settings in the Inspector, shown in the following image:

congaMusic_inspector

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.

Note: Unity also supports tracker modules, which are similar to MIDI files but better because they include the instrument samples as part of the file. If your game has demanding audio requirements, you should look into using tracker modules.

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:

uncheck_3d_sound

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:

asset_conversion_progress

Note: In 2D games, you generally won’t need 3D sound clips. These are used to produce effects that change based on their distance from and motion relative to the listener. For example, sounds that get louder as you approach their source, cars producing a doppler effect as they race toward or away from the listener, and sounds that are behind the player and seem to move in space as the player turns toward them.

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:

audio_source_in_inspector

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:

select_audioclip

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:

loop_audio

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.

Artist's rendering of musical good times.

Artist’s rendering of musical good times.

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 AudioClips 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:

zombie_sounds_empty

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:

missing_component_exception

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!

meow2

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.

Solution Inside SelectShow
Open WinScene. Add an Audio Source to Main Camera, then set the Audio Source component’s Audio Clip to winMusic. Be sure Play On Awake is checked and Loop is unchecked.

Open LoseScene. Add an Audio Source to Main Camera, then set the Audio Source component’s Audio Clip to loseMusic. Be sure Play On Awake is checked and Loop is unchecked.

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!

Who's the big winner? You are!

Who’s the big winner? You are!

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.

4
Like
Save

Comments

Write a comment

*