Advertisement
  1. Game Development
  2. Tile-Based Games

Введение в создание механизма отображения плитки

Scroll to top
Read Time: 16 min

() translation by (you can also view the original English article)

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

Вот что мы рассмотрим:

  • Что такое игра на основе плитки?
  • Поиск или создание собственных плиток.
  • Написание кода для отображения фрагментов на экране.
  • Редактирование макетов плитки для разных уровней.

Вы тоже можете начать новую игру!


Что такое игра на основе плитки?

Естественно, в Википедии есть подробное определение того, что такое игра на основе плитки, но чтобы получить базовый смысл, вам нужно всего лишь несколько вещей:

  1. Плитка представляет собой небольшое изображение, обычно прямоугольное или изометрическое, которое действует как произведение искусства головоломки для создания больших изображений.
  2. Карта представляет собой группировку плиток, собранных вместе для создания (надеюсь) визуально привлекательного «раздела» (например, уровня или области).
  3. Tile-based относится к методу построения уровней в игре. Код будет отображать плитки в определенных местах, чтобы покрыть предполагаемую область.

Чтобы получить еще более базовый характер, я скажу так:

В игре на основе плитки выложены плитки, чтобы создать каждый уровень.

В отношении обычных типов плиток - прямоугольных и изометрических - для этой простоты мы будем использовать прямоугольные плитки в этой статье. Если вы когда-нибудь решите попробовать изометрические уровни, для его работы требуется дополнительная математика. (Как только вы закончите здесь, ознакомьтесь с этим замечательным учебником по созданию изометрических миров.)

Есть некоторые довольно крутые преимущества, которые вы получаете от использования механизма плитки. Наиболее очевидным преимуществом является то, что вам не нужно создавать массивные изображения вручную для каждого отдельного уровня. Это сократит время разработки и сократит размеры файлов. Имея 50 изображений 1280x768px для 50-уровневой игры, имея один образ со 100 плитами, имеет огромное значение.

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


Поиск или изготовление собственных плиток

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

Если вы решите использовать плитки, которые уже были сделаны, вы можете найти свободно доступное искусство во всем Интернете. Недостатком этого является то, что искусство не было сделано специально для вашей игры. С другой стороны, если вы просто прототипируете или пытаетесь освоить новую концепцию, свободные плитки сделают трюк.

Где найти плитки

Существует немало ресурсов для бесплатного и открытого исходного искусства. Вот несколько мест, чтобы начать поиск:

  1. OpenGameArt.org
  2. Давайте сделаем игры
  3. Изыскатель пикселей

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

Для этого урока я использовал некоторые фрагменты из The Open Game Art Bundle для платформеров. Вы можете загрузить мои уменьшенные версии или оригиналы.

Как сделать свои собственные плитки

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

Многие разработчики начинают использовать пиксель для своих игр, и вот несколько отличных инструментов для этого:

  1. Aseprite
  2. Изменить Pyxel
  3. Графический шлейф
  4. Pixen для пользователей Mac

Pixen для пользователей Mac Если вы хотите что-то более мощное, GIMP - отличный вариант. Вы также можете сделать некоторое векторное искусство с Inkscape и следовать некоторым удивительным учебникам в 2D Game Art для программистов.

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

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

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


Написание кода для отображения плит

Теперь у нас есть все это искусство, мы можем погрузиться в код, чтобы поместить ваши вновь приобретенные (или созданные) плитки на экран.

Как отобразить одиночную плиту на экране

Начнем с самой простой задачи - отобразить одну плиту на экране. Убедитесь, что ваши плитки имеют одинаковый размер и сохранены в отдельных файлах изображений (мы поговорим подробнее о листах спрайтов позже).

Когда у вас есть плитки в папке ваших ресурсов вашего проекта, вы можете написать очень простой класс Tile. Вот пример в Haxe:

1
import flash.display.Sprite;
2
import flash.display.Bitmap;
3
import openfl.Assets;
4
5
class Tile extends Sprite {
6
7
    private var image:Bitmap;
8
9
    public function new() {
10
        super();
11
12
        image = new Bitmap(Assets.getBitmapData("assets/grassLeftBlock.png"));
13
        addChild(image);
14
    }
15
}

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

Теперь, когда у нас есть класс Tile, нам нужно создать экземпляр Tile и добавить его в наш Основной класс:

1
import flash.display.Sprite;
2
import flash.events.Event;
3
import flash.Lib;
4
5
class Main extends Sprite {
6
7
    public function new() {
8
        super();
9
10
        var tile = new Tile();
11
        addChild(tile);
12
    }
13
14
    public static function main() {
15
        Lib.current.addChild(new Main());
16
    }
17
}

Основной класс создает новый Tile, когда вызывается конструктор (функция new (), вызываемая при запуске игры) и добавляет его в список отображения.

Когда игра будет запущена, также будет вызвана функция main (), и на сцену будет добавлен новый Основной объект. Теперь вы должны увидеть свою плитку в самом верхнем левом углу экрана. Все идет нормально.

Совет. Если это не имеет смысла - не волнуйтесь! Совет. Если это не имеет смысла - не волнуйтесь! Главное, чтобы понять, что это создает объект Tile и добавляет его на экран.

Использование массивов для размещения всего экрана плиток

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

У вас есть возможность использовать типичный массив или использовать двумерный массив. Если вы не знакомы с 2D-массивами, это в основном единственный массив, заполненный множеством массивов. Большинство языков обозначают это как nameOfArray [x] [y], где x и y - индексы для доступа к одному элементу.

Поскольку x и y также используются для экранных координат, имеет смысл использовать x и y в качестве координат для наших плит. Трюк с 2D-массивами - это понимание того, как организованы целые числа. .Возможно, вам придется перевернуть y и x при повторении через массивы (пример ниже).

1
private var exampleArr = [ [0, 0, 0, 0, 0],
2
                           [0, 0, 0, 0, 0],
3
                           [0, 0, 0, 0, 0],
4
                           [0, 0, 0, 0, 0],
5
                           [0, 0, 0, 0, 0] ];

Обратите внимание, что в этой реализации элемент «0» в массиве сам по себе представляет собой массив из пяти целых чисел. Это означает, что вы сначала получаете доступ к каждому элементу с y, затем x. Если вы попытаетесь получить доступ к exampleArr [1] [0], вы получите доступ к шестой плитке.

Если вы не совсем понимаете, как работают 2D-массивы прямо сейчас, не беспокойтесь. Для этого урока я буду использовать обычный массив, чтобы все было просто и облегчить визуализацию:

1
private var exampleArr = [ 0, 0, 0, 0, 0,
2
                           0, 0, 0, 0, 0,
3
                           0, 0, 0, 0, 0,
4
                           0, 0, 0, 0, 0,
5
                           0, 0, 0, 0, 0 ];

В приведенном выше примере показано, как обычный массив может быть немного проще. Мы можем точно определить, где будет плитка, и все, что нам нужно сделать, это использовать простую формулу (не волнуйтесь, она придет!), Чтобы получить нужную нам плитку.

Теперь давайте напишем некоторый код, чтобы создать наш массив и заполнить его. Номер один будет идентификатором, который представляет нашу первую плитку.

Сначала нам нужно создать переменную внутри нашего основного класса для хранения нашего массива:

1
private var map:Array<Int>;

Это может показаться немного странным, поэтому я сломаю его для вас.

Имя переменной - это карта, и я дал ей очень специфический тип: Array. Часть < Int > просто сообщает нашей программе, что массив будет хранить целые числа. Массивы могут содержать практически любой тип, который вы хотите, поэтому, если вы используете что-то еще, чтобы идентифицировать свои плитки, не стесняйтесь изменять этот параметр здесь.

Затем нам нужно добавить код в конструктор нашего основного класса (помните, что это новая функция ()), чтобы мы могли создать экземпляр нашей карты:

1
map = new Array<Int>();

Эта строка создаст пустой массив, который мы вскоре можем заполнить нашими, чтобы проверить его. Во-первых, давайте определим некоторые ценности, которые помогут нам в нашей математике:

1
public static var TILE_WIDTH = 60;
2
public static var TILE_HEIGHT = 60;
3
public static var SCREEN_WIDTH = 600;
4
public static var SCREEN_HEIGHT = 360;

Я сделал эти значения общедоступными, потому что это даст нам доступ к ним из любой точки нашей программы (через Main.Tile_WIDTH и т. д.). Кроме того, вы могли заметить, что разделение SCREEN_WIDTH на TILE_WIDTH дает нам 10 и разделение SCREEN_HEIGHT на TILE_HEIGHT дает нам 6. Эти два числа будут использоваться для определения количества целых чисел для хранения в нашем массиве.

1
        var w = Std.int(SCREEN_WIDTH / TILE_WIDTH);
2
        var h = Std.int(SCREEN_HEIGHT / TILE_HEIGHT);
3
4
        for (i in 0...w * h) {
5
            map[i] = 1
6
        }

В этом блоке кода мы назначаем 10 и 6 в w и h, как я уже упоминал выше. Затем нам нужно прыгнуть в цикле  для создания массива, который может поместиться 10 * 6 целых чисел. Это будет учитывать количество фрагментов, чтобы заполнить весь экран.

tileMapDiagram1tileMapDiagram1tileMapDiagram1

Теперь у нас есть наша основная карта, но как мы собираемся рассказать плитки, чтобы попасть в их надлежащее место? Для этого нам нужно вернуться к классу Tile и создать функцию, которая позволит нам перемещать фрагменты по желанию:

1
    public function setLoc(x:Int, y:Int) {
2
        image.x = x * Main.TILE_WIDTH;
3
        image.y = y * Main.TILE_HEIGHT;
4
    }

Когда мы вызываем функцию setLoc (), мы передаем координаты x и y в соответствии с нашим классом карты (формула скоро появится, я обещаю!). Функция принимает эти значения и преобразует их в координаты пикселей, умножая их на TILE_WIDTH и TILE_HEIGHT соответственно.

Единственное, что осталось сделать, чтобы отобразить наши плитки на экране, - это рассказать нашему основному классу о создании плиток и разместить их на месте на основе их местоположений на карте. Вернемся к Main и реализуем это:

1
        for (i in 0...map.length) {
2
            var tile = new Tile();
3
            var x = i % w;
4
            var y = Math.floor(i / w);
5
            tile.setLoc(x, y);
6
            addChild(tile);
7
        }

О, да! Правильно, теперь у нас есть экран, полный плиток. Давайте сломаем то, что происходит выше.

Формула

Формула, о которой я постоянно упоминаю, наконец-то здесь.

Мы вычисляем x, беря модуль (%) i и w (который помнит 10).

Модуль - это просто остаток после целочисленного деления: \ (14 \ div 3 = 4 \ text {остаток} 2 \), поэтому \ (14 \ text {moduleulus} 3 = 2 \).

Мы используем это, потому что хотим, чтобы наше значение х возвращалось обратно к 0 в начале каждой строки, поэтому мы рисуем соответствующий фрагмент в левом нижнем углу:

tile_grid_colstile_grid_colstile_grid_cols

Что касается y, мы берем слово floor - пол () i / w (т. е. мы округляем этот результат вниз), потому что мы хотим, чтобы y увеличивался только после того, как мы достигли конца каждой строки и переместились на один уровень:

tile_grid_rowstile_grid_rowstile_grid_rows
Совет. Большинство разделов не требуют, чтобы вы вызывали Math.floor () при делении целых чисел; Haxe просто обрабатывает целочисленное деление по-разному. Если вы используете язык, который выполняет целочисленное деление, вы можете просто использовать i / w (если они оба являются целыми).

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

Например: размер экрана 500x500, ваши плитки 100x100, а ваш размер в мире - 1000x1000. Вам просто нужно выполнить быструю проверку перед рисованием плиток, чтобы узнать, какие плитки на экране. Используя расположение вашего окна просмотра - скажем, 400x500 - вам нужно будет только нарисовать плитки из строк с 4 по 9 и столбцы с 5 по 10. Вы можете получить эти числа, разделив местоположение на размер плитки, а затем смещая эти значения с размером экрана, разделенным на размер плитки. Простой.

Возможно, это не похоже на то, что все плитки одинаковы, но фундамент практически отсутствует. Осталось только создать различные типы плиток и спроектировать нашу карту, чтобы они выстроились в линию и создали что-то приятное.


Редактирование макетов плитки для разных уровней

Хорошо, теперь у нас есть карта, полная тех, которые покрывают экран. К этому моменту вы должны иметь более одного типа плитки, а это значит, что мы должны изменить наш конструктор Tile для учета этого:

1
public function new(id:Int) {
2
        super();
3
4
        switch(id) {
5
            case 1:
6
                image = new Bitmap(Assets.getBitmapData("assets/grassLeftBlock.png"));
7
            case 2:
8
                image = new Bitmap(Assets.getBitmapData("assets/grassCenterBlock.png"));
9
            case 3:
10
                image = new Bitmap(Assets.getBitmapData("assets/grassRightBlock.png"));
11
            case 4:
12
                image = new Bitmap(Assets.getBitmapData("assets/goldBlock.png"));
13
            case 5:
14
                image = new Bitmap(Assets.getBitmapData("assets/globe.png"));
15
            case 6:
16
                image = new Bitmap(Assets.getBitmapData("assets/mushroom.png"));
17
        }
18
        addChild(image);
19
    }

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

Теперь нам нужно вернуться к нашему главному конструктору и исправить создание плитки в нашем цикле  для for:

1
        for (i in 0...map.length) {
2
            var tile = new Tile(map[i]);
3
            var x = i % w;
4
            var y = Math.floor(i / w);
5
            tile.setLoc(x, y);
6
            addChild(tile);
7
        }

Все, что нам нужно было сделать, - передать карту [i] в ​​конструктор Tile, чтобы он снова работал. Если вы попытаетесь запустить без передачи целого числа в Tile, это даст вам некоторые ошибки.

Почти все на месте, но теперь нам нужен способ разработки карт вместо того, чтобы просто заполнять их случайными фрагментами. Сначала удалите цикл for в главном конструкторе, который устанавливает каждый элемент в один. Затем мы можем создать собственную карту вручную:

1
        map = [ 0, 4, 0, 0, 0, 0, 0, 5, 0, 0,
2
                0, 0, 0, 0, 0, 1, 2, 2, 3, 0,
3
                0, 0, 0, 6, 0, 0, 0, 0, 0, 0,
4
                1, 2, 2, 3, 0, 0, 0, 0, 0, 0,
5
                0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
6
                0, 0, 0, 0, 0, 0, 1, 2, 2, 3 ];

Если вы отформатируете массив, как я уже выше, вы можете легко увидеть, как наши плитки будут организованы. Вы также можете просто ввести массив как одну длинную строку чисел, но первая хорошая, потому что вы можете видеть 10 и 6 вниз.

Когда вы пытаетесь запустить программу сейчас, вы должны получить исключения с нулевым указателем. Проблема в том, что мы используем нули на нашей карте, и наш класс Tile не знает, что делать с нулем. Во-первых, мы исправим конструктор, добавив в чек, прежде чем добавить дочерний элемент изображения:

1
        if (image != null) addChild(image);

Эта быстрая проверка гарантирует, что мы не добавляем ни одного дочернего элемента в объекты Tile, которые мы создаем. Последнее изменение для этого класса - функция setLoc (). Сейчас мы пытаемся набор x и y значения для переменной, которая не была инициализирована.

Последнее изменение для этого класса - функция setLoc ().

1
    public function setLoc(x:Int, y:Int) {
2
        if (image != null) {
3
            image.x = x * Main.TILE_WIDTH;
4
            image.y = y * Main.TILE_HEIGHT;
5
        }
6
    }

С этими двумя простыми исправлениями и плитами, которые я предоставил выше, вы должны иметь возможность запускать игру и видеть простой уровень платформы. Поскольку мы оставили 0 в качестве «не-плитки», мы можем оставить его прозрачным (пустым). Если вы хотите оживить уровень, вы можете установить фон, прежде чем выкладывать плитки; Я просто добавил голубой градиент, чтобы выглядеть как небо в фоновом режиме.

tileMapDiagram2tileMapDiagram2tileMapDiagram2

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

Third-Party Software for Designing Levels

Как только у вас будут основы, вы можете использовать другие инструменты, которые помогут вам быстро разработать уровни и посмотреть, как они выглядят, прежде чем бросать их в игру. One option is to create your own level editor. Альтернативой является захват некоторого программного обеспечения, которое доступно для использования в свободном доступе. Двумя популярными инструментами являются Tiled Map Editor и Ogmo Editor. Они упрощают редактирование уровней с помощью нескольких опций экспорта.


Вы только что создали плитку!

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

Кроме того, я предоставил исходные файлы для проверки. Вот как выглядит финальный SWF:

Скачайте SWF здесь.

... и здесь, опять же, это массив, из которого он создан:

1
        map = [ 0, 4, 0, 0, 0, 0, 0, 5, 0, 0,
2
                0, 0, 0, 0, 0, 1, 2, 2, 3, 0,
3
                0, 0, 0, 6, 0, 0, 0, 0, 0, 0,
4
                1, 2, 2, 3, 0, 0, 0, 0, 0, 0,
5
                0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
6
                0, 0, 0, 0, 0, 0, 1, 2, 2, 3 ];

Не останавливайся здесь!

Ваша следующая задача - сделать некоторые исследования и попробовать некоторые новые вещи, чтобы улучшить то, что вы здесь сделали. Листы Sprite - отличный способ улучшить производительность и сделать жизнь немного легче. Трюк состоит в том, чтобы получить твердый код, который выкладывали для чтения из листа спрайта, чтобы ваша игра не должна была считывать каждое изображение по отдельности.

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

Как всегда, оставьте несколько комментариев ниже о том, как ваш движок работает на вас - и, возможно, даже некоторые демоверсии, чтобы мы могли попробовать вашу следующую игру.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Game Development tutorials. Never miss out on learning about the next big thing.
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.