Unity 2D Tron Light-Cycles Tutorial

Foreword

Let's make a very simple Tron style 2D game in Unity with less than 60 lines of code and only three assets. Two players will be able to compete with each other.

The goal is to move your lightcycle in a way that traps the other player, kinda like a multiplayer snake game.

As usual, everything will be explained as easy as possible so everyone can understand it.

Here is a preview of the final gameplay:



Requirements

Knowledge

This Tutorial will only require the most basic Unity features. If you know your way around Unity and heard about GameObjects, Prefabs and Transforms before, then you are ready to go. And if you didn't, don't worry about it too much.

Feel free to read our easier Unity Tutorials like the Unity 2D Pong Game to get used to the Unity Engine.

Unity Version

Our Tron Light-Cycles Tutorial will use Unity 2018.4, also known as the 2018 "Long Term Support" (LTS) version. Newer versions should work fine as well apart from containing some UI changes, and older versions may or may not work.

Project Setup

Note: If you are not using the Unity Hub, this process will be a little different. Please adapt accordingly.

Let's get to it. We will start the Unity Hub and select New Project:



We will name it something like Tron or TronLightCycles, select the 2D template, select any location like C:\GameDev and click Create Project:



Give Unity a few moments to initialize and unpack itself. Even on the top of the line computers, this process takes a little while - grab a cup of Tea/Coffee and a snack while Unity does its thing.

Once the Unity Editor loads, click the Main Camera in the Hierarchy. This allows us to set the Background Color to black and adjust the Size like shown in the following image:



Note: Depending on the version of Unity you're using, some things might be different. Please adapt accordingly.

The Background Image

A plain black background is rather boring, so let's use our drawing tool of choice to draw some kind of grid image that we can use for our background:



Note: right click on the image, select Save As..., navigate to the project's Assets folder and save it in a new Sprites folder.

Let's select the image in our Project Area:



And then take a look at the Inspector where we can modify the Import Settings:



Note: a Pixels Per Unit value of 2 means that 2 x 2 pixels will fit into one unit in the game world. We will use this value for all our textures, because the player sprite will have the size of 2 x 2 pixels later on. The other settings are just visual effects. We want to make the image look perfectly sharp and without any compression, otherwise it might look strange.

Now we can add the grid to our game world by simply dragging it from the Project Area into the Hierarchy:



Note: we can also drag it from the Project Area into the Scene, but then we also have to re-adjust the position to (0, 0, 0).

We will also change the grid's Order in Layer property to -1 to make sure that it's always drawn in the background later:



Note: usually we would create a whole new Background Sorting Layer, but for such a simple game, using the Order in Layer property is enough.

If we press Play then we can already see the grid in game:



If your scene looks like the above, then so far, so good. Let's keep going.

The Lightwalls

The Players should leave a lightwall wherever they move, so let's create the first one.

The Cyan Lightwall

We will begin by drawing a 2 x 2 px image that only consists of a cyan color:



We will use the following Import Settings for it:



Afterwards we can drag the image from the Project Area into the Scene in order to create a GameObject from it:



Right now the Lightwall is really just an image in the game world, nothing would collide with it. Let's select Add Component -> Physics 2D -> Box Collider 2D in the Inspector in order to make it part of the physics world:



Now the Lightwall is finished. Let's create a Prefab from it by dragging it from the Hierarchy into a new Prefabs folder in our Project Area:



Having saved the Lightwall as a Prefab means that we can load it into the game whenever we want. And since we don't need it to be in the game just yet, we can right click the lightwall_cyan GameObject in the Hierarchy and select Delete:



The Pink Lightwall

Let's repeat the above workflow for the pink Lightwall image:

Note: right click on the image, select Save As... and save it in the project's Assets/Sprites folder.

If done correctly, we should now have both the Cyan and Pink prefabs in our Prefabs folder like so:



That's the light walls done. Now it's time to add the Player sprite.

The Player

Now it's time to add the Player. The Player should be a simple white square that is moveable by pressing some keys. The Player will also drag a Lightwall everywhere they go in our game.

Let's draw a white 2 x 2 px image for the player:



We will use the following Import Settings for it:



Now we can drag the image from the Project Area into the Scene in order to create a GameObject from it. We will then rename it to player_cyan and position it at the right center of our game at (3, 0, 0). While we're at it, let's make sure the Order in Layer property is set to 1 as this will ensure the player will always be in the foreground. Once we've done that, we'll select Add Component -> Physics 2D -> Box Collider 2D and configure the Box Collider 2D component to be a Trigger.



Some notes: For the player, we would usually use a Sorting Layer for this, just like mentioned before. However since we will only have 3 elements in our game: the background, the player and the Lightwalls, we will keep it simple and just use three different Order in Layer values.

We also add a Box Collider 2D now to save some time later on as well as enabling the Box Collider 2D's Is Trigger to avoid collisions with the player's own Lightwall later on. As long as we have IsTrigger enabled, the player will only receive collision information, without actually colliding with anything. This will make sense very soon.

That was a bit of a lengthy setup just for the player, but hang in there - we're almost done.

Player Physics

The player is also supposed to move around. A Rigidbody takes care of stuff like gravity, velocity and other forces that make things move. As a rule of thumb, everything in the physics world that is supposed to move around needs a Rigidbody.

Let's select Add Component -> Physics 2D -> Rigidbody 2D in the Inspector and assign the following settings to it:



Note: we set the Gravity Scale to 0 because we don't need any gravity in our game. Furthermore, we enable Freeze Rotation on the Z axis to prevent the player from rotating around.

Our player is now part of the physics world, it's simple as that.

Player Movement

We will use Scripting to make the player move. Our Script will be rather simple for now, all we have to do is check for arrow key presses and modify the Rigidbody's velocity property. The Rigidbody will then take care of all the movement itself.

Note: the velocity is the movement direction multiplied by the movement speed.

Let's select Add Component -> New Script, name it Move:



Afterwards we can double click the Script in the Project Area in order to open it:



using UnityEngine ;

using System.Collections ;



public class Move : MonoBehaviour {



// Use this for initialization

void Start ( ) {



}



// Update is called once per frame

void Update ( ) {



}

}

First of all we want to find out if the movement keys were pressed. Now we only want to create one movement Script for both players, so let's make the movement keys customizable so that we can use the Arrow keys for one player and the WSAD keys for the other player:



using UnityEngine ;

using System.Collections ;



public class Move : MonoBehaviour {

// Movement keys (customizable in Inspector)

public KeyCode upKey ;

public KeyCode downKey ;

public KeyCode rightKey ;

public KeyCode leftKey ;



// Use this for initialization

void Start ( ) {



}



// Update is called once per frame

void Update ( ) {



}

}

If we save the Script and take a look at the Inspector then we can set the key variables to the Arrow keys:



Alright, let's check for key presses in our Update function:



// Update is called once per frame

void Update ( ) {

// Check for key presses

if ( Input . GetKeyDown ( upKey ) ) {

// Do stuff...

}

else if ( Input . GetKeyDown ( downKey ) ) {

// Do stuff...

}

else if ( Input . GetKeyDown ( rightKey ) ) {

// Do stuff...

}

else if ( Input . GetKeyDown ( leftKey ) ) {

// Do stuff...

}

}

Now as soon as the player presses any of those keys, we want to make the player move into that direction. As mentioned before, we will use the Rigidbody's velocity property for that. The velocity is always the movement direction multiplied by the movement speed. Let's add a movement speed variable first:



using UnityEngine ;

using System.Collections ;



public class Move : MonoBehaviour {

// Movement keys (customizable in Inspector)

public KeyCode upKey ;

public KeyCode downKey ;

public KeyCode rightKey ;

public KeyCode leftKey ;



// Movement Speed

public float speed = 16 ;



...

}

The rest will be really simple. All we have to do is modify our Update function one more time to set the Rigidbody's velocity property:



// Update is called once per frame

void Update ( ) {

// Check for key presses

if ( Input . GetKeyDown ( upKey ) ) {

GetComponent < Rigidbody2D > ( ) . velocity = Vector2 . up * speed ;

}

else if ( Input . GetKeyDown ( downKey ) ) {

GetComponent < Rigidbody2D > ( ) . velocity = - Vector2 . up * speed ;

}

else if ( Input . GetKeyDown ( rightKey ) ) {

GetComponent < Rigidbody2D > ( ) . velocity = Vector2 . right * speed ;

}

else if ( Input . GetKeyDown ( leftKey ) ) {

GetComponent < Rigidbody2D > ( ) . velocity = - Vector2 . right * speed ;

}

}

Let's also modify our Start function really quick to give the player a initial velocity:



// Use this for initialization

void Start ( ) {

// Initial Velocity

GetComponent < Rigidbody2D > ( ) . velocity = Vector2 . up * speed ;

}

If we press Play then we can now move the player with the arrow keys:



Player Lightwalls

We want to add a feature that creates a Lightwall wherever the player goes. All we really need to do is create a new Lightwall as soon as the player turns into a new direction, and then always scale the Lightwall along where the player goes, until he moves into another direction.

We will need a helper function that spawns a new Lightwall. At first we will add two variables to our Script. One of them will be the Lightwall Prefab and the other will be the wall that is currently being dragged along by the player:



public class Move : MonoBehaviour {

// Movement keys (customizable in Inspector)

public KeyCode upKey ;

public KeyCode downKey ;

public KeyCode rightKey ;

public KeyCode leftKey ;



// Movement Speed

public float speed = 16 ;



// Wall Prefab

public GameObject wallPrefab ;



// Current Wall

Collider2D wall ;



...

Now we can use Instantiate to create a function that spawns a new Lightwall at the player's current position:



void spawnWall ( ) {

// Spawn a new Lightwall

GameObject g = Instantiate ( wallPrefab, transform . position , Quaternion . identity ) ;

wall = g . GetComponent < Collider2D > ( ) ;

}

Let's save the Script and then drag the lightwall_cyan Prefab from the Project Area into the Script's wallPrefab slot:



Alright, it's time to make use of our helper function. We will now modify our Script's Update function to spawn a new Lightwall after changing the direction:



// Update is called once per frame

void Update ( ) {

// Check for key presses

if ( Input . GetKeyDown ( upKey ) ) {

GetComponent < Rigidbody2D > ( ) . velocity = Vector2 . up * speed ;

spawnWall ( ) ;

}

else if ( Input . GetKeyDown ( downKey ) ) {

GetComponent < Rigidbody2D > ( ) . velocity = - Vector2 . up * speed ;

spawnWall ( ) ;

}

else if ( Input . GetKeyDown ( rightKey ) ) {

GetComponent < Rigidbody2D > ( ) . velocity = Vector2 . right * speed ;

spawnWall ( ) ;

}

else if ( Input . GetKeyDown ( leftKey ) ) {

GetComponent < Rigidbody2D > ( ) . velocity = - Vector2 . right * speed ;

spawnWall ( ) ;

}

}

We will also spawn a new Lightwall when the game starts:



// Use this for initialization

void Start ( ) {

// Initial Velocity

GetComponent < Rigidbody2D > ( ) . velocity = Vector2 . up * speed ;

spawnWall ( ) ;

}

If we save the Script and press Play, then we can see how a new Lightwall is being spawned after each direction change:



So far, so good.

Right now the Lightwalls are only little squares, we still have to scale them. Let's create a new fitColliderBetween function that takes a Collider and two points, and then fits the Collider between those two points:



void fitColliderBetween ( Collider2D co, Vector2 a, Vector2 b ) {

// Calculate the Center Position

co . transform . position = a + ( b - a ) * 0 . 5f ;



// Scale it (horizontally or vertically)

float dist = Vector2 . Distance ( a, b ) ;

if ( a . x != b . x )

co . transform . localScale = new Vector2 ( dist, 1 ) ;

else

co . transform . localScale = new Vector2 ( 1 , dist ) ;

}

First of all, we calculate the direction from a to b by using (b - a). Then we simply add half of that direction to the point a, which results in the center point. Afterwards we find out if the line is supposed to go horizontally or vertically by comparing the two x coordinates. If they are equal, then the line goes horizontally, otherwise vertically. Finally we adjust the scale so the wall is dist units long and 1 unit wide.

Let's make use of our fitColliderBetween function. We always want to fit the Collider between the end of the last Collider and the player's current position. So first of all, we will have to keep track of the end of the last Collider.

We will add a lastWallEnd variable to our Script:



public class Move : MonoBehaviour {

// Movement keys (customizable in Inspector)

public KeyCode upKey ;

public KeyCode downKey ;

public KeyCode rightKey ;

public KeyCode leftKey ;



// Movement Speed

public float speed = 16 ;



// Wall Prefab

public GameObject wallPrefab ;



// Current Wall

Collider2D wall ;



// Last Wall's End

Vector2 lastWallEnd ;



...

And set the position in our spawnWall function:



void spawnWall ( ) {

// Save last wall's position

lastWallEnd = transform . position ;



// Spawn a new Lightwall

GameObject g = Instantiate ( wallPrefab, transform . position , Quaternion . identity ) ;

wall = g . GetComponent < Collider2D > ( ) ;

}

Almost done. Now we can modify our Update function again to always fit the current wall between the last wall's end position and the player's current position:



// Update is called once per frame

void Update ( ) {

// Check for key presses

if ( Input . GetKeyDown ( upKey ) ) {

GetComponent < Rigidbody2D > ( ) . velocity = Vector2 . up * speed ;

spawnWall ( ) ;

}

else if ( Input . GetKeyDown ( downKey ) ) {

GetComponent < Rigidbody2D > ( ) . velocity = - Vector2 . up * speed ;

spawnWall ( ) ;

}

else if ( Input . GetKeyDown ( rightKey ) ) {

GetComponent < Rigidbody2D > ( ) . velocity = Vector2 . right * speed ;

spawnWall ( ) ;

}

else if ( Input . GetKeyDown ( leftKey ) ) {

GetComponent < Rigidbody2D > ( ) . velocity = - Vector2 . right * speed ;

spawnWall ( ) ;

}



fitColliderBetween ( wall, lastWallEnd, transform . position ) ;

}

If we save the Script and press Play, then we can see how the Lightwalls are being created behind the player:



If we take a closer look, then we can see how the walls are slightly too short on the corners:



There is a very easy solution to our problem. All we have to do is go back to our fitColliderBetween function and always make the wall one unit longer:



void fitColliderBetween ( Collider2D co, Vector2 a, Vector2 b ) {

// Calculate the Center Position

co . transform . position = a + ( b - a ) * 0 . 5f ;



// Scale it (horizontally or vertically)

float dist = Vector2 . Distance ( a, b ) ;

if ( a . x != b . x )

co . transform . localScale = new Vector2 ( dist + 1 , 1 ) ;

else

co . transform . localScale = new Vector2 ( 1 , dist + 1 ) ;

}

If we save the Script and press Play then we can see some perfectly matching corners:



Adding another Player

Alright, let's add the second player to our game. We will begin by right clicking the player_cyan GameObject in the Hierarchy and then selecting Duplicate:



We will rename the duplicated player to player_pink and change its position to (-3, 0, 0). While we're at it, let's also change the movement keys to WSAD and drag the lightwall_pink Prefab into the Wall Prefab slot:

If we press Play then we can now control two players, one with the WSAD keys and one with the Arrow keys:



Collision Detection

Alright so let's add a lose condition to our game. A player will lose the game whenever he moves into a wall.

We already added the Physics components (Colliders and Rigidbodies) to the Lightwalls and to the players, so all we have to do now is add a new OnTriggerEnter2D function to our Move Script. This function will automatically be called by Unity if a player collides with something:



void OnTriggerEnter2D ( Collider2D co ) {

// Do Stuff...

}

The 'Collider2D co' parameter is the Collider that the player collided with. Let's make sure that this Collider is not the wall that the player is currently dragging along behind him:



void OnTriggerEnter2D ( Collider2D co ) {

// Not the current wall?

if ( co != wall ) {

// Do Stuff...

}

}

In which case it must be any other wall, which means that the player lost the game. We will keep it simple here and only Destroy the player:



void OnTriggerEnter2D ( Collider2D co ) {

// Not the current wall?

if ( co != wall ) {

print ( "Player lost: " + name ) ;

Destroy ( gameObject ) ;

}

}

Summary

We just created a Tron Light-Cycles style 2D game in Unity. As usual, most of the features were really easy to implement - thanks to the power of the Unity Engine. The game offers lots of potential, as there are all kinds of features that could still be added:



Win/Lose Screen

More than 2 Players

Online Multiplayer

AI

Some special effects

Better Sprites

...and so on. As usual, now it's up to the reader to make the game fun.