Как сделать 2048 unity
Как сделать 2048 unity
A 2048 clone written in Unity as a practice to see how well it could handle a basic puzzle game.
Use arrow keys or WASD to move.
R to reset.
Join matching tiles to advance powers of 2. Try to get to 2048!
Make the game over button better.
Clean up and DRY the code.
Use object pooling for the tiles just to.
Maybe add slide animation.
Maybe add score ticking animation.
Copyright (c) 2014, Justin Hamilton All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS «AS IS» AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
About
A 2048 clone written in an afternoon to see how easy it would be to do in Unity
Dimitrios Ilias Gkanatsios
Stories about Game Development, Azure, Docker, Kubernetes and more
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.
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.
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
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:
We have a simple enumeration to hold the state of the swipe. Has it been started or not?
We use some fields to help us with the implementation. The maximum swipe duration is 1 second and the minimum is 100 miliseconds.
If the user touches (or clicks with the mouse) the screen, we begin the swipe process.
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 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
Globals class contains static variables about Rows, Columns and AnimationDuration.
ItemMovementDetails
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.
– 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.
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.
This method fetches a non null item in the array. It is used to create a new item after every swipe.
This method is called after every swipe and sets all the WasJustDuplicated values to false.
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.
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.
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.
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.
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.
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.
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
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)
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.
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
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.
This method is used to create the array via the information obtained by the GetMatrixFromResourcesData that was previously described.
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.
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.
The InitialPositionBackgroundSprites method creates a new background sprite for all the array positions.
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.
The AnimateItems begins by looping through the movementDetails list. Then, it animates each item to its new position in the world space.
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.
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.
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.
dacongy / 2048-unity Go PK Goto Github PK
A 2048 clone written in an afternoon to see how easy it would be to do in Unity
2048-unity’s Introduction
A 2048 clone written in Unity as a practice to see how well it could handle a basic puzzle game.
Use arrow keys or WASD to move.
R to reset.
Join matching tiles to advance powers of 2. Try to get to 2048!
Make the game over button better.
Clean up and DRY the code.
Use object pooling for the tiles just to.
Maybe add slide animation.
Maybe add score ticking animation.
Copyright (c) 2014, Justin Hamilton All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS «AS IS» AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Как сделать игру 2048 на React
Автор этого туториала сосредоточился на анимации. Он использовал хуки библиотеки React, её Context API, а также TypeScript и LESS. В конце вы найдёте ссылки на игру, её код и демо анимаций. Подробности рассказываем под катом, пока у нас начинается курс по Frontend-разработке.
Правила игры 2048
Числа на плитках — только степени двойки, начиная с самой 2. Игрок объединяет плитки с одинаковыми числами. Числа суммируются, пока дело не дойдёт до 2048. Игрок должен добраться до плитки с числом 2048 за наименьшее количество шагов.
Если доска заполнена и нет возможности сделать ход, например объединить плитки вместе, — игра окончена.
Для целей статьи я сосредоточился на игровой механике и анимации и пренебрёг деталями:
Число на новой плитке всегда 2, а в полной версии игры оно случайно.
Играть можно и после 2048, а если ходов на доске не осталось, то не произойдёт ничего. Чтобы начать сначала, нажмите кнопку сброса.
И последнее: очки не подсчитываются.
Структура проекта
Приложение состоит из этих компонентов React:
Grid рендерит сетку 4×4.
Tile отвечает за все связанные с плиткой анимации и рендеринг самой плитки.
Как сделать компонент плитки
В этом проекте больше времени хочется уделить анимации, поэтому я начинаю рассказ с компонента Tile. Именно он отвечает за все анимации. В 2048 есть две простых анимации — выделение плитки и её перемещение по доске. Написать их мы можем при помощи CSS-переходов:
Я определил только один переход, выделяющий плитку, когда она создаётся или объединяется с другой плиткой. Пока оставим его таким.
Посмотрим, как должны выглядеть метаданные Tile, чтобы легко с ними работать. Я решил назвать тип метаданных TileMeta : не хочется, чтобы его имя конфликтовало с другими, например Tile:
id — уникальный идентификатор плитки. Он нужен, чтобы DOM React при каждом изменении не перерисовывал все плитки с самого начала. Иначе мы увидим подсвечивание плиток на каждом действии игрока.
position — положение плитки на доске. Это массив с двумя элементами, то есть координатами x и y и значениями от 0 до 3.
value — число на плитке.
mergeWith — необязательный идентификатор плитки, которая поглотит текущую. Если он существует, то плитка должна слиться с другой плиткой и исчезнуть.
Как создавать и объединять плитки
Как-то нужно отметить, что плитка изменилась после действия игрока. Думаю, лучший способ — изменить масштаб плитки. Изменение масштаба покажет, что была создана новая плитка или изменена другая:
Чтобы запустить анимацию, нужно рассмотреть два случая:
создаётся новая плитка — предыдущее значение будет равно null ;
плитка изменяет значение — предыдущее значение будет отличаться от текущего.
Я мог бы использовать ссылки, но они громоздкие, поэтому решил выделить код в отдельный хук ради читабельности и для того, чтобы использовать хук повторно. Если вы хотите задействовать его в своём проекте, просто скопируйте фрагмент ниже:
Как двигать плитки по доске
Объявив стили, можно написать логику изменения положения плитки:
Свойство useBoard позволяет получить доступ к свойствам доски внутри дочерних компонентов, не передавая их ниже. Чтобы найти нужное место на доске, компоненту Tile нужно знать ширину и общее количество плиток. Благодаря React Context API мы можем обмениваться свойствами между несколькими слоями компонентов, не загрязняя их свойства (props).
zIndex — это свойство CSS, которое определяет порядок расположения плиток. В нашем случае это id плитки. На рисунке ниже видно, что плитки могут укладываться друг на друга. Свойство zIndex позволяет указать, какая плитка находится наверху.
Как сделать доску
Другой важной частью игры является доска. За рендеринг сетки и плиток отвечает компонент Board. Кажется, что Board дублирует логику компонента Tile, но есть небольшая разница. В Board хранится информация о его размере (ширине и высоте), а также о количестве столбцов и строк. Это противоположно плитке, которая знает только собственную позицию:
Board использует BoardProvider для распределения ширины контейнера плитки и количества плиток в строке и столбце между всеми плитками и компонентом сетки:
Эту тему я пропушу: более подробно я рассказал о ней в своём видео о Feature Toggles в React. Если вы хотите узнать о них больше, вы можете посмотреть его:
Компонент Game
Теперь можно задать правила игры и раскрыть интерфейс для игры. Я собираюсь начать с навигации: это поможет вам понять, почему логика игры реализована именно так:
moveLeft перемещает все плитки на левую сторону доски.
moveRight сдвигает все плитки на правую сторону доски.
moveUp перемещает все плитки в верхнюю часть доски.
moveDown перемещает все плитки в нижнюю часть доски.
Как работать с useGame
Состояние useReducer содержит следующие поля:
tiles — это хэш-таблица, отвечающая за хранение плиток. Она позволяет легко найти записи по их ключам, поэтому подходит идеально: находить плитки мы хотим по их идентификаторам.
byIds — это массив, содержащий все идентификаторы по возрастанию. Мы должны сохранить правильный порядок плиток, чтобы React не перерисовывал всю доску при каждом изменении состояния.
hasChange отслеживает изменения плиток. Если ничего не изменилось, новая плитка не создаётся.
inMotion указывает на то, движутся ли плитки. Если это так, то новая плитка не создаётся вплоть до завершения движения.
Экшены
useReducer требуется указать экшены, которые поддерживаются этим хуком:
За что отвечают эти экшены?
CREATE_TILE создаёт новую плитку и добавляет её в хэш-таблицу плиток. Флаг hasChange меняется на false : это действие всегда срабатывает при добавлении новой плитки на доску.
START_MOVE сообщает редюсеру, что он должен ожидать нескольких экшенов, а значит, должен дождаться завершения всех анимаций, прежде чем сможет сгенерировать новую плитку.
END_MOVE сообщает редюсеру, что все действия завершены, и он может создать новую плитку.
Логику этого редюсера вы можете написать самостоятельно или скопировать мою:
Если вы не понимаете, для чего мы определили эти экшены, не беспокойтесь — сейчас мы реализуем хук, который, я надеюсь, всё объяснит.
Как внедрить хук
Посмотрим на функцию, которая отвечает за ходы игрока. Сосредоточимся только на ходе влево: остальные ходы практически одинаковы.
Видно, что к функции хода я решил привязать два колбека. Эта техника называется инверсией управления — таким образом потребитель функции сможет подставлять в выполняемую функцию собственные значения.
Колбек retrieveTileIdsByRow отвечает за поиск всех доступных в ряду непустых плиток (для перемещений влево или вправо). Если игрок делает движения вверх или вниз, будем искать все плитки в столбце.
Посмотрим на логику функции перемещения. Её код я объяснил в комментариях. Алгоритм может быть немного сложным, поэтому я решил, что построчные комментарии помогут его понять:
Код колбека RetrieveTileIdsByRowColumnCallback
Полный код useGame содержит более 400 строк.
Продолжить изучение современной веб-разработки вы сможете на наших курсах:
Data Science и Machine Learning