In this chapter, we will start an entirely new project: a 2D adventure game where the player controls an alien character and explores and navigates a dangerous world, complete with a quest to collect a gem. Platform games have been hugely popular since their inception in the 1980s with the first platforming game Space Panic. Since then, the genre has grown drastically to include the likes of Super Mario, Mega Man, Crash Bandicoot, and Limbo.
This project will build on the ideas we introduced in previous chapters but will also introduce new techniques, such as complex collisions, 2D physics, singletons, and more. Learning these techniques will empower you to create your own 2D games. In this chapter alone, we will cover the following topics:
Let's get started!
This chapter assumes that you have not only completed the projects from the previous chapters but also have a good, basic knowledge of C# scripting in general, though not necessarily in Unity.
The starting assets can be found in this book's companion files, in the Chapter05/Assets folder. You can start here and follow along with this chapter. The end project can be found in the Chapter05/End folder.
Adventure games require the player to use their cunning, dexterity, mental sharpness, and acumen to make progress. Such games feature dangerous obstacles, challenging missions, and character interaction, as opposed to all-out action like many first-person shooter games. Our adventure game will be no exception. The following is a screenshot of the game that we'll be creating:
In this game, the player moves around using the keyboard arrows or W, A, S, and D keys. Furthermore, they can jump with the spacebar and interact with other characters by approaching them. During the game, the player will be tasked with a mission from an NPC character to collect an ancient gem hidden somewhere within the level. The player must then navigate dangerous obstacles in search of the gem before returning to the NPC, completing the game.
To get started, do the following:
Tip
Remember that you can always use the Thumbnail Size Slider (at the bottom-right corner of the Project panel) to adjust the size of thumbnail previews in order to get an easier view of your texture assets.
Now that we've created the project using the 2D template, the textures should be imported as sprites rather than as textures for a 3D model. To confirm this, do the following:
With the changes applied, Unity flags the assets as having a 2D usage internally. This allows transparent backgrounds to be used where applicable (such as for PNG sprites) and also has important performance implications for graphics rendering, as we'll see later in this chapter. Before that, we'll configure the game view in preparation for creating the environment and player objects.
Now that we've imported all the essential textures for the project, let's configure the Game panel resolution and game camera. We should configure the game view correctly at the beginning of the project as it controls how we and, consequently, the end user sees the game. This, believe it or not, will help us when we come to creating the environment. We'll start with the Game panel's resolution, before moving on to configuring the game's camera.
We'll use a resolution of 1024 x 600 for our game, which works well across many devices. To do this, follow these steps:
If the required resolution is not available, then do the following:
Your target resolution should then be added as a selectable option from the Game tab, as shown in the following screenshot:
The resolution for the Game panel is not necessarily the resolution of the final game. You can set the resolution of the compiled game using the Project Settings panel. For more details, see Chapter 2, Creating a Collection Game.
With the correct game resolution, we can move on to configuring the game camera in preparation for creating our first 2D level.
In this section, we'll configure the scene camera for use in a 2D game so that our textures, when added as sprites, will display onscreen at a 1:1 ratio, Texel (texture pixel) for pixel. The camera will already be configured with an orthographic projection because we used the 2D template. The alternative (perspective projection) is often used with 3D games and would be the default configuration if we had selected any of the 3D project templates.
Important Note
Orthographic projection removes any sense of perspective and is often used for 2D games. For more information on the perspective and orthographic projections, see Chapter 3, Creating a Space Shooter.
In our game, we want the sprites to appear onscreen exactly the same size as they are in the texture files. To achieve this, we need to adjust the Size field of the Main Camera object in the Inspector window. The Size field defines the viewing volume of the camera. A larger size results in a larger viewing volume, making all visible objects appear smaller, and vice versa.
The formula we'll use for this field to ensure that the sprites are the same size as they appear in their image files is Screen Height / 2 / Pixel to World. The pixel to world ratio details how many pixels in the texture will be mapped to 1 unit in the world. A Unity unit is an arbitrary length measurement, usually interpreted as equaling a meter. Therefore, 1 unit is equal to 1 meter in our game. This value can be viewed by doing the following:
The sprites in our game are configured with the default value of 100 pixels to units. Now that we know the pixel per unit value, we can use the aforementioned formula to calculate the required camera size:
We now know the value we should use for the Camera Size field, so let's set it:
Looking good! In the next section, we'll create something interesting for the player to view using our newly configured 2D camera.
Our adventure game will feature three separate but connected scenes that the player can explore, moving from one scene to the next. The player may travel between scenes by traversing the edge of a scene. Each scene consists primarily of platforms and ledges and, in some cases, obstacles that must be overcome. In terms of graphical assets, each scene is made up from two textures or sprites: the background and foreground. The preceding screenshot shows the background of the first scene, while the following screenshot shows the foreground, which includes a complete layout of all the platforms and ledges that the player must traverse:
Important Note
These files are included in this book's companion files, in the Chapter05/Assets folder.
Let's create the first level:
Important Note
If you drag and drop both the background and foreground textures together as one selection from the Project panel to the scene, Unity may ask you to create an Animation when you release your mouse. In such cases, Unity assumes that you want to create an animated sprite in which each selected texture becomes a frame of animation played in a sequence. You don't want to do this; instead, drag and drop each sprite individually.
With both sprites added to the scene at the same world position, the question arises now as to which sprite Unity should display on top, given that both sprites overlap one another. Left as it is right now, there is a conflict and ambiguity about depth order, and we cannot rely on Unity consistently showing the correct sprite on top. We can solve this problem with two methods: one is to move the sprite forward on the Z-axis, closer to the Orthographic camera, while the other is to change its Order setting from the Inspector window. High values for Order result in the sprite appearing atop lower-order sprites. Here, I'll use both methods, and that's fine too!
Note, however, that order always takes precedence over position. Objects with a higher order value will always appear on top of objects with lower order values, even if higher-order objects are positioned behind lower-order objects.
Before moving further, let's organize the scene hierarchy to prevent overcomplication and confusion occurring later:
Important Note
Having an empty parent object like this is a standard method of grouping all related objects easily.
Our game is already looking great. However, by adding post-processing effects, we can further improve the appearance of the game. Let's look at how to do that.
By switching to the Game tab, we can get an early preview of the level as it will appear to the gamer in terms of mood and emotional resonance. This feeling can be enhanced further by adding some camera post-process effects with the post-processing stack. These refer to pixel-based effects that can be applied to the camera in order to improve the atmosphere for the final, rendered image on a per-frame basis. Let's take a look:
Good work. The scene so far features a background and foreground taken from texture files with enhanced visual effects by using the Post-processing stack package. This is a great start, but there's still much to do. For instance, while the levels now look the part, if we did have a player, they would fall straight through them as we have no environmental physics. We'll fix this now by adding colliders to the levels and creating a temporary character to test the colliders.
The main problem with our level, as it stands, is that it lacks interactivity. If we dragged and dropped a player object into the level and pressed play on the toolbar, the player would fall through the floor and walls because Unity doesn't recognize the foreground texture as a solid object. It's just a texture and exists only in appearance and not in substance. In this section, we'll correct this using Physics and Colliders. To get started, we'll create a player object (not the final version but just a temporary White Box version used only for testing purposes). Let's get started:
Generate a capsule object in the scene by navigating to GameObject | 3D Object | Capsule from the application menu.
By default, the Capsule is assigned a 3D collider (such as the Capsule Collider), which is useful primarily for 3D physics. However, our game will be in 2D, hence the need to remove the existing collider.
To make the object compatible with 2D physics, add a Circle Collider component:
To aid you in positioning the Circle Collider, you can switch the Scene viewport mode to Wireframe and 2D, as shown in the following screenshot:
You can confirm that this has worked by previewing the game in Play mode. When you click on the Play icon, the Capsule object should fall down and through the foreground floor under the effect of gravity.
Now, it's time to configure the foreground texture so that it works as a unified whole with physics. Right now, our test player character falls through the floor, and this is not what we want. To fix this, we'll need to also add a collider to the foreground environment. One method to accomplish this is to use Edge Collider 2D. The edge collider lets you draw out a low polygon mesh collider around your ground image manually, approximating the terrain. To get started, follow these steps:
By default, adding an Edge Collider 2D appears to have little effect on the selected object or any other objects, except for a single horizontal line drawn across the width of the scene. As with all colliders, it can be seen in the Scene tab when the foreground object is selected and in the Game tab if the Gizmos tool button is enabled.
Of course, our terrain isn't merely a straight-edged surface. Instead, it has elevations, bumps, and platforms. These can be approximated closely with the Edge Collider 2D component using the Collider Edit mode. Let's take a look:
With multiple Edge Collider components being used to approximate the complete terrain for the scene, we can now test play collisions against the Player Capsule object. You'll notice that, this time, the capsule will collide and interact with the ground as opposed to passing through it. This interaction confirms that the terrain has been configured appropriately with the physics system.
Congratulations! In this section, we've created a complete terrain for a single scene using Edge Collider components. This terrain not only fits the screen and appears as intended but acts as a physical obstacle for the player character and other physics-based objects. So far, we've only been using a rough approximation of the player. Now, it's time to expand upon this by implementing the final version of the player character.
The player character is a small, green alien-looking creature that can be controlled and guided by the gamer through a level using many conventional platforming game mechanics, such as walking and jumping. In the previous section, we built a white box (prototype) character to test physical interactions with the environment, but here, we'll develop the player character in more depth. We'll look at the following aspects:
That's a lot to cover, so let's jump right in with creating the player sprites using the Sprite Editor.
The following image illustrates our character texture, which we imported earlier in this chapter, representing all the limbs and parts of the player:
This player texture is called an Atlas Texture or Sprite Sheet because it contains all the frames or parts of a character in a single texture space. The problem with this texture, as it stands, is that when dragged and dropped from the Project panel into the scene, it'll be added as a single sprite, as shown in the following screenshot:
To divide the character texture into separate parts on a per-limb basis, we'll use the Sprite Editor:
With the Sprite Editor tool, you can separate different parts of a texture into discrete and separate units. One method to achieve this is by drawing a rectangle around each image area that should be separate, and then clicking and dragging your mouse to draw a texture region, as shown in the following screenshot:
Although a sprite can be separated manually, as we've just seen, Unity can often cut apart the texture automatically, identifying isolated areas of pixels and saving us time. We'll do that here for the player character:
The texture is now divided into several sprites: head, body, arm, and leg. The final character in-scene will have two arms and two legs, but these will be formed from duplicated sprites. The next step is to set the pivot point for each sprite – the point around which the sprite will rotate. This will be important later to animate the character correctly, as we'll see. Let's start by setting the pivot for the head:
Important Note
As you move the pivot around, you should see the X and Y values change in the Custom Pivot field in the Sprite Properties dialog, shown in the bottom-right-hand corner of the Sprite Editor window.
On returning to the main Unity interface, the appearance of the character texture will have changed in the Project panel. The character texture should now feature a small arrow icon attached to the right-hand side. When you click this, the texture expands to review all the separate sprites in a row, which can be dragged and dropped individually into the scene:
Now that we've isolated all the player sprite textures, we can start building a game character in the scene.
As mentioned previously, the player object for this game will be slightly more complicated than the objects that we have created previously. For the player, we need to create several child objects to represent the limbs of the player:
The following screenshot shows the complete hierarchical arrangement:
Even though all of the player's limbs have now been correctly positioned, the rendering order of the body parts may not be correct yet, as each item will have an identical order in the Sprite Renderer component. We'll look at this next.
With the limbs having the same draw order, Unity could potentially render them in any order, thereby allowing arms to appear in front of the head, legs to appear in front of the body, and so on. To correct this, do the following:
I've assigned the following values:
Tip
When assigning a sorting order to objects in your game, it's a good idea to leave gaps in-between the orders. For example, we may, in the future, want to add a scarf to the player. Ideally, the scarf should be drawn in-between the body and head. As the body is assigned a sort order of 103 and the head a value of 105, we can assign the scarf a sorting order of 104, and it will fit in nicely. If there were no gap between the numbers, we would then have to adjust the sorting value of the head and body to accomplish the same task. This isn't the end of the world for smaller projects, such as this one, but as your project's complexity grows, you may find that having that gap between the sorting order can save you significant time in the future.
Now that the player looks the part, let's make sure they also act the part by adding them to the game's physics engine.
The rendering order for limbs has now been configured successfully. Now, let's set up collisions and physics for the player:
The Circle Collider is of particular importance because it's the primary means to determine whether the character is touching the ground. For this reason, a Physics Material should be assigned to this collider to prevent friction effects from stopping or corrupting character motion as it moves around the scene.
By using this material, the character will interact with the level more realistically. Lastly, add a Rigidbody 2D to our character:
Now, you have a fully completed physical object representing the player. Great work! As the player can now interact with the world, it's a good time to look at moving the player using a custom script.
The game, as it currently stands, features an environment with collision data and a multipart player object that interacts and responds to this environment. The player, however, cannot yet be controlled. We'll correct this situation now as we explore controller functionality further by writing and implementing a player control script.
The user will have two main input mechanics; namely, movement (walking left and right) and jumping. This input will be read using CrossPlatformInputManager, which is a native Unity package that was imported during the project creation phase. Let's take a look:
public class PlayerControl : MonoBehaviour
{
private bool GetGrounded()
{
Vector2 CircleCenter = new Vector2(transform.position.x, transform. position.y)+ FeetCollider.offset;
Collider2D[] HitColliders = Physics2D.OverlapCircleAll(CircleCenter, FeetCollider.radius, GroundLayer);
return HitColliders.Length > 0; true;
}
private void Jump()
{
if(!isGrounded || !CanJump)return;
ThisBody.AddForce(Vector2.up * JumpPower);
CanJump = false;
Invoke ("ActivateJump", JumpTimeOut);
}
}
The following points summarize this code sample:
We're still missing the function that actually reads and applies input. Let's add it now:
public class PlayerControl : MonoBehaviour
{
…
void FixedUpdate ()
{
if(!CanControl || Health <= 0f) { return; }
isGrounded = GetGrounded();
float Horz = CrossPlatformInputManager. GetAxis(HorzAxis);
ThisBody.AddForce(Vector2.right * Horz * MaxSpeed);
if(CrossPlatformInputManager.GetButton(JumpButton)) {
Jump();
}
ThisBody.velocity = new
Vector2(Mathf.Clamp(ThisBody.velocity.x, -MaxSpeed, MaxSpeed),
Mathf.Clamp(ThisBody.velocity.y, -Mathf.Infinity, JumpPower));
if((Horz < 0f && Facing != FACEDIRECTION.FACELEFT)
|| (Hor > 0f && Facing != FACEDIRECTION. FACERIGHT) { FlipDirection(); }
}
Let's summarize the preceding code:
Important Note
Often, it makes sense to show a snippet of the code, rather than the full listing, as the full code listing may be irrelevant or too long. The previous code listing excludes many of the variables required for it to run, so if you copied it verbatim, you would run into compiler errors. You can always find the full code listings in the relevant chapter folders. For example, PlayerControl.cs can be found in the Chapter05/End/Assets/Scripts folder.
Before we can use our new PlayerControl script, we need to configure the environment as the script expects, which involves adding the level object to a specific layer and adding a collider to the player object. We'll do this now.
For the preceding code to work correctly, a few tweaks must be made to both the scene and player character. Specifically, the GetGrounded function requires that the floor area of the level is grouped as a single layer. This means that the level foreground should be on a distinctive layer from other objects. Let's do this now:
The player character is an ideal candidate for a prefab because it must feature in all the other scenes we create. Let's create a prefab from the player. To do this, drag and drop the Player object into the Project panel in a separate folder called Prefabs (create the folder if it doesn't exist already).
Our work so far has produced a stimulating environment and a character that can traverse that environment. Before moving forward, let's turn our attention to optimization – an issue that should be considered early during development. Optimization refers to the tips and tricks that we can apply to improve runtime performance, as well as our workflow in general. In the next section, we'll look at an optimization technique that's specific to 2D sprites called sprite packing.
Right now, when running the game, Unity will perform a separate draw call for every unique texture or sprite on screen at the time. A draw call refers to a step or process cycle that Unity must run through to correctly display a graphic on-screen, such as a mesh, material, or texture. Draw calls represent a computational expense, and so it's a good idea to reduce them wherever possible.
For 2D games, we can reduce draw calls by batching together related textures, such as all the props for a scene, all the enemies, or all the weapons. By indicating to Unity that a group of textures belong together, Unity can perform internal optimizations that increase render performance. Unity will add all related textures to a single internal texture that it uses instead. To achieve this optimization, follow these steps:
When you Play the game next, Unity will automatically batch and organize the textures for optimal performance based on your groupings. This technique can significantly reduce draw calls. On pressing the play button, you may see a loading or progress bar while Unity internally generates a new texture set.
You can view how Unity has organized the textures through the Sprite Packer window. To access this window, select Window | 2D | Sprite Packer from the application menu:
And it's as simple as that. Now, Unity can batch the sprites, thereby reducing the number of draw calls dramatically and increasing the performance of the game.
Superb work! We've come a long way in this chapter, from a new project to a working 2D game. The player character can navigate a complete 2D environment with 2D physics by moving left, right, and jumping. The player's sprite will dynamically update to match the direction of travel, and by using sprite packing, we've improved runtime performance, which is useful for mobile devices.
By completing this chapter, you've learned the fundamentals of 2D sprites, movement, and physics. By building on this foundation, you will have the required knowledge to create any 2D game, not just a platformer, of your own design.
The next chapter is a big one: in it, we'll complete the game by adding additional levels, obstacles (including moving platforms), a friendly character that assigns a quest – oh, and the quest system itself!
Q1. Edge Colliders let you...
A. Create pretty patterns
B. Draw out collider edges
C. Create 3D volumes
D. Create physics animations
Q2. The Sprite Packer is useful for...
A. Creating sprites in rows and columns
B. Grouping sprites onto a single atlas texture
C. Animating sprites
D. Creating multiple color sprites
Q3. Physics Materials can...
A. Help you define how 2D objects behave
B. Scale 2D objects
C. Rotate objects
D. Edit object vertices
Q4. The Sprite Editor lets you...
A. Divide an image into multiple sprites
B. Animate textures
C. Create mesh objects
D. Edit material properties
For more information on Unity scripting, take a look at the following links: