Тайловая игра на BlitzMax
Введение
Привет! Этот тутор поможет вам понять, как строятся игры на основе плиток (далее - тайлов). Надеюсь, что приведенная здесь информация будет полезной для начинающих гейм-девелоперов.
Я не претендую на абсолютное знание предмета и всех его тонкостей, это, так сказать, мой взгляд на данную тему. Многое, что здесь написано, взято не с потолка, а уже много лет используется другими людьми, я лишь адаптировал код под BlitzMax, конечно, добавив некоторые моменты и от себя. Вот главные ресурсы, с которых была взята информация по данной теме:
Это далеко не всё, есть и другие источники, можете накопать еще что-нибудь в Интернете.
Сразу хочу заметить несколько моментов:
- Я пишу программы с директивой SuperStrict. Где-то читал, что эта директива заставляет программу работать быстрее, каких-то документальных подтверждений этой теории я не нашел, но если это правда, то почему бы ей и не пользоваться. А использую я ее лишь для того, чтобы избежать ошибок в правильном написании переменных и чтобы видеть к каким типам они принадлежат.
- Я не использую всякие значки типа $, % и т.д. в объявлении переменных. И все параметры функций заключены в скобки ( ). Так уж у меня повелось, и это единственное обоснование.
- В названии типа я всегда ставлю вначале букву T , так если хочу создать тип Level, то я называю его TLevel,(буква T от слова Type).
- Функцию создания любого объекта, я называю Create() и она всегда находится внутри типа, для которого используется. Доступ к ней производится через точку. Например: TLevel.Create() .
- Функция, в которой происходит рисование объекта на экране, всегда называется Draw().
- Функция, в которой происходит изменение состояния объекта, изменение размеров, перемещение и другие действия, всегда называется Update().
- Я не утверждаю, что надо делать так и только так. Вовсе нет, каждый сам хозяин своего кода. Но на протяжении всего тутора, я буду придерживаться своих правил, поэтому и считаю правильным заранее вас об этом предупредить.
Тайловые карты
Что же такое тайл? Тайл в переводе с англицкого - это плитка. В нашем случае это прямоугольник (чаще - квадрат, реже - шестиугольник) с картинкой. Вы когда-нибудь собирали пазл? Одну большую картинку из множества кусочков. Каждый из кусочков не имел никакой ценности. Помните, что нельзя было понять, что изображено на каждом кусочке, и только соединив несколько соседних можно было увидеть часть рисунка или картины. Так же и в играх, разработчики дробят весь уровень игры (или карту) на маленькие равные кусочки. Вы ,конечно, спросите, а зачем они это делают? Попытаюсь объяснить подоходчивей. Даже имея 5 разных тайлов, можно нарисовать приличную карту для игры. Просто размещая эти тайлы в нужном вам порядке. Гляньте:
Мои художественные способности оставляют желать лучшего, так что эта карта - ради объяснения принципа, а не ради красоты. Видите, слева я нарисовал тайлы, которые использую в своей карте, а справа карта, собранная из этих тайлов. Думаю, всё наглядно и понятно. Зачем рисовать новую большую карту, затратив при этом много усилий, когда можно собрать ее по кусочкам, за несколько минут. Разработчики просто рисуют набор тайлов для карты и нумеруют их. Например так: трава - 0, вода - 1, деревцо - 2, дом - 3, куст - 4. И тогда в памяти компьютера карту можно представить так :
Думаю всем очевидны преимущества данного метода: можно собрать множество разных карт с помощью ограниченного набора тайлов. Тайлы обычно бывают квадратными с размерами (ширина х высота) : 16х16, 24х24, 32х32, 64х64, т.е. ширина = высоте. Но вас никто не ограничивает и вы можете делать тайлы, например, 16х8 или другого размера, всё зависит лишь от вашей фантазии и движка вашей игры. Я использую такие тайлы лишь потому, что наборы тайлов таких размеров легко можно найти в интернете, чтобы не рисовать самому(мои худ. способности вы видели выше). Хорошо, с этим мы разобрались. Но есть и еще один параметр тайла. Этот параметр - проходимость. То есть, сможет ли игрок пройти по этому тайлу. Допустим, в вашей игре можно идти по тайлам, обозначающим траву, дорогу или песок, а по другим тайлам идти нельзя, таким как дерево или куст, стена или еще что-то. Так что, договоримся , что для каждого тайла установим параметр <проходимость>! Который будет принимать значения <Да> и <Нет>, разные для каждого тайла.(?) Посмотрите на нашу карту:
Она имеет размеры: 6 тайлов в ширину (от 0 до 5) и 7 тайлов в высоту (от 0 до 6). Или по-другому - 6х7 тайлов. Теперь мы можем видеть, что тайлы по координатам (1,3) ; (3,3) и (4,6) содержат цифру 3, то есть тайл - дом, ну и так далее. Видите: все очень просто. Итак, перечислим все параметры для нашего тайла. Это: ширина и высота тайла, его координаты на тайловой карте, координаты на экране, проходимость и номер картинки в нашем наборе тайлов.
Так будет выглядеть тип тайла в Блитце:
Type TTile
Field width ' ширина тайла Field height ' высота тайла Field xTile,yTile ' координаты на карте(тайловые координаты) Field x,y ' координаты на экране Field walkable ' проходимость Field image ' картинка End type
Постойте, я ничего не рассказывал про экранные координаты. Ладно, тут дело состоит в том, что у нас пока имеются только координаты тайла в карте, но не на экране. Как их перевести в экранные? Давайте подумаем. Допустим размеры тайла 32х32.
Тайл по координатам (0,0) - это трава, следующий за ним тайл (1,0) - это куст, дальше (2,0) - опять трава и т. д. Допустим, первый тайл рисуется прямо с левого верхнего угла экрана, т.е. его х и у равны 0, второй тайл будет рисоваться сразу за первым, правильно. Третий будет рисоваться начиная с окончания второго. Ладно смотрите:
Я изобразил три тайла, идущих друг за другом, разного цвета, для наглядности примера.
Ок. Перевод тайловых координат в экранные очень прост. Давайте домножать тайловые координаты на ширину тайла. Смотрите сами первый тайл (0,0) домножим на 32 - экранные координаты стали (0,0) потому что умножение на 0 дает 0, дальше интереснее - второй тайл (1,0) домножим на 32 (1*32, 0*32) , экранные координаты - (32, 0), третий тайл -(2*32,0*32) - экранные координаты (64, 0). А теперь посмотрите на картинку выше. Ого! Так оно и есть - первый тайл (синенький) начинается с (0,0) ; второй(зелененький) - начинается с (32,0), третий(малиновый) - (64,0). Итак повторю, чтобы перейти от тайловых координат к экранным надо домножать на РАЗМЕР ТАЙЛА. Вот так:
x = xTile * Tile_Width y = yTile * Tile_Height
ну ,соответственно, если ширина и высота тайла равны, то проще:
x = xTile * TileSize y = yTile * TileSize
Ну теперь нам ничего не стоит сделать функцию создания тайла, но для начала создадим список(или лист) в котором будут содержаться(храниться) все созданные нами тайлы, не буду ничего придумывать и назову лист так: TileList.
Global TileList:TList = CreateList()
А теперь функция создания тайла:
Function Create (sx: Int, sy: Int, WB: Byte)
Local TT:TTile = New TTile TT.width = TiLESIZE TT.height = TiLESIZE TT.xTile = sx TT.yTile = sy TT.x = TT.xTile * TT.width TT.y = TT.yTile * TT.height TT.Walkable = WB
ListAddLast(ALLGameObjList , TT)
End Function
Инициализацию картинки сделаете сами, ведь это не сложно. Так же сами напишите метод рисования тайла на экране ( method Draw() ), никаких сложностей возникнуть не должно. До этого я писал, про тайловые карты с видом сверху, но хочу заметить, что также все это относится и к аркадам, у которых вид сбоку. Таким как Марио, Соник и им подобные. Так вот посмотрите еще раз на рисунок карты: как проще ее хранить? Ну конечно же в массиве. Если перевести нашу карту в массив языка Блиц, то получим что-то наподобие этого:
Global TileMap:Int[] =
[0, 4, 0, 1, 2, 0, .. 1, 1, 1, 1, 1, 1, .. 0, 0, 2, 0, 0, 0, .. 0, 3, 0, 3, 0, 0, .. 4, 0, 0, 0, 4, 0, .. 0, 2, 0, 0, 0, 4, ..
0, 0, 0, 4, 3, 0]
Ну, теперь создадим наш главный тип - УРОВЕНЬ! Он будет читать карту из массива, создаст на основе этой карты тайлы и затем уже нарисует нашу тайловую карту!
Type TLevel
Field Width : Byte Field Height : Byte
Field Map:Int[,]
Function Create:TLevel(MyMap:Int[],map_width:int,map_height:int) Local TM : TLevel = New TLevel TM.Width = map_width ' ширина карты в тайлах TM.Height = map_height ' высота карты в тайлах
TM.map = New Int [TM.Width, TM.Height] TM.load(myMap)
Return TM End Function End type
Для карты я специально использовал одномерный массив (TileMap:Int[]), но в типе TLevel этот массив будет переведен в двумерный, я сделал так, а вы делайте, как пожелаете, хоть грузите из файла на диске или прочитайте из DefData, это, опять же, зависит от вашей фантазии. Функция принимает в параметрах одномерный массив карты и ее размеры - ширину и высоту. Для нашего случая это будет так:
MyLevel:TLevel = TLevel.Create(TileMap, 6, 7)
Теперь напишем метод load, который грузит нашу карту в двумерный массив Мар , создает нужные тайлы и заносит их в список объектов.
Method Load(arrMap:Int[])
For Local i:Int = 0 Until Height For Local j:Int = 0 Until Width Map[j , i] = arrMap[j + (i * Width)] If Map[j , i] < 15 TTile.Create(j , i , true) Else TTile.Create(j , i , false) End if Next Next End Method
Что же, ничего хитрого в этой функции тоже нет, мы проходим по всему одномерному массиву и заполняем значениями двумерный массив, а заодно и создаем на этих местах тайлы уже известной нам функцией Create, которую мы писали ранее в типе TTile. Здесь стоит заметить, что я разделил тайлы на две группы: одни проходимы (true), а другие нет (false). Разделение произошло в условном операторе (if): первые тайлы, до 15, все проходимы, а остальные - не проходимы. Думаю, это понятно. Кстати, если вы переписали функцию Create в TTile, чтобы еще и загружалась картинка, то надо переделать и эту функцию, чтобы кол-во параметров совпадало. Например так:
TTile.Create(j , i , true, tileSet ])
Но это, опять же, зависит от вашего кода и полностью на ваше усмотрение. Ок. Теперь давайте перейдем непосредственно к созданию нашего тайлового уровня. До этого момента мы только создали (инициализировали) наши тайлы и поместили их в отдельный список (лист), теперь их надо нарисовать. Я не буду использовать никакие картинки, я буду использовать простые квадраты рисуемые БлитцМаксом функцией DrawRect (x,y,width,height). А чтобы отличать проходимые тайлы от непроходимых, я сделаю их разноцветными. Вот так: зеленый - проходимые (true), а непроходимые (false) - красный.
$IMAGE6$
Тип TTile остается прежним, просто добавим 2 метода Update и Draw:
Method Update() ' здесь установим цвет тайла
If Walkable = false SetColor(255 , 0 , 0) 'red Else SetColor(0 , 255 , 0) 'green EndIf End Method
Method Draw() ' здесь нарисуем DrawRect(x , y , Width , Height) End Method
Теперь приступим к самому волнительному моменту: нарисуем уровень, то есть все созданные тайлы, на экране. Для этого в типе TLevel сделаем функцию Render(), которая и нарисует все тайлы.
Method Render()
For Local CurTile:TTile = EachIn TileList ' пройти по всему листу тайлов CurTile.Update() ' установить цвет текущего тайла CurTile.Draw() ' нарисовать текущий тайл Next End Method
Уровень я рисовал прямо в массиве. Вот он:
Global LevMap:Int[]=[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,.. 1,0,0,0,1,0,0,0,1,1,0,0,0,1,0,0,0,0,0,1,.. 1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,.. 1,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,1,.. 1,0,1,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,.. 1,0,0,0,0,0,1,1,0,0,1,1,1,1,1,1,1,1,1,1,.. 1,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,.. 1,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,1,1,1,1,.. 1,0,0,0,0,0,0,0,0,1,1,0,0,0,1,0,0,0,0,1,.. 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
Global Level : TLevel = New TLevel.Create(LevMap , 20 , 10)
А вот главный цикл всей программы:
Graphics(640, 480) ' установить видеорежим 640х480
Repeat ' повторять .. Cls ' очистить экран
Level.Render() ' рисовать уровень в буфер
Flip ' поместить буфер на экран Until KeyDown(KEY_ESCAPE) Or AppTerminate() ' .. пока не нажат ESC или крестик окна
End
Если у вас не получилось, то вот исходник:
SuperStrict
Global ScrWidth : Int = 640 Global ScrHeight : Int = 480
Global TileList:TList = CreateList()
Const TILESIZE:Int = 32
Type TTile Field x:Int Field y:Int
Field xTile:Int Field yTile:Int Field Width:Byte Field Height:Byte
Field Walkable:Byte Function Create(sx:Int, sy:Int, WB:Byte) Local TT:TTile = New TTile TT.width = TILESIZE TT.height = TILESIZE TT.xTile = sx TT.yTile = sy TT.x = TT.xTile * TT.width TT.y = TT.yTile * TT.height TT.Walkable = WB
ListAddLast(TileList , TT) End Function
Method update() If walkable 'walkable SetColor(255 , 0 , 0) 'red Else 'NOT walkable SetColor(0 , 255 , 0) 'green End If End Method
Method Draw() DrawRect(x,y,Width,Height) End Method
End Type
Type TLevel Field Width : Byte Field Height : Byte
Field Map:Int[,] Function Create:TLevel(MyMap:Int[],map_width:Int,map_height:Int) Local TM : TLevel = New TLevel TM.Width = map_width TM.Height = map_height
TM.map = New Int [TM.Width, TM.Height] TM.load(myMap) Return TM End Function Method Load(arrMap:Int[]) For Local i:Int = 0 Until Height For Local j:Int = 0 Until Width Map[j , i] = arrMap[j + (i * Width)] If Map[j , i] TTile.Create(j , i , True) Else TTile.Create(j , i , False) End If Next Next End Method
Method Render() For Local CurTile:TTile = EachIn TileList CurTile.Update() CurTile.Draw() Next End Method End Type
Global LevMap:Int[]=[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,.. 1,0,0,0,1,0,0,0,1,1,0,0,0,1,0,0,0,0,0,1,.. 1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,.. 1,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,1,.. 1,0,1,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,.. 1,0,0,0,0,0,1,1,0,0,1,1,1,1,1,1,1,1,1,1,.. 1,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,.. 1,0,1,1,1,1,1,0,0,0,0,0,0,0,0,0,1,1,1,1,.. 1,0,0,0,0,0,0,0,0,1,1,0,0,0,1,0,0,0,0,1,.. 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
Global MyLevel : TLevel = New TLevel.Create(LevMap , 20 , 10)
Graphics(ScrWidth, ScrHeight)
Repeat Cls
MyLevel.Render() Flip Until KeyDown(KEY_ESCAPE) Or AppTerminate()
End
|