Unity 2D Tron Light-Cycles Tutorial
link souce: https://drive.google.com/file/d/0B8-j62SXiaNscFlNbHF1NG83emc
link origin : http://noobtuts.com/unity/2d-tron-lightcycles-game
Foreword
Let's make a Tron style 2D game in Unity. 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.
Our game will be very simple, with less than 60 lines of code and only three assets.
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, Prefabsand 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 Unity 2D Pong Game to get used to the engine.
Unity Version
Our Tron Light-Cycles Tutorial will use Unity 5.0.0f4. Newer versions should work fine as well, older versions may or may not work. The free version of Unity 5 now comes with all the engine features, which makes it the recommended version.
Project Setup
Let's get to it. We will start Unity and select New Project:
We will name it tron_lightcycles, select any location like C:\, select 2Dand click Create Project:
If we select the Main Camera in the Hierarchy then we can set theBackground Color to black and adjust the Size like shown in the following image:
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'sAssets folder and save it in a new Sprites folder.
Note: right click on the image, select Save As..., navigate to the project'sAssets 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, without any compression.
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, without any compression.
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).
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.
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 ingame:
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:
- lightwall_cyan.png
Note: right click on the link, select Save As... and save it in the project'sAssets/Sprites folder.
We will use the following Import Settings for it:
Afterwards we can drag the image from the Project Area into the Scenein 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 theHierarchy and select Delete:
The Pink Lightwall
Let's repeat the above workflow for the pink Lightwall image:
- lightwall_pink.png
Note: right click on the link, select Save As... and save it in the project'sAssets/Sprites folder.
So that we end up with another Prefab:
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 he goes.
Let's draw a white 2 x 2 px image for the player:
- player.png
Note: right click on the link, select Save As... and save it in the project'sAssets/Sprites folder.
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 toplayer_cyan and position it at the right center of our game at (3, 0, 0):
Let's also modify the Order in Layer property again to make sure that the player will be in the foreground:
Note: just like mentioned before, we would usually use a Sorting Layer for this. 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 differentOrder in Layer values.
Note: just like mentioned before, we would usually use a Sorting Layer for this. 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 differentOrder in Layer values.
Player Physics
Right now the player is not part of the physics world, things won't collide with him and he can't move around. We will need to add aCollider again in order to make him part of the physics world.
Let's select Add Component->Physics 2D->Box Collider 2D in theInspector:
Note: we enabled IsTrigger 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.
Note: we enabled IsTrigger 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.
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 enabled the Fixed Angle property to prevent the player from rotating around.
Note: we set the Gravity Scale to 0 because we don't need any gravity in our game. Furthermore we enabled the Fixed Angle property 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.
Note: the velocity is the movement direction multiplied by the movement speed.
Let's select Add Component->New Script, name it Move and selectCSharp as the language:
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 Arrowkeys 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 themovement 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 Updatefunction 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;
}
}
Note: -Vector2.up means down and -Vector2.right means left.
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 = (GameObject)Instantiate(wallPrefab, transform.position, Quaternion.identity);
wall = g.GetComponent<Collider2D>();
}
Note: transform.position is the player's current position andQuaternion.identity is the default rotation. We also save the GameObject'sCollider2D in our wall variable to keep track of the current wall.
Let's save the Script and then drag the lightwall_cyan Prefab from theProject 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);
}
Note: this function may look a bit confusing at first. The obvious way to fit a Collider between two points would be something like collider.setMinMax(), but Unity doesn't allow that. Instead we will simply use thetransform.position property to position it exactly between the two points, and then use the transform.localScale property to make it really long, so it fits exactly between the points. The formula a + (b - a) * 0.5 is very easy to understand, too. 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 isdist 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 = (GameObject)Instantiate(wallPrefab, transform.position, Quaternion.identity);
wall = g.GetComponent<Collider2D>();
}
Note: technically the last wall's position should be wall.transform.position, but we used the player's transform.position here. The reason is that when spawning the first wall, there was no last wall yet, hence why we couldn't set the lastWallEnd position. Instead we always set it to the player's current position before spawning the next wall, which pretty much ends up being the same thing.
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):
Let's also change the movement keys to WSAD and drag thelightwall_pink Prefab into the Wall Prefab slot:
If we press Play then we can now control two players, one with theWSAD 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 newOnTriggerEnter2D 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);
}
}
Note: feel free to add some kind of win/lose screen at this point.
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 this powerful game engine. The game offers lot's of potential, 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.
I am trying my hands on Unity 2D Tron Light-Cycles it was quite difficult until I came across this post.
ReplyDeleteThanks a lot for sharing.
tks you. :)
ReplyDelete