• Что можно приготовить из кальмаров: быстро и вкусно

    Что такое тайл
    Вы, наверное, представляете себе, что такое тайл, но давайте все же познакомимся с ним поближе. Тайл выглядит как картинка фиксированного размера. Причем нарисованная таким образом, что при сопоставлении с другими тайлами получается единое изображение без заметных “швов”. Легче всего представить тайлы как кафель, которым облицовывают стены или полы. Самый простой тайл - квадратное изображение, симметричное по вертикали и горизонтали (вы можете видеть его на иллюстрации). Если составить эти картинки друг с другом, то получится большое полотно дерна. А если внести в эту живописную картину для разнообразия еще один вид тайла, скажем, изображающий дорогу, то можно уже составить примитивную карту.
    Давайте теперь посчитаем затраты, которые от нас потребуются для создания карты размером 512x512 клеток. Допустим, что сам тайл будет размером 32x32 пикселя, тогда он будет занимать в памяти 1024 байта - 1 килобайт. После подсчетов оказывается, что простейшая карта занимает в памяти около 262 килобайт плюс память для хранения изображений самих тайлов. А вот если бы мы рисовали такую же по площади область вручную, то нам пришлось бы положить на алтарь технического прогресса 268 мегабайт (!). И это при том, что я брал для простоты режим в 256 цветов. Понятно, что даже сейчас не каждый компьютер обладает таким количеством памяти, так что тайловое строение карт используется в подавляющем большинстве двухмерных и в достаточном количестве трехмерных игр. Простой прямоугольный тайл.
    Прямоугольные и квадратные тайлы хороши для представления вида с высоты птичьего полета, но существует и другой вид проекции, называемый изометрией. Он используется, когда надо дать ощущение объема, глубины, сделать псевдо-3D . Отсюда изометрические системы иногда называют 2.5D. В чем же здесь суть? Теперь тайл будет выглядеть повернутым одним углом к зрителю и как бы уходящим вглубь (посмотрите на иллюстрацию). Двухмерная по своей сути картинка обретает третье измерение. При этом размеры квадрата (тайла) определяются следующим образом:
    1. Длина - от самой левой до самой правой точки изображения.
    2. Ширина - от самой “дальней” до самой “ближней” точки к зрителю.
    3. Высота - "толщина" тайла.
    Для создания эффекта объема ширина должны быть меньше длины примерно в два раза. Экспериментируя с эти отношением, вы как бы вертите "плиткой" в трехмерном пространстве, выбирая нужную проекцию. Для этих целей полезно использовать какой-либо аналог 3D-студии. Создайте там плитку, выберите ее положение, а затем измерьте стороны и получившиеся соотношения.
    Но как же теперь нарисовать Простой изометрический тайл. такой тайл на экране? Ведь обычные функции рисования привыкли к тому, что им дают прямоугольную область для вывода, с чем они прекрасно справляются. Задача эта решается с помощью масок. На иллюстрации вы увидите, что контуры плитки попадают в белую область, а то, что остается за бортом, окрашивается в черный цвет и формирует прямоугольник. Получается принцип трафарета: изображение накладывается на маску и на “холст”, а проходят только те точки, которые попадают в белый цвет. Даже стандартные функции рисования Windows обучены вытворять такие штуки, не говоря уже о функциях DirectX. При этом использование масок позволяет делать и другие эффекты. Например, вы можете нарисовать только шапку тайла, отбросив его высоту. Или сделать разные участки травы с использованием только одной картинки. Если создать маску, состоящую сплошь из дырок, и с ее помощью отобразить тайл с травой на месте уже существующего тайла другого типа, у нас получится плавный переход от одной поверхности к другой. Подобный прием можно применять во множестве случаев, а “дырявые” маски генерировать случайным образом. Тогда никто и не догадается, что у вас не пререндеренный пейзаж. Получается своеобразное мультитекстурирование: когда на один участок накладываются два взаимно прозрачных изображения. Правда, это отнимает достаточное количество компьютерного времени.
    В самом простом случае высота тайла равна нулю и не просчитывается. Однако если мы будем использовать только такие "кирпичики", то мы добьемся создания в лучшем случае приятной полянки. Используя различающиеся высоты, мы добьемся создания более реалистичного пейзажа, со стенками и пригорками. Конечно, выводить такие “кирпичики” на экран будет сложнее. Теперь мы должны познакомиться с понятием базовой линии тайла.
    Базовая линия - это место, в котором тайл соприкасается с “землей”. На картинке они выделены розовым цветом. Базовая линия может проходить в любом месте на линии вертикали, ведь для стены разной величины может использоваться одно и то же изображение. Тогда на карте размещения тайлов отображаются базовые линии, а сами тайлы начинают отрисовываться из более верхнего положения.

    2Dfx

    Мы уже рассматривали, какие эффекты можно создавать, используя несколько тайловых слоев. В реальных играх Так считаются размеры тайла. вся карта является многослойной, как пресловутый пирог: первый слой - это земля, камни, грязь, вода и тому подобное; второй слой - всякая периферия типа деревьев, скал, столов и стульев; третий слой - объекты: люди, монстры, ключи и магические зелья.
    Вы можете добавлять еще слои для вышеупомянутых эффектов, но обычно для обеспечения лучшей производительности используют четыре-пять. Скажем, четвертый слой в нашей схеме можно добавить для создания объектов, которые будут находиться выше уровня головы персонажей, например крыш домов. Вы все встречались с таким эффектом: когда герой подходит к зданию вплотную, у того вдруг исчезает крыша, и вы можете увидеть внутренности строения. Так вот, очень легко заставить тайловый движок перестать отрисовывать четвертый слой, чтобы пользователь увидел все, что скрывается под ним.
    Тайловые движки, основывающие свою графику на принципе палитр, активно используют эффект ротации цветов. Пусть некая синяя гамма используется для отрисовки воды. Тогда, изменяя эту гамму или, наоборот, переставляя индексы в тайле, можно получить эффект перетекающей воды. Тот же самый фокус проходит и для других поверхностей. Скажем, в пещере тайлы, изображающие скалу, Тайл и его маска. При рисовании на экране
    видна только та часть, что попадает в белую
    область маски. можно сделать темнее. А если вручить персонажу в руки факел и в зависимости от его позиции осветлять окружающую местность - эффект получится очень красивый. Я уже не говорю о динамической смене дня и ночи. Менеджмент палитр - очень интересный вопрос, вдобавок очень хорошо разработанный и изученный в период тотального 256-цветия, когда без палитр не обходился никто. Даже сейчас, когда цветность меньше 16 бит считается неприличной, для хранения спрайтов используются собственные форматы, основанные на все тех же палитрах.
    Кстати, кто сказал, что для вывода тайлов нужно обязательно использовать заезженные процедуры DirectDraw? Конечно, за долгое время существования были разработаны алгоритмы для многих эффектов: и динамического освещения, и даже систем частиц. Но все они работают очень медленно. Но у нас на дворе не 1996 год, и Voodoo Graphics является редкостью только из-за того, что все его уже повыкидывали. Поэтому было бы очень соблазнительно запрячь для повседневных нужд возможности 3D-акселератора.
    3D-библиотеки могут помочь с выводом текстурированных полигонов (те же тайлы) - самым тормозным моментом в 2D-процедурах. При этом фильтрация (aka художественное размытие текстур) является само собой разумеющимся фактом. Кроме того, становятся доступны все эффекты, которые только есть в трехмерных играх: свет, прозрачность, тени, световые переходы и так далее. Предмет этот достаточно интересен для того, чтобы в нем попрактиковаться. Возможно, за ним лежит будущее тайловых игр.

    Создание карт
    Любая карта - это массив, задающий расположение тайлов на своих слоях, а также их параметры. Самая простая карта представляет собой матрицу MxN, где каждый ее элемент содержит номер тайла из условно принятой последовательности. Если вы когда-либо пользовались редактором карт двухмерной стратегии или РПГ (или работали в "Фотошопе"), вы легко уловите суть. Когда Одинаковые тайлы могут различаться по высоте,
    когда используется метод базовой линии. добавляются новые слои, матрица усложняется, получается уже набор (структура) матриц, а с введением каких-либо эффектов получается собственный формат карты, который занимает на диске не один мегабайт.
    Тайлы не обязательно должны задаваться последовательными порядковыми номерами. Наоборот, для начала хорошо бы разбить весь ваш числовой ряд на несколько промежутков. Например, в промежуток от 0 до 63 входят обыкновенные тайлы, по которым можно ходить (земля, плиты). От 63 до 127 - непроходимые заросли джунглей, стены и другие твердые объекты. Следующий промежуток - воды озер и болот, наконец, специальные группы, которые выполняют какие-либо действия, когда герой “наступает” на них: порталы, ловушки и прочее. Теперь уже, по мере производства "Фотошопом" графики, вы присваиваете новичкам номера, исходя из принятых правил, и в дальнейшем освобождаетесь от многих проблем, связанных с определением проходимости участка или того, как надо реагировать, когда герой вошел в портал.
    При прорисовке карты у вас поначалу могут возникнуть проблемы с рисованием изометрических тайлов, ведь они такие “кривые”, что непонятно, где надо рисовать один и продолжать второй. Трудности могут возникать при программировании плавного скроллинга или обрезки экраном участков карты. Кстати, размеры тайлов лучше всего делать в какой-либо степени двойки (16x16 или 32x32), это очень сильно помогает как вам, так и процессору при вычислениях. То же самое относится и к размеру карты. Она должна быть по возможности квадратной и по размерностям делиться на размер составляющих "кирпичиков". Таким образом, при расчетах у вас никогда не будет дробных величин, с которыми невозможно работать. Поэтому большинство карт обладают размерами вроде 128x128 или 512x512.
    Между прочим, перемещение персонажей по изометрической карте тоже происходит не просто так. Ведь наша карта повернута “вглубь” и абсолютный ее размер по вертикали в два раза меньше, чем по горизонтали. Поэтому и “вверх” любой ходячий предмет должен двигаться соответствующим образом. Вообще говоря, здесь надо применять формулы, в простейшем же случае на два шага в сторону приходится один шаг вглубь. Если вас интересуют точные расчеты, их легко будет отыскать в Интернете, в крайнем случае "мыльте" за советом. Это же относится к любому перемещению в изометрии, имеющей место хоть в абсолютно плоском квесте.
    К счастью, все эти вопросы уже давно не относятся к разряду малоизученных технологий, и в Сети есть масса учебников по программированию изометрических двухмерных движков. Советую заглядывать в раздел статей таких сайтов, как www.gamedev.net и www.flipcode.com , или просто скормить какому либо Yahoo-подобному поисковику подходящие ключевые слова.

    IsometriX

    Ну а для любителей "поковырять программки" специально выкладываем на компакт движок по имени IsometriX . Он аккумулирует в себе все, что было сказано выше. В отличие от большинства трехмерных энжайнов и конструкторов игр, которые рассматривались в прошлом "Самопале", этот движок не ставит своей целью быть основой вашей игры. Он прежде всего предназначен для того, чтобы вы на практике посмотрели, как исполняются всякие теоретические вопросы. Движок полностью написан на Visual C++ , имеет исходный код и свой редактор уровней. Ежели вам не интересно программирование, вы все равно можете побродить по тестовому уровню, ничего при этом не компилируя.
    По функциям IsometriX может показаться более чем скромным: 640x480 в 8-битном цвете - это, конечно, не дело. Но, как я уже сказал, это скорее пример, на основе которого вы можете научиться работать с картами, передвигать персонажей, проверять столкновения и делать многое другое. Кроме того, все прекрасно работает в Windows 9x/2000 и, скорее всего, будет в XP.

    В напутствие
    Что бы там ни говорили, двухмерные тайловые движки и сейчас живее всех живых. Никакая 3D-игра еще не может сравниться с классическими двухмерками по детальности и красоте прорисовки. Возьмите хотя бы Fallout Tactics - в этой игре применены, пожалуй, самые прогрессивные достижения в области 2D. И никакой язык не повернется назвать графику этой игры убогой.
    Кроме этого, тайлы идеально подходят для создания громадных миров. С использованием сегментирования, когда карта дробится на несколько кусков, каждый из которых динамически подгружается, можно делать карты просто фантастических размеров. И это активно используется в играх жанра RPG. Это еще один фактор, по которому разработчики не торопятся уходить в мир акселераторов и мультитекстурирования.
    Ну и, наконец, тайловая графика гораздо проще в изучении, чем любой 3D-движок (хотя это может быть моим субъективным мнением). А уж изучение DirectDraw для оперирования спрайтами несравненно легче и понятнее, чем Direct3D и даже OpenGL. Поэтому не стремитесь бросаться сразу на передовой фронт. Возможно, вам стоит обратить внимание и на старый добрый 2D-мир. Ну, может быть, чуточку 2.5D...

    Что такое тайл
    Вы, наверное, представляете себе, что такое тайл, но давайте все же познакомимся с ним поближе. Тайл выглядит как картинка фиксированного размера. Причем нарисованная таким образом, что при сопоставлении с другими тайлами получается единое изображение без заметных “швов”. Легче всего представить тайлы как кафель, которым облицовывают стены или полы. Самый простой тайл - квадратное изображение, симметричное по вертикали и горизонтали (вы можете видеть его на иллюстрации). Если составить эти картинки друг с другом, то получится большое полотно дерна. А если внести в эту живописную картину для разнообразия еще один вид тайла, скажем, изображающий дорогу, то можно уже составить примитивную карту.
    Давайте теперь посчитаем затраты, которые от нас потребуются для создания карты размером 512x512 клеток. Допустим, что сам тайл будет размером 32x32 пикселя, тогда он будет занимать в памяти 1024 байта - 1 килобайт. После подсчетов оказывается, что простейшая карта занимает в памяти около 262 килобайт плюс память для хранения изображений самих тайлов. А вот если бы мы рисовали такую же по площади область вручную, то нам пришлось бы положить на алтарь технического прогресса 268 мегабайт (!). И это при том, что я брал для простоты режим в 256 цветов. Понятно, что даже сейчас не каждый компьютер обладает таким количеством памяти, так что тайловое строение карт используется в подавляющем большинстве двухмерных и в достаточном количестве трехмерных игр.
    Прямоугольные и квадратные тайлы хороши для представления вида с высоты птичьего полета, но существует и другой вид проекции, называемый изометрией. Он используется, когда надо дать ощущение объема, глубины, сделать псевдо-3D . Отсюда изометрические системы иногда называют 2.5D. В чем же здесь суть? Теперь тайл будет выглядеть повернутым одним углом к зрителю и как бы уходящим вглубь (посмотрите на иллюстрацию). Двухмерная по своей сути картинка обретает третье измерение. При этом размеры квадрата (тайла) определяются следующим образом:
    1. Длина - от самой левой до самой правой точки изображения.
    2. Ширина - от самой “дальней” до самой “ближней” точки к зрителю.
    3. Высота - "толщина" тайла.
    Для создания эффекта объема ширина должны быть меньше длины примерно в два раза. Экспериментируя с эти отношением, вы как бы вертите "плиткой" в трехмерном пространстве, выбирая нужную проекцию. Для этих целей полезно использовать какой-либо аналог 3D-студии. Создайте там плитку, выберите ее положение, а затем измерьте стороны и получившиеся соотношения.
    Но как же теперь нарисовать
    такой тайл на экране? Ведь обычные функции рисования привыкли к тому, что им дают прямоугольную область для вывода, с чем они прекрасно справляются. Задача эта решается с помощью масок. На иллюстрации вы увидите, что контуры плитки попадают в белую область, а то, что остается за бортом, окрашивается в черный цвет и формирует прямоугольник. Получается принцип трафарета: изображение накладывается на маску и на “холст”, а проходят только те точки, которые попадают в белый цвет. Даже стандартные функции рисования Windows обучены вытворять такие штуки, не говоря уже о функциях DirectX. При этом использование масок позволяет делать и другие эффекты. Например, вы можете нарисовать только шапку тайла, отбросив его высоту. Или сделать разные участки травы с использованием только одной картинки. Если создать маску, состоящую сплошь из дырок, и с ее помощью отобразить тайл с травой на месте уже существующего тайла другого типа, у нас получится плавный переход от одной поверхности к другой. Подобный прием можно применять во множестве случаев, а “дырявые” маски генерировать случайным образом. Тогда никто и не догадается, что у вас не пререндеренный пейзаж. Получается своеобразное мультитекстурирование: когда на один участок накладываются два взаимно прозрачных изображения. Правда, это отнимает достаточное количество компьютерного времени.
    В самом простом случае высота тайла равна нулю и не просчитывается. Однако если мы будем использовать только такие "кирпичики", то мы добьемся создания в лучшем случае приятной полянки. Используя различающиеся высоты, мы добьемся создания более реалистичного пейзажа, со стенками и пригорками. Конечно, выводить такие “кирпичики” на экран будет сложнее. Теперь мы должны познакомиться с понятием базовой линии тайла.
    Базовая линия - это место, в котором тайл соприкасается с “землей”. На картинке они выделены розовым цветом. Базовая линия может проходить в любом месте на линии вертикали, ведь для стены разной величины может использоваться одно и то же изображение. Тогда на карте размещения тайлов отображаются базовые линии, а сами тайлы начинают отрисовываться из более верхнего положения.

    2Dfx

    Мы уже рассматривали, какие эффекты можно создавать, используя несколько тайловых слоев. В реальных играх
    вся карта является многослойной, как пресловутый пирог: первый слой - это земля, камни, грязь, вода и тому подобное; второй слой - всякая периферия типа деревьев, скал, столов и стульев; третий слой - объекты: люди, монстры, ключи и магические зелья.
    Вы можете добавлять еще слои для вышеупомянутых эффектов, но обычно для обеспечения лучшей производительности используют четыре-пять. Скажем, четвертый слой в нашей схеме можно добавить для создания объектов, которые будут находиться выше уровня головы персонажей, например крыш домов. Вы все встречались с таким эффектом: когда герой подходит к зданию вплотную, у того вдруг исчезает крыша, и вы можете увидеть внутренности строения. Так вот, очень легко заставить тайловый движок перестать отрисовывать четвертый слой, чтобы пользователь увидел все, что скрывается под ним.
    Тайловые движки, основывающие свою графику на принципе палитр, активно используют эффект ротации цветов. Пусть некая синяя гамма используется для отрисовки воды. Тогда, изменяя эту гамму или, наоборот, переставляя индексы в тайле, можно получить эффект перетекающей воды. Тот же самый фокус проходит и для других поверхностей. Скажем, в пещере тайлы, изображающие скалу,
    можно сделать темнее. А если вручить персонажу в руки факел и в зависимости от его позиции осветлять окружающую местность - эффект получится очень красивый. Я уже не говорю о динамической смене дня и ночи. Менеджмент палитр - очень интересный вопрос, вдобавок очень хорошо разработанный и изученный в период тотального 256-цветия, когда без палитр не обходился никто. Даже сейчас, когда цветность меньше 16 бит считается неприличной, для хранения спрайтов используются собственные форматы, основанные на все тех же палитрах.
    Кстати, кто сказал, что для вывода тайлов нужно обязательно использовать заезженные процедуры DirectDraw? Конечно, за долгое время существования были разработаны алгоритмы для многих эффектов: и динамического освещения, и даже систем частиц. Но все они работают очень медленно. Но у нас на дворе не 1996 год, и Voodoo Graphics является редкостью только из-за того, что все его уже повыкидывали. Поэтому было бы очень соблазнительно запрячь для повседневных нужд возможности 3D-акселератора.
    3D-библиотеки могут помочь с выводом текстурированных полигонов (те же тайлы) - самым тормозным моментом в 2D-процедурах. При этом фильтрация (aka художественное размытие текстур) является само собой разумеющимся фактом. Кроме того, становятся доступны все эффекты, которые только есть в трехмерных играх: свет, прозрачность, тени, световые переходы и так далее. Предмет этот достаточно интересен для того, чтобы в нем попрактиковаться. Возможно, за ним лежит будущее тайловых игр.

    Создание карт
    Любая карта - это массив, задающий расположение тайлов на своих слоях, а также их параметры. Самая простая карта представляет собой матрицу MxN, где каждый ее элемент содержит номер тайла из условно принятой последовательности. Если вы когда-либо пользовались редактором карт двухмерной стратегии или РПГ (или работали в "Фотошопе"), вы легко уловите суть. Когда добавляются новые слои, матрица усложняется, получается уже набор (структура) матриц, а с введением каких-либо эффектов получается собственный формат карты, который занимает на диске не один мегабайт.
    Тайлы не обязательно должны задаваться последовательными порядковыми номерами. Наоборот, для начала хорошо бы разбить весь ваш числовой ряд на несколько промежутков. Например, в промежуток от 0 до 63 входят обыкновенные тайлы, по которым можно ходить (земля, плиты). От 63 до 127 - непроходимые заросли джунглей, стены и другие твердые объекты. Следующий промежуток - воды озер и болот, наконец, специальные группы, которые выполняют какие-либо действия, когда герой “наступает” на них: порталы, ловушки и прочее. Теперь уже, по мере производства "Фотошопом" графики, вы присваиваете новичкам номера, исходя из принятых правил, и в дальнейшем освобождаетесь от многих проблем, связанных с определением проходимости участка или того, как надо реагировать, когда герой вошел в портал.
    При прорисовке карты у вас поначалу могут возникнуть проблемы с рисованием изометрических тайлов, ведь они такие “кривые”, что непонятно, где надо рисовать один и продолжать второй. Трудности могут возникать при программировании плавного скроллинга или обрезки экраном участков карты. Кстати, размеры тайлов лучше всего делать в какой-либо степени двойки (16x16 или 32x32), это очень сильно помогает как вам, так и процессору при вычислениях.

    То же самое относится и к размеру карты. Она должна быть по возможности квадратной и по размерностям делиться на размер составляющих "кирпичиков". Таким образом, при расчетах у вас никогда не будет дробных величин, с которыми невозможно работать. Поэтому большинство карт обладают размерами вроде 128x128 или 512x512.
    Между прочим, перемещение персонажей по изометрической карте тоже происходит не просто так. Ведь наша карта повернута “вглубь” и абсолютный ее размер по вертикали в два раза меньше, чем по горизонтали. Поэтому и “вверх” любой ходячий предмет должен двигаться соответствующим образом. Вообще говоря, здесь надо применять формулы, в простейшем же случае на два шага в сторону приходится один шаг вглубь. Если вас интересуют точные расчеты, их легко будет отыскать в Интернете, в крайнем случае "мыльте" за советом. Это же относится к любому перемещению в изометрии, имеющей место хоть в абсолютно плоском квесте.
    К счастью, все эти вопросы уже давно не относятся к разряду малоизученных технологий, и в Сети есть масса учебников по программированию изометрических двухмерных движков. Советую заглядывать в раздел статей таких сайтов, как www.gamedev.net и www.flipcode.com , или просто скормить какому либо Yahoo-подобному поисковику подходящие ключевые слова.

    IsometriX

    Ну а для любителей "поковырять программки" специально выкладываем на компакт движок по имени IsometriX . Он аккумулирует в себе все, что было сказано выше. В отличие от большинства трехмерных энжайнов и конструкторов игр, которые рассматривались в прошлом "Самопале", этот движок не ставит своей целью быть основой вашей игры. Он прежде всего предназначен для того, чтобы вы на практике посмотрели, как исполняются всякие теоретические вопросы. Движок полностью написан на Visual C++ , имеет исходный код и свой редактор уровней. Ежели вам
    не интересно программирование, вы все равно можете побродить по тестовому уровню, ничего при этом не компилируя.
    По функциям IsometriX может показаться более чем скромным: 640x480 в 8-битном цвете - это, конечно, не дело. Но, как я уже сказал, это скорее пример, на основе которого вы можете научиться работать с картами, передвигать персонажей, проверять столкновения и делать многое другое. Кроме того, все прекрасно работает в Windows 9x/2000 и, скорее всего, будет в XP.

    В напутствие


    Что бы там ни говорили, двухмерные
    тайловые движки и сейчас живее всех живых. Никакая 3D-игра еще не может сравниться с классическими двухмерками по детальности и красоте прорисовки. Возьмите хотя бы Fallout Tactics - в этой игре применены, пожалуй, самые прогрессивные достижения в области 2D. И никакой язык не повернется назвать графику этой игры убогой.
    Кроме этого, тайлы идеально подходят для создания громадных миров. С использованием сегментирования, когда карта дробится на несколько кусков, каждый из которых динамически подгружается, можно делать карты просто фантастических размеров. И это активно используется в играх жанра RPG. Это еще один фактор, по которому разработчики не торопятся уходить в мир акселераторов и мультитекстурирования.
    Ну и, наконец, тайловая графика гораздо проще в изучении, чем любой 3D-движок (хотя это может быть моим субъективным мнением). А уж изучение DirectDraw для оперирования спрайтами несравненно легче и понятнее, чем Direct3D и даже OpenGL. Поэтому не стремитесь бросаться сразу на передовой фронт. Возможно, вам стоит обратить внимание и на старый добрый 2D-мир. Ну, может быть, чуточку 2.5D...
    • Перевод

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


    На изображении ниже показан набор тайлов (тайлсет), на основании которого сгенерирован мир из видео. Мир снабжён примечаниями, которые помогут представить его в настоящей среде.


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

    Правильный тайлинг выглядит так:

    Это тайлинг, который должен представлять карту с водой, побережьями, травой, городами со зданиями (синие прямоугольники), и горами со снежными вершинами. Чёрными линиями показаны границы между тайлами.

    Я думаю, что это интересный способ описания и создания миров, потому что очень часто в алгоритмах процедурной генерации используется подход «сверху вниз». Например, в L-системах используется рекурсивное описание объекта, при котором высокоуровневые, крупные детали, определяются раньше, чем низкоуровневые. В таком подходе нет ничего плохого, но я думаю, что интересно создавать наборы тайлов, способные кодировать только простые низкоуровневые взаимоотношения (например, морская вода и трава должны быть разделены побережьем, у зданий должны быть только выпуклые углы под углами в 90 градусов) и наблюдать за возникновением высокоуровневых паттернов (например, квадратных зданий).

    Тайлинг - это NP-полная задача удовлетворения ограничений

    Для читателя, знакомого с задачами удовлетворения ограничений (ЗУО, constraint satisfaction problems, CSP), уже очевидно, что тайлинг конечного мира является ЗУО. В ЗУО у нас имеется множество переменных, множество значений, которые может принимать каждая переменная (называемое областью её определения) и множество ограничений. В нашем случае переменные - это координаты на карте, область определения каждой переменной - это тайлсет, а ограничения заключаются в том, что грани тайлов должны соответствовать граням соседей.

    Интуитивно понятно, что задача правильного создания нетривиального тайлинга сложна, поскольку тайлсеты могут кодировать произвольные обширные зависимости. Когда мы рассматриваем тайлсет в целом, то с формальной точки зрения это является NP-полной задачей удовлетворения ограничений. Наивный алгоритм создания тайлингов заключается в исчерпывающем поиске пространства тайлингов и выполняется за экспоненциальное время. Существует надежда, что мы можем создавать интересные миры на основе тайлсетов, решаемых поиском, который можно ускорить эвристиками. Другой вариант - создавать тайлинги, которые приблизительно верны, но содержат небольшое количество неверных расположений. Я нашёл два алгоритма, которые хорошо работают с некоторыми интересными тайлсетами, и ниже опишу их.

    Способ 1: жадное расположение с обратными переходами

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

    Инициализируем всю карту как НЕРЕШЁННУЮ

    Пока на карте остаются НЕРЕШЁННЫЕ тайлы
    если на карте можно разместить любой подходящий тайл
    t <- коллекция всех возможных расположений подходящих тайлов
    l <- случайная выборка из t, взвешенная по вероятностям тайлов
    располагаем l на карте
    иначе
    выбираем случайный НЕРЕШЁННЫЙ тайл и присваиваем всем его соседям состояние НЕРЕШЁННЫЙ


    Первая попытка создания тайлинга из тайлсета заключалась в том, что вся сетка находилась в нерешённом состоянии, после чего я итеративно располагал случайный тайл в подходящем для него месте, или, если подходящих мест не было, присваивал небольшой области рядом с нерешённым тайлом состояние «нерешённая» и продолжал жадное расположение тайлов. «Жадное расположение» - это стратегия расположения тайла до тех пор, пока его грани соответствуют уже размещённым тайлам вне зависимости от того, будет ли такое расположение создавать частичный тайлинг, который невозможно завершить без замены уже поставленных тайлов. Когда возникает такая ситуация и мы больше не можем расположить тайлы, то мы должны удалить некоторые из ранее расположенных тайлов. Но мы не знаем, какие из них лучше всего убирать, потому что если бы мы могли решить задачу, то, вероятно, могли бы также в первую очередь решить задачу правильного расположения тайлов. Чтобы предоставить алгоритму ещё один шанс на нахождение подходящего тайла для заданной области, мы присваиваем всем тайлам вокруг нерешённой точки состояние «нерешённый» и продолжаем выполнять стратегию жадного расположения. Мы надеемся, что рано или поздно правильный тайлинг будет найден, но гарантий этого нет. Алгоритм будет выполняться, пока не найдётся правильный тайлинг, что может занять бесконечное время. Он не обладает способностью определять, что тайлсет нерешаем.

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

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

    Нам нужен алгоритм, способный заглядывать вперёд и создавать расположения тайлов, каким-то образом осознавая варианты при которых эти расположения открыты для будущих размещений тайлов. Это позволит нам эффективно решать сложные тайлсеты.

    С точки зрения удовлетворения ограничениям

    Этот алгоритм аналогичен поиску с обратным переходом. На каждом шаге мы пытаемся присвоить одну переменную. Если мы не можем, то отменяем присвоение переменной и всех переменных, которые присоединены к ней ограничениями. Это называется «обратным переходом» (backjumping) и отличается от возврата назад (backtracking), при котором мы отменяем присвоение одной переменной за раз, пока не сможем продолжать выполнять правильные присвоения. При возвратах назад мы в общем случае отменяем присвоение переменных в обратном порядке относительно их присвоения, однако при обратном переходе мы отменяем присвоение переменных согласно структуре рассматриваемой задачи. Логично, что если мы не можем поместить любой тайл в конкретную точку, то мы должны изменить расположение соседних тайлов, поскольку их расположение создало нерешаемую ситуацию. Возврат назад вместо этого может привести нас к отмене присвоения переменных, которые пространственно находятся далеко друг от друга, но были присвоены недавно.

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

    Способ 2: расположение с наибольшими ограничениями и распространением локальной информации

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

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

    Интуитивно понятно, алгоритм для этой задачи должен иметь возможность «заглядывать вперёд» и рассматривать хотя бы несколько переходов, которые могут стать последствиями выбранного расположения. Чтобы реализовать это, можно рассматривать тайловую карту как распределение вероятностей для тайлов в каждой точке. Когда алгоритм располагает тайл, он обновляет распределения вероятностей вокруг этого тайла в ответ на его расположение таким образом, что повышаются вероятности соседних тайлов, которые вероятно могут быть совместимы с текущим расположением.

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


    На каждом шаге алгоритм исследует все нерешённые места расположения тайлов, каждое из которых имеет распределение вероятностей по тайлам, и выбирает одно место для «схождения» к тайлу. Он выбирает распределение из карты с минимальной энтропией. У мультиномиальных распределений с низкой энтропией вероятности обычно сконцентрированы в нескольких модах, поэтому схождение этих первых приводит к эффекту расположения тайлов, для которых уже есть некоторые из ограничений. Именно поэтому алгоритм располагает тайлы рядом с тайлами, которые мы решили сначала.

    Это самый эффективный алгоритм, который мне удалось реализовать для этой задачи. Он имеет ещё одно преимущество: создаёт при выполнении красивые визуализации. Возможно, существует способ улучшить этот алгоритм, введя некую форму возврата назад. Если в получившемся готовом тайлинге существует неправильная точка, то отмена расположений соседних тайлов и ресэмплинг из получившихся распределений в их точках может позволить найти исправление этого готового тайлинга. Разумеется, если вы хотите продолжать поиск вплоть до нахождения верного тайлинга, то превысите пределы указанного гарантированного времени выполнения.

    Оптимизации

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

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


    Чтобы реализовать это, я представлю, что при расположении тайла на пустой карте он вносит значительные изменения в распределения соседних точек карты. Я назову эти обновления сферой тайла, то есть сферой влияния тайла, проецируемой вокруг него при помещении его на пустую карту. Когда два тайла располагаются друг рядом с другом, то их сферы взаимодействуют и создают конечные распределения, на которые влияют оба расположения. Учитывая то, что многие тайлы могут быть расположены рядом с заданным нерешённым местом, будет существовать большое количество взаимодействующих ограничений, делающее решение на основе подсчёта для определения вероятности появления разных тайлов в этой точке очень медленным процессом. Что, если вместо этого мы рассмотрим только простую модель взаимодействия между предварительно вычисленными сферами тайлов, которые уже были расположены на карте?
    При расположении тайла я обновляю карту вероятностей для каждого элемента, умножая распределение сфера этого тайла на каждую точку карты на распределение, уже хранящееся в этой точке карты. Возможно, будет полезно рассмотреть пример того, что это может сделать с картой распределений. Допустим, у заданной точки карты в текущий момент есть распределение, которое с равной вероятностью может выбрать траву или воду, и мы помещаем рядом с этой точкой тайл воды. Сфера тайла воды будет иметь высокую вероятность воды рядом с тайлом воды и низкую вероятность тайла травы. Когда мы поэлементно перемножаем эти распределения, то вероятность воды в результате будет высокой, потому что это произведение двух больших вероятностей, но вероятность травы станет низкой, потому что это произведение высокой вероятности, хранящейся на карте, с низкой вероятностью, хранящейся в сфере.
    Эта стратегия позволяет нам эффективно аппроксимировать влияние, которое должно иметь каждое расположение тайла на карту вероятностей.

    С точки зрения удовлетворения ограничений

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

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

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

    Манипуляции тайлингами изменением вероятностей выбора тайлов

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

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

    Создаём собственные тайлсеты

    Вкратце:
    • клонируйте мой репозиторий на github
    • установите Processing
    • в папке data/ репозитория измените tiles.png
    • используйте processing для открытия wangTiles.pde и нажмите на кнопку play
    С помощью выложенного на github кода (можете изменять его, но делайте это на собственный страх и риск - бОльшую его часть я написал в старшей школе) вы можете создавать собственные тайлсеты, пользуясь редактором изображений, и наблюдать, как решатель тайлинга создаёт с их помощью миры. Просто клонируйте репозиторий и отредактируйте изображение dungeon.png, а затем воспользуйтесь Processing для запуска wangTiles.pde и просмотра анимации генерируемой карты. Ниже я опишу «язык», который требуется этому решателю тайлинга.

    Спецификация тайлсетов


    Тайлы расставлены сеткой из ячеек 4x4. Каждая ячейка в верхней левой области 3x3 содержит цветной тайл, а в оставшихся 7 пикселях содержатся метаданные тайла. Пиксель под центром тайла можно окрасить красным цветом, чтобы закомментировать тайл и исключить его из тайлсета. Решатели никогда не будут помещать его на карту. Верхний пиксель в правой части тайла можно закрасить чёрным, чтобы добавить в тайлсет все четыре поворота тайла. Это удобная функция, если вы хотите добавить что-то вроде угла, который существует в четырёх ориентациях. Наконец, самой важной частью разметки является пиксель в нижней левой части тайла. Он управляет базовой вероятностью появления тайла на карте. Чем темнее пиксель, тем больше вероятность появления тайла.

    Близкие по теме работы

    Многие люди исследовали Плитки Вана , которые являются тайлсетами с цветными гранями и должны соответствовать гранями с тайлами, рядом с которыми их помещают, точно так же, как и рассматриваемые нами тайлы.

    Решатель «Most Constrained Placement with Fuzzy Arc Consistency» похож на проект Wave Function Collapse пользователя Twitter

    (tile - плитка) - это по сути дела метод создания больших картин (изображений) с помощью более мелких элементов.

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

    // Плохая идея, миксовать тут тематики блога...


    Какая крыша? Тайлд. Замечательное свойство , можно превращать существительное в прилагательное, но потом такое нельзя нормально перевести, однако понять легко.
    Вернёмся к нашей теме, тайловая графика в играх, что это такое?

    Это графика, которая состоит из тайлов (Кэп) . Пример:


    Иногда очень красиво получается


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

    Так-же тайловая графика чем-то напоминает пиксель арт по принципу, тоже как мозаика, рисуется картинка по пикселям. Элемент за элементом.


    Больше пиксель арта тут


    Но вот на этой картинке (это вообще не игра ), графика не тайловая. Тут всё прорисовано индивидуально, . Вот именно тут мы и подходим к первой проблеме тайловой графики - иногда она выглядит слишком монотонно .


    Стандартный пример тайлов в game maker studio


    Вспомните даже редактор карт StarCraft:


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

    Берём этот-же скрин и увеличиваем.


    Плитка не повторяется


    И когда был этот мой любимый первый старкрафт?

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

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

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

    Не только всё это рисовать, но и потом расставлять, а потом еще придумывать какой-то способ рандомизации элементов, если он будет делаться.

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

    Тайлы обычно делают как-бы пачкой (set), называется - матрица тайлов. Это большой фрагмент, который делят на более мелкие. Примерно вот такая штука:



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

    Тут всё довольно просто и в то-же время красиво. Снежная тема. Имея готовый сет тайлов у вас уже сделано пол дела, вам нужно их только красиво расставить.

    Тайлы легче чем объекты, они лучше для скорости игры, с ними будет выше FPS и быстродействие, если использовать их, а не объекты. Объекты со спрайтами вам нужны только если у вас есть взаимодействие с ними. Допустим, если огнемёт будет сжигать лес, тогда да, скорее всего придётся делать деревья через объект.

    Но почти все декоративные (чисто визуальные) элементы, можно сделать через тайлы.

    • Читайте так-же:
    Допустим, вот эти-же столбы. Ну зачем вам нужны какие-то взаимодействия героя со столбами? Вам нужна только коллизия (когда нельзя через него пройти) .

    А для коллизии вы можете просто сделать твёрдую невидимую область объектом, и уже её повесть под столб. Тогда через столб (спрайт - тайл) нельзя будет пройти, движение будет блокировать "твёрдая" область.

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

    Довольно удобно, особенно для мелких тайлов.


    Для новичка, который хочет создавать игры в гейм мейкер, в принципе не обязательно использовать тайлы, часто новички просто делает всё это (поле, деревья и прочее) обычными объектами со спрайтами и для простых игр это может быть ок.

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

    А если вас интересуют конкретные примеры и как всё это работает в GameMaker Studio 2, вы можете посмотреть моё видео (9 минут) . Подписывайтесь на мой канал Econ Dude - про создание игр с нуля , если еще не подписались.

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

    Немного о том, что будет описано в статье

    • Что такое tile-based игра?
    • Создание или поиск собственных «плиток»
    • Написание кода для отображения уровня
    • Редактирование уровней

    Что такое tile-based игра?

    Конечно, Вы можете найти определение тайловой графики в википедии, но, чтобы понять, что это, достаточно усвоить несколько вещей
    • Tile — плитка — маленькой изображение, обычно прямоугольной формы, которое выступает в роли кусочка пазла при построении больших изображений
    • Карта — группа плиток, объединенных вместе
    • Tile-based отсылает нас к методу созданий уровней в играх. Код размещает плитки в заранее определенных местах
    В нашей статье плитки будут иметь прямоугольную форму. Существует несколько крутых фишек, которые Вы получаете, используя тайловую графику. Самое крутое — нет необходимости рисовать огромные изображения для каждого уровня. Пятьдесят изображений с разрешением 1280x768px для 50-ти уровневой игры против одного изображения с сотней плиток имеют огромные различия. Другое преимущество состоит в том, что размещать предметы, используя тайловую графику, становится заметно проще.

    Создание или поиск плиток

    Первое, что нужно для построения движка — набор плиток. У Вас есть два варианта: использовать уже готовые tiles или сделать свои собственные. Если Вы решите использовать готовы изображение, то их можно легко найти по всему интернетом. Минусом является то, что эта графика не была сделана специально для вашей игры. С другой стороны, если Вы просто экспериментируете, то данный вариант вполне подойдет.

    Думаю, что с поиском плиток возникнуть проблем не должно, поэтому я не буду подробно на этом останавливаться.

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

    Это — самые популярные инструменты для создания пиксель-арта. Если Вам нужно что-то более мощное, то GIMP идеально подойдет.

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

    Написание кода

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

    Как только все Ваши плитки находятся в папке с ресурсами проекта, Вы можете написать простой Tile класс. Вот пример на Haxe
    import flash.display.Sprite; import flash.display.Bitmap; import openfl.Assets; class Tile extends Sprite { private var image:Bitmap; public function new() { super(); image = new Bitmap(Assets.getBitmapData("assets/grassLeftBlock.png")); addChild(image); } }
    Сейчас, все, что мы делаем — помещаем плитку на экран. Единственная вещь, которую делает класс — импортирование изображение из папки с ресурсами и добавление в качестве дочернего объекта. То, как будет выглядеть данный класс очень сильно зависит от языка программирования, который Вы используете.

    Теперь, когда у нас есть класс Tile , нам нужно создать экземпляр Tile и добавить его в нашем главном классе.
    import flash.display.Sprite; import flash.events.Event; import flash.Lib; class Main extends Sprite { public function new() { super(); var tile = new Tile(); addChild(tile); } public static function main() { Lib.current.addChild(new Main()); } }
    Класс Main создает новый объект Tile при вызове конструктора и добавляет его в список отображения.

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

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

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

    X и Y мы используем, как координаты на экране. Может быть нам удастся использоваться эти X и Y для отображения наших плиток? Итак, давайте взглянем на матрицу
    private var exampleArr = [ , , , ,
    Заметим, что нулевой элемент в данном массиве — массив из пяти чисел. Это значит, что сначала вы получаете элемент y, а затем x. Если Вы попытаетесь взять элемент , то Вы получите доступ к шестой плитке.

    Если Вы не понимаете, как работают матрицы, не беспокойтесь. В данной статье я буду использовать обычный массив, чтобы упростить нашу задачу.
    private var exampleArr = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ];
    Пример Выше показывает, что использовать обычный массив — гораздо проще. Мы можем отобразить конкретный элемент, вычислив координаты, используя простую формулу.

    Теперь, давайте напишем код, создающий наш массив. Заполним его 1. Цифра один будет означать ID нашей первой плитки.

    Для хранения массива нам нужно создать переменную внутри класса Main
    private var map:Array;
    Может выглядеть немного странно, поэтому я поясню.
    Имя переменной — map, а тип — Array (массив). означает, что массив содержит числа.
    Теперь, добавим немного кода в наш конструктор нашего класса, чтобы инициализировать нашу карту
    map = new Array();
    Этот фрагмент создает пустой массив, который скоро мы заполним. Но сначала, давайте объявим несколько переменных, которые помогут нам с математикой
    public static var TILE_WIDTH = 60; public static var TILE_HEIGHT = 60; public static var SCREEN_WIDTH = 600; public static var SCREEN_HEIGHT = 360;
    Эти переменные являются public static для того, чтобы дать нам доступ к ним из любого места нашей программы. Вы могли заметить, что исходя из данных чисел, мы будем решать, сколько ячеек хранить в массиве.
    var w = Std.int(SCREEN_WIDTH / TILE_WIDTH); var h = Std.int(SCREEN_HEIGHT / TILE_HEIGHT); for (i in 0...w * h) { map[i] = 1 }
    Здесь мы задаем значение 10 для переменной w и 6 для h, как я уже сказал. Дальше, мы должны пройтись циклом по массиву, чтобы уместить 10 * 6 чисел.

    Теперь у нас есть простенькая карта, но нам же нужно расположить плитки правильно, так? Для этого вернемся обратно в класс Tile и создадим функцию, позволяющую нам это сделать
    public function setLoc(x:Int, y:Int) { image.x = x * Main.TILE_WIDTH; image.y = y * Main.TILE_HEIGHT; }
    Когда мы вызываем функцию setLoc() , мы передаем координаты x и y. Функция берет эти значения и переводит их в координаты в пикселях, умножая на TILE_WIDTH и TILE_HEIGHT

    Единственная вещь, которую осталось сделать — описать в классе Main процесс создания и позиционирования плиток на карте
    for (i in 0...map.length) { var tile = new Tile(); var x = i % w; var y = Math.floor(i / w); tile.setLoc(x, y); addChild(tile); }
    Да! Все правильно. Экран заполнен плитками. Давайте разберемся, что происходит выше

    Формула
    Давайте обсудим формулу, о которой я упоминал выше.

    Мы вычисляем x , присваивая ему остаток от деления i на w . Это делается для того, чтобы вернуть x значение 0 в начале каждой линии.

    Для y мы берем floor() от i / w.

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

    Например: Экран размером 500х500, а плитки — 100х100, а Ваш мир — 1000х1000. Вам нужно делать простую проверку перед отрисовкой плитки.

    Все, что нам осталось — создать разные типы плиток и отобразить что-нибудь красивое.

    Разные типы плиток
    Окей, теперь у нас есть карта, полная одинаковых элементы. Не плохо было бы иметь более одного типа элементов, а это значит, что нам придется изменить наш конструктор в классе Tile
    public function new(id:Int) { super(); switch(id) { case 1: image = new Bitmap(Assets.getBitmapData("assets/grassLeftBlock.png")); case 2: image = new Bitmap(Assets.getBitmapData("assets/grassCenterBlock.png")); case 3: image = new Bitmap(Assets.getBitmapData("assets/grassRightBlock.png")); case 4: image = new Bitmap(Assets.getBitmapData("assets/goldBlock.png")); case 5: image = new Bitmap(Assets.getBitmapData("assets/globe.png")); case 6: image = new Bitmap(Assets.getBitmapData("assets/mushroom.png")); } addChild(image); }
    Теперь у нас есть шесть разных типов плиток. Мне нужна конструкция switch , чтобы выбирать, какое изображение нужно отобразить. Вы могли заметить, что теперь конструктор принимает в качестве параметра число, означающее тип плитки.

    Вернемся в конструктор класса Main и отредактируем наш цикл
    for (i in 0...map.length) { var tile = new Tile(map[i]); var x = i % w; var y = Math.floor(i / w); tile.setLoc(x, y); addChild(tile); }
    Единственное изменение состоит в том, что теперь мы передаем конструктору тип блока. Если бы Вы попробовали запустить программу без данной правки, до выполнение окончилось бы ошибкой.

    Сейчас все стало на свои места, но нужно придумать дизайн карты вместо того, чтобы заполнять ее случайными блоками. Первое, удалите цикл в конструкторе Main , заполняющий массив единицами. А затем создайте Вашу карту вручную
    map = [ 0, 4, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 3, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 1, 2, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 3 ];
    Если отформатировать массив, как сделано у меня, то можно легко увидеть примерный облик уровня. Но никто не запрещает Вам забить массив просто в одну строку.

    При запуске программы Вы увидите несколько ошибок. Проблема заключается в том, что наша карта содержит нулевой тип блока, который не имеет изображения и наш класс просто не знает, что с этим делать. Исправим это недоразумение
    if (image != null) addChild(image);
    Эта короткая проверка избавит нас от надоедливой ошибки с нулевым указателем. Последнее изменение затронет функцию setLoc() . Мы пытаемся использовать переменные x и y , которые не были инициализированы
    public function setLoc(x:Int, y:Int) { if (image != null) { image.x = x * Main.TILE_WIDTH; image.y = y * Main.TILE_HEIGHT; } }
    Благодаря этим двум простым условиям Вы теперь можете запустить игру и увидеть простой уровень. Тайлы, которые умеют id 0 мы воспринимаем, как пустое место. Добавьте какой-нибудь фон. чтобы это выглядело более привлекательно.

    Заключение

    Вы только что создали tale-based движок. Теперь Вы знаете, что такое тайловая графика и как ей пользоваться. Вы научились создавать и редактировать уровни. Но не останавливайтесь на этом, ведь столько всего еще можно улучшить в нашем движке.