Skip to main content

Super Mario Bros


Yes, this classic game. Who doesn't know this game? Mario, our favourite plumber!

The overarching goal of our labs is to recreate basics this classic platform game: Super Mario Bros step by step while learning Unity's features along the way. We will try to rebuild World 1-1 as closely as possible, although due to constraints of time, some features may be omitted. We then discuss a few technical details in depth: such as game and asset management, events, and callbacks.

If you've never played this game before, give it a try before proceeding.

Lab Checkoff Requrements

The lab handout uses Super Mario Bros as assets to demonstrate certain Unity features and functionalities. You are free to follow along and submit it for checkoff, OR you can also create an entirely new project to demonstrate the requested feature(s).

The requirement(s) for the lab checkoff can be found here.

Unity Basics: The Scene

GameObject

Everything you see on the game scene is a GameObject. It is the base class of all entities in the Unity Scene.

Let’s add Mario to the scene. Right click in the Hierarchy tab and create a 2D Object with Sprite component (doesn't matter which shape you select). Change its name to “Mario”. Right now Mario doesnt seem like much. We need to do these later:

  1. Add Mario Sprite (image) to it, so it looks like Mario
  2. Control it (move it around): left, right, jump, down (enter pipe)
  3. Add some basic animations and sound effects

Camera

Every scene has one Main Camera. The Main Camera renders what the player "see" in the Game window. If you place Mario GameObject at Position (0,0,0), the Camera can "see" it if it's placed at some Z-distance away from Mario, as follows:

You can move the x, and y Transform Position of Mario GameObject and notice how the view at the Game window changes.

note

Toggle the 2D to 3D view in the Scene and familiarise yourself with navigation around the Scene. This is your Game World.

Notice that over at the Inspector Area, there are three components attached at the MainCamera GameObject: Transform (to place the object in the Scene), Camera, and Audio Listener. The title pretty much explains itself.

Background

Right now we only have one "Mario" GameObject in the scene and that's what the Game window shows. The blue color comes from the Background property of the Camera. You can set it to any other solid color if you want, or a Skybox.

A skybox is a cube with a different texture on each face. When you use a skybox to render a sky, Unity essentially places your Scene inside the skybox cube. Unity renders the skybox first, so the sky always renders at the back. Read more about it here.

Projection

There are two types of camera projection: Ortographic and Perspective, the name explains itself. Since we are working with 2D side-scrol platform game for these labs, we stick with orthographic projection.

Read more about Camera Component here.

Game Window

It is worth noting that you might want to set the Game aspect ratio under the Game tab.

  • Click the drag-down menu as shown and select the option that you’re most comfortable with.
  • E.g: selecting “Free Aspect” means that your window size will affect the camera “view”.

Mario Sprite

Open Assets >> Sprites and notice that we have provided you with various spritesheets: characters, enemies, mario, misc-3, and title.

A sprite sheet is a bitmap image file that contains several smaller graphics in a tiled grid arrangement.

Now click on Mario's sprite, change its Sprite Mode to Multiple, and launch the Sprite Editor. Notice that the sprites are blurry. Go back to the inspector and change the Filter Mode to Point (no filter) to disable smoothing and get clean edges for the sprites. This is something to keep in mind when you work with pixel art.

Afterwards, in the Sprite Editor, you can define sections of the sprite that you want manually, or automatically. In the case of our Mario sprite, they're not placed in regular spacing, so slicing them automatically will not work. We need to manually slice each Sprite as such:

Yes, it's tedious. Don't worry, we will give you pre-sliced sprites later.

Setting up Inputs

Go to Edit >> Project Settings and click on Input Manager.

We want to test if we can control the movement of Mario using the keys a and d for movement to the left and right respectively.

  • Check if the setting of “horizontal” axis is correct as per the screenshot below.
    • You can also add your own key bindings here and give it your own name.
  • Later in the script, you can decide what to do if a certain named key is pressed.

Scripting

Creating a Script

Right click inside the Scripts folder in the Project window, create a new C# script PlayerMovement.cs. Here we will programmatically control Mario. Open the script with an editor of your choice.

Here, we use VSCode.

You will see that there are two methods pre-made for you: Start and Update, and that your instance inherits MonoBehaviour:

PlayerMovement.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerMovement : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{

}

// Update is called once per frame
void Update()
{

}
}

Unity Order Execution of Event Functions.

We can implement the event functions in the script that’s attached to a particular GameObject. Notice that in the script we created,

  • It inherits from MonoBehaviour, the base class from which every Unity script derives. It offers some life cycle functions that makes it easier for us to manage our game.
  • It comes with two starting functions for you to implement if you want: Start() and Update().

Start() is always called once in the beginning when the GameObject is instantiated, and then Update() is called per frame. This is where you want to implement your game logic. The diagram below shows the order of execution of event functions.

Important

These event functions run on a single Unity main thread. Please read the official documentation.

Usually we don’t implement all of them. One of the more common ones to implement are: Start, Update, FixedUpdate, LateUpdate, OnTrigger, OnCollision, OnMouse OnDestroy, and some internal animation state machines if you use AnimationControllers. We will learn that in the next series.

Unity 2D Physics Engine

Move Mario

Let's attempt to move Mario via the script. Firstly, Add Rigidbody2D component in the Inspector and set:

  • Gravity Scale to 0,
  • Linear Drag to 3
  • BodyType to Dynamic

We can then control this component from the script. Add the following code inside PlayerMovement.cs:

PlayerMovement.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerMovement : MonoBehaviour
{
public float speed = 10;
private Rigidbody2D marioBody;

// Start is called before the first frame update
void Start()
{
// Set to be 30 FPS
Application.targetFrameRate = 30;
marioBody = GetComponent<Rigidbody2D>();

}

// Update is called once per frame
void Update()
{

}

// FixedUpdate is called 50 times a second
void FixedUpdate()
{
float moveHorizontal = Input.GetAxisRaw("Horizontal");
Vector2 movement = new Vector2(moveHorizontal, 0);
marioBody.AddForce(movement * speed);
}
}

Add the script to Mario: Add Component >> Script >> PlayerMovement.

test

You can test run that now Mario can be moved to the left and to the right using the keys “a” and “d” respectively. You can change the value of speed in Mario's Inspector under Script component.

However may not feel quite right. We will fix this later but first, lets learn about Unity event functions.

RigidBody2D Setting

The problem: Mario seems to be sliding. We would expect him to stop the moment we lift the key, wouldn’t we?

Setting the BodyType to Dynamic allows the physics engine to simulate forces, collisions, etc on the body. Since we’re adding Force to Mario’s body, it will obviously “glide” until the drag forces it to stop. We need to fix this.

Setting BodyType to Kinematic allows movement unders simulation but under very specific user control, that is you want to compute its behavior yourself: simulating Physics under your own rule instead of relying on Unity's Physics engine. Read the documentation here.

Stop Mario

To prevent this “sliding” feature that’s not very intuitive for platform game like this, we need to

  • Reduce Mario's velocity as much as possible, even near 0 when key “a” or “d” is lifted up
  • Clamp his speed to a maximum value so he doesn’t run faster and faster when we hold that “a” or “d” button.

Add the global variable maxSpeed and implement FixedUpdate() in PlayerMovement.cs.

    public float maxSpeed = 20;

// FixedUpdate may be called once per frame. See documentation for details.
void FixedUpdate()
{
float moveHorizontal = Input.GetAxisRaw("Horizontal");

if (Mathf.Abs(moveHorizontal) > 0){
Vector2 movement = new Vector2(moveHorizontal, 0);
// check if it doesn't go beyond maxSpeed
if (marioBody.velocity.magnitude < maxSpeed)
marioBody.AddForce(movement * speed);
}

// stop
if (Input.GetKeyUp("a") || Input.GetKeyUp("d")){
// stop
marioBody.velocity = Vector2.zero;
}
}


Make Mario Jump

Let’s make him jump to a fixed height whenever we press the Spacebar key once. We can leverage on the physics engine for this, but we need to enable gravity. Otherwise we have to make the Kinematics computation ourselves.

Nobody’s stopping you to do that, but due to time constraints let’s not reinvent the wheel.

Enable Gravity

In Mario's Inspector, set RigidBody2D property GravityScale to 1.

test

If you press play now, Mario will fall to oblivion.

Collider2D

We need to add some sort of a “floor” to prevent him from falling down (via collision). A GameObject will not collide with each other unless they have the Collider component attacked to it.

  1. Create a new 2D Sprite GameObject and name it Ground.

  2. Add BoxCollider2D component to it:

    • Enable Auto Tiling property
    • This allows the Collider to follow the SpriteRenderer's tiling properties
  3. Add a Tag called Ground

  4. Set its Transform to:

    • Rotation (0,0,0)
    • Scale (1,1,0)
    • Position: Anywhere below y-axis of Mario

Now we want some sort of Ground Sprite first, so you can go and edit misc-3 Sprite and extract a little Ground sprite from it. Name it "Ground Brown". Then, set the following properties on misc-3 Texture:

When we drag .png files into Assets/Sprite folder, it is automatically converted into Textures. We will learn more about it later, but give the docs a read. The Wrap Mode property allows us to automatically "repeat" the ground tiles as we scale the Ground GameObject.

Go back to the Sprite Renderer component of Ground GameObject, and set the following properties:

  • Draw Mode: Tiled
  • Size: Width of 20
danger

Use the Size property of Sprite Renderer if you want to adjust the Ground's width or height. Do NOT use Transform's scale. Otherwise, the Collider2D will not be able to properly tell the boundaries of the GameObject.

Finally, also add a BoxCollider2D to Mario so that they can "collide" and prevents him from falling to oblivion. You can press Edit Collider in the component to adjust the collider edges to match Mario's sprite.

test

Test it and you should see Mario not falling to oblivion anymore.

OnCollision2D and Double Jump

Now we implement the Collider callback function called OnCollision2D in PlayerMovement.cs. The idea is that if Mario is on the ground, and if spacebar is pressed, we will add an Impulse force upwards. Pressing spacebar again should not cause Mario to double jump.

We need to have some kind of state variable for this, and an upward "speed". Add the following code to PlayerMovement.cs:

PlayerMovement.cs
  public float upSpeed = 10;
private bool onGroundState = true;

void OnCollisionEnter2D(Collision2D col)
{
if (col.gameObject.CompareTag("Ground")) onGroundState = true;
}

void FixedUpdate()
{
// other instructions

if (Input.GetKeyDown("space") && onGroundState){
marioBody.AddForce(Vector2.up * upSpeed, ForceMode2D.Impulse);
onGroundState = false;
}
}
test

Test your jumping Mario. You should have something like this.

You can improve the controls and adjust the parameters: speed, upSpeed, and maxSpeed accordingly to get the right “feel”. It can take quite a lot of time to get the kinesthetics right, but it is an important part of your journey in making a good game.

tip

Focus more on these details instead of “expanding” your game. We don’t require you to create a 1-hour long game, but rather a short and well designed game. Invest your time wisely.

Beautify

The following shows a simple screenshot of World-1-1 of Super Mario Bros. Let's try to recreate a part of it as much as possible before advancing.

We need the following Sprites. You can slowly slice and create them all from misc-3 Sprite:

  1. Big Hill, small hill
  2. 3-shrubs, 1-shrub
  3. 1, 2, and 3 clouds
  4. Pipe head, pipe body
  5. Goombas
  6. Brick
  7. Question Blocks
  8. Magic Mushroom
  9. Coin
  10. Brick
A Shortcut
You can download the texture metadata from here. Then, paste all these .meta files under Assets/Sprites. Don't forget to set Texture's Mesh Type and Wrap Mode as above to enable tiling.

We shall have this before proceeding:

Housekeeping Tips

You might want to set the Ground Gameobject's Sprite Renderer >> Additional Settings >> Order in Layer property to 1 (instead of 0) so that static assets like Mountains, Shrubs, etc can be "behind" the ground.

You might also want to group related gameobjects together as shown in the Screenshot's Hierarchy.

Also, you can "drag" and move GameObjects together in the scene.

Here's a speedup recording demonstrating all of the above:

Flip Mario

Now let’s fix mario’s facing. If he is going to the left, he should be facing the left side and vice versa. The direction he’s facing should conform to the last pressed key.

We can do this by enabling the flipX property of its SpriteRenderer whenever key “a” is pressed, and disabling it whenever key “d” is pressed. We also have to control the SpriteRenderer component via the script. You can pretty much get any component via GetComponent<type>() method in the script attached to the game object.

Add the following changes to PlayerMovement.cs:

PlayerMovement.cs

// global variables
private SpriteRenderer marioSprite;
private bool faceRightState = true;

void Start(){
marioSprite = GetComponent<SpriteRenderer>();

// other instructions
}

void Update(){
// toggle state
if (Input.GetKeyDown("a") && faceRightState){
faceRightState = false;
marioSprite.flipX = true;
}

if (Input.GetKeyDown("d") && !faceRightState){
faceRightState = true;
marioSprite.flipX = false;
}

// other instructions
}
note

We do not implement the flipping of Sprite under FixedUpdate since it has nothing to do with the Physics Engine.

test

Your Mario will now face right and left accordingly as "a" or "d" key is pressed.

Add Obstacles

There are many obstacles that can be added to a game: enemies, physical obstacles, etc.

Goombas

Now its time to create the Enemy.

  • Create an empty GameObject onto the scene, name it Enemies
  • Create a child GameObject under Enemy, name it Goomba:
    • add SpriteRenderer Component,
      • Set Order in Layer as 2 because we want it to be in front of the static background images and the ground.
    • add Rigidbody2D and Collider2D Components
  • Put brown_goomba_1 as its sprite, edit its Transform so it is placed beside Mario

Move Goomba

Now we want Goomba to patrol left and right up to a certain offset X from its starting position. Create a new script called EnemyMovement.cs with the following content:

EnemyMovement.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyMovement : MonoBehaviour
{

private float originalX;
private float maxOffset = 5.0f;
private float enemyPatroltime = 2.0f;
private int moveRight = -1;
private Vector2 velocity;

private Rigidbody2D enemyBody;

void Start()
{
enemyBody = GetComponent<Rigidbody2D>();
// get the starting position
originalX = transform.position.x;
ComputeVelocity();
}
void ComputeVelocity()
{
velocity = new Vector2((moveRight) * maxOffset / enemyPatroltime, 0);
}
void Movegoomba()
{
enemyBody.MovePosition(enemyBody.position + velocity * Time.fixedDeltaTime);
}

void Update()
{
if (Mathf.Abs(enemyBody.position.x - originalX) < maxOffset)
{// move goomba
Movegoomba();
}
else
{
// change direction
moveRight *= -1;
ComputeVelocity();
Movegoomba();
}
}
}

The idea is to allow the enemy to patrol up to 5.0 units to the left and to the right, and change direction accordingly when the max offset distance is reached. We also want to control its speed:

  • If goomba isn’t too far away from its starting position yet, move it to the designated direction
  • Else, flip direction

We can compute the required velocity by dividing supposed distance travelled with time, and then compute the position at each Time.fixedDeltaTime. Then, we can move the enemy to the calculated position: original_position_vector + velocity_vector * delta_time

Finally, since we do not need to perform a full-blown physics simulation on the enemy, we can set its RigidBody2D BodyType to Kinematic. We are simply moving it to patrol around desired location, and later on to detect “collision”.

tip

Make sure to place Goomba nicely above the Ground. Gravity does not apply to it anymore.

test

Now is a good time to test. Notice how the Goomba "pushes" Mario. That's because both objects have colliders in it.

Collision between Goomba and Mario

We want Mario to be “damaged” when it collides with Goomba, and we do not need the two bodies to push each other or simulate Physics. The way to do this is to set the collider attached at the enemy’s GameObject as a Trigger.

Tick that IsTrigger option in BoxCollider2D element. Also, change Goomba's Tag to Enemy (create it).

info

If a Collider collides with another Collider that is a Trigger, then “collision effect” will not be computed, and rather the callback OnTriggerEnter will be invoked (on both GameObject).

Implement the callback function OnTriggerEnter2D in PlayerMovement.cs, and EnemyMovement.cs:

  void OnTriggerEnter2D(Collider2D other)
{
if (other.gameObject.CompareTag("Enemy"))
{
Debug.Log("Collided with goomba!");
}
}
test

When you test it, you will see the following printout in the Console. When the game starts, Goomba printed Ground because it collided with the Ground. Notice how this is only printed once (upon initial collision and not continuously). Then, both OnTriggerEnter2D methods are called when Mario and Goomba collides.

Eventually, this collision will cause Mario to lose lives or game to be over. Technically, you can implement this logic once in any of the two scripts, but since it affects Mario (and not Goomba), it makes more sense to do it on PlayerMovement.cs instead. We now need some sort of UI elements to indicate score or HP or "Game Over" text. Let's venture into how UI Element works.