Building the 2048 game in Unity via C# and Visual Studio

2048 is a very cool game that can make you spend hours playing it. Goal is to ‘merge’ tiles of identical values together, in order to have their value duplicated. When player swipes in her desired direction, items are moved towards there and a new item is created. If the player reaches the number 2048, then she has won the game. In this blog post, we’ll discuss how we can create it in Unity via C# and Visual Studio 2015.

As always, you can find the source code here on GitHub and test the game here via WebGL.

image

Screenshot showing the game in the Unity editor. On the left we can see the score and a restart button, on the middle the main game screen and on the right a visualization of the game’s 2 dimensional array contents, for debugging purposes.

Input methods

We have implemented two methods to get user input in the game. First one is via keyboard’s arrow keys, the other is via swipe in a touch screen (or mouse). We have implemented an enumeration to get user’s input and an interface which must be implemented by each input method we want to use. Moreover, if we need to add another input method in the future, e.g. input from an XBOX controller, we could simply implement the IInputDetector interface.


public enum InputDirection
{
    Left, Right, Top, Bottom
}

public interface IInputDetector
{
    InputDirection? DetectInputDirection();
}

The return value of the DetectInputDirection method has been implemented a s a Nullable type, since we may have no input at all by the user. Now, let’s visit the first input method that gets input via the keyboard. As you can see below, code is pretty simple and straightforward


public class ArrowKeysDetector : MonoBehaviour, IInputDetector
{
    public InputDirection? DetectInputDirection()
    {
        if (Input.GetKeyUp(KeyCode.UpArrow))
            return InputDirection.Top;
        else if (Input.GetKeyUp(KeyCode.DownArrow))
            return InputDirection.Bottom;
        else if (Input.GetKeyUp(KeyCode.RightArrow))
            return InputDirection.Right;
        else if (Input.GetKeyUp(KeyCode.LeftArrow))
            return InputDirection.Left;
        else
            return null;
        }
}

Method detects if an arrow key is pressed and returns the respective direction. Bear in mind that we are using GetKeyUp method as we do not want to have multiple inputs per arrow key press.

The method to get the direction via swipe is more complicated. Let’s see the code in parts:


public enum State
{
    SwipeNotStarted,
    SwipeStarted
}

We have a simple enumeration to hold the state of the swipe. Has it been started or not?


public class SwipeDetector : MonoBehaviour, IInputDetector
{

    private State state = State.SwipeNotStarted;
    private Vector2 startPoint;
    private DateTime timeSwipeStarted;
    private TimeSpan maxSwipeDuration = TimeSpan.FromSeconds(1);
    private TimeSpan minSwipeDuration = TimeSpan.FromMilliseconds(100);

We use some fields to help us with the implementation. The maximum swipe duration is 1 second and the minimum is 100 miliseconds.


public InputDirection? DetectInputDirection()
{
    if (state == State.SwipeNotStarted)
    {
        if (Input.GetMouseButtonDown(0))
        {
            timeSwipeStarted = DateTime.Now;
            state = State.SwipeStarted;
            startPoint = Input.mousePosition;
        }
}

If the user touches (or clicks with the mouse) the screen, we begin the swipe process.


else if (state == State.SwipeStarted)
{
    if (Input.GetMouseButtonUp(0))
    {
        TimeSpan timeDifference = DateTime.Now - timeSwipeStarted;
        if (timeDifference <= maxSwipeDuration && timeDifference >= minSwipeDuration)
        {
            Vector2 mousePosition = Input.mousePosition;
            Vector2 differenceVector = mousePosition - startPoint;
            float angle = Vector2.Angle(differenceVector, Vector2.right);
            Vector3 cross = Vector3.Cross(differenceVector, Vector2.right);

When the user finishes the drag, we check if the duration os the drag is within the desired time limits. If this is the case, we have to find the angle of the swipe via the Vector2.Angle method. We also use the Vector3.Cross method (calculates the cross product) to determine the direction of the differenceVector.


if (cross.z > 0)
    angle = 360 - angle;

state = State.SwipeNotStarted;

if ((angle >= 315 && angle < 360) || (angle >= 0 && angle <= 45)) return InputDirection.Right; else if (angle > 45 && angle <= 135) return InputDirection.Top; else if (angle > 135 && angle <= 225)
    return InputDirection.Left;
else
    return InputDirection.Bottom;

        }
    }
}
return null;

If the z in the cros vector is positive, it means that we need to recalculate the angle, since the correct one is the opposite (360-) of the one we have. Finally, we determine to which one of the four direction the angle belongs to and we return the correct enumeration value. Needless to say, if the user hasn’t done any swipe or it is not within the accepted time limits, we return null.

Globals


public static class Globals
{
    public readonly static int Rows = 4;
    public readonly static int Columns = 4;
    public static readonly float AnimationDuration = 0.05f;

}

Globals class contains static variables about Rows, Columns and AnimationDuration.

ItemMovementDetails


public class ItemMovementDetails
{
public GameObject GOToAnimateScale { get; set; }
public GameObject GOToAnimatePosition { get; set; }

public int NewRow { get; set; }
public int NewColumn { get; set; }

public ItemMovementDetails(int newRow, int newColumn, GameObject goToAnimatePosition, GameObject goToAnimateScale)
{
    NewRow = newRow;
    NewColumn = newColumn;
    GOToAnimatePosition = goToAnimatePosition;
    GOToAnimateScale = goToAnimateScale;
}

}

The ItemMovementDetails class is used to carry details regarding an object that is about to be moved and/or duplicated. The NewRow/NewColumn properties contain the information of the item’s location in the array, whereas the GOToAnimateScale and GOToAnimatePosition properties contain information regarding the game objects that are about to be moved and/or scaled out. The normal process is to move an item (change its position), but if this item will merge with another one, then this will also have its scale changed (and then disappear). More on that shortly!

Item and ItemArray

The Item class is a simple one.


public class Item
{
    public int Value { get; set; }
    public int Row { get; set; }
    public int Column { get; set; }
    public GameObject GO { get; set; }
    public bool WasJustDuplicated { get; set; }
}

– The Value property contains the value of the item (e.g. 2,4,8,16, etc.)

– Row and Column properties contain the respective row and column values of the array that this item belonds to

– The GO property contains a reference to the Unity GameObject this Item refers to

– The WasJustDuplicated value contains information whether this item was duplicated in this movement/swipe.


public class ItemArray
{
//the array, exposed only internally in the class
private Item[,] matrix = new Item[Globals.Rows, Globals.Columns];

//indexer for the class - <a href="https://msdn.microsoft.com/en-us/library/6x16t2tx.aspx">https://msdn.microsoft.com/en-us/library/6x16t2tx.aspx</a>
public Item this[int row, int column]
{
    get
    {
        return matrix[row, column];
    }
    set
    {
        matrix[row, column] = value;
    }
}

The ItemArray class contains a private member, a two dimensional item array called matrix. It also exposes an indexer to provide access to this array. If an item occupies a position in the array, then the matrix[row,column] item contains a reference to it. Otherwise, matrix[row,column] is null.


//searches for a random null column and returns it via output variables - <a href="https://msdn.microsoft.com/en-us/library/t3c3bfhx.aspx">https://msdn.microsoft.com/en-us/library/t3c3bfhx.aspx</a>
public void GetRandomRowColumn(out int row, out int column)
{
    do
    {
        row = random.Next(0, Globals.Rows);
        column = random.Next(0, Globals.Columns);
    } while (matrix[row, column] != null);
}

This method fetches a non null item in the array. It is used to create a new item after every swipe.


private void ResetWasJustDuplicatedValues()
{
    for (int row = 0; row < Globals.Rows; row++)
        for (int column = 0; column < Globals.Columns; column++)
        {
            if (matrix[row, column] != null && matrix[row, column].WasJustDuplicated)
                matrix[row, column].WasJustDuplicated = false;
        }
}

This method is called after every swipe and sets all the WasJustDuplicated values to false.


private ItemMovementDetails AreTheseTwoItemsSame(
int originalRow, int originalColumn, int toCheckRow, int toCheckColumn)
{
    if (toCheckRow < 0 || toCheckColumn < 0 || toCheckRow >= Globals.Rows || toCheckColumn >= Globals.Columns)
    return null;

        if (matrix[originalRow, originalColumn] != null && matrix[toCheckRow, toCheckColumn] != null
        && matrix[originalRow, originalColumn].Value == matrix[toCheckRow, toCheckColumn].Value
        && !matrix[toCheckRow, toCheckColumn].WasJustDuplicated)
        {
            //double the value, since the item is duplicated
            matrix[toCheckRow, toCheckColumn].Value *= 2;
            matrix[toCheckRow, toCheckColumn].WasJustDuplicated = true;
            //make a copy of the gameobject to be moved and then disappear
            var GOToAnimateScaleCopy = matrix[originalRow, originalColumn].GO;
            //remove this item from the array
            matrix[originalRow, originalColumn] = null;
            return new ItemMovementDetails(toCheckRow, toCheckColumn, matrix[toCheckRow, toCheckColumn].GO, GOToAnimateScaleCopy);

}
else
{
    return null;
}
}

This method checks if two items passed as arguments (via their column/row indexing) have the same value. First, it checks if the indexes passed are out of bounds. Then it checks if the item in this array position is not null and if it was not just duplicated (i.e. it was not duplicated after the current swipe). If all these checks are true, then

– we duplicate the first item value and set the WasJustDuplicated field to true

– we remove the second item from the array after we keep a reference to it, in order to animate it

– we return a new instance of ItemMovementDetails class, carrying the information of the item to have its position animated and the item to have its scale animated (and in the end, disappear).

Regarding the movement of the items according to user’s swipe, we have various scenarios we have to cover. Remember that null items in the array represent empty spaces.

So, let’s assume that X is a null column and 2 is a column with the value “2”. Let’s also assume a left swipe. Some of the scenarios that may occur are the following, along with the respective item movements after the swipe.

a) 2|2|X|X => 4|X|X|X

b) 2|X|2|X => 4|X|X|X

c) 2|2|X|2 => 4|2|X|X. First two ‘2’ will have merged, and the third one will move to the second column

d) X|2|2|2 => 4|2|X|X. Same situation as previous option. First two ‘2’ are merged, moved to the first column and the third ‘2’ is moved to the second column.

e) 4|2|2|X => 4|4|X|X


private ItemMovementDetails MoveItemToNullPositionAndCheckIfSameWithNextOne
(int oldRow, int newRow, int itemToCheckRow, int oldColumn, int newColumn, int itemToCheckColumn)
{
    //we found a null item, so we attempt the switch ;)
    //bring the first not null item to the position of the first null one
    matrix[newRow, newColumn] = matrix[oldRow, oldColumn];
    matrix[oldRow, oldColumn] = null;

    //check if we have the same value as the next one
    ItemMovementDetails imd2 = AreTheseTwoItemsSame(newRow, newColumn, itemToCheckRow,
    itemToCheckColumn);
    if (imd2 != null)//we have, so add the item returned by the method
    {
        return imd2;
    }
    else//they are not the same, so we'll just animate the current item to its new position
    {
        return
        new ItemMovementDetails(newRow, newColumn, matrix[newRow, newColumn].GO, null);

    }
}

This method moves the item to the place where it’s supposed to go (based on value checks). It assigns the item to its new position and “nullifies” the old one. Moreover, it checks if the item next to it has the same value. If this is the case, we return this information whereas if they are different we just return details of the item that was moved.

We have two methods for moving the items. One that is called when the swipe is horizontal and one for the vertical ones. While the code was written, I began by creating a “MoveLeft” method. After several tests, fixes etc., I created the “MoveRight” one. Then, it was clear to me that they could be merged into one method, so I created the MoveHorizontal one. Again, after several tests and fixes, the method was converted and adjusted in order to create the MoveVertical one. These methods have a lot in common and they could certainly be merged into a single “Move” method. However, I strongly feel that it would complicate this tutorial. Hence, I decided to leave them as is. Now, they are very similar in functionality, so we’ll only describe the “MoveHorizontal” one.


public List<ItemMovementDetails> MoveHorizontal(HorizontalMovement horizontalMovement)
{
    ResetWasJustDuplicatedValues();

    var movementDetails = new List<ItemMovementDetails>();

    //the relative column we will compare with
    //if swipe is left, we will compare with the previous one (the -1 position)
    int relativeColumn = horizontalMovement == HorizontalMovement.Left ? -1 : 1;
    //to get the column indexes, to do the loop below
    var columnNumbers = Enumerable.Range(0, Globals.Columns);

    //for left swipe, we will traverse the columns in the order 0,1,2,3
    //for right swipe, we want the reverse order
    if (horizontalMovement == HorizontalMovement.Right)
    {
        columnNumbers = columnNumbers.Reverse();
    }

Method begins by resetting all the WasJustDuplicated values. Then, depending on whether the movement is left or right, we get either –1 or 1. This will help in determining the item to compare. If the swipe is left, we move all the items left, so we need to compare each item with the previous one (the –1 one), in order to test for similarity. Moveover, we use the Enumerable.Range method to get column indexes. This method will return a list containing [0,1,2,3,…,Globals.Columns-1]. If the swipe is right, then we reverse the order of the columnNumbers list. This is because we need to loop the colums in the correct direction. If the swipe is left, we’ll start by checking the first column for null, then the second etc. This is why because we want to move the first not-null item to the first null position, starting from the left. If we have a right swipe, we need to do this in the opposite direction. That is why we reverse the columnNumbers list.


for (int row = Globals.Rows - 1; row >= 0; row--)
{ //we're doing foreach instead of for in order to traverse the columns
//in the appropriate order
    foreach (int column in columnNumbers)
    {
        //if the item is null, continue checking for non-null items
        if (matrix[row, column] == null) continue;

        //since we arrived here, we have a non-null item
        //first we check if this item has the same value as the previous one
        //previous one's position depends on whether the relativeColumn variable is -1 or 1, depending on the swipe
        ItemMovementDetails imd = AreTheseTwoItemsSame(row, column, row, column + relativeColumn);
        if (imd != null)
        {
            //items have the same value, so they will be "merged"
            movementDetails.Add(imd);
            //continue the loop
            //the new duplicated item may be moved on a subsequent loop
            continue;
        }

Here we begin our loop. Of course, we’ll check all rows. Then, we loop through all columns, taking the indexes from the columnNumbers list. While traversing each row, we first check each item for null. If it is null, we continue checking the next item (by checking the next column – next means either –1 or 1, depending on the swipe. When we reach a non-null column, we check if this column is the same as the one next to it. Again, “next to” means either –1 or 1, depending on whether the swipe is left or right. If these items are the same, then we add this information to the movementDetails list and continue the loop in the next column.


//matrix[row,column] is the first not null item
//move it to the first null item space
int columnFirstNullItem = -1;

//again, this is to help on the foreach loop that follows
//for a left swipe, we want to check the columns 0 to [column-1]
//for a right swipe, we want to check columns [Globals.Columns-1] to column+1
int numberOfItemsToTake = horizontalMovement == HorizontalMovement.Left
? column : Globals.Columns – column;

bool emptyItemFound = false;

//keeping it for documentation/clarity
//this for loop would run for a left swipe ;)
//for (columnFirstNullItem = 0; columnFirstNullItem < column; columnFirstNullItem++)
foreach (var tempColumnFirstNullItem in columnNumbers.Take(numberOfItemsToTake))
{
    //keep a copy of the index on the potential null item position
    columnFirstNullItem = tempColumnFirstNullItem;
    if (matrix[row, columnFirstNullItem] == null)
    {
        emptyItemFound = true;
        break;//exit the loop
    }
}

If these items are not the same, then we have to move the item we currently reference in the then first null position. For a left swipe, if the item is [row,column], then the only possible positions are from [row,0] to [row,column-1], hence we would need the first column items from the columnNumbers list. For a right swipe, the only possible positions are from [row,Globals.Columns-1] to [row,column+1], so we need the first Globals.Columns – column items from the reversed columnNumbers list. We perform a loop in these columns (using the Take LINQ method) keeping a reference to each column number (via the columnFirstNullItem variable) and checking each item if it’s null. If we find one, we exit the loop.


//we did not find an empty/null item, so we cannot move current item
if (!emptyItemFound)
{
    continue;
}

ItemMovementDetails newImd =
MoveItemToNullPositionAndCheckIfSameWithNextOne
(row, row, row, column, columnFirstNullItem, columnFirstNullItem + relativeColumn);

movementDetails.Add(newImd);

        }
    }
return movementDetails;
}

If we do not find a null item, then the currently referenced item is in its correct position, so we leave it as is. If we do, then we move the currently referenced item to the null position and we create an instance of the ItemMovementDetails class, in order to carry animation information. At the end of the MoveHorizontal method, we return the movementDetails list, that contains information for all the animations that must be performed.

Utilities

Utilities class contains two static methods, useful for debugging. Let’s pay them a visit


public static string ShowMatrixOnConsole(ItemArray matrix)
{
    string x = string.Empty;
    for (int row = Globals.Rows - 1; row >= 0; row--)
    {
        for (int column = 0; column < Globals.Columns; column++)
        {
            if (matrix[row, column] != null)
            {
                x += matrix[row, column].Value + "|";
            }
            else
            {
                x += "X" + "|";
            }
        }
        x += Environment.NewLine;
    }
    Debug.Log(x);
    return x;
}

The ShowMatrixOnConsole method displays values for all non-null items in the matrix and ‘X’ for null items. Furthermore, it returns the entire string to the caller, this is used by the GameManager class to display the array contents in the right of the screen. You may wonder why the rows loop is backwards. Well, in Unity the X axis has a right direction and the Y axis has a top direction. If we are to “dump” the array contents, they should be like (coordinates included)

(0,0)|(0,1)|…
(1,0)|(1,1)|…

So, the column increases on the right and the row on the bottom. That is different from the Unity direction, so we reverse the rows loop to be visually accurate.


public static string[,] GetMatrixFromResourcesData()
{
    string[,] shapes = new string[Globals.Rows, Globals.Columns];

    TextAsset txt = Resources.Load("debugLevel") as TextAsset;
    string level = txt.text;

    string[] lines = level.Split(new string[] { Environment.NewLine, "\n" }, StringSplitOptions.RemoveEmptyEntries);
    for (int row = 0; row < Globals.Rows; row++)
    {
        string[] items = lines[row].Split('|');
        for (int column = 0; column < Globals.Columns; column++)
        {
            shapes[row, column] = items[column];
        }
    }
    return shapes;

}

This is a method similar to one I used when I developed the match-3 game mechanism. It loads a file called “debugLevel” from the Resources folder. File contains integers (that represent the item values) and X (that represent the null values). We use the pipe (‘|’|) character as separator. Method returns a two-dimensional string array that contains the contents of the file.

GameManager


public enum GameState
{
Playing,
Won
}

public class GameManager : MonoBehaviour
{
    private GameState gameState = GameState.Playing;
    ItemArray matrix;
    public GameObject GO2, GO4, GO8, GO16, GO32, GO64, GO128, GO256, GO512, GO1024, blankGO;
    public Text ScoreText, DebugText;
    private float distance = 0.109f;

    public IInputDetector inputDetector;

    private int ZIndex = 0, score = 0;

GameManager class can have two states, either Playing or Won. We also declare all the relevant item GameObjects (2,4,8,…), the two Text items (one for score display and another for debug information). The blankGO is used as the background sprite for empty (null) item positions.


//will read a file from Resources folder
//and create the matrix with the preloaded data
void InitArrayWithPremadeData()
{
    string[,] sampleArray = Utilities.GetMatrixFromResourcesData();
    for (int row = 0; row < Globals.Rows; row++)
    {
        for (int column = 0; column < Globals.Columns; column++)
        {
            int value;
            if (int.TryParse(sampleArray[Globals.Rows - 1 - row, column], out value))
            {
                CreateNewItem(value, row, column);
            }
        }
    }
}

This method is used to create the array via the information obtained by the GetMatrixFromResourcesData that was previously described.


public void Initialize()
{
    if (matrix != null)
    for (int row = 0; row < Globals.Rows; row++)
        for (int column = 0; column < Globals.Columns; column++)
        {
            if (matrix[row, column] != null && matrix[row, column].GO != null)
                Destroy(matrix[row, column].GO);
        }

    matrix = new ItemArray();

    //InitArrayWithPremadeData();
    CreateNewItem();
    CreateNewItem();

    score = 0;
    UpdateScore(0);

    gameState = GameState.Playing;
}

The Initialize method is called either when the game begins or then the player touches the Restart button. First, it destroys all existing items in the game. Then, it initializes the array, creates two new items and initializes score and gameState variables.


private void CreateNewItem(int value = 2, int? row = null, int? column = null)
{
    int randomRow, randomColumn;

    if (row == null && column == null)
    {
        matrix.GetRandomRowColumn(out randomRow, out randomColumn);
    }
    else
    {
        randomRow = row.Value;
        randomColumn = column.Value;
    }

    var newItem = new Item();
    newItem.Row = randomRow;
    newItem.Column = randomColumn;
    newItem.Value = value;

    GameObject newGo = GetGOBasedOnValue(value);
    newGo.transform.localScale = new Vector3(0.01f, 0.01f, 0.01f);

    newItem.GO = Instantiate(newGo, this.transform.position +
    new Vector3(randomColumn + randomColumn * distance, randomRow + randomRow * distance, ZIndex),
    Quaternion.identity) as GameObject;

    newItem.GO.transform.scaleTo(Globals.AnimationDuration, new Vector3(1.0f, 1.0f, 1.0f));

    matrix[randomRow, randomColumn] = newItem;
}

The CreateNewItem method is used to create a new item on the array. It is used either at the beginning of the game or after each user’s swipe. As you can see, it has optional arguments. These allow the caller to invoke the method in different ways, depending on the function’s desired functionality.

If row and column arguments are null, this means that we want to place the item in a random position. If they are not null, this means that we are debugging the level via the premade file in the Resources folder. Same stands for the value argument, except for the fact that this will either have a value from the premade level or have the value 2 (default value for new items).

The method creates a new instance of the Item class, assigns the proper fields to it, instantiates a new GameObject based on the item value and animates its scale via the excellent GoKit animation/tween library. In the end, it assigns the new item instance to its proper location in the item array.


private void InitialPositionBackgroundSprites()
{
    for (int row = 0; row < Globals.Rows; row++)
    {
        for (int column = 0; column < Globals.Columns; column++)
        {
            Instantiate(blankGO, this.transform.position +
            new Vector3(column + column * distance, row + row * distance, ZIndex), Quaternion.identity);
        }
    }
}

The InitialPositionBackgroundSprites method creates a new background sprite for all the array positions.


void Update()
{
if (gameState == GameState.Playing)
{
    InputDirection? value = inputDetector.DetectInputDirection();

    if (value.HasValue)
    {
        List<ItemMovementDetails> movementDetails = new List<ItemMovementDetails>();
        //Debug.Log(value);
        if (value == InputDirection.Left)
            movementDetails = matrix.MoveHorizontal(HorizontalMovement.Left);
        else if (value == InputDirection.Right)
            movementDetails = matrix.MoveHorizontal(HorizontalMovement.Right);
        else if (value == InputDirection.Top)
            movementDetails = matrix.MoveVertical(VerticalMovement.Top);
        else if (value == InputDirection.Bottom)
            movementDetails = matrix.MoveVertical(VerticalMovement.Bottom);

        if (movementDetails.Count > 0)
        {
            StartCoroutine(AnimateItems(movementDetails));
        }
            string x = Utilities.ShowMatrixOnConsole(matrix);
            DebugDisplay(x);
        }
    }
}

In the Update method, we check if the user has given us any input. If this is the case, we perform the related swipe and gather the movementDetails list, containing any potential animations that have to be performed. If this list has at least one item, then we cal the Animate Items method.


IEnumerator AnimateItems(IEnumerable<ItemMovementDetails> movementDetails)
{
    List<GameObject> objectsToDestroy = new List<GameObject>();
    foreach (var item in movementDetails)
    {
        //calculate the new position in the world space
        var newGoPosition = new Vector3(item.NewColumn + item.NewColumn * distance,
        item.NewRow + item.NewRow * distance, ZIndex);

        //move it there
        var tween =
        item.GOToAnimatePosition.transform.positionTo(Globals.AnimationDuration, newGoPosition);
        tween.autoRemoveOnComplete = true;

The AnimateItems begins by looping through the movementDetails list. Then, it animates each item to its new position in the world space.


//the scale is != null => this means that this item will also move and duplicate
if (item.GOToAnimateScale != null)
{
    var duplicatedItem = matrix[item.NewRow, item.NewColumn];

    UpdateScore(duplicatedItem.Value);

    //check if the item is 2048 => game has ended
    if (duplicatedItem.Value == 2048)
    {
        gameState = GameState.Won;
        yield return new WaitForEndOfFrame();
    }

    //create the duplicated item
    var newGO = Instantiate(GetGOBasedOnValue(duplicatedItem.Value), newGoPosition, Quaternion.identity) as GameObject;

    //make it small in order to animate it
    newGO.transform.localScale = new Vector3(0.01f, 0.01f, 0.01f);
    newGO.transform.scaleTo(Globals.AnimationDuration, 1.0f);

    //assign it to the proper position in the array
    matrix[item.NewRow, item.NewColumn].GO = newGO;

If the current item in the movementDetails list has a non-null value in the GOToAnimateScale field, this means that the object will be moved and then disappear. So, the method updates the score and checks the new, duplicated value. Is this is equal to 2048, this means that the game has ended. If not, we create the duplicated item, make it small and assign it to the proper position in the array.


//we need two animations to happen in chain
//first, the movement animation
var moveTween = new GoTween(item.GOToAnimateScale.transform, Globals.AnimationDuration, new GoTweenConfig().position(newGoPosition));
//then, the scale one
var scaleTween = new GoTween(item.GOToAnimateScale.transform, Globals.AnimationDuration, new GoTweenConfig().scale(0.1f));

var chain = new GoTweenChain();
chain.autoRemoveOnComplete = true; //important -> <a href="https://github.com/prime31/GoKit/wiki/5.-TweenChains:-Chaining-Multiple-Tweens">https://github.com/prime31/GoKit/wiki/5.-TweenChains:-Chaining-Multiple-Tweens</a>
chain.append(moveTween).appendDelay(Globals.AnimationDuration).append(scaleTween);
chain.play();

//destroy objects after the animations have ended
objectsToDestroy.Add(item.GOToAnimateScale);
objectsToDestroy.Add(item.GOToAnimatePosition);
}
}

CreateNewItem();
//hold on till the animations finish
yield return new WaitForSeconds(Globals.AnimationDuration * movementDetails.Count() * 3);
foreach (var go in objectsToDestroy)
    Destroy(go);

The item that will be merged and duplicated will a)move and then b)disappear. In order to do this, we need two animations to happen in chain, a movement animation and a scale animation. We create a new GoTweenChain object that allows us to perform these two animations. We start this animation and we add the two GameObjects that were merged to the objectsToDestroyList. We then CreateNewItem, call WaitForSeconds to hold on the execution until all animations have stopped and then loop through the objectsToDestroy list to destroy all unnecessary GameObjects. The reason we are using this list is because we do not want to Destroy these GameObjects immediately since the animations will not be executed.


private void UpdateScore(int toAdd)
{
    score += toAdd;
    ScoreText.text = "Score: " + score;
}

private GameObject GetGOBasedOnValue(int value)
{
    GameObject newGo = null;
    switch (value)
    {
        case 2: newGo = GO2; break;
        case 4: newGo = GO4; break;
        case 8: newGo = GO8; break;
        case 16: newGo = GO16; break;
        case 32: newGo = GO32; break;
        case 64: newGo = GO64; break;
        case 128: newGo = GO128; break;
        case 256: newGo = GO256; break;
        case 512: newGo = GO512; break;
        case 1024: newGo = GO1024; break;
        default:
        throw new System.Exception("Uknown value:" + value);
    }
    return newGo;
}

The UpdateScore method just updates the score and displays it on screen whereas the GetGOBasedOnValue method returns a specific prefab in response to the value argument. For example, for a value 2, the method will return the prefab GameObject for the value 2.

That’s it! Thanks for reading this far, let me know in the comments about what you think. And do not forget, you can find the source code here on GitHub and test the game here via WebGL.

3 thoughts on “Building the 2048 game in Unity via C# and Visual Studio

Leave a comment