Журнал о программированнии на языках Blitz3D, BlitzPlus, BlitzMax

Тайлы и изометрия

Материал из Blitz Et Cetera

Перейти к: навигация, поиск

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


Если не хочется много читать, то вкратце так:

Вывод массива на экран в изометрии:

  • x = sx + ( i - j )*32
  • y = sy + ( i + j )*16

где:

  • x, y – собственно искомые координаты тайла на экране
  • sx, sy – смещение на экране, чтоб можно было начать рисовать с любой точки экрана, а не с ноля.
  • i, j – позиция ячейки в массиве (строки и столбцы соответственно)
  • W, H – половинные размеры тайла на плоскости.


Поиск элемента массива, по указателю мыши:

  • i = ( x / a + y / b ) / c
  • j = -( x / a - y / b ) / c

где:

  • i, j – искомые координаты в массиве
  • x, y – координаты точки на экране (позиция курсора мыши)
  • a, b, c – делители.

Изометрия – это вид объемного объекта под углом. Обычно 45 градусов по оси Y и примерно в 30 градусов по X относительно камеры (взгляда или горизонта). К примеру кубик на чертеже выглядит квадратиком, т.к. видим только одну сторону. В изометрии мы видим три стороны и понимаем, что объект объемный. Вот и с двухмерной графикой на плоскости, если отобразить тайлы изометрически, то будет выглядеть вполне объемисто. Самым известным примером приведу игру Дьябло.

Из выше сказанного делаем вывод, что если наше плоское поле повернуть на 45 градусов и немного наклонить, то получится вполне похожая на 3Д картинка. Так как мы говорим о плоскости, то все объемы сугубо теоретические и эффект сильно зависит от медиа контента, т.е. непосредственно от красоты тайлов.

Визуально это выглядит так:

Это скрин моего простенького тайлово-изометрического редактора.

Слишком большое вступление получилось, переходим к работе.

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

На данной картинке тайлы размером 64х64 + несколько больших объектов для красоты. Так как я говорю о изометрии, то отличия от обычных тайлов бросаются в глаза – кусочки все повернуты на 45 и 30 градусов по осям, отсюда у объектов появляется иллюзия объемности. Иногда я встречал примеры, когда объем пытаются придать и без изометрии, т.е. рисуют обычную квадратную матрицу, но с объектами выглядящими как эти. Получается не очень. Заметьте, что картинка изометрического тайла имеет форму ромба, обычные тайлы квадратные. Если мы сложим два камня квадратами, то получим тоже, что и на картинке – пустоты в нижней части. Их можно зарисовать травкой, но стены не получается. Можно и стену нарисовать… Так и делают. К примеру, «Герои Меча и Магии 3» мир нарисован обычной матрицей (квадратными тайлами), но смотрится вполне прилично. Там только земля тайловая, все остальными объекты нарисованы поверх. Вот только человечки там всегда на плоскости, нельзя подняться на горку или спуститься в ямку – всегда бродишь по идеально плоской равнине, на которой местами нарисованы горы. Вот вам горка тайлами в изометрии:

Я специально удалил тайлы вокруг для наглядности.

С тайлами разобрались. Теперь разберем как же с ними работать? А работают с изометрическими тайлами точно также как и с обычными, т.е. движок изометрических тайлов работает точно также как и с обычными. Тут вся прелесть: работаем мы с обычными матрицами (массивами) по обычным принципам и методам, вот только отображаем на экран не квадратиками по квадратам, а ромбиками по ромбам.

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

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

For i=0 To 15

        For j=0 To 15
                x=sx+(i-j)*32
                y=sy+(i+j)*16
                DrawImage img,x,y,mas(i,j)
        Next

Next

Видим, что отличий от обычного тайлового движка практически нет, тот же массив mas(i,j), те же два цикла перебирающие строки и столбцы. Вот только координаты рассчитываются иначе.

А рассчитываются они просто. Точнее не очень просто, есть формулы поворота точки вокруг осей, там поворотные матрицы и прочее, но все это фигня, т.к. градусы поворота всегда одинаковые, то все вычесления сокращаются до минимума и получаем:

  • x = sx + ( i - j ) * W
  • y = sy + ( i + j ) * H

где:

  • x, y – собственно искомые координаты тайла на экране
  • sx, sy – смещение на экране, чтоб можно было начать рисовать с любой точки экрана, а не с ноля.
  • i, j – позиция ячейки в массиве (строки и столбцы соответственно)
  • W, H – половинные размеры тайла на плоскости. Тут подробнее:

Изометрический тайл на плоскости имеет форму ромба, а не квадрата. Таким образом его высота в два раза меньше ширины. «В два раза» это в частном случае, зависит от угла поворота относительно горизонта. Проще: зависит от ваших тайликов. У меня картинка тайла 64 пикселя в ширину и 32 по высоте. Половинные соответственно 32 и 16. Попробуйте потом поиграть этими параметрами – увеличивая, появятся зазоры между тайлами, уменьшая – будут наложения.

Теперь в бой! Смело и решительно начинаем писать движок для изометрических тайлов. И все прекрасно: и алгоритм *А прекрасно работает, и персонажи красиво перемещаются, причем изменяя только строку координат персонажа в массиве, на экране он будет красиво перемещаться по диагонали.

И тут возникает трудность: как провести обратные расчеты? Т.е. узнать где рисовать тайл, зная его позицию в массиве легко, а как узнать какому элементу массива принадлежит точка экрана? Самый яркий пример – это мышка. Как узнать в какой тайлик ткнул мышкой пользователь? На моём примере (см. картинки выше) видно как за мышкой бегает ромбовидный курсор, который выделяет нужный тайл и показывает его координаты в массиве.

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

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

  • i = ( x / a + y / b ) / c
  • j = -( x / a - y / b ) / c

где:

  • i, j – искомые координаты в массиве
  • x, y – координаты точки на экране (позиция курсора мыши)
  • a, b, c – делители

Тут совсем интересно:

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

a и b – это соотношение размеров нашего плоского изображения тайла, т. е. соотношение высоты и ширины ромба. В моем случае это 2 к 1. Ширина 64, высота 32 = 64 к 32, сокращаем и получаем 2 к 1. отсюда a=2; b=1.

Тут интересный эффект. Если не сокращать, то увеличится масштаб проверяемой фигуры, если этот масштаб скомпенсировать «маcштабером» (переменной «с»), то грани проверяемого ромба будут как бы запикселенными:

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

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

Global GH=600

Global GW=800
Graphics GW,GH
While Not KeyDown(KEY_ESCAPE)
        Cls
        dr(2,1,32,100,200)
        dr(4,2,32,350,200)
        dr(8,4,8,600,200)
        Flip
Wend
Function DR(a,b,c,dx,dy)
        For x=-200 To 200
        For y=-200 To 200
                Cx = (X/a+Y/b)/c
                Cy = -(X/a-Y/b)/c
                If cx=0 And cy=0 Then DrawRect dx+x,dy+y,1,1
        Next
        Next
        DrawText "a="+a,dx,dy+100
        DrawText "b="+b,dx,dy+110
        DrawText "c="+c,dx,dy+120

End Function

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

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

Предупреждаю! код совершенно не оптимизирован. Местами даже слишком. Картинки для кода ниже.

Global GH=600

Global GW=800

Graphics GW,GH

Global MapW%=50
Global MapH%=50

SetClsColor(240,240,240)
 
SetImageFont(LoadImageFont("arial.ttf",10))

Global Img=LoadAnimImage("Iso-64x64-outside.png",64,64,0,150)
Global Cur=LoadImage("cur.png")
Global no=LoadImage("no.png")
MidHandleImage img
MidHandleImage cur
MidHandleImage no

SetBlend ALPHABLEND

Global Mas%[MapW,MapH,4]

Global WB=64  ' Ширина блока
Global HB=32 'Высота блока

Global StartX%=400
Global StartY%=30

Global VisX%=0
Global VisY%=0

Global CurX:Int
Global CurY:Int

Global MenuLine%=0
Global CurNum%=0
Global Lay%=0
Global Lay0=1
Global Lay1=1
Global Lay2=1
Global Lay3=1


Global x1=2,x2=1,x3=32

For i=0 To MapW-1
        For j=0 To MapH-1
                mas(i,j,0)=Rnd(2,5)
                mas(i,j,1)=-1
                mas(i,j,2)=-1
                mas(i,j,3)=1
        Next
Next


While Not KeyDown(KEY_ESCAPE)

        Cls
       
        DrawMap(StartX,StartY)
       
        If KeyHit(KEY_UP) Then VisX=VisX-1 ; VisY=VisY-1 ; CorrectVisXY()
        If KeyHit(KEY_DOWN) Then VisX=VisX+1 ; VisY=VisY+1; CorrectVisXY()
        If KeyHit(KEY_LEFT) Then VisX=VisX-1;VisY=VisY+1; CorrectVisXY()
        If KeyHit(KEY_RIGHT) Then VisX=VisX+1;VisY=VisY-1; CorrectVisXY()
       
        If KeyHit(KEY_TAB) Then lay=lay+1 ; If lay>3 Then lay=0
        If KeyHit(KEY_1) Then lay0 = Not lay0
        If KeyHit(KEY_2) Then lay1 = Not lay1
        If KeyHit(KEY_3) Then lay2 = Not lay2
        If KeyHit(KEY_4) Then lay3 = Not lay3
       
        Mx=MouseX()
        My=MouseY()
        tz=MouseZSpeed()
       
        If tz>0 Then MenuLine=Menuline-1 ; CorrectMenuLine()
        If tz<0 Then MenuLine=Menuline+1 ; CorrectMenuLine()
       
        If MouseDown(1) Then
                If (Mx<642)And(My<66) Then
                        CurNum=Mx/64+MenuLine*10
                Else
                        If lay=3 Then
                                mas(CurX,CurY,lay)=1
                        Else
                                mas(CurX,CurY,lay)=CurNum
                        EndIf
                EndIf
        End If
       
        If MouseDown(2) Then
                If lay=3 Then
                        mas(curx,cury,lay)=0
                Else
                        mas(curx,cury,lay)=-1
                EndIf
        End If
       
        GetCurPos(MX,MY)
       
        DrawMiniMap(690,10)
       
        DrawMenuLine()
       
        SetColor 0,0,0
        DrawText "Lay : " + Lay,0,66
        DrawText "Lay0 = "+lay0,0,76
        DrawText "Lay1 = "+lay1,0,86
        DrawText "Lay2 = "+lay2,0,96
        DrawText "Lay3 = "+lay3,0,106
       
        Flip

Wend

Function DrawMenuLine()
        SetColor 255,255,255
        DrawRect 0,0,642,66
        For i=0 To 9
                SetColor 255,255,255
                DrawImage img,32+i*64,32+0,i+MenuLine*10
                SetColor 0,0,0
                DrawLine (i+1)*64,0,(i+1)*64,64
        Next
        SetColor 200,200,0
        DrawRect 0,64,66,66
        DrawImage img,33,97,CurNum
End Function

Function CorrectVisXY()
        If VisX<0 Then VisX=0
        If VisY<0 Then VisY=0
        If (VisX+16)>MapW Then VisX=MapW-16
        If (VisY+16)>MapH Then VisY=MapH-16
End Function

Function CorrectMenuLine()
        If MenuLine>14 Then MenuLine=0
        If MenuLine<0 Then Menuline=14
End Function

Function GetCurPos(x,y)
       
        x=x-StartX
        y=y-StartY
       
        CurX = (X/x1+Y/x2)/x3 +VisX
        CurY = -(X/x1-Y/x2)/x3 +VisY

End Function

Function DrawMap(sx%,sy%)
        Local x%=0
        Local y%=0
        If lay0 Then
                For i=0 To 15
                        For j=0 To 15
                                If mas(i+VisX,j+VisY,0)>-1 Then
                                        x=sx+(i-j)*32
                                        y=sy+(i+j)*16
                                        SetColor(255,255,255)
                                        DrawImage img,x,y,mas(i+VisX,j+VisY,0)
                                EndIf   
                        Next
                Next   
        EndIf
        If lay1 Then
                For i=0 To 15
                        For j=0 To 15
                                If mas(i+VisX,j+VisY,1)>-1 Then
                                        x=sx+(i-j)*32
                                        y=sy+(i+j)*16
                                        SetColor(255,255,255)
                                        DrawImage img,x,y,mas(i+VisX,j+VisY,1)
                                EndIf
                        Next
                Next
        EndIf
        If lay2 Then
                For i=0 To 15
                        For j=0 To 15
                                If mas(i+VisX,j+VisY,2)>-1 Then
                                        x=sx+(i-j)*32
                                        y=sy+(i+j)*16
                                        SetColor(255,255,255)
                                        DrawImage img,x,y,mas(i+VisX,j+VisY,2)
                                EndIf
                        Next
                Next
        EndIf
        If lay3 Then
                For i=0 To 15
                        For j=0 To 15
                                If mas(i+VisX,j+VisY,3)=0 Then
                                        x=sx+(i-j)*32
                                        y=sy+(i+j)*16
                                        SetColor(0,0,0)
                                        DrawImage no,x,y
                                EndIf
                        Next
                Next
        EndIf
       
       
        SetColor(255,255,255)
        x=sx+((CurX-VisX)-(CurY-VisY))*32
        y=sy+((CurX-VisX)+(CurY-VisY))*16
        If lay<>3 Then DrawImage img,x,y,CurNum
        DrawImage cur,x,y
        SetColor(0,0,0)
        DrawText curX + ":" + CurY,x-TextWidth(curX + ":" + CurY)/2,y+8
               
End Function

Function DrawMiniMap(x%,y%)
        SetColor 0,0,0
        Local Dx%=(MapW-MapH)
        Local Dy%=(MapW+MapH)/2
       
        DrawLine x,y,x+MapW,y+MapW/2
        DrawLine x+Dx,y+Dy,x+MapW,y+MapW/2
        DrawLine x,y,x-MapH,y+MapH/2
        DrawLine x+Dx,y+Dy,x-MapH,y+MapH/2
       
        SetColor 255,200,100
       
        DrawLine x+(VisX-VisY),y+(VisX+VisY)/2,x+(VisX-(VisY+15)),y+(visX+(VisY+15))/2
        DrawLine x+(VisX-VisY),y+(VisX+VisY)/2,x+((VisX+15)-VisY),y+((visX+15)+VisY)/2
        DrawLine x+((VisX+15)-(VisY+15)),y+((VisX+15)+(VisY+15))/2,x+(VisX-(VisY+15)),y+(visX+(VisY+15))/2
        DrawLine x+((VisX+15)-(VisY+15)),y+((VisX+15)+(VisY+15))/2,x+((VisX+15)-VisY),y+((visX+15)+VisY)/2
       
        SetColor 255,255,255

End Function

Автор: Grover (e-mail: mail_grover_sobaka_mail.ru)

Другие

Друзья