Welcome back to our Unity 4.3 2D Tutorial series!
In the first part of the series, you started making a fun game called Zombie Conga, learning the basics of Unity’s 4.3′s built-in 2D support along the way.
In the second part of the series, you learned how to animate the zombie and the cat using Unity’s powerful built-in animation system.
In this third part of the series, you got more practice creating Animation Clips, and learned how to control the playback of and transition between those clips.
In this fourth part of the series, you’ll learn about some common issues you may encounter while making your own games, such as 2D physics and dealing with different screen sizes and aspect ratios.
This tutorial picks up where the third 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.
With your project ready to go, let’s get physical! Or physics-le, as it were.
Getting Started
In Zombie Conga, you don’t actually need to use Unity’s built-in physics engine to write the game. Sure, you’ll need to know when the zombie collides with an enemy or a cat, but you can accomplish that with some basic math.
However, in the spirit of doing unnecessary things in the pursuit of education, this tutorial will show you how to use physics to handle Zombie Conga’s collision detection. By the time you’re done here, you’ll be better prepared to explore other physics topics on your own.
If you’ve ever used physics with 3D objects in Unity, then you already understand a lot about its 2D physics engine, too, because they are quite similar. They both rely on rigidbodies, colliders, physics materials and forces to represent an object’s state in a physics simulation.
One difference is that 2D rigidbodies can only move within the XY plane and can only rotate around the z-axis, which now makes 2D collisions a lot easier to deal with than they were before Unity 4.3 when you had to trick 3D objects into reacting only in two dimensions.
The following demonstrates this point by dropping a cube and a sprite and letting physics take over:
Another difference is that 3D colliders have a size in all three dimensions, whereas 2D colliders have an infinite z-depth. This means that 2D objects will collide with each other regardless of their positions along the z-axis.
The following image shows the colliders for two cubes and two sprites. The positions of the two cubes differ only along the z-axis, and the positions of the two sprites differ only along the z-axis. As you’d expect, the two cubes are not currently colliding, but what you might not expect is that the two sprites are colliding.
In order for the zombie to participate in physics-based collisions, it needs a collider component. In 3D you would add some subclass of Collider
, such as BoxCollider
or MeshCollider
. When working with the 2D physics engine, you use instances of Collider2D
instead of Collider
, such as BoxCollider2D
or CircleCollider2D
.
Note: You can mix both 2D and 3D physics in the same game, but objects using physics of one time cannot interact with objects using physics of the other type because the two simulations run independently. This also means you cannot add 2D colliders or rigidbodies on a GameObject that contains 3D colliders or rigidbodies. Don’t worry, if you ever accidentally try, Unity will complain about it.
In the zombie’s case, you’ll add what is basically the 2D equivalent of a MeshCollider
, called a PolygonCollider2D
.
To do so, select zombie in the Hierarchy and add the collider by choosing Component\Physics 2D\Polygon Collider 2D from Unity’s menu.
Unity automatically calculated a group of vertices that fit your sprite fairly well, which you can see highlighted in green in the Scene view, as shown below:
However, there is actually a slight problem here. The collider you created was for the first frame of animation, because that was the Sprite set on the zombie when you added the component. Unfortunately, it won’t match up with the other Sprites displayed during the zombie’s walk animation, as you can see in the images below:
In many games, this will be fine. In fact, you’d get perfectly acceptable results in Zombie Conga if you used a much simpler collider, such as a BoxCollider2D
or a CircleCollider2D
. But at some point you’re going to want to have collision areas that match the shape of an animating sprite, so now is a good time to learn how to do it.
Double-click ZombieController inside the Scripts folder in the Project browser to open the script in MonoDevelop.
Rather than use a single collider, you’re going to create a separate one for each frame of animation and then swap them to match the animation. Add the following instance variables to ZombieController
to keep track of these colliders:
[SerializeField]
private PolygonCollider2D[] colliders;
private int currentColliderIndex = 0; |
Let’s go over this line by line:
- The
[SerializeField]
directive tells Unity to expose the instance variable below (colliders
) in the Inspector. This allows you to make the variable private in code while still giving you access to it from Unity’s editor.
colliders
will hold a separate PolygonCollider2D
for each frame of animation.
currentColliderIndex
will keep track of the index into colliders
for the currently active collider.
Save the file (File\Save) and go back to Unity.
Select zombie in the Hierarchy. In the Inspector, expand the Colliders field in the Zombie Controller (Script) component to reveal its Size field.
This field currently contains the value zero, meaning it’s an array with no elements. You want to store a different collider for each of the zombie’s Sprites, so change this value to 4, as shown below:
Inside the Inspector, click the Polygon Collider 2D component and drag it into the Zombie Controller (Script) component, releasing it over the field labeled Element 0 in the Colliders array, as demonstrated below:
Next, change the zombie’s sprite to zombie_1 by clicking the target icon to the right of the Sprite field and double-clicking on zombie_1 in the dialog that appears, as shown below:
With this new Sprite set on the zombie, add a new Polygon Collider 2D. Check the following Spoiler if you don’t remember how.
Solution Inside: Need help adding a collider? |
SelectShow> |
With zombie selected in the Hierarchy, add a collider by choosing Component\Physics 2D\Polygon Collider 2D from Unity’s menu.
|
The zombie now has two colliders attached to it, as shown below:
Inside the Inspector, click the new Polygon Collider 2D component you just added and drag it into the Element 1 field in the Colliders array of the Zombie Controller (Script) component.
Repeat the previous steps to create colliders for the zombie_02 and zombie_03 Sprites and add these to the Colliders array in the Element 2 and Element 3 fields, respectively.
When you are finished, the Inspector should look like this:
Select zombie in the Hierarchy and reset its Sprite to zombie_0. Inside the Scene view, you can see that he now has colliders for each of his Sprites, but all at the same time!
This isn’t exactly what you want. Rather than have them all active at the same time, you want to activate only the collider that matches the current frame of animation. But how do you know which is the current frame?
Swapping Colliders at Runtime
As you learned in part 3 of this series (on Animation Controllers), you can configure an Animation Clip to call methods on a GameObject at specific frames of the animation. You’ll take advantage of that feature to update the zombie’s collider.
Switch back to ZombieController.cs in MonoDevelop and add the following method to the class:
public void SetColliderForSprite( int spriteNum )
{
colliders[currentColliderIndex].enabled = false;
currentColliderIndex = spriteNum;
colliders[currentColliderIndex].enabled = true;
} |
This first disables the current collider, then it updates currentColliderIndex
and enables the new Sprite’s collider.
Save the file (File\Save) and switch back to Unity.
The code you just wrote will take care of setting the zombie’s collider, so you want to make sure the zombie starts without any colliders enabled.
Select zombie in the Hierarchy and disable each of the Polygon Collider 2D components in the Inspector. Your zombie’s colliders should now look like this in the Inspector:
With its colliders disabled, you’ll need to call SetColliderForSprite
each time the zombie’s Sprite changes to ensure the zombie’s current collider and Sprite match each other. To do so, you’ll set up ZombieWalk to fire Animation Events at each keyframe.
Note: You learned how to add Animation Events in the
part 3 of this series, so review that if you have any problems with these next few steps.
Select zombie in the Hierarchy and open the Animation view (Window\Animation). Be sure the clip drop-down menu in the Animation view’s control bar shows ZombieWalk.
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 SetColliderForSprite(int) from the Function combo box in the Edit Animation Event dialog that appears. Make sure the Int field listed under Parameters contains the value 0, as shown in the following image, and then close the dialog.
Now add similar events at frames 1, 2, 3, 4 and 5. When setting the parameters for each event, pass in the values 1, 2, 3, 2 and 1, respectively, to correspond with the Sprites shown at each keyframe.
When you’re done, ZombieWalk‘s events should be configured as shown below:
Play the scene with zombie selected in the Hierarchy. Pause the scene by pressing the Pause button in the Scene controls at the top of Unity’s interface, shown here:
Now step through the scene a frame at a time using the Frame Advance button to the right of the Pause button, shown here:
While stepping through the animation, you can see in the Scene view that the collider matches the current frame of animation, as demonstrated below:
Note: Technically, the zombie’s collision detection may use the wrong collider ten frames out of every second. That’s because during each of
ZombieWalk‘s keyframes, Unity may process collisions
before it fires the Animation Event that updates the zombie’s collider. In other words, Unity may detect collisions before changing the collider, so that the detected collision was actually for the previous frame’s collider.
Is there a solution? Yes, you could change the clip’s frame rate to something much higher, like 60, and then fire the SetColliderForSprite
event exactly one frame before the keyframe that changes the Sprite.
Should you bother? Probably not. Zombie Conga’s players probably won’t notice any difference either way because the zombie’s various colliders are so similar. If any bad collision occurs, it will most likely be valid the next frame anyway and the player will probably never notice what happened.
The zombie looks ready, but it needs something to collide with. Hey, hasn’t that old lady been just sitting around doing nothing ever since she showed up in part 1 of this series? It’s time to put the elderly to work!
Collision Detection
For the old lady to participate in collisions, she’ll need a collider. Try attaching a polygon collider to her, checking the following Spoiler if you need help.
Solution Inside: Still need help adding a collider? |
SelectShow> |
Select enemy in the Hierarchy and add the collider by choosing Component\Physics 2D\Polygon Collider 2D from Unity’s menu.
|
Run the scene and now the zombie…walks right through the old lady just like he always did.
The zombie and enemy each have colliders, but for colliders to actually have any effect during a collision, at least one of the GameObjects involved needs a RigidBody2D
component, too. In this case, you’ll add one to the zombie.
Select zombie in the Hierarchy and choose Component\Physics 2D\Rigidbody 2D from Unity’s menu.
Run the scene now and let the zombie walk straight into the old lady. Hmm, there are a couple bad things happening here.
First, as soon as you pressed Play, the zombie slipped down a bit in the scene. Then, when the zombie hit the enemy, it seemed to actually bump into her, probably getting stuck for a bit before eventually walking around her.
The first problem is a result of the scene’s gravity acting on the zombie’s Rigidbody 2D component. It only happens when the scene first starts playing because after processing the gravity in the first frame, the code you wrote in ZombieController.cs starts setting the zombie’s position directly.
Fortunately, you have your choice of two easy fixes: you can turn off gravity for the entire scene, or just for the zombie. Because Zombie Conga doesn’t need any gravity, you’ll simply turn it off for the entire scene.
In Unity’s menu, go to Edit\Project Settings\Physics 2D and then change Gravity‘s Y value to 0 in the Inspector, as shown below:
The Gravity values simply accelerates all objects along the X and/or Y-axes by the specified amount. But setting them to zero, you have effectively disabled gravity.
Note: If you ever want to disable or adjust gravity for a particular object, adjust Gravity Scale in the object’s Rigidbody 2D component in the Inspector.
Run again and the zombie doesn’t have that annoying dip when the scene starts.
That fixes the first problem, but the zombie is still bumping into the enemy. That’s because your zombie and the old lady are using colliders defined as solid objects, but what you really want are triggers.
Triggers
Triggers are a special kind of collider. They still test for collisions like regular colliders do, but they don’t actually have any effect on the objects with which they collide. That is, they allow other colliders to pass through them, but Unity notifies the objects involved that a collision occurred, allowing you to “trigger” some game logic.
Because Zombie Conga only requires collision detection and not physics-based reactions, trigger colliders are the perfect solution.
Turn the zombie’s colliders into triggers by selecting zombie in the Hierarchy and checking the box labeled Is Trigger in each of the Polygon Collider 2D components in the Inspector, shown below:
Run the scene and the zombie walks through the enemy once again, but this time, with colliders!
But how is this different from what you had before you added colliders?
When two colliders touch and at least one of them is a trigger, Unity calls various methods on the scripts attached to the GameObject(s) containing the trigger(s). Unity calls OnTriggerEnter2D
when a collider enters a trigger, OnTriggerExit2D
when a collider exits a trigger, and OnTriggerStay2D
every frame during which the collider remains inside a trigger.
Note: You made the zombie’s
four colliders triggers instead of the enemy’s
one collider, by why? Honestly, you could have done it either way, but you chose the zombie because the game logic you are going to trigger will make more sense in a script on the zombie than it would in a script on the enemy.
However, you should also know that you can attach multiple scripts to a GameObject, and each one can define one or more of these trigger handling methods. During collisions, Unity calls these methods on every script containing them, so you can have multiple scripts react to the collision if necessary. And if two triggers ever collide, Unity calls the appropriate methods on the scripts for both objects.
In Zombie Conga, you’ll only ever need to know when a collider enters a trigger, so you’ll only implement OnTriggerEnter2D
.
Switch back to ZombieController.cs in MonoDevelop and add the following method to the class:
void OnTriggerEnter2D( Collider2D other )
{
Debug.Log ("Hit " + other.gameObject);
} |
This just prints out a log to Unity’s console when the zombie collides with any other Collider2D
, which you’ll use to test that things are working properly before writing any real logic.
Save the file (File\Save) and go back to Unity.
Play the scene and ram that zombie right into that old lady! Check your Console view (Window\Console) and you’ll see the following message print out many times (or 1 time if you have the Collapse option set in your Console window).
You see so many log messages because Unity calls the method multiple times. But why?
There are actually many different contact points generated as the zombie walks through the enemy, and Unity sends you a message for every one. In most games you’ll want to make sure you don’t respond to all of these collisions, so you’ll add some code later to solve this problem.
Most games also have different types of objects with which to collide and each one may need to trigger different game logic. Zombie Conga is no exception. But before you can handle different types of collisions differently, you’ll need some different types of collisions, right? It’s time to add a collider to the cat so the zombie can hit that kitty like it’s some old lady on the beach!
Hitting Cats
Public Service Announcement: It’s wrong to hit the elderly, and it’s wrong to hit cats. Also, don’t hit elderly people with cats. But if you can manage to hit a cat with an elderly person, then kudos to you, because that’s not easy!
You’ve been adding polygon colliders to your sprites so far, but the collisions in Zombie Conga really don’t need to be as exact as what you get with these colliders. When writing video games, doing the simplest thing is usually the best for performance reasons, so for the cat, you’ll use a simple circle.
Select cat in the Hierarchy and add a collider by choosing Component\Physics 2D\Circle Collider 2D.
In the Scene view you can see a green circle indicating the bounds of the cat’s collider, like this:
Unity picks a size that tries to fill your sprite’s bounding box, but the cat’s collider doesn’t need to be so big. Inside the Inspector, change the Radius value to 0.3 for the Circle Collider 2D component, as shown below:
The cat’s collider now looks like this:
Play the scene and you’ll see it prints out a bunch of messages like this when the zombie touches the cat:
Again, you’ll fix the multiple collision issue later. For now, you need to fix something about your colliders that you probably didn’t even know was broken.
Static Colliders, and Why They’re Secretly Bad For You
You’ve added colliders to both the enemy and the cat, but there is something else you did that you may not have realized. You inadvertently told Unity that these colliders are static, meaning they won’t move within the scene and they won’t get added, removed, enabled or disabled at runtime.
Unfortunately, those assumptions are incorrect. The enemies in Zombie Conga will move and you’ll disable a cat’s collider once the zombie touches it. You need to tell Unity that these colliders are not static.
But first, why does it matter?
Unity builds up a physics simulation containing all of the colliders in the scene, and it optimizes this process by handling differently colliders it believes to be static. If a static collider changes at runtime, Unity needs to rebuild the physics simulation, which can be an expensive operation (read: slow) and can also cause the physics to behave oddly. So when you know you’ll be moving GameObjects around, be sure they don’t have static colliders attached.
How do you make a collider dynamic (i.e. not static)? In Unity, if a GameObject has colliders but no rigidbody, then those colliders are considered static. That means you just need to add rigidbodies to the cat and the enemy.
Select cat in the Hierarchy and add a Rigidbody 2D component by choosing Component\Physics 2D\Rigidbody 2d in Unity’s menu.
Repeat the previous step for the enemy to give it a Rigidbody 2D component as well.
With enemy selected in the Hierarchy, check the box labeled Is Kinematic in the RigidBody 2D component inside the Inspector, shown below:
This indicates that you will be controlling this object’s movement via scripts rather than relying on the physics engine to move it around.
Note: Ideally, you would make the zombie’s Rigidbody 2D component kinematic, too, because you are moving the zombie via a script rather than physics forces. However, there seems to be a bug in Unity’s 2D physics engine that keeps it from registering trigger collisions unless at least one rigidbody in the collision is not kinematic.
For the enemy to move, she’ll need a script. With enemy still selected, add a new C# script called EnemyController. You should remember how to add scripts from the earlier tutorials in this series, but if not, the following Spoiler gives a brief explanation.
Solution Inside: Need help adding a script? |
SelectShow> |
There are various ways to create scripts and attach them to GameObjects in Unity, so here is just one:
- Open the Scripts folder in the Project browser.
- Choose Assets\Create\C# Script from Unity’s menu.
- Name the new script EnemyController. Do not include the .cs filename extension because Unity adds that for you.
- Drag EnemyController from the Project browser onto enemy in the Hierarchy to attach the script to the enemy.
|
Open EnemyController.cs in MonoDevelop and then add the following instance variable to the script:
Like you’ve done in other parts of this tutorial, you declared speed
as public
so you can adjust it from within the editor, allowing you to tweak the feel of the game. You used a negative value for speed
to move enemies across the screen from the right to the left.
Now add the following line to Start
:
rigidbody2D.velocity = new Vector2(speed, 0); |
This simply starts the old lady walking along the x-axis. You won’t ever change her velocity, so there is no need to set the velocity anywhere other than inside Start
.
Note: Be aware that setting the enemy’s velocity
only in
Start
means it will ignore any adjustments you make to
speed
at runtime. You can still tweak this value from within Unity’s editor, but you’ll need to restart the scene to try each new value, which makes the process a bit more tedious.
If you’d like to be able to adjust the enemy’s speed at runtime, simply add the same line you added to Start
to a method called FixedUpdate
, which is similar to Update
but is called at fixed intervals based on the physics simulation.
Save the file (File\Save) and go back to Unity.
Play the scene and cheer as Grandma has clearly recovered nicely from her hip surgery.
Enemy walking with a speed of -2
It won’t take you long to realize that the old lady has wandered off and probably isn’t ever coming back. Rather than issuing a Silver Alert, you’ll solve this problem by doing the following two things: you’ll determine when she leaves the screen and then you’ll respawn her on the other side of the screen, just out of sight. That will make it look like another lady walked onto the beach. But to build your Old Lady Army, you’ll need to know the screen’s bounds.
Dealing with Screen Boundaries
When making games, you’ll often want to position sprites so they look good when running on devices with different aspect ratios. For example, imagine trying to display the player’s score in the upper right corner of the screen. If you positioned the score a fixed number of units away from the screen’s center, it would appear to be in different spots on devices with different aspect ratios, such as a 3.5″ iPhone, a 4″ iPhone and an iPad.
The following images demonstrate this point:
Score Label positioned at (3.5, 2.75) on iPhone 4
Score Label positioned at (3.5, 2.75) on iPhone 5
Respawning the enemy just off screen so she can walk into view poses the same problem as positioning a score label. You can’t pick a specific location because it might be off screen on a small device, but visible on a larger device, as shown in the following images:
Point positioned at (5.5, 0) on iPhone 5
To solve the above situation, you might think to try placing the spawn point further to the right to account for the largest possible device. However, that would also be incorrect because the game would then play differently on different devices, because the enemy would spend more time off screen on smaller devices than on larger ones. The following images show why:
Point at (7.5, 0). Enemy needs to traverse 2.7 units to appear on screen on iPhone 4.
Point at (7.5, 0). Enemy needs to traverse 1.82 units to appear on screen on iPhone 5.
Neither of those two scenarios is acceptable.
In this section, you’ll create a script that you can attach to a GameObject and it will position the object relative to one of the edges of the camera’s view.
Note: This script will only work if your camera has an orthographic projection. Unity allows you to have more than one camera in a scene, so if you’re working on a game using a perspective camera, simply add a separate camera with an orthographic projection and set up your cameras so the second camera only renders the user interface.
If you don’t know how to do that, take a look at Unity’s tutorial video on cameras.
Go to the Scripts folder in the Project browser, right-click and choose Create\C# Script. Name the new script ScreenRelativePosition.
Open ScreenReltaivePosition.cs in MonoDevelop and add the following instance variables to the script:
public enum ScreenEdge {LEFT, RIGHT, TOP, BOTTOM};
public ScreenEdge screenEdge;
public float yOffset;
public float xOffset; |
This first line defines an enum
type used to identify the four sides of the screen. You’ll assign screenEdge
in the Inspector to position an object at the center of that edge of the screen, and you’ll set xOffset
and yOffset to adjust the object's position away from that point.
Add the following code inside Start
:
// 1
Vector3 newPosition = transform.position;
Camera camera = Camera.main;
// 2
switch(screenEdge)
{
// 3
case ScreenEdge.RIGHT:
newPosition.x = camera.aspect * camera.orthographicSize + xOffset;
newPosition.y = yOffset;
break;
// 4
case ScreenEdge.TOP:
newPosition.y = camera.orthographicSize + yOffset;
newPosition.x = xOffset;
break;
}
// 5
transform.position = newPosition; |
To keep things easier to digest, the above code only includes the logic for positioning along the right and top sides of the camera's view. Here is what it does:
- It copies the object's current position, ensuring the object maintains whatever z position you set in the editor. It also gets a reference to the scene's main camera, which it needs to calculate the new position. If you ever use this script in a scene with more than one camera, modify it so you can specify which camera it should use.
- It uses a
switch
statement to calculate the correct position based on screenEdge
. All calculations assume the center of the screen is at position (0,0) because that keeps the sample code the simplest. However, it means that if the camera in your game moves, as it will in Zombie Conga, then this version of the script will only work on child objects of the camera. You'll see more about this in a bit.
- If
screenEdge
equals ScreenEdge.RIGHT
, it positions the object based on the camera's orthographicSize
. Remember, the orthographic size is half the view's height, so multiplying it by the camera's aspect
value gives you half the width, to which you add xOffset
. You assign the result as newPosition
's x
value.
As for newPosition
's y
value, you simply use yOffset
because you know the center of the right side of the view has a y value of zero.
- If
screenEdge
equals ScreenEdge.TOP
, it simply adds yOffset
to the camera's orthographicSize
and assigns the result as newPosition
's y
value. Because you know the center of the top of the view has an x value of zero, you simply use xOffset
for newPosition
's x
value.
- Finally, it updates the object's position with
newPosition
.
Before moving on with this script, you should test your code. Save the file (File\Save) and go back to Unity.
Choose GameObject\Create Other\Cube to add a cube to your scene. Don't worry, it's just for a quick test.
Now drag ScreenRelativePosition from the Project browser onto Cube in the Hierarchy.
Inside the Inspector, set Screen Edge to RIGHT in the Screen Relative Position (Script) component, as shown below:
Make a note of the cube's position within the scene, then play the scene and notice that the cube centers itself on the center of the right edge of the Game view, as shown below:
Feel free to try out some other settings, remembering that you've only implemented RIGHT
and TOP
thus far. The following shows the cube positioned with Screen Edge set to RIGHT, YOffset set to 2, and XOffset set to -1.5:
Once you're satisfied that your code works, right-click on Cube in the Hierarchy and choose Delete.
With positioning along the top and right edges working, switch back to MonoDevelop and try to add the cases for the left and bottom edges yourself. Check out the Spoiler below for a solution.
Solution Inside: Need help finding the left or bottom of your screen? |
SelectShow> |
Inside ScreenRelativePosition.cs, add the following two case statements to the switch statement in Start :
case ScreenEdge.LEFT:
newPosition.x = -camera.aspect * camera.orthographicSize + xOffset;
newPosition.y = yOffset;
break;
case ScreenEdge.BOTTOM:
newPosition.y = -camera.orthographicSize + yOffset;
newPosition.x = xOffset;
break; |
The only difference between the case for ScreenEdge.LEFT and the case you wrote earlier for ScreenEdge.RIGHT is that you want to move in the opposite direction before applying xOffset . You accomplish this by using negative camera.aspect rather than positive camera.aspect .
Likewise, the ScreenEdge.BOTTOM case uses negative camera.orthographicSize rather than the positive value used by the case for ScreenEdge.TOP .
|
When you're done with the script, save it (File\Save) and switch back to Unity.
With ScreenRelativePosition.cs complete, you can now position objects in the same place regardless of screen size. You'll use this in Zombie Conga to place the enemy's spawn point just off the right edge of the screen.
Spawn Points
Create a new empty GameObject by going to GameObject\Create Empty in Unity's menu. Inside the Inspector, name this new object SpawnPoint and then drag it onto Main Camera in the Hierarchy. SpawnPoint should now be a child of the camera, as shown below:
You added SpawnPoint as a child of Main Camera because later you'll be moving the camera and you want to ensure the spawn point moves with it.
Note: When you added SpawnPoint to the Main Camera, you may have seen its Transform values change in the Inspector. Although its Transform may have appeared to change, it still occupies the same location in the scene.
That's because when you make one object a child of another, the child's Transform component gets modified to take its parent's Transform into account.
SpawnPoint exists only to represent a position in your game and as such, you won't actually see it. However, it's difficult to keep track of invisible objects while you're developing. To remedy this, do the following:
Select SpawnPoint in the Hierarchy. Click the icon that looks like a cube in the upper left of the Inspector, shown below:
After clicking the above icon, a menu of different icons appears. Select the green oval shown below:
Inside the Scene view, you should now see a green oval labeled SpawnPoint representing the object's location, shown here:
You're only using this icon to help yourself keep track of SpawnPoint's location, so feel free to choose a different one if you'd prefer. Some of them scale their size when you zoom in and out, while others remain a fixed size regardless of the Scene view's zoom level, so experiment and see what you like the best.
Select SpawnPoint in the Hierarchy. Press Add Component and choose Scripts\Screen Relative Position from the menu that appears.
In the Inspector, set the Screen Relative Position (Script) component's Screen Edge value to RIGHT and set its XOffset value to 1, as shown below:
Select SpawnPoint in the Hierarchy and then play the scene. You'll see in the Scene view that SpawnPoint immediately moves to the right of the scene, just outside of the camera's viewable area, indicated by the white rectangle in the following image:
With the spawn point in place, it's time to change EnemyController.cs to move the enemy back to the spawn point whenever she moves off screen.
Open EnemyController.cs in MonoDevelop and add the following instance variable to the script:
private Transform spawnPoint; |
You'll store the spawn point's Transform
here so that the enemy can reference its position whenever she needs to respawn.
Add the following line of code to Start
:
spawnPoint = GameObject.Find("SpawnPoint").transform; |
This simply finds the object named "SpawnPoint" and gets its Transform
component. You could have made spawnPoint
a public or serialized field and then assigned the object in the Inspector, but this shows another way to locate objects in your scene.
Note: Keep in mind that using GetObject.Find
is slower at runtime than referencing a value set in the Inspector, so don't use this technique if you need to find the same object often, such as from within every execution of Update
.
There are several ways you could handle detecting when an object leaves the view, but the easiest for Zombie Conga is to implement OnBecameInvisible
. Unity calls this method whenever an object ceases to be visible to a camera.
Add the following implementation of OnBecameInvisible
in EnemyController.cs:
void OnBecameInvisible()
{
float yMax = Camera.main.orthographicSize - 0.5f;
transform.position = new Vector3( spawnPoint.position.x,
Random.Range(-yMax, yMax),
transform.position.z );
} |
This code calculates a new position for the enemy. It always uses the x
value from spawnPoint
, but it chooses a y
value by picking a random point within the available vertical space. The random vertical position keeps the player guessing.
Notice how the first line subtracts 0.5
when calculating the maximum y value. This ensures the enemy doesn't spawn too close to the top or bottom of the screen. If you don't understand why the y value is chosen between negative and positive yMax
, remember that the center of the screen is at (0,0), meaning there is Camera.main.orthographicSize
vertical space available in each direction.
Save the file (File\Save) and go back to Unity.
Important Note: When you play scenes in Unity, it is not exactly the same as running a real build on a target platform. One major difference is that the various Scene and Game views that you have visible in Unity are
all considered when determining an object's visibility. In other words, if the enemy moves off screen in the Game view but is still visible in the Scene view, Unity will not call
OnBecameInvisible
.
This means that if you want this respawning logic to work, you need to make sure you only see Zombie Conga in the Game view while playing the scene. You can still have a Scene tab open, but it has to be behind another tab so that it isn't rendering.
Play the scene inside Unity. After the enemy moves off the left side of the screen, you should see a new one walk in on the right side, as you can see below:
Note: While testing, Unity will probably log the following error when you stop the scene:
This seems to occur because the camera gets removed from the scene when you stop playing it and the enemy receives one last notification that it became invisible. I'm not sure if it's an error that will occur in a real build or if it only happens when testing in the editor, but you can fix it by adding the following line at the beginning of OnBecameInvisible
in EnemyController.cs:
if (Camera.main == null)
return; |
This simply aborts the method if the camera is not present.
The above video shows one small problem: the old lady ran off with your cat! You saw a similar problem earlier between the zombie and the enemy. Try to figure it out yourself, and then check the following Spoiler to see if you're right.
Solution Inside: Need help keeping a cat away from an old lady? |
SelectShow> |
The solution to this problem is to make the collider for the cat and/or the enemy into a trigger. Because Zombie Conga doesn't need any solid objects, you might as well make them both triggers.
Select cat in the Hierarchy and then check Is Trigger in the Circle Collider 2D component in the Inspector.
Select enemy in the Hierarchy and then check Is Trigger in the Polygon Collider 2D component in the Inspector.
|
Now that you know that respawning works, it's time to point out why you went through all that screen-relative positioning in the first place. Stop the scene and then change the Game view's aspect ratio by choosing one of the presets in the Aspect drop down menu in the Game view's control bar, shown below:
Play the scene again and notice that the enemy spends the same amount of time off screen, no matter which size you chose. Repeat the test with different aspect ratios until you're satisfied it works.
The following images show the spawn point's location when running with a few different aspect ratios. The white rectangles indicate the area viewable by the camera:
5:4 Aspect Ratio
3:2 Aspect Ratio
iPhone 5's Aspect Ratio
Note: Even though Unity allows you to change the Game view's aspect ratio while playing the scene, you must change the aspect ratio while the scene isn't running because ScreenRelativePosition.cs sets SpawnPoint's location only once, when the scene first starts.
Keeping the Zombie On Screen
Now that the enemy moves and respects the world bounds, you should probably get the zombie to do the same. You could do all sorts of fancy things with physics and colliders to keep the zombie in the proper area, but sometimes the easiest thing to do is to use a few if
checks and some basic math.
Open ZombieController.cs in MonoDevelop and add the following method:
private void EnforceBounds()
{
// 1
Vector3 newPosition = transform.position;
Camera mainCamera = Camera.main;
Vector3 cameraPosition = mainCamera.transform.position;
// 2
float xDist = mainCamera.aspect * mainCamera.orthographicSize;
float xMax = cameraPosition.x + xDist;
float xMin = cameraPosition.x - xDist;
// 3
if ( newPosition.x < xMin || newPosition.x > xMax ) {
newPosition.x = Mathf.Clamp( newPosition.x, xMin, xMax );
moveDirection.x = -moveDirection.x;
}
// TODO vertical bounds
// 4
transform.position = newPosition;
} |
The code above only handles the horizontal bounds. You'll add code to handle the vertical bounds once you know this works. Here is what it does:
- It copies the zombie's current position, ensuring the zombie maintains whatever z position you set in the editor. It also gets a reference to the scene's main camera and copies the camera's position, both of which will be necessary to calculate the zombie's new position.
- It calculates the x values in world coordinates for the edges of the screen. It does so by first calculating the distance from the center of the screen to one of its edges, and then adding that to the camera's x position. This means that if the camera is at (50, 0), and
xDist
is 4.8, the right side of the screen has an x position of 54.8 in world coordinates.
- This checks to see if the zombie's current position (stored in
newPosition
, confusingly enough) exceeds the view's horizontal limits. If so, it sets newPosition
's x
value to the boundary value and reverses the x
component of moveDirection
.
You haven't seen it since part 1 of this series, but moveDirection
is the vector that Update
uses to advance the zombie's position each frame, so reversing its x
component will start it moving in the opposite direction, effectively bouncing it off the edge of the screen.
- Finally, it updates the zombie's position with
newPosition
. This will be the same position the zombie had when you called this method if the zombie was already within its allowable space.
Note: If you want to make the zombie turn slightly before it reached the edge of the screen, simply reduce the size of xDist
.
Now add a call to EnforceBounds
at the end of Update
:
Save the file (File\Save) and go back to Unity.
Play the scene and try to walk the zombie off the left and right sides of the beach. He should turn right around each time rather than walking off into the great unknown.
With the left and right constraints in place, try limiting the zombie's vertical movement yourself. Put your code inside EnforceBounds
in ZombieController.cs, just after the comment that reads // TODO vertical bounds
. It should be similar to what you wrote for the horizontal bounds, but even simpler. The following Spoiler has a solution.
Solution Inside: Zombie getting away from you? |
SelectShow> |
You know the camera always has a y position of zero because the scene will only scroll horizontally in Zombie Conga. You also know that the camera's Orthographic Size is half the height of the view. That means that the view's upper limit is its size, and its lower limit is negative its size.
With those facts in mind, the following code will limit the zombie's vertical movement:
float yMax = mainCamera.orthographicSize;
if (newPosition.y < -yMax || newPosition.y > yMax) {
newPosition.y = Mathf.Clamp( newPosition.y, -yMax, yMax );
moveDirection.y = -moveDirection.y;
} |
This code simply checks the zombie's vertical position against the bounds of the view and reverses the zombie's vertical direction if the position exceeds those bounds. If you wanted to make the zombie turn slightly before it reached these boundaries, you could simply reduce the size of yMax .
|
Run again and now you're zombie stays in its sandbox.
At this point you might want a little break, so you'll finish up Zombie Conga in the fifth and final part of this series!
Where to Go From Here?
In this part of the tutorial, you learned how to use Unity's 2D physics engine to detect collisions, and you saw how you can handle some issues that arise when trying to support different aspect ratios. You can find a copy of the project up to this point here.
The main thing you should do next is the next part of this tutorial, of course! But if you want more information about 2D physics in Unity, take a look at Unity's Component References for its 2D Components and Unity's RigidBody2D and Physics 2D Overview videos.
I hope you're enjoying this tutorial series. If you've made it this far, you've got a lot of free time on your hands. Also, you're really close to the end, so don't stop now!
Please ask questions or leave remarks in the comments section. Thanks for reading!
Unity 4.3 2D Tutorial: Physics and Screen Sizes is a post from: Ray Wenderlich
The post Unity 4.3 2D Tutorial: Physics and Screen Sizes appeared first on Ray Wenderlich.