Unlimited WordPress themes, graphics, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Game Development
  2. Tile-Based Games

Создаем 2D игру "Сокобан" в Unity

by
Difficulty:BeginnerLength:LongLanguages:

Russian (Pусский) translation by Dmitriy Sirosh (you can also view the original English article)

Final product image
What You'll Be Creating

В этом учебном пособии мы будем изучать подход к созданию игры "Сокобан" или, игры в которой игрок передвигает ящики, с использованием логики на основе тайловой графики и двумерного массива для хранения данных уровня. Мы используем движок Unity для разработки и C# в качестве языка сценариев. Пожалуйста, загрузите исходные файлы, поставляемые вместе с этим руководством.

1. Игра Сокобан

Некоторые из нас, возможно, не играли в вариант игры "Сокобан". Оригинальная версия может быть даже старше вас. Для получения подробной информации посетите страницу википедии. По существу, у нас есть игрок или элемент, управляемый пользователем, который должен выталкивать ящики или подобные элементы на свой конечный тайл.

Уровень состоит из квадратной или прямоугольной сетки тайлов, где тайл может быть блокирующим или неблокирующим. Мы можем ходить по неблокирующим тайлам и толкать на них ящики. Специальные проходимые тайлы будут помечаться как конечные точки назначения, в которые ящик должен в конце концов переместиться, чтобы завершился уровень. Игроком обычно управляют с помощью клавиатуры. Когда все ящики достигли конечной точки назначения, уровень завершен.

Разработка на основе тайловой графики по существу означает, что наша игра состоит из множества тайлов (клеток), распределенных заранее. Элемент данных уровня будет представлять, как должны быть распределены тайлы для создания нашего уровня. В нашем случае мы будем использовать квадратную, тайловую сетку. Вы можете прочитать больше про игры созданные на основе тайловой (плиточной) графики здесь, на Envato Tuts+.

2. Подготовка Unity проекта 

Посмотрим, как мы организовали наш проект Unity для этого урока.

Арт игры

Для этого учебного проекта мы не используем никаких внешних арт ассетов, но будем использовать примитивные спрайты, созданные с помощью последней версии Unity 2017.1. На следующем рисунке показано, как мы можем создавать разные формы спрайтов в Unity.

How to create sprites within United 20171

Для уровня мы будем использовать спрайт Square для отображения одного тайла на нашей сокобан сетке. Для нашего игрока будем использовать треугольный спрайт Triangle, и мы будем использовать спрайт Circle для ящика, или в этом случае мяча. Обычные тайлы являются белыми, в то время как конечный тайл имеет другой цвет.

Данные уровня

Мы будем представлять наши данные уровня в виде двумерного массива, который обеспечивает идеальную корреляцию между логическими и визуальными элементами. Мы используем простой текстовый файл для хранения данных уровня, что упрощает редактирование уровня за пределами Unity или изменение уровней просто путем изменения загруженных файлов. Папка Resources содержит текстовый файл уровня - level, в котором хранится наш уровень по умолчанию.

Уровень состоит из семи столбцов и пяти строк. Значение 1 означает, что у нас есть основной тайл в этом положении. Значение -1 означает, что это блокирующий тайл, тогда как значение 0 означает, что это конечный целевой тайл. Значение 2 определяет нашего игрока, а значение 3 — перемещающийся мяч. Просто глядя на данные уровня, мы можем визуализировать, как будет выглядеть наш уровень.

3. Создание игрового уровня Сокобан

Чтобы все было просто, и поскольку это не очень сложная логика, у нас есть только один файл сценария для проекта Sokoban.cs, и он прикреплен к камере сцены. Сохраните его в своем редакторе, пока вы будете выполнять остальную часть урока.

Специальные данные уровня

Данные уровня, представленные двумерным массивом, используются не только для создания начальной сетки, но также используются во всей игре для отслеживания изменений уровня и прогресса игры. Это означает, что текущие значения недостаточны для представления некоторых состояний уровня во время игры.

Каждое значение представляет состояние соответствующего тайла на уровне. Нам нужны дополнительные значения для представления мяча и игрока на конечном тайле, которые соответственно равны -3 и -2. Эти значения могут быть любым значением, присваиваемым в игровой сценарий, но не обязательно теми же значениями, которые мы использовали здесь.

Парсинг текстового файла уровня

Первый шаг - загрузить наши данные уровня в двумерный массив из внешнего текстового файла. Мы используем метод ParseLevel для загрузки строкового значения string и разбиваем его на заполнение нашего двумерного массива данными уровня levelData.

Во время парсинга мы определяем количество строк и столбцов нашего уровня, когда мы заполняем наш levelData.

Прорисовка уровня

Как только у нас будут данные уровня, мы можем нарисовать наш уровень на экране. Для этого мы используем метод CreateLevel.

Для нашего уровня мы установили значение tileSize 50, которое является длиной стороны одного квадратного тайла в нашей сетке уровней. Мы циклически проходим по нашему двумерному массиву и определяем значение, хранящееся в каждом из индексов i и j массива. Если это значение не равно invalidTile (-1), мы создаем новый GameObject с именем tile. Мы присоединяем компонент SpriteRenderer к тайлу tile и присваиваем соответствующий спрайт Sprite или цвет Color в зависимости от значения в индексе массива.

При размещении игрока hero или мяча ball нам нужно сначала создать тайл основание, а затем создать остальные тайлы. Поскольку игрок и мяч должны быть наложены на основной тайл, мы даем их компоненту SpriteRenderer более высокий sortingOrder. Всем тайлам присваивается значение localScale для tileSize, поэтому они 50x50 на нашей сцене.

Мы следим за количеством мячей в нашей сцене с помощью переменной ballCount, и должно быть то же самое или большее количество конечных тайлов на нашем уровне, чтобы сделать возможным завершение уровня. Магия происходит в одной строке кода, где мы определяем положение каждого тайла, используя метод GetScreenPointFromLevelIndices (int row, int col).

Положение в игровом мире плитки определяется путем умножения индексов уровня на значение tileSize. Переменная middleOffset используется для выравнивания уровня в середине экрана. Обратите внимание, что значение row умножается на отрицательное значение для поддержки инвертированной оси y в Unity.

4. Логика игры Сокобан

Теперь, когда мы отобразили наш уровень, давайте перейдем к логике игры. Нам нужно прослушивать событие нажатие клавиши пользователя и перемещать игрока hero на основе входных данных. Нажатие клавиши определяет требуемое направление движения, и игроку hero нужно перемещаться в этом направлении. Существуют различные сценарии для рассмотрения, как только мы определили требуемое направление движения. Предположим, что тайл рядом с игроком hero в этом направлении - tileK.

  • Есть ли в сцене тайл на этой позиции или он находится за пределами нашей сетки?
  • Является ли tileK неблокирующим тайлом?
  • Занята ли tileK мячом?

Если позиция tileK находится вне сетки, нам не нужно ничего делать. Если tileK доступна и проходима, тогда нам нужно переместить игрока hero в эту позицию и обновить наш массив levelData. Если в tileK мяч, то нам нужно подумать о следующем соседе в том же направлении, скажем, tileL.

  • Тайл tileL за пределами сетки?
  • Тайл tileL неблокирующий?
  • Занят ли tileL мячом?

Только в том случае, если tileL - это неблокирующий, незанятый тайл, мы должны переместить игрока hero и мяч в tileK на tileK и tileL соответственно. После успешного перемещения нам нужно обновить массив levelData.

Вспомогательные функции

Вышеупомянутая логика означает, что нам нужно знать, какой тайл принадлежит нашему игроку hero. Мы также должны определить, имеет ли конкретный тайл мяч и должен ли иметься доступ к этому мячу.

Для облегчения этого мы используем класс Dictionary именуемый occupants, в котором хранится компонент GameObject в качестве ключа и индексы массива, хранящиеся как Vector2 в качестве значения. В методе CreateLevel мы заполняем occupants, когда создаем игрока hero или мяч. После заполнения dictionary мы можем использовать GetOccupantAtPosition, чтобы вернуть GameObject по заданному индексу массива.

Метод IsOccupied определяет, является ли значение levelData по предоставленным индексам мячом.

Нам также нужен способ проверить, находится ли данная позиция внутри нашей сетки, и неблокирующий ли это тайл. Метод IsValidPosition проверяет индексы уровня, передаваемые в качестве параметров, для определения того, попадают ли они в размеры нашего уровня. Он также проверяет, есть ли у этого invalidTile, этот индекс в файле levelData.

Ответ на вводимые пользователем данные

В методе обновления Update нашего игрового скрипта мы проверяем события пользователя KeyUp и сравниваем с нашими клавишами ввода, хранящимися в массиве userInputKeys. Как только будет определено требуемое направление движения, мы вызываем метод TryMoveHero с направлением в качестве параметра.

Метод TryMoveHero - это объяснение нашей основной логики игры, которая описана в начале этого раздела. Пожалуйста, внимательно прочитайте следующий метод, чтобы узнать, как реализована логика, как описано выше.

Чтобы получить следующую позицию в определенном направлении на основе предоставленной позиции, мы используем метод GetNextPositionAlong. Это всего лишь просто вопрос увеличения или уменьшения любого из индексов в соответствии с направлением.

Перед перемещением игрока или мяча нам нужно очистить занимаемую ими в настоящее время позицию в массиве levelData. Это делается с помощью метода RemoveOccupant.

Если мы найдем heroTile или ballTile по данному индексу, нам нужно установить его в groundTile. Если мы найдем heroOnDestinationTile или ballOnDestinationTile, нам нужно установить его в destinationTile.

Завершение уровня

Уровень завершен, когда все мячи находятся в их пунктах назначения.

A Completed Level

После каждого успешного движения мы вызываем метод CheckCompletion, чтобы узнать, завершен ли уровень. Мы циклически проходим по нашему массиву levelData и подсчитываем количество событий ballOnDestinationTile. Если это число равно нашему общему числу мячей, определяемых переменной ballCount, уровень завершен.

Заключение

Это простая и эффективная реализация логики sokoban. Вы можете создать свои собственные уровни, изменив текстовый файл или создав новый и изменив переменную levelName, чтобы указать на новый текстовый файл.

Текущая реализация использует клавиатуру для управления игроком. Я предлагаю вам попробовать изменить элемент управления на касания, чтобы мы могли поддерживать сенсорные устройства. Для этого придется найти некоторый двумерный путь, а также, если вы хотите использовать нажатие на любой тайл для того, чтобы провести туда игрока.

Появится дополнительное руководство, в котором мы рассмотрим, как можно использовать текущий проект для создания изометрических и шестиугольных версий игры сокобан с минимальными изменениями.

Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.