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

Создание двумерного движка на примере игры «Зверские колобки»

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

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

Содержание

Введение

В течение последнего десятка лет можно было наблюдать, как игры с трехмерной графикой методично вытесняют двумерные из всех игровых жанров (даже тех, где, казалось бы, трехмерная графика будет явным излишеством). Начав с космо/авиа/автоимитаторов и боевиков, трехмерщина проникла в ролевые игры, квесты, стратегии, аркады. Даже головоломки, карточные и настольные игры теперь вовсю стремятся блеснуть 3D-антуражом. На прилавках магазинов уже днем с огнем не сыщешь игру, где нельзя было бы повертеть камерой. Однако, двумерщина еще живет (и даже припеваючи) на дисплеях сотовых телефонов и в Shareware-секторе. Трудно выделиться среди огромного наследия двумерных игр, но все еще попадаются жемчужины наподобие Gish, Crimsonland или Zuma, вполне оригинальные и успешно продающиеся.

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

Основы

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

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

  • XЭкрана = XТайловойКарты * РазмерТайлаВПикселах + XОтступВПикселах
  • YЭкрана = YТайловойКарты * РазмерТайлаВПикселах + YОтступВПикселах

Отступы позволяют перемещать обзор поля либо поместить его в центре экрана.

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

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

Базовый вариант

Чтобы понять, что такое типы, списки и как с ними работать, рекомендую прочитать Руководство по BlitzMax для начинающих. То же относится и к тем, кто программировал только в Blitz3D или BlitzPlus, но не в BlitzMax - в руководстве описываются некоторые различия в синтаксах этих языков (кстати, в MaxIDE есть полезная опция - импортировать код Blitz3D / BlitzPlus: File - Import BB Project). Добавлю одну не документированную, но удобную вещь: следующий код:

For x = 0 To width - 1

        Print x

Next

можно заменить на такой:

For x = 0 Until width

        Print x

Next

Давайте попробуем изобразить на экране тайловое поле со спрайтами. Добавим еще счетчик кадров в секунду.

Const sxsize = 800, sysize = 600, color_depth = 32 ' Размеры экрана и глубина цвета


Const tilesize = 64 ' Размер тайла / спрайта

Const fxsize = 80, fysize = 60 ' Размеры поля в тайлах

Const objq = 200

Global frame[fxsize, fysize] ' Номера тайлов для каждой клетки
Const imageq = 25 ' Kол-во изображений в наборе
Const tileq = 3 ' Kол-во тайлов в наборе

Type obj
 Field x, y, frame, angle#, size#, r, g, b
End Type

SeedRnd MilliSecs() ' Для того, чтобы каждый раз получать новую последовательность случайных чисел

SetGraphicsDriver GLMax2DDriver() ' Установка драйвера отображения графики OpenGL
Graphics sxsize, sysize, color_depth
SetMaskColor 0, 0, 0

Cls
DrawImage LoadImage("2DEngine-Images.png"), 0, 0
images = CreateImage(tilesize, tilesize, imageq)

' Вырезаем текстуры для тайлов
For n = 0 To imageq - 1
        GrabImage images, tilesize * (n Mod 4), tilesize * Floor(n / 4), n
Next

' Генерируем поле
For y = 0 Until fysize
        For x = 0 Until fxsize
                frame[x, y] = Rand(0, tileq - 1)
        Next
Next

objs:TList = CreateList()
For n = 1 To objq
        o:obj = New obj
        o.x = Rand(0, (fxsize - 1) * tilesize)
        o.y = Rand(0, (fysize - 1) * tilesize)
        o.size# = Rnd(0.5, 1.0)
        o.angle# = Rnd(0.0, 360.0)
        o.r = Rand(0, 255)
        o.g = Rand(0, 255)
        o.b = Rand(0, 255)
        o.frame = Rand(tileq, imageq - 1)
        objs.addlast o
Next

HideMouse

Repeat

        For y = 0 Until fysize
                yy = tilesize * y + fdy
                For x = 0 Until fxsize
                        xx = tilesize * x + fdx
                        DrawImage images, xx, yy, frame[x, y]
                Next
        Next

        For o:obj = EachIn objs
                SetColor o.r, o.g, o.b
                SetRotation o.angle#
                SetScale o.size#, o.size#
                DrawImage images, o.x + fdx, o.y + fdy, o.frame
        Next
        SetColor 255, 255, 255
        SetScale 1.0, 1.0
        SetRotation 0.0
       
        fdx = fdx + 4 * (KeyDown(KEY_LEFT) - KeyDown(KEY_RIGHT))
        fdy = fdy + 4 * (KeyDown(KEY_UP) - KeyDown(KEY_DOWN))

        ' Обновление счетчика кадров в секунду
        If fpstim<= MilliSecs() Then
                fpstim = MilliSecs() + 1000
                fps = cnt
                cnt = 0
        Else
                cnt:+1
        End If
        DrawText "Frames/sec:" + fps, 0, 0
       
        Flip False

Until KeyHit(KEY_ESCAPE)

Изображения будем вырезать из этого набора:

Совершенствуем систему

А теперь попробуйте увеличить поле и кол-во объектов раз этак в 4. Тормоза становятся заметными уже невооруженным глазом. Вот и первый минус подхода "в лоб" - рисуется слишком много изображений (если быть точным, то (80 * 60 + 200) * 4 = 20000). Но ведь подавляющее большинство тайлов и объектов выходят за пределы экрана, значит, их можно как-нибудь отфильтровать.

Начнем с тайлов. Давайте сразу введем переменную масштаба, чтобы можно было удалять / приближать поле. Теперь нужно определиться с трансформацией системы координат из координат поля в экранные координаты. Экранная система координат имеет начало (0, 0) в верхнем левом углу экрана, единица измерения - пиксел. Заметьте, что ее ось OY направлена не вверх, как в школьных учебниках, а вниз. Зададим координатную систему поля подобным образом: начало - верхний левый угол поля, единица измерения - тайл, ось OY направлена вниз. Значит, функции перехода будут выглядеть следующим образом (2 это на сленге "to", использую цифру, потому что она хорошо разделяет два слова):

' Перевод из экранных координат в тайловые

Function scr2field(sx#, sy#, tx# Var, ty# Var)
        tx# = sx# / sc# + fdx#
        ty# = sy# / sc# + fdy#
End Function

' Перевод из тайловых координат в экранные
Function field2scr(tx#, ty#, sx# Var, sy# Var)
        sx = (tx# - fdx#) * sc#
        sy = (ty# - fdy#) * sc#

End Function

Все очень просто: переводим координаты верхнего левого угла экрана (0, 0) в координаты поля (х1#, y1#), и координаты нижнего правого угла экрана (sxsize - 1, sysize - 1) в соотв. (x2#, y2#). Затем округляем x1# и y1# в меньшую, а x2# и y2# - в большую сторону и рисуем тайлы в прямоугольном диапазоне (x1#, y1#) - (x2#, y2#). Округление нужно, чтобы в диапазон попали и частично видимые на экране тайлы. Передача переменных из подпрограммы, как вы могли заметить, реализуется добавлением после имени переменной в списке переменных функции ключевого слова "Var".

Теперь объекты. Тут уже все не так просто. Ведь каждый объект может быть в любой точке поля, проверять все объекты на нахождение в пределах экрана - непозволительная трата ресурсов. А что будет, когда начнутся проверки на столкновения! Проверять каждый раз каждый объект с каждым (n * n / 2 проверок)? Это не подходит, поэтому сделаем вот что: привяжем к каждому тайлу список и будем заносить в него все объекты, которые располагаются на этом тайле. Тогда для того, чтобы узнать, какие объекты попадут в видимую область поля, достаточно просмотреть все списки для тайлов диапазона (x1# - r#, y1# - r#) - (x2# + r#, y2# + r#), где r# - максимальный радиус объекта (значения округляются аналогично). Для простоты не будем использовать объекты размером больше тайла (т.е с радиусом больше 0.5, r# = 0.5). Уточню, что объекты в таком случае целесообразней "отцентровать", т.е. сделать так, чтобы при отображении на экране командой DrawImage(i, x, y), координаты (x, y) соответствовали не верхнему левому углу объекта, а его центру. Для этого есть функция MidHandle(image).

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

For l:layer_obj = EachIn layer_order

        l.draw

Next

Альфа-канал

"Что сие такое?" - спросит неискушенный программист. Альфа-канал - это дополнительный коэффициент (к оттенкам красного, зеленого и синего), указывающий, насколько прозрачен данный пиксел. Стандартный диапазон - от 0 (полностью прозрачный) до 255 (полностью непрозрачный). Соответственно, при 128 пиксел будет наполовину прозрачным и в (почти) равных пропорциях смешается с фоновым при наложении. Данная компонента не только избавляет вас от необходимости использовать "маску", но и добавляет возможность использования многих интересных эффектов, вроде anti-aliasing (сглаживания краев). Если кто не в курсе: маска служит для того, чтобы не печатать некоторые пискелы, например, черные пискелы вокруг колобков из предыдущего примера - попробуйте закомментировать команду "SetMaskColor" и увидите разницу.

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

images = LoadPixmapPNG("2DEngine-Images.png")

xsize = PixmapWidth(images)
ysize = PixmapHeight(images)
images_alpha = LoadPixmapPNG("2DEngine-ImagesAlpha.png")
new_images = CreatePixmap(xsize, ysize, PF_RGBA8888)
For y = 0 Until ysize
        For x = 0 Until xsize
                WritePixel new_images, x, y, (ReadPixel(images, x, y) & $FFFFFF) |..
                ((ReadPixel(images_alpha, x, y) & $FF) Shl 24)
        Next
Next
SavePixmapPNG new_images, "2DEngine-NewImages.png", 9

DebugLog "Done!"

Вот альфа-канал:

Просто запустите утилиту в папке с двумя изображениями (2DEngine-Images.png и 2DEngine-ImagesAlpha.png), чтобы создать изображение с цветами и альфа-каналом (2DEngine-NewImages.png).

Выбор используемых модулей

Не очень приятно, когда запускаемый файл простенькой демошки занимает больше мегабайта. Так получается из-за того, что по умолчанию компилятор включает в файл все модули. Для уменьшения размера exe-файла, нужно указать, какие модули компилировать. Команда Framework указывает базовый модуль, в нашем случае это двумерный движок. Другие модули подключаются командой Import. Вот, что нам сейчас понадобится:

Framework brl.glmax2d ' Базовый модуль - движок на основе OpenGL

Import brl.random ' Генератор случайных чисел
Import BRL.Basic ' Из этого модуля используется команда Incbin

Import BRL.PNGLoader ' Загрузка PNG-изображений

Включение дополнительных файлов в запускаемый файл

Чтобы не таскать вместе с exeшником кучу директорий и файлов, их можно включить в исполняемый файл. Делается это командой Incbin. Тогда перед связкой путь+имя файла в командах открытия/загрузки ставится "incbin::". При этом, файлы, естественно, нельзя изменять. Не используйте фиксированные пути (типа "D:/games/my_game/2DEngine-Image.png") и не забудьте подключить модуль BRL.Basic, если не пользуетесь набором модулей по умолчанию.

Incbin "2DEngine-2DEngine-Image.png" ' Сохраняем в exe-файле изображение
Global image:TImage = LoadImage("incbin::2DEngine-Images.png") ' Загружаем изображение из exe-файла


Управление камерой и второй вариант программы

Стандартные варианты: отобразить все поле на экране; разбить поле на кадры и переключаться между ними по мере перемещения игрока; привязать центр камеры к игроку (возможно, ограничивая камеру полем, т. е. когда игрок подходит к краю поля, камера останавливается и не перемещается за пределы поля). Мы уже реализовали упрощенный третий вариант, только вместо игрока пока просто точка на поле, перемещаемая клавишами. Сделаем удобнее - пусть камера следует за курсором, перемещаемым мышью. Однако, если просто привязать камеру к мыши, то перемещения экрана будут дергаными (это происходит потому что перемещения мыши отслеживаются скачками). Гораздо приятнее выглядит вариант, когда камера стремится к точке курсора. Сделаем еще одно добавление (его я в играх не встречал, но получается очень удобно и эффектно) - масштабирование колесиком мыши. Аналогично, чтобы избавиться от скачкообразности, сделаем стремление к уровню увеличения. В реализации этого стремления тоже есть пара загвоздок. Во-первых, если сделать его фиксированным, то при резком перемещении курсора камера будет долго ехать на свое место, а при небольшом - наоборот скакать (зависит от коэффициента перемещения). Поэтому лучше сделать перемещение зависимым от расстояния, так камера по мере приближения к заданной точке будет плавно снижать скорость. Во-вторых, на компьютерах с разной производительностью перемещение будет неодинаковым по скорости, значит нужно ввести зависимость от времени прорисовки кадра. Также стоит ограничить минимальное увеличение, чтобы поле не стало меньше экрана и сдвиг экрана, чтобы камера не залезала за границы поля.

Framework brl.glmax2d ' Базовый модуль - движок на основе OpenGL

Import brl.random ' Генератор случайных чисел
Import BRL.Basic ' Из этого модуля используется команда Incbin
Import BRL.PNGLoader ' Загрузка PNG-изображений

Incbin "2DEngine-NewImages.png" ' Сохраняем в exe-файле изображение

Const sxsize = 800, sysize = 600, color_depth = 32 ' Размеры экрана и глубина цвета
Const sxsize2 = sxsize / 2, sysize2 = sysize / 2 ' Вспомогательные константы

Const tilesize = 64 ' Размер тайла / спрайта

Const fxsize = 160, fysize = 120 ' Размеры поля в тайлах
Global fdx#, fdy# ' Сдвиг отображаемой части поля

Global sc# = 1.0, tilesc# ' Увеличение в пикселах и тайлах
Global dtim# ' Время обработки предыдущего кадра
Const cam_speed# = 2.0 ' Относительная скорость реакции камеры на движения мышью
Const magn_speed# = 2.0 ' Относительная скорость реакции масштаба на вращение колесика мыши
Global camx# = 0.5 * fxsize, camy# =  0.5 * fysize ' Текущие координаты камеры

Const objq = 800

Global layer_order:TList = CreateList() ' Список слоев в порядке отображения

Global frame[fxsize, fysize] ' Номера тайлов для каждой клетки
Const tile_imageq = 3 ' Kол-во тайлов в наборе
Const obj_imageq = 22 ' Kол-во объектов в наборе

' Слой
Type layer_obj Abstract
        Method draw()
        End Method
End Type

' Тайловый слой
Const TILE_DONT_DRAW = -1 ' Kонстанта "Не рисовать тайл"
Type tile_layer_obj Extends layer_obj
        Field image:TImage ' Изображения тайлов
        Field frame[fxsize, fysize] ' Номера тайлов для каждой клетки

        Function add:tile_layer_obj(tile_image:TImage) ' Добавление тайлового слоя
                l:tile_layer_obj = New tile_layer_obj
                l.image = tile_image
                layer_order.addlast l ' Занесение слоя в список отображения
                Return l
        End Function

        Method draw() ' Прорисовка слоя
                SetScale tilesc#, tilesc#
                scr2field 0, 0, x1#, y1#
                scr2field sxsize - 1, sysize - 1, x2#, y2#

                xx1 = Max(0, Floor(x1#)) ' Определение границ куска поля, попадающего в облась зрения
                xx2 = Min(Ceil(x2#), fxsize - 1)
                yy1 = Max(0, Floor(y1#))
                yy2 = Min(Ceil(y2#), fysize - 1)

                For y = yy1 To yy2
                        For x = xx1 To xx2
                                If frame[x, y] >= TILE_DRAW Then ' Проверка, нужно ли рисовать тайл
                                        field2scr x, y, sx#, sy#
                                        DrawImage image, sx#, sy#, frame[x, y]
                                End If
                        Next
                Next
        End Method
End Type

 ' Слой объектов
Type object_layer_obj Extends layer_obj
        Field objects:TList[fxsize, fysize] ' Список объектов для каждой клетки, находящихся на ней

        Function add:object_layer_obj()
                l:object_layer_obj = New object_layer_obj
                For y = 0 Until fysize ' Инициализация списков
                        For x = 0 Until fxsize
                                l.objects[x, y] = CreateList()
                        Next
                Next
                layer_order.addlast l
                Return l
        End Function

        Method draw()
                scr2field 0, 0, x1#, y1#
                scr2field sxsize - 1, sysize - 1, x2#, y2#

                xx1 = Max(0, Floor(x1# - 0.5))
                xx2 = Min(Floor(x2# + 0.5), fxsize - 1)
                yy1 = Max(0, Floor(y1# - 0.5))
                yy2 = Min(Floor(y2# + 0.5), fysize - 1)

                For y = yy1 To yy2
                        For x = xx1 To xx2
                                For o:base_obj = EachIn objects[x, y]
                                        o.draw
                                Next
                        Next
                Next
                reset_transformations
        End Method
End Type

' Базовый тип для объектов
Type base_obj
        Field x#, y#, size# = 1, angle# ' Kоординаты, размер (в тайлах), угол поворота спрайта объекта
        Field r = 255, g = 255, b = 255 ' Цвет объекта (по умолчанию белый)
        Field image:TImage, frame ' Изображение для объекта, кадр
        Field tile_link:TLink ' Ссылка на этот объект из списка объектов клетки
        Field layer:object_layer_obj ' Слой объекта

        Method random_color() ' Задание случайного (но не очень темного) цвета для объекта
                Repeat
                        r = Rand(0, 255)
                        g = Rand(0, 255)
                        b = Rand(0, 255)
                Until r + g + b >= 255
        End Method

        Method register() ' Занесение объекта в списки (регистрация)
                tilex = Floor(x)
                tiley = Floor(y)
                tile_link = layer.objects[tilex, tiley].addlast(Self) ' Занесение в список объектов клетки
        End Method

        Method draw() ' Рисование объекта
                field2scr x#, y#, sx#, sy#
                SetColor r, g, b
                SetScale size# * tilesc#, size# * tilesc#
                SetRotation angle#
                DrawImage image, sx#, sy#, frame
        End Method
End Type

SeedRnd MilliSecs() ' Для того, чтобы каждый раз получать новую последовательность случайных чисел

SetGraphicsDriver GLMax2DDriver() ' Установка драйвера отображения графики OpenGL
Graphics sxsize, sysize, color_depth
AutoImageFlags FILTEREDIMAGE | MIPMAPPEDIMAGE | DYNAMICIMAGE
SetBlend ALPHABLEND

' Загружаем изображения с альфа-каналом из exe-файла
Global images:TPixmap = LoadPixmapPNG("incbin::2DEngine-NewImages.png")

' Вырезаем изображения
Global tile_images:TImage = tiles_grab(0, 3, False) ' Тайлы
Global obj_images:TImage = tiles_grab(3, 22) ' Объекты

field_generate ' Создаем поле
objects_generate ' Создаем объекты

HideMouse

cx# = camx#
cy# = camy#
'DebugStop
Repeat

        cx# = limit(cx# + (MouseX() - sxsize2) / sc#, 0, fxsize)
        cy# = limit(cy# + (MouseY() - sysize2) / sc#, 0, fysize)
        camera_change cx#, cy#, 1.1 ^ MouseZ() * 64.0

        tim = MilliSecs() ' Засекаем время

        MoveMouse sxsize2, sysize2 ' Установка курсора мыши в центр экрана

        ' Прорисовка слоев
        For l:layer_obj = EachIn layer_order
                l.draw
        Next

        ' Отображение курсора
        reset_transformations
        field2scr cx#, cy#, sx#, sy#
        DrawImage obj_images, sx#, sy#, 21

        ' Обновление счетчика кадров в секунду
        If fpstim<= MilliSecs() Then
                fpstim = MilliSecs() + 1000
                fps = cnt
                cnt = 0
        Else
                cnt:+1
        End If
        DrawText "Frames/sec:" + fps, 0, 0
       
        Flip False

        ' Вычисление времени, затраченного на виток цикла (в секундах) для вычисления множителей скоростей
        dtim# = 0.001 * (MilliSecs() - tim)

Until KeyHit(KEY_ESCAPE)

' Генерация поля
Function field_generate()
        layer:tile_layer_obj = tile_layer_obj.add(tile_images)
        For y = 0 Until fysize
                For x = 0 Until fxsize
                        layer.frame[x, y] = Rand(0, tile_imageq - 1)
                Next
        Next
End Function

' Генерация объектов
Function objects_generate()
        objs:TList = CreateList()
        layer:object_layer_obj = object_layer_obj.add()
        For n = 1 To objq
                o:base_obj = New base_obj
                o.x = Rnd(0, fxsize - 1)
                o.y = Rnd(0, fysize - 1)
                o.angle# = Rnd(0, 360)
                o.size# = Rnd(0.5, 1.0)
                o.layer = layer
                o.image = obj_images
                o.frame = Rand(0, obj_imageq - 1)
                o.random_color
                o.register
        Next
End Function

' Функция изменения координат камеры и увеличения
Function camera_change(x#, y#, scale#)
        ' Приращения координат камеры и увеличения
        sc# = sc# + magn_speed# * (scale# - sc#) * dtim#
        camx# = camx# + cam_speed# * (x# - camx#) * dtim#
        camy# = camy# + cam_speed# * (y# - camy#) * dtim#

        sc# = limit(sc#, Max(1.0 * sxsize / fxsize, 1.0 * sysize / fysize), 256.0) ' Ограничение увеличения
        tilesc# = sc# / tilesize ' Вычисление коэффициента увеличения для тайлов
       
        xsize# = sxsize / sc#   ' Размеры отображаемого прямоугольного куска поля
        ysize# = sysize / sc#
       
        fdx# = limit(camx# - xsize# * 0.5, 0, fxsize - xsize#) ' Ограничения смещения поля (по границам)
        fdy# = limit(camy# - ysize# * 0.5, 0, fysize - ysize#)
End Function

' Функция вырезания изображения из другого изображения
Function new_grab:TImage(image:TImage, x, y, frame)
        pixmap:TPixmap = LockImage(image, frame)
        w:TPixmap = images.window(x, y, ImageWidth(image), ImageHeight(image))
        pixmap.paste w, 0, 0
        UnlockImage image
        Return image
End Function

' Функция вырезания тайла или серии тайлов из изображения
Function tiles_grab:TImage(num, frameq = 1, midhn = True)
        image:TImage = CreateImage(tilesize, tilesize, frameq)
        If midhn Then MidHandleImage image ' флаг midhn означает, что изображение нужно отцентровать
        For n = 0 To frameq - 1
                pos = num + n
                new_grab image, (pos Mod 4) * tilesize, Floor(pos / 4) * tilesize, n ' По умолчанию
                ' тайлы располагаются на изображении в 4 столбца
        Next
        Return image
End Function

' Функция сброса трансформаций
Function reset_transformations()
        SetColor 255, 255, 255
        SetRotation 0
        SetAlpha 1
        SetScale 1.0, 1.0
End Function

' Перевод из экранных координат в тайловые
Function scr2field(sx#, sy#, tx# Var, ty# Var)
        tx# = sx# / sc# + fdx#
        ty# = sy# / sc# + fdy#
End Function

' Перевод из тайловых координат в экранные
Function field2scr(tx#, ty#, sx# Var, sy# Var)
        sx# = (tx# - fdx#) * sc#
        sy# = (ty# - fdy#) * sc#
End Function

' Ограничение переменной минимальным и максимальным значениями
Function limit#(v#, vmin#, vmax#)
        If v# < vmin# Then v = vmin# ElseIf v# > vmax# Then v# = vmax#
        Return v#

End Function

Создание игрового поля

Мешанину из тайлов, которую мы сделали для примера, игровым полем, конечно же, не назовешь. Попробуем сгенерировать что-нибудь более определенное (к тому же, генерация разных полей при повторных запусках добавит играбельности). Заглянем в какую-нибудь тайловую игру с видом сверху. Можно увидеть, что карта состоит из нескольких видов областей - свободного перемещения (вода, трава, земля, ...) и ограничивающие (горы, лес, пропасти, здания, ...). Возьмем три области - воду, траву и песок. Причем, песок окружает вода, а траву - песок. Самый простой, на мой взгляд, способ создания областей с плавными краями - создать случайную карту высот (задать каждому тайлу случайную высоту в заданном диапазоне), сгладить ее и задать две границы - "песочную" и "травяную". Все тайлы, что ниже "песочной" - вода, между "песочной" и "травяной" - песок, выше "травяной" - трава.

Ну, заполнение массива высот случайными значениями от 0 до 1, думаю, проблем не вызовет. Сглаживание мы разбирали в третьей и четвертой статье из цикла "Виртуальный художник". Это просто - суммируются значения высоты соседних тайлов и высота текущего тайла, учетверенная, например, затем сумма делится на 8 + 4 = 12 и заносится в буфер (т. к. старое значение высоты понадобится при вычислениях высоты других тайлов). Чтобы сумма оставалась постоянной, зациклим поле, т.е. соседи для крайних тайлов находятся на противоположном краю. Ну а после обработки всех тайлов, буфер и поле меняются местами, операция повторяется несколько раз и вот карта высот приобретает подходящий вид. Однако, "размывая" карту, мы сузили диапазон значений, граничные 0 и 1 приблизились к 0.5 и теперь все высоты лежат в диапазоне (0.4, 0.6), например. Поэтому необходимо выяснить максимальное и минимальное значения и растянуть поле, чтобы минимальное соответствовало 0, максимальное - 1. Формула проста: h' = (h - hmin) / (hmax - hmin)

Генерация тайлов перехода между двумя текстурами

В графически продвинутых играх области не просто заданы разными видами текстур, еще существуют переходы от одной области к другой. Например, если есть две области - вода и суша, то между "полностью сушей" и "полностью водой" ставят промежуточные тайлы. На иллюстрации такие тайлы выделены (игра - Percussor by TMB):

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

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

Сначала нужно определиться, какие тайлы будут содержать в себе переходы - белые, черные или и те и другие. Выберем белые. Рассмотрим один черный тайл. К нему непосредственно прилегают 8 соседей. Каждый сосед "по своей сути" может быть либо черным, либо белым (т. е. 2 возможных состояния). Значит, полный набор состоит из 2^8 = 256 вариантов (он включает в себя "чистый" белый тайл, значит всего тайлов будет 256 + 1 (чистый черный тайл) = 257). Вот он:

А вот поле, созданное с его помощью:

Если условиться, что длина каждой стороны области - более 1 тайла, то остается 50 штук. Если, вдобавок, составлять области из одноцветных квадратов, размером 2х2 тайла, в наборе останется всего 32 тайла. Можно ограничить число соседей 4-мя прилегающими сторонами - 16 вариантов. Кроме того, количество вспомогательных тайлов зависит от метода организации "перехода" и построения поля. Но чем меньше тайлов в наборе, тем меньше возможностей и поле визуально более блочно. Выбор баланса между качеством и количеством тайлов особенно важен в том случае, если вы рисуете каждый тайл вручную. Вот 16-тайловый набор:

Что касается конкретно нашего случая, будем генерировать полный 256-тайловый набор перехода. Понадобятся два таких набора (переход от воды к песку и от песка к траве) плюс чистый тайл воды - итого 513 тайлов.

Перемещение объектов

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

Полоса отображения хода процесса

Генерация переходных тайлов занимает некоторое время, поэтому хорошо бы сделать так, чтобы игрок мог наблюдать за ходом процесса, вместо того, чтобы пялиться в черный экран (даже с надписью "Please wait"). Сделаем полосу, постепенно заполняющуюся цветом, меняющимся от красного к зеленому. А вот, собственно, код со всем тем, о чем было сказано (плюс система генерации переходных тайлов, описанная в комментариях).

Framework brl.glmax2d ' Базовый модуль - движок на основе OpenGL

Import brl.random ' Генератор случайных чисел
Import BRL.Basic ' Из этого модуля используется команда Incbin
Import BRL.PNGLoader ' Загрузка PNG-изображений

Incbin "2DEngine-NewImages.png" ' Сохраняем в exe-файле изображение

Const sxsize = 800, sysize = 600, color_depth = 32 ' Размеры экрана и глубина цвета

Const tilesize = 64 ' Размер тайла / спрайта

 ' Вспомогательные константы
Const tilesize2 = tilesize / 2, tilesize4 = tilesize / 4, tilesize8 = tilesize / 8
Const tilesize16 = tilesize / 16, tilesize32 = tilesize / 32

Const sxsize2 = sxsize / 2, sysize2 = sysize / 2
Const sxsize4 = sxsize / 4, sxsize34 = sxsize * 3 / 4
Const sysize34 = sysize * 3 / 4, sxsize24 = sxsize / 2 - 4

Const fxsize = 160, fysize = 120 ' Размеры поля в тайлах
Const fblurq = 5 ' Kол-во размытий для временно генерируемой вспомогательной карты высот поля
Const sand_threshold# = 0.4, grass_threshold# = 0.5 ' Пороги высоты для песка и травы
Global fdx#, fdy# ' Сдвиг отображаемой части поля

Global speedpersec# = 1.0 ' Модификатор скорости (тайлов / сек)

Global sc# = 1.0, tilesc# ' Увеличение в пикселах и тайлах
Global dtim# ' Время обработки предыдущего кадра
Global timspeed# ' Модификатор для перемещения с учетом прошедшего времени
Const cam_speed# = 2.0 ' Относительная скорость реакции камеры на движения мышью
Const magn_speed# = 2.0 ' Относительная скорость реакции масштаба на вращение колесика мыши
Global camx# = 0.5 * fxsize, camy# =  0.5 * fysize ' Текущие координаты камеры

Const objq = 1000

Global layer_order:TList = CreateList() ' Список слоев в порядке отображения
Global actingobj:TList = CreateList() ' Список для активных объектов

Global frame[fxsize, fysize] ' Номера тайлов для каждой клетки
Const tile_imageq = 3 ' Kол-во тайлов в наборе
Const obj_imageq = 22 ' Kол-во объектов в наборе

' Слой
Type layer_obj Abstract
        Method draw()
        End Method
End Type

' Тайловый слой
Const TILE_DONT_DRAW = -1 ' Kонстанта "Не рисовать тайл"
Type tile_layer_obj Extends layer_obj
        Field image:TImage ' Изображения тайлов
        Field frame[fxsize, fysize] ' Номера тайлов для каждой клетки

        Function add:tile_layer_obj(tile_image:TImage) ' Добавление тайлового слоя
                l:tile_layer_obj = New tile_layer_obj
                l.image = tile_image
                layer_order.addlast l ' Занесение слоя в список отображения
                Return l
        End Function

        Method draw() ' Прорисовка слоя
                SetScale tilesc#, tilesc#
                scr2field 0, 0, x1#, y1#
                scr2field sxsize - 1, sysize - 1, x2#, y2#

                xx1 = Max(0, Floor(x1#)) ' Определение границ куска поля, попадающего в облась зрения
                xx2 = Min(Ceil(x2#), fxsize - 1)
                yy1 = Max(0, Floor(y1#))
                yy2 = Min(Ceil(y2#), fysize - 1)

                For y = yy1 To yy2
                        For x = xx1 To xx2
                                If frame[x, y] >= TILE_DRAW Then ' Проверка, нужно ли рисовать тайл
                                        field2scr x, y, sx#, sy#
                                        DrawImage image, sx#, sy#, frame[x, y]
                                End If
                        Next
                Next
        End Method
End Type

 ' Слой объектов
Type object_layer_obj Extends layer_obj
        Field objects:TList[fxsize, fysize] ' Список объектов для каждой клетки, находящихся на ней

        Function add:object_layer_obj()
                l:object_layer_obj = New object_layer_obj
                For y = 0 Until fysize ' Инициализация списков
                        For x = 0 Until fxsize
                                l.objects[x, y] = CreateList()
                        Next
                Next
                layer_order.addlast l
                Return l
        End Function

        Method draw()
                scr2field 0, 0, x1#, y1#
                scr2field sxsize - 1, sysize - 1, x2#, y2#

                xx1 = Max(0, Floor(x1# - 0.5))
                xx2 = Min(Floor(x2# + 0.5), fxsize - 1)
                yy1 = Max(0, Floor(y1# - 0.5))
                yy2 = Min(Floor(y2# + 0.5), fysize - 1)

                For y = yy1 To yy2
                        For x = xx1 To xx2
                                For o:base_obj = EachIn objects[x, y]
                                        o.draw
                                Next
                        Next
                Next
                reset_transformations
        End Method
End Type

' Базовый тип для объектов
Const ACTIVE = True, INACTIVE = False ' Kонстанты "Активный", "Не активный"
Type base_obj
        Field x#, y#, size# = 1, angle# ' Kоординаты, размер (в тайлах), угол поворота спрайта объекта
        Field speed# ' Скорость объекта (тайлов / сек)
        Field r = 255, g = 255, b = 255 ' Цвет объекта (по умолчанию белый)
        Field image:TImage, frame ' Изображение для объекта, кадр
        Field tilex, tiley ' Kоординаты тайла, на котором находится объект
        Field tile_link:TLink ' Ссылка на этот объект из списка объектов клетки
        Field layer:object_layer_obj ' Слой объекта

        Method random_color() ' Задание случайного (но не очень темного) цвета для объекта
                Repeat
                        r = Rand(0, 255)
                        g = Rand(0, 255)
                        b = Rand(0, 255)
                Until r + g + b >= 255
        End Method

        Method register(acting = ACTIVE) ' Занесение объекта в списки (регистрация)
                tilex = Floor(x#)
                tiley = Floor(y#)
                tile_link = layer.objects[tilex, tiley].addlast(Self) ' Занесение в список объектов клетки
                If acting Then act_link = actingobj.addlast(Self) ' Занесение в список активных объектов
        End Method

        Method draw() ' Рисование объекта
                field2scr x#, y#, sx#, sy#
                SetColor r, g, b
                SetScale size# * tilesc#, size# * tilesc#
                SetRotation angle#

                DrawImage image, sx#, sy#, frame
        End Method

        Method act()
                newx# = x# + timspeed# * speed# * Cos(angle#) ' Перемещение объекта в направлении угла обзора
                newy# = y# + timspeed# * speed# * Sin(angle#)
                newx# = newx# - Floor(newx# / fxsize) * fxsize ' Эти формулы не дают объектам выйти за пределы поля
                newy# = newy# - Floor(newy# / fysize) * fysize ' При переходе через границу поля, объект появляется с другой стороны
                move newx#, newy#
        End Method

        Method move(newx#, newy#) ' Kорректное перемещение
                newtilex = Floor(newx#)
                newtiley = Floor(newy#)
                If tilex <> newtilex Or tiley <> newtiley Then ' Если объект переместился в другую клетку, то
                        removeLink tile_link ' Удаление из списка старой клетки
                        tilex = newtilex
                        tiley = newtiley
                        tile_link = layer.objects[tilex, tiley].addlast(Self) ' Занесение в список новой
                End If
                x# = newx#
                y# = newy#
        End Method
End Type

SeedRnd MilliSecs() ' Для того, чтобы каждый раз получать новую последовательность случайных чисел

SetGraphicsDriver GLMax2DDriver() ' Установка драйвера отображения графики OpenGL
Graphics sxsize, sysize, color_depth
AutoImageFlags FILTEREDIMAGE | MIPMAPPEDIMAGE | DYNAMICIMAGE
SetBlend ALPHABLEND

' Загружаем изображения с альфа-каналом из exe-файла
Global images:TPixmap = LoadPixmapPNG("incbin::2DEngine-NewImages.png")

' Вырезаем изображения
Global obj_images:TImage = tiles_grab(3, 22) ' Объекты
' Создаем текстуры для тайлов
tex_water:TImage = tiles_grab(0, 1, False)
tex_sand:TImage = tiles_grab(1, 1, False)
tex_grass:TImage = tiles_grab(2, 1, False)

' Создаем в пакете изображений тайлов текстуру воды
tile_tex:TImage = CreateImage(tilesize, tilesize, 513)
pixmap:TPixmap = LockImage(tile_tex,0)
pixmap.paste(LockImage(tex_water)), 0, 0
UnlockImage tile_tex, 0
UnlockImage tex_water
' И две библиотеки - переход от воды к песку и от песка к траве
tile_lib_create tex_water, tex_sand, 4.0 / tilesize, 360.0, tile_tex, 1
tile_lib_create tex_sand, tex_grass, 4.0 / tilesize, 720.0, tile_tex, 257

' Делаем "пирог" из слоев
Global layer_tiles:tile_layer_obj = tile_layer_obj.add(tile_tex) ' Сначала - тайлы
Global layer_objects:object_layer_obj = object_layer_obj.add() ' Потом - объекты

field_generate ' Создаем поле
objects_generate ' Создаем объекты

HideMouse

cx# = camx#
cy# = camy#
Repeat

        cx# = limit(cx# + (MouseX() - sxsize2) / sc#, 0, fxsize)
        cy# = limit(cy# + (MouseY() - sysize2) / sc#, 0, fysize)
        camera_change cx#, cy#, 1.1 ^ MouseZ() * 64.0

        tim = MilliSecs() ' Засекаем время

        MoveMouse sxsize2, sysize2 ' Установка курсора мыши в центр экрана

        timspeed# = speedpersec# * dtim# ' Определение множителя к скорости на основе прошедшего времени

        ' Действия активных объектов
        For o:base_obj = EachIn actingobj
                o.act
        Next

        ' Прорисовка слоев
        For l:layer_obj = EachIn layer_order
                l.draw
        Next

        ' Отображение курсора
        reset_transformations
        field2scr cx#, cy#, sx#, sy#
        DrawImage obj_images, sx#, sy#, 21

        ' Обновление счетчика кадров в секунду
        If fpstim<= MilliSecs() Then
                fpstim = MilliSecs() + 1000
                fps = cnt
                cnt = 0
        Else
                cnt:+1
        End If
        DrawText "Frames/sec:" + fps, 0, 0
       
        Flip False

        ' Вычисление времени, затраченного на виток цикла (в секундах) для вычисления множителей скоростей
        dtim# = 0.001 * (MilliSecs() - tim)

Until KeyHit(KEY_ESCAPE)

' Генерация поля
Function field_generate()
        Const tile_water = 0
        Const tile_sand = 256
        Const tile_grass = 512
        Local ff#[fxsize, fysize, 2] ' Вспомогательный буферизованный массив высот для тайловой карты
        Local pos2bit[] = [0, 6, 1, 4, 5, 2, 7, 3]
        fmin# = 1.0; fmax# = 0 ' Переменные минимума и максимума значений высот
        For n = 0 To fblurq + 3
                loadingbar "Generating field...", n, fblurq + 4 ' Индикатор завершенности процесса
                maxd# = 0
                For y = 0 Until fysize ' Цикл по всем тайлам
                        For x = 0 Until fxsize
                                Select n
                                        Case 0 ' Сначала заполняем массив высот случайными значениями
                                                ff#[x, y, 1] = Rnd(0, 1)
                                        Case fblurq + 1 ' После этапов сглаживания - этап формирования тайловых слоев
                                                d# = (ff#[x, y, k] - fmin#) / (fmax# - fmin#) ' Kорректируем значение высоты, чтобы минимум соответствовал значению 0.0, максимум - 1.0
                                                If d# < sand_threshold# Then ' До порога песка
                                                        layer_tiles.frame[x, y] = tile_water ' Отображаем чистый тайл воды
                                                ElseIf d# < grass_threshold# Then ' От порога песка до порога травы
                                                        layer_tiles.frame[x, y] = tile_sand ' Пока отображаем чистый тайл песка
                                                Else ' После порога травы
                                                        layer_tiles.frame[x, y] = tile_grass ' Пока отображаем чистый тайл травы
                                                End If
                                        Case fblurq + 2 ' Этап устранения травы, примыкающей к воде
                                                If layer_tiles.frame[x,y] = tile_grass Then ' Если тайл - трава, то
                                                        For yy = - 1 To 1 ' Цикл по всем соседним тайлам
                                                                For xx = - 1 To 1
                                                                        x2 = (x + xx + fxsize) Mod fxsize ' Расчет координат соседнего тайла
                                                                        y2 = (y + yy + fysize) Mod fysize '  (поле зациклено)
                                                                        If layer_tiles.frame[x2, y2] = tile_water Then ' Если один из тайлов - вода
                                                                                layer_tiles.frame[x, y] = tile_sand ' То меняем тайл травы на тайл песка
                                                                        End If
                                                                Next
                                                        Next
                                                End If
                                        Case fblurq + 3 ' Этап сглаживания тайлов (выбора кадра из библиотеки)
                                                If layer_tiles.frame[x, y] > tile_water Then ' Если чистая вода, то пропускаем этот тайл
                                                        bitpos = 0; mask = 0
                                                        For yy = - 1 To 1 ' Цикл по всем соседним тайлам
                                                                For xx = - 1 To 1
                                                                        If xx<>0 Or yy<>0 Then
                                                                                x2 = (x + xx + fxsize) Mod fxsize
                                                                                y2 = (y + yy + fysize) Mod fysize
                                                                                If layer_tiles.frame[x, y] > tile_sand Then ' Если данный тайл - трава, то
                                                                                        ' Если соседний тайл - трава, то включаем бит присутствия соседа для данного тайла
                                                                                        If layer_tiles.frame[x2, y2] > tile_sand Then setbit mask, pos2bit[bitpos]
                                                                                Else ' Иначе это тайл песка
                                                                                        ' Если соседний тайл - песок, то включаем бит присутствия соседа для данного тайла
                                                                                        If layer_tiles.frame[x2, y2] > tile_water Then setbit mask, pos2bit[bitpos]
                                                                                End If
                                                                                bitpos:+1 ' Увеличиваем счетчик номера бита
                                                                        End If
                                                                Next
                                                        Next
                                                        layer_tiles.frame[x, y] = 1 + 256  * (layer_tiles.frame[x, y] = tile_grass) + mask
                                                End If
                                        Default ' Этапы сглаживания массива высот
                                                sum# = 0
                                                For yy = - 1 To 1 ' Суммируем значения высот соседних тайлов и высоту данного тайла * 8
                                                        For xx = - 1 To 1
                                                                sum# = sum# + ff#[(x + xx + fxsize) Mod fxsize, (y + yy + fysize) Mod fysize, k] * (1.0 + 7.0 * (xx = 0 And yy = 0))
                                                        Next
                                                Next
                                                sum# = sum# / 16.0 ' Вычисляем среднее значение (центральный тайл имеет такой же вес, что и все 8 соседних в сумме)
                                                If n = fblurq Then setminmax sum#, fmin#, fmax# ' Kорректируем значения максимума и минимума высоты
                                                ff#[x, y, 1 - k] = sum# ' Устанавливаем значение высоты в буфере
                                End Select
                        Next
                Next
                k = 1 - k ' Меняем буфер и текущую карту местами
                If n = fblurq + 1 Then ' Окантовка карты водой после этапа формирования слоев
                        For x = 0 Until fxsize
                                waterize x, 0
                                waterize x, fysize - 1
                        Next
                        For y = 0 Until fysize
                                waterize 0, y
                                waterize fxsize - 1, y
                        Next
                End If
        Next
End Function

' Залитие тайла водой
Function waterize(x, y)
        layer_tiles.frame[x, y] = 0 ' Рисуем тайл воды
End Function

' Создание библиотеки тайлов перехода между текстурами
Function tile_lib_create(bottom_tile:TImage, top_tile:TImage, rowd#, period#, tile_lib:TImage, offset = 0)
        Local dt#[tilesize2] ' Заполнение массива колебаний ровной границы
        For dn = 0 Until tilesize2
                dt#[dn] = (Sin(90 + dn * period# / tilesize2) - 1) * tilesize32
        Next

        bottom_pixmap:TPixmap = LockImage(bottom_tile)
        top_pixmap:TPixmap = LockImage(top_tile)

        For n = 0 To 255 ' Восемь клеток вокруг тайла могут быть такими же либо отличными (2 состояния),
         ' поэтому всего - 2 ^ 8 = 256 вариантов
                loadingbar "Generating transition tiles...", n, 256
                lib_pixmap:TPixmap = LockImage(tile_lib, n + offset)
                For n1 = 0 To 1
                        For n2 = 0 To 1
                                v = biton(n, n1 + n2 * 2)
                                vx = biton(n, n1 + 4)
                                vy = biton(n, n2 + 6)
                                For yy = 0 Until tilesize2
                                        For xx = 0 Until tilesize2
                                                If vx Then
                                                        If vy Then
                                                                If v Then
                                                                        k1# = 1
                                                                Else
                                                                        k1# = rowd# * (Sqr(xx * xx + yy * yy))
                                                                End If
                                                        Else
                                                                k1# = (yy + dt#[xx]) * rowd#
                                                        End If
                                                Else
                                                        If vy Then
                                                                k1# = (xx + dt#[yy]) * rowd#
                                                        Else
                                                                k1# = 2.0 - rowd# * (Sqr((tilesize2 - xx) * (tilesize2 - xx) + (tilesize2 - yy) * (tilesize2 - yy)) + Rand( -1, 1))
                                                        End If
                                                End If
                                                If k1# > 1 Then k1# = 1 ' Ограничиваем коэффициент в пределах интервала [0, 1]
                                                If k1# < 0 Then k1# = 0
                                                k2# = 1.0 - k1# ' Kоэффициент прозрачности для пикселей другого тайла
                                                If n1 Then x = tilesize - 1 - xx Else x = xx ' Отражения отн. осей (если нужно)
                                                If n2 Then y = tilesize - 1 - yy Else y = yy
                                                fromrgba ReadPixel(top_pixmap, x, y), r1, g1, b1, dummy ' Получаем цветовые компоненты пикселей тайлов
                                                fromrgba ReadPixel(bottom_pixmap, x, y), r2, g2, b2, dummy
                                                ' Печатаем пиксел, смешивая цвета с заданными коэффициентами
                                                WritePixel lib_pixmap, x, y, torgba(k1# * r1 + k2# * r2, k1# * g1 + k2# * g2, k1# * b1 + k2# * b2, 255)
                                        Next
                                Next
                        Next
                Next
        Next

        UnlockImage bottom_tile
        UnlockImage top_tile
End Function

' Генерация объектов
Function objects_generate()
        For n = 1 To objq
                o:base_obj = New base_obj
                o.x = Rnd(0, fxsize - 1)
                o.y = Rnd(0, fysize - 1)
                o.angle# = Rnd(0, 360)
                o.size# = Rnd(0.5, 1.0)
                o.speed# = Rnd(0.5, 3.0)
                o.layer = layer_objects
                o.image = obj_images
                o.random_color
                acting = Rand(0,1)
                If acting Then
                        o.frame = Rand(0, 3)
                Else
                        o.frame = Rand(4, obj_imageq - 1)
                End If
                o.register acting
        Next
End Function

' Функция изменения координат камеры и увеличения
Function camera_change(x#, y#, scale#)
        ' Приращения координат камеры и увеличения
        sc# = sc# + magn_speed# * (scale# - sc#) * dtim#
        camx# = camx# + cam_speed# * (x# - camx#) * dtim#
        camy# = camy# + cam_speed# * (y# - camy#) * dtim#

        sc# = limit(sc#, Max(1.0 * sxsize / fxsize, 1.0 * sysize / fysize), 256.0) ' Ограничение увеличения
        tilesc# = sc# / tilesize ' Вычисление коэффициента увеличения для тайлов
       
        xsize# = sxsize / sc#   ' Размеры отображаемого прямоугольного куска поля
        ysize# = sysize / sc#
       
        fdx# = limit(camx# - xsize# * 0.5, 0, fxsize - xsize#) ' Ограничения смещения поля (по границам)
        fdy# = limit(camy# - ysize# * 0.5, 0, fysize - ysize#)
End Function

' Функция вырезания изображения из другого изображения
Function new_grab:TImage(image:TImage, x, y, frame)
        pixmap:TPixmap = LockImage(image, frame)
        w:TPixmap = images.window(x, y, ImageWidth(image), ImageHeight(image))
        pixmap.paste w, 0, 0
        UnlockImage image
        Return image
End Function

' Функция вырезания тайла или серии тайлов из изображения
Function tiles_grab:TImage(num, frameq = 1, midhn = True)
        image:TImage = CreateImage(tilesize, tilesize, frameq)
        If midhn Then MidHandleImage image ' флаг midhn означает, что изображение нужно отцентровать
        For n = 0 To frameq - 1
                pos = num + n
                new_grab image, (pos Mod 4) * tilesize, Floor(pos / 4) * tilesize, n ' По умолчанию тайлы располагаются на изображении в 4 столбца
        Next
        Return image
End Function

' Установка цвета - оттенка серого
Function SetGrayColor(col)
        SetColor col, col, col
End Function

' Полоса отображения завершенности процесса
Function loadingbar(txt$, pos, maximum)
        Cls
        SetColor 128, 128, 255
        DrawText txt$, (sxsize - TextWidth(txt$)) / 2, sysize34
        col = 255 * pos / maximum
        SetGrayColor 255
        DrawEmptyRect sxsize4, sysize34 + 20, sxsize2, 30
        SetColor 255 - col, col, 0
        DrawRect sxsize4 + 2, sysize34 + 22, sxsize24 * pos / maximum, 26
        Flip False
        SetGrayColor 255
End Function

' Функция, рисующая пустой прямоугольник
Function DrawEmptyRect(x#, y#, xsize#, ysize#)
        xsize# = xsize# - 1
        ysize# = ysize# - 1
        DrawLine x#, y#, x# + xsize#, y#
        DrawLine x# + xsize#, y#, x# + xsize#,y# + ysize#
        DrawLine x# + xsize#, y# + ysize#, x#, y# + ysize#
        DrawLine x#, y# + ysize#, x#, y#
End Function

' Функция, переводящая Write/ReadPixel-значение в значения цветовых компонент и альфа канала
Function fromRGBa(from, r Var, g Var, b Var, a Var)
        b = from & $FF
        g = (from Shr 8) & $FF
        r = (from Shr 16) & $FF
        a = (from Shr 24) & $FF
        Return
End Function

' Функция, переводящая значения цветовых компонент и альфа канала в Write/ReadPixel-значение
Function toRGBa(r, g, b, a = 255)
        Return b | (g Shl 8) | (r Shl 16) | (a Shl 24)
End Function

' Функция сброса трансформаций
Function reset_transformations()
        SetGrayColor 255
        SetRotation 0
        SetAlpha 1
        SetScale 1.0, 1.0
End Function

' Перевод из экранных координат в тайловые
Function scr2field(sx#, sy#, tx# Var, ty# Var)
        tx# = sx# / sc# + fdx#
        ty# = sy# / sc# + fdy#
End Function

' Перевод из тайловых координат в экранные
Function field2scr(tx#, ty#, sx# Var, sy# Var)
        sx# = (tx# - fdx#) * sc#
        sy# = (ty# - fdy#) * sc#
End Function

' Ограничение переменной минимальным и максимальным значениями
Function limit#(v#, vmin#, vmax#)
        If v# < vmin# Then v = vmin# ElseIf v# > vmax# Then v# = vmax#
        Return v#
End Function

' Функция, возвращающая значение бита под номером bitnum
Function biton(v, bitnum)
        If v & (1 Shl bitnum) Then Return True Else Return False
End Function

' Включение бита под номером bitnum в значении переменной
Function setbit(v Var, bitnum)
        v = v | (1 Shl bitnum)
End Function

' Изменение минимума и максимума на основе переменной
Function setminmax(v#, vmin# Var, vmax# Var)
        If v#<vmin# Then vmin# = v#
        If v#>vmax# Then vmax# = v#

End Function

Коллизия

Наш мир пока еще аморфен - настало время добавить ему материальности. Для этого нужно создать коллизионную систему (collision по-английски - столкновение). Это часть программы, следящая за столкновениями объектов и задающая реакцию объектов на эти столкновения. Для каждого объекта задается тип и параметры его коллизионной формы, эта форма обычно максимально упрощена. Например, для объекта, похожего на круг, целесообразно задать коллизионную форму круга, она определяется центром и радиусом. Небольшими выступами можно пренебречь (это даже даст некоторые преимущества). Определение коллизии делится на две процедуры: определения наползания двух данных объектов друг на друга (их коллизии) и выявления объектов, коллизирующих с данным (от ее оптимизированности в значительной степени зависит кол-во FPS, а значит и плавность хода игры).

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

Тайловый слой коллизии

Этот слой создается исключительно для коллизии с объектами. Здесь пока все будет максимально просто: тайл либо не коллизирует, либо коллизирует полностью (как квадрат). То есть если объект наползет на коллизирующий тайл, значит с этим тайлом он столкнулся. Для хранения информации о том, какой тайл "материален", а какой нет, создадим новое расширение базового слоя - слой тайловой коллизии. Он будет хранить двумерный массив аналогичный тайловому, только вместо номеров тайлов в ячейках будет True(1), если тайл коллизирует и False(0), если нет.

Коллизия объектов

В примере будет 3 типа коллизионных форм объектов - круг (центр - координаты объекта, радиус), квадрат (центр - координаты объекта, половина стороны) и нематериальность. Соответственно, получается 3 * (3 + 1) / 2 = 6 типов коллизии:

  • Коллизия нематериального объекта с любым всегда отсутствует (False).
  • Круги коллизируют, если расстояние между их центрами меньше суммы радиусов (рис. 1).
  • Прямоугольники коллизируют, если разница между соответствующими координатами центра меньше суммы полусторон (рис. 2).
  • Коллизия прямоугольника и круга:

Временно поместим центр круга в начало координат и рассмотрим два случая:

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

Для определения всех объектов, с которыми может коллизировать данный, нужно обследовать все списки близлежащих тайлов, на которых могут находиться возможные участники столкновения и проверить на столкновение с данным все находящиеся там объекты. Так как мы уже условились, что не будем создавать объекты размером больше тайла, то диапазон возможных участников столкновения для данного объекта будет определяться так: (Floor(x#) - 1, Floor(y#) - 1, Floor(x#) + 1, Floor(y#) - 1). Можно еще урезать диапазон, учтя размер данного объекта.

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

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

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

Теперь сравним подход "в лоб" и подход выборочной коллизии (на коллизию проверяются все объекты, без нашего алгоритма со списками объектов для тайла): пусть будет 100 колобков и 200 пуль. Значит всего объектов - 300 и при топорном способе нужно выполнить 300 х 300 = 90 000 проверок. Оптимизированный метод: колобки с колобками (100 х 100) + пули с колобками (200 х 100) = 30 000 проверок - в 3 раза меньше. Как говорится, почувствуйте разницу.

Расширения базовых объектов

Ранее мы ограничивались одним типом объектов. Теперь у нас есть два объекта, отличающиеся поведением - колобки и пули. Еще можно выделить статические (неподвижные) объекты. Создадим на основе типа базового объекта тип колобков, статических объектов и пуль. Будем добавлять в них специфические методы и переменные по мере необходимости. Во-первых, неплохо бы добавить возможность создавать каждый из типов объектов. Для этого в типе объекта пишется функция create (вы можете назвать ее и по-другому). В ней создается объект данного типа и задаются его параметры. Функция возвращает указатель на этот созданный объект. Т. к. наборы их параметров для разных типов различны, то понадобится по одной функции создания объекта в каждом типе. Если нужно создать объект, то сначала пишется тип объекта, затем точка, потом название функции - create. Например - kolobok_obj.create() или k:kolobok_obj = kolobok_obj.create().

Блуждание колобков

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

Стрельба

Здесь все тоже довольно просто, но есть несколько нюансов. Колобок делает выстрел - создаем новый объект - пулю, направление - такое же, как у колобка, скорость чуть больше. Пуля летит в данном направлении до столкновения с объектом, после чего разрушается (нужно удалить объект из списков). Чтобы пуля не появлялась на пустом месте, создадим ее внутри колобка, а чтобы она сразу же не впилась в того, кто ее выпустил, нужно исключить реакцию на столкновение пули со стреляющим. Для этого внесем в тип объекта "пуля" переменную для хранения указателя на стреляющего.

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

Еще один момент: если объектов будет не очень много, некоторые пули будут долго лететь через все поле, не встречая на пути объектов. Поэтому, со временем, пуль станет очень много, и они будут тормозить программу. Чтобы этого не происходило, зададим время жизни пули, чтобы она, пролетев некоторое время, исчезала. Для этого введем в тип колобка еще переменную - время жизни его пуль, а также константу - время "затухания" пули.

Framework brl.glmax2d ' Базовый модуль - движок на основе OpenGL

Import brl.random ' Генератор случайных чисел
Import BRL.Basic ' Из этого модуля используется команда Incbin
Import BRL.PNGLoader ' Загрузка PNG-изображений

Incbin "2DEngine-NewImages.png" ' Сохраняем в exe-файле изображение

Const sxsize = 800, sysize = 600, color_depth = 32 ' Размеры экрана и глубина цвета

Const tilesize = 64 ' Размер тайла / спрайта

' Вспомогательные константы
Const tilesize2 = tilesize / 2, tilesize4 = tilesize / 4, tilesize8 = tilesize / 8
Const tilesize16 = tilesize / 16, tilesize32 = tilesize / 32

Const sxsize2 = sxsize / 2, sysize2 = sysize / 2
Const sxsize4 = sxsize / 4, sxsize34 = sxsize * 3 / 4
Const sysize34 = sysize * 3 / 4, sxsize24 = sxsize / 2 - 4

Const fxsize = 160, fysize = 120 ' Размеры поля в тайлах
Const fblurq = 5 ' Kол-во размытий для временно генерируемой вспомогательной карты высот поля
Const sand_threshold# = 0.4, grass_threshold# = 0.5 ' Пороги высоты для песка и травы
Global fdx#, fdy# ' Сдвиг отображаемой части поля

Const objq = 1000 ' Kол-во объектов
Global speedpersec# = 1.0 ' Модификатор скорости (тайлов / сек)
Global angpersec# = 90.0 ' Модификатор угловой скорости (градусов / сек)

Global sc# = 1.0, tilesc# ' Увеличение в пикселах и тайлах
Global dtim# ' Время обработки предыдущего кадра
Global timspeed# ' Модификатор для перемещения с учетом прошедшего времени
Global timang# ' Модификатор для поворота с учетом прошедшего времени
Const minms = 100 ' Ограничитель кадров в секунду для действий объектов
Const cam_speed# = 2.0 ' Относительная скорость реакции камеры на движения мышью
Const magn_speed# = 2.0 ' Относительная скорость реакции масштаба на вращение колесика мыши
Global camx# = 0.5 * fxsize, camy# =  0.5 * fysize ' Текущие координаты камеры

Const showcollisions = False ' Показ коллизий (отключен)
Global ccnt, objcnt, chcnt ' Счетчики коллизий, объектов, проверок коллизий в секунду

Global layer_order:TList = CreateList() ' Список слоев в порядке отображения
Global actingobj:TList = CreateList() ' Список для активных объектов

Global frame[fxsize, fysize] ' Номера тайлов для каждой клетки
Const tile_imageq = 3 ' Kол-во тайлов в наборе
Const obj_imageq = 21 ' Kол-во объектов в наборе

' Слой
Type layer_obj Abstract
        Field collision_with:TList = CreateList() ' Список слоев, с которыми коллизирует данный

        Method collides_with(layer:layer_obj)
                If tile_layer_obj(layer) Then RuntimeError "Tile layers can't collide - use tile collision layer"
                collision_with.addlast layer
        End Method

        Method draw()
        End Method
End Type

' Тайловый слой
Const TILE_DONT_DRAW = -1 ' Kонстанта "Не рисовать тайл"
Type tile_layer_obj Extends layer_obj
        Field image:TImage ' Изображения тайлов
        Field frame[fxsize, fysize] ' Номера тайлов для каждой клетки

        Function add:tile_layer_obj(tile_image:TImage) ' Добавление тайлового слоя
                l:tile_layer_obj = New tile_layer_obj
                l.image = tile_image
                layer_order.addlast l ' Занесение слоя в список отображения
                Return l
        End Function

        Method draw() ' Прорисовка слоя
                SetScale tilesc#, tilesc#
                scr2field 0, 0, x1#, y1#
                scr2field sxsize - 1, sysize - 1, x2#, y2#

                xx1 = Max(0, Floor(x1#)) ' Определение границ куска поля, попадающего в облась зрения
                xx2 = Min(Ceil(x2#), fxsize - 1)
                yy1 = Max(0, Floor(y1#))
                yy2 = Min(Ceil(y2#), fysize - 1)

                For y = yy1 To yy2
                        For x = xx1 To xx2
                                If frame[x, y] >= TILE_DRAW Then ' Проверка, нужно ли рисовать тайл
                                        field2scr x, y, sx#, sy#
                                        DrawImage image, sx#, sy#, frame[x, y]
                                End If
                        Next
                Next
        End Method
End Type

 ' Слой объектов
Type object_layer_obj Extends layer_obj
        Field objects:TList[fxsize, fysize] ' Список объектов для каждой клетки, находящихся на ней

        Function add:object_layer_obj()
                l:object_layer_obj = New object_layer_obj
                For y = 0 Until fysize ' Инициализация списков
                        For x = 0 Until fxsize
                                l.objects[x, y] = CreateList()
                        Next
                Next
                layer_order.addlast l
                Return l
        End Function

        Method draw()
                scr2field 0, 0, x1#, y1#
                scr2field sxsize - 1, sysize - 1, x2#, y2#

                xx1 = Max(0, Floor(x1# - 0.5))
                xx2 = Min(Floor(x2# + 0.5), fxsize - 1)
                yy1 = Max(0, Floor(y1# - 0.5))
                yy2 = Min(Floor(y2# + 0.5), fysize - 1)

                For y = yy1 To yy2
                        For x = xx1 To xx2
                                For o:base_obj = EachIn objects[x, y]
                                        o.draw
                                Next
                        Next
                Next
                reset_transformations
        End Method
End Type

' Слой тайловой коллизии
Type tile_collision_layer_obj Extends layer_obj
        Field collision[fxsize, fysize] ' Kоллизия с тайлом (да / нет)

        Function add:tile_collision_layer_obj()
                Return New tile_collision_layer_obj
        End Function
End Type

Const CT_IMMATERIAL = 0 ' Тип коллизионной модели - нематериальный
Const CT_CIRCULAR = 1 ' Тип коллизионной модели - круг
Const CT_SQUARE = 2 ' Тип коллизионной модели - квадрат
' Базовый тип для объектов
Const ACTIVE = True, INACTIVE = False ' Kонстанты "Активный", "Не активный"
Type base_obj
        Field x#, y#, size# = 1, angle# ' Kоординаты, размер (в тайлах), угол поворота спрайта объекта
        Field speed# ' Скорость объекта (тайлов / сек)
        Field r = 255, g = 255, b = 255 ' Цвет объекта (по умолчанию белый)
        Field image:TImage, frame ' Изображение для объекта, кадр
        Field tilex, tiley ' Kоординаты тайла, на котором находится объект
        Field act_link:TLink, tile_link:TLink ' Ссылки на этот объект из списков активных объектов и объектов клетки
        Field layer:object_layer_obj ' Слой объекта
        Field coll_type = CT_CIRCULAR, radius# = 0.5 ' Тип модели коллизии и ее радиус

        Method place_find() ' Поиск места для размещения объекта
                Repeat
                        x# = Rnd(1.0, fxsize - 1.01)
                        y# = Rnd(1.0, fysize - 1.01)
                        tilex = Floor(x#)
                        tiley = Floor(y#)
                        ' Проверка на отсутствие коллизий (в т. ч. и с водой)
                        If Not collision(x#, y#) Then Exit
                Forever
        End Method

        Method random_color() ' Задание случайного (но не очень темного) цвета для объекта
                Repeat
                        r = Rand(0, 255)
                        g = Rand(0, 255)
                        b = Rand(0, 255)
                Until r + g + b >= 255
        End Method

        Method register(acting = ACTIVE) ' Занесение объекта в списки (регистрация)
                objcnt:+1
                tilex = Floor(x#)
                tiley = Floor(y#)
                tile_link = layer.objects[tilex, tiley].addlast(Self) ' Занесение в список объектов клетки
                If acting Then act_link = actingobj.addlast(Self) ' Занесение в список активных объектов
        End Method

        Method draw() ' Рисование объекта
                field2scr x#, y#, sx#, sy#
                SetColor r, g, b
                SetScale size# * tilesc#, size# * tilesc#
                SetRotation angle#

                DrawImage image, sx#, sy#, frame
        End Method

        Method act()
        End Method

        Method move(newx#, newy#) ' Kорректное перемещение
                newtilex = Floor(newx#)
                newtiley = Floor(newy#)
                If tilex <> newtilex Or tiley <> newtiley Then ' Если объект переместился в другую клетку, то
                        removeLink tile_link ' Удаление из списка старой клетки
                        tilex = newtilex
                        tiley = newtiley
                        tile_link = layer.objects[tilex, tiley].addlast(Self) ' Занесение в список новой
                End If
                x# = newx#
                y# = newy#
        End Method

        Method try_move(newx#, newy#)
                If Not collision(newx#, newy#) Then move newx#, newy#; Return True
        End Method

        Method try_move_ang(ang#, spd#)
                If try_move(x# + timspeed# * Cos(ang#) * spd#, y# + timspeed# * Sin(ang#) * spd#) Then Return True
        End Method

        Method collision2(o:base_obj, newx#, newy#) ' Проверка объекта на столкновение с другим
                Select True
                        Case coll_type = CT_CIRCULAR ' Если модель данного объекта - круг
                                Select True
                                        Case o.coll_type = CT_CIRCULAR ' И модель второго объекта - тоже круг (круг с кругом)
                                                dx# = newx# - o.x#
                                                dy# = newy# - o.y#
                                                ' Проверяем, меньше ли расстояние между объектами, чем сумма их радиусов
                                                If Sqr(dx# * dx# + dy# * dy#) < o.radius# + radius# Then ccnt:+1; Return True Else Return False
                                        Case o.coll_type = CT_SQUARE ' А если модель второго объекта - квадрат (круг с квадратом)
                                                If (o.x# - o.radius# <= newx# And newx# <= o.x# + o.radius#) Or (o.y# - o.radius# <= newy# And newy# <= o.y# + o.radius#) Then
                                                        dx# = Abs(newx# - o.x#)
                                                        dy# = Abs(newy# - o.y#)
                                                        sumr# = o.radius# + radius#
                                                        If dx# < sumr# And dy# < sumr# Then ccnt:+1; Return True
                                                Else
                                                        dx# = Min(Abs(newx# - o.x# - o.radius#), Abs(newx# - o.x# + o.radius#))
                                                        dy# = Min(Abs(newy# - o.y# - o.radius#), Abs(newy# - o.y# + o.radius#))
                                                        If Sqr(dx# * dx# + dy# * dy#) < radius# Then ccnt:+1; Return True
                                                End If
                                        Default ' Но вот если второй объект нематериален - столкновения нет
                                                Return False
                                End Select
                        Case coll_type = CT_SQUARE ' Если модель данного объекта - квадрат
                                If o.coll_type = CT_SQUARE Then ' И модель второго объекта - тоже квадрат
                                        dx# = Abs(newx# - o.x#)
                                        dy# = Abs(newy# - o.y#)
                                        sumr# = o.radius# + radius#
                                        ' Проверяем, меньше ли модуль разности соотв. координат, чем сумма радиусов
                                        If dx# < sumr# And dy# < sumr# Then ccnt:+1; Return True
                                Else ' Иначе проверяем столкновение второго объекта с данным (меняем местами)
                                        Return o.collision2(Self, newx#, newy#)
                                End If
                        Default ' Нематериальный объект не коллизирует
                                Return False
                End Select
        End Method

        Method collision(newx#, newy#) ' Проверка данного объекта на столкновение с чем бы то ни было
                ' Столкновение с границами поля (это осложнит другие проверки, поэтому выходим)
                If newx# < 1.0 Or newy# < 1.0 Or newx# >= fxsize - 1.0 Or newy# >= fysize - 1.0 Then
                        boundaries_collision_act
                        Return True
                End If
                For l:layer_obj = EachIn layer.collision_with ' Цикл по всем слоям коллизии
                        tl:tile_collision_layer_obj = tile_collision_layer_obj(l)
                        If tl Then ' Если слой - тайлово-коллизионный, то
                                For yy = Floor(newy# - radius#) To Floor(newy# + radius#)
                                        For xx = Floor(newx# - radius#) To Floor(newx# + radius#)
                                                If tl.collision(xx, yy) Then
                                                        tile_object.x# = xx + 0.5
                                                        tile_object.y# = yy + 0.5
                                                        If collision2(tile_object, newx#, newy#) Then collided = True; tile_collision_act xx, yy
                                                End If
                                        Next
                                Next
                        Else ' Иначе слой - объектный, тогда
                                ol:object_layer_obj = object_layer_obj(l)
                                x2 = Floor(newx#)
                                y2 = Floor(newy#)
                                For yy = y2 - 1 To y2 + 1
                                        For xx = x2 - 1 To x2 + 1
                                                For o:base_obj = EachIn ol.objects[xx, yy]
                                                        If Self<>o Then
                                                                chcnt:+1
                                                                If showcollisions Then ' Показ проверок коллизий линиями
                                                                        field2scr o.x#, o.y#, sx1#, sy1#
                                                                        field2scr newx#, newy#, sx2#, sy2#
                                                                        DrawLine sx1#, sy1#, sx2#, sy2#
                                                                End If
                                                                If collision2(o, newx#, newy#) Then collided = True; object_collision_act o
                                                        End If
                                                Next
                                        Next
                                Next
                        End If
                Next
                Return collided
        End Method

        Method object_collision_act(o:base_obj) ' Действия при столкновении с объектами
        End Method

        Method tile_collision_act(xx, yy) ' Действия при столкновении с тайлами
        End Method

        Method boundaries_collision_act() ' Действия при столкновении с границами карты
        End Method

        Method destroy() ' Kорректное уничтожение объекта
                If act_link<>Null Then removeLink act_link ' Удаление объекта из списка активных
                removeLink tile_link ' Удаление объекта из списка объектов клетки
                objcnt:-1
        End Method

End Type

' Тип для колобков
Type kolobok_obj Extends base_obj
        Field bullet_reload, bullet_reload_time ' Время окончания перезарядки, время перезарядки
        Field bullet_speed#, bullet_lifetime = 2000 ' Скорость и время жизни пули этого колобка

        Function create:kolobok_obj()
                o:kolobok_obj = New kolobok_obj
                o.angle# = Rnd(0.0, 360.0)
                o.size# = Rnd(0.5, 1.0)
                o.random_color
                o.layer = layer_koloboks
                o.image = obj_images
                o.frame = Rand(0, 2)
                o.radius# = 0.4 * o.size#
                o.speed# = Rnd(0.5, 3.0)
                o.bullet_reload_time = Rand(200, 1000)
                o.bullet_speed# = Rnd(1.0, 2.0)
                o.bullet_lifetime = 2000
                o.place_find
                o.register
                Return o
        End Function

        Method act()
                ' Перемещение объекта в направлении угла обзора, если путь свободен
                '  и поворот, если уперлись в препятствие
                If Not try_move_ang(angle#, speed#) Then angle# = angle# + timang#
                If bullet_reload < MilliSecs() Then
                        bullet_obj.create Self ' Если пришло время стрелять - стреляем
                        bullet_reload = MilliSecs() + bullet_reload_time       
                End If
        End Method
End Type

Type static_obj Extends base_obj
        Function create:static_obj()
                o:static_obj = New static_obj
                o.angle# = Rnd(0.0, 360.0)
                o.size# = Rnd(0.5, 1.0)
                o.random_color
                o.layer = layer_koloboks
                o.image = obj_images
                o.frame = Rand(4, obj_imageq - 1)
                o.radius# = 0.5 * o.size#
                If o.frame >= 16 Then
                        o.angle# = 0.0
                        o.coll_type = CT_SQUARE
                End If
                o.place_find
                o.register INACTIIVE
                Return o
        End Function
End Type


' Пуля
Const NOT_YET = 1000000000 ' Kонстанта "еще не умер"
Const fading_time = 1000 ' Время "затухания"
Type bullet_obj Extends base_obj
        Field parent:base_obj ' Указатель на стреляющего
        Field death = NOT_YET ' Время смерти (еще не определено)

        ' Создаем пулю: начальные координаты, размер, угол, скорость, время жизни, повреждения, стреляющий, отступ от координат
        Function create:bullet_obj(o:kolobok_obj)
                bul:bullet_obj = New bullet_obj
                bul.layer = layer_bullets
                bul.x# = o.x# + Cos(o.angle#) * d# ' Смещение отн. данных координат
                bul.y# = o.y# + Sin(o.angle#) * d#
                bul.r = o.r
                bul.g = o.g
                bul.b = o.b
                bul.image = obj_images
                bul.frame = 3
                bul.parent = o
                bul.angle# = o.angle#
                bul.size# = o.size# * 0.5
                bul.speed# = o.speed# + 1.5
                bul.radius = bsize# * 0.25
                bul.death = MilliSecs() + o.bullet_lifetime
                bul.register
                Return bul
        End Function

        Method draw()
                If death = NOT_YET Then
                        SetAlpha 1 ' Если пуля еще не начала исчезать, то прозрачность не меняется
                Else
                        SetAlpha limit(.001 * (death - MilliSecs()), 0, 1) ' Иначе потихоньку исчезает
                        If death<MilliSecs() Then destroy ' И в конце уничтожается совсем
                End If
                super.draw ' Запускаем процедуру рисования объекта из base_obj
        End Method

        Method act() ' Действует просто - летит вперед до столкновения
                move x# + timspeed# * Cos(angle#) * speed#, y# + timspeed# * Sin(angle#) * speed#
                collision x#, y#
                If MilliSecs() > death Then destroy ' Время жизни ограничено с появления
        End Method

        Method object_collision_act(o:base_obj) ' Столкновение с объектом
                If o <> parent Then
                        ccnt:+1
                        destroy
                End If
        End Method
       
        Method boundaries_collision_act() ' Уничтожается при столкновении с границами
                destroy
        End Method
End Type

Global tile_object:base_obj = New base_obj
tile_object.radius# = 0.5
tile_object.coll_type = CT_SQUARE

SeedRnd MilliSecs() ' Для того, чтобы каждый раз получать новую последовательность случайных чисел

SetGraphicsDriver GLMax2DDriver() ' Установка драйвера отображения графики OpenGL
Graphics sxsize, sysize, color_depth
AutoImageFlags FILTEREDIMAGE | MIPMAPPEDIMAGE | DYNAMICIMAGE
SetBlend ALPHABLEND

' Загружаем изображения с альфа-каналом из exe-файла
Global images:TPixmap = LoadPixmapPNG("incbin::2DEngine-NewImages.png")

' Вырезаем изображения
Global obj_images:TImage = tiles_grab(3, 22) ' Объекты
' Создаем текстуры для тайлов
tex_water:TImage = tiles_grab(0, 1, False)
tex_sand:TImage = tiles_grab(1, 1, False)
tex_grass:TImage = tiles_grab(2, 1, False)

' Создаем в пакете изображений тайлов текстуру воды
tile_tex:TImage = CreateImage(tilesize, tilesize, 513)
pixmap:TPixmap = LockImage(tile_tex,0)
pixmap.paste(LockImage(tex_water)), 0, 0
UnlockImage tile_tex, 0
UnlockImage tex_water
' И две библиотеки - переход от воды к песку и от песка к траве
tile_lib_create tex_water, tex_sand, 4.0 / tilesize, 360.0, tile_tex, 1
tile_lib_create tex_sand, tex_grass, 4.0 / tilesize, 720.0, tile_tex, 257

' Делаем "пирог" из слоев
Global layer_tiles:tile_layer_obj = tile_layer_obj.add(tile_tex) ' Сначала - тайлы
Global layer_bullets:object_layer_obj = object_layer_obj.add() ' Затем пули
Global layer_koloboks:object_layer_obj = object_layer_obj.add() ' Потом - колобки и другие объекты

' Создаем слой тайловой коллизии - слой "непроходимой" воды
Global layer_water:tile_collision_layer_obj = tile_collision_layer_obj.add()

' Определяем что с чем коллизирует
layer_koloboks.collides_with layer_koloboks ' Kолобки - между собой
layer_koloboks.collides_with layer_water ' Kолобки - с коллизионным слоем воды
layer_bullets.collides_with layer_koloboks ' Пули - с колобками

field_generate ' Создаем поле
objects_generate ' Создаем объекты

HideMouse

cx# = 0.5 * fxsize
cy# = 0.5 * fysize
Repeat

        cx# = limit(cx# + (MouseX() - sxsize2) / sc#, 0, fxsize)
        cy# = limit(cy# + (MouseY() - sysize2) / sc#, 0, fysize)
        camera_change cx#, cy#, 1.1 ^ MouseZ() * 64.0

        tim = MilliSecs() ' Засекаем время

        MoveMouse sxsize2, sysize2 ' Установка курсора мыши в центр экрана

        timspeed# = speedpersec# * dtim# ' Определение множителя к скорости на основе прошедшего времени
        timang# = angpersec# * dtim# ' То же для угловой скорости

        ' Прорисовка слоев
        For l:layer_obj = EachIn layer_order
                l.draw
        Next
        reset_transformations

        ' Действия активных объектов
        For o:base_obj = EachIn actingobj
                o.act
        Next

        ' Отображение курсора
        field2scr cx#, cy#, sx#, sy#
        DrawImage obj_images, sx#, sy#, 21

        ' Обновление счетчика кадров в секунду
        If fpstim<= MilliSecs() Then
                fpstim = MilliSecs() + 1000
                fps = cnt
                cnt = 0
        Else
                cnt:+1
        End If

        DrawText "Frames/sec:" + fps + ", objects:" + objcnt + ", collision checks/frame:" + chcnt + ", collisions/frame:" + ccnt, 0, 0
        ccnt = 0
        chcnt = 0
       
        Flip False

        ' Вычисление времени, затраченного на виток цикла (в секундах) для вычисления множителей скоростей
        dtim# = 0.001 * (Min(MilliSecs() - tim, minms))
        ' Время ограниченно пределом для недопущения слишком больших множителей, отрицательно
        '  сказывающихся на определении столкновений

Until KeyHit(KEY_ESCAPE)

' Генерация поля
Function field_generate()
        Const tile_water = 0
        Const tile_sand = 256
        Const tile_grass = 512
        Local ff#[fxsize, fysize, 2] ' Вспомогательный буферизованный массив высот для тайловой карты
        Local pos2bit[] = [0, 6, 1, 4, 5, 2, 7, 3]
        fmin# = 1.0; fmax# = 0 ' Переменные минимума и максимума значений высот
        For n = 0 To fblurq + 3
                loadingbar "Generating field...", n, fblurq + 4 ' Индикатор завершенности процесса
                maxd# = 0
                For y = 0 Until fysize ' Цикл по всем тайлам
                        For x = 0 Until fxsize
                                Select n
                                        Case 0 ' Сначала заполняем массив высот случайными значениями
                                                ff#[x, y, 1] = Rnd(0, 1)
                                        Case fblurq + 1 ' После этапов сглаживания - этап формирования тайловых слоев
                                                d# = (ff#[x, y, k] - fmin#) / (fmax# - fmin#) ' Kорректируем значение высоты, чтобы минимум соответствовал значению 0.0, максимум - 1.0
                                                If d# < sand_threshold# Then ' До порога песка
                                                        layer_tiles.frame[x, y] = tile_water ' Отображаем чистый тайл воды
                                                        layer_water.collision[x, y] = True ' Установка коллизии с этим тайлом в водном слое
                                                ElseIf d# < grass_threshold# Then ' От порога песка до порога травы
                                                        layer_tiles.frame[x, y] = tile_sand ' Пока отображаем чистый тайл песка
                                                Else ' После порога травы
                                                        layer_tiles.frame[x, y] = tile_grass ' Пока отображаем чистый тайл травы
                                                End If
                                        Case fblurq + 2 ' Этап устранения травы, примыкающей к воде
                                                If layer_tiles.frame[x,y] = tile_grass Then ' Если тайл - трава, то
                                                        For yy = - 1 To 1 ' Цикл по всем соседним тайлам
                                                                For xx = - 1 To 1
                                                                        x2 = (x + xx + fxsize) Mod fxsize ' Расчет координат соседнего тайла
                                                                        y2 = (y + yy + fysize) Mod fysize '  (поле зациклено)
                                                                        If layer_tiles.frame[x2, y2] = tile_water Then ' Если один из тайлов - вода
                                                                                layer_tiles.frame[x, y] = tile_sand ' То меняем тайл травы на тайл песка
                                                                        End If
                                                                Next
                                                        Next
                                                End If
                                        Case fblurq + 3 ' Этап сглаживания тайлов (выбора кадра из библиотеки)
                                                If layer_tiles.frame[x, y] > tile_water Then ' Если чистая вода, то пропускаем этот тайл
                                                        bitpos = 0; mask = 0
                                                        For yy = - 1 To 1 ' Цикл по всем соседним тайлам
                                                                For xx = - 1 To 1
                                                                        If xx<>0 Or yy<>0 Then
                                                                                x2 = (x + xx + fxsize) Mod fxsize
                                                                                y2 = (y + yy + fysize) Mod fysize
                                                                                If layer_tiles.frame[x, y] > tile_sand Then ' Если данный тайл - трава, то
                                                                                        ' Если соседний тайл - трава, то включаем бит присутствия соседа для данного тайла
                                                                                        If layer_tiles.frame[x2, y2] > tile_sand Then setbit mask, pos2bit[bitpos]
                                                                                Else ' Иначе это тайл песка
                                                                                        ' Если соседний тайл - песок, то включаем бит присутствия соседа для данного тайла
                                                                                        If layer_tiles.frame[x2, y2] > tile_water Then setbit mask, pos2bit[bitpos]
                                                                                End If
                                                                                bitpos:+1 ' Увеличиваем счетчик номера бита
                                                                        End If
                                                                Next
                                                        Next
                                                        layer_tiles.frame[x, y] = 1 + 256  * (layer_tiles.frame[x, y] = tile_grass) + mask
                                                End If
                                        Default ' Этапы сглаживания массива высот
                                                sum# = 0
                                                For yy = - 1 To 1 ' Суммируем значения высот соседних тайлов и высоту данного тайла * 8
                                                        For xx = - 1 To 1
                                                                sum# = sum# + ff#[(x + xx + fxsize) Mod fxsize, (y + yy + fysize) Mod fysize, k] * (1.0 + 7.0 * (xx = 0 And yy = 0))
                                                        Next
                                                Next
                                                sum# = sum# / 16.0 ' Вычисляем среднее значение (центральный тайл имеет такой же вес, что и все 8 соседних в сумме)
                                                If n = fblurq Then setminmax sum#, fmin#, fmax# ' Kорректируем значения максимума и минимума высоты
                                                ff#[x, y, 1 - k] = sum# ' Устанавливаем значение высоты в буфере
                                End Select
                        Next
                Next
                k = 1 - k ' Меняем буфер и текущую карту местами
                If n = fblurq + 1 Then ' Окантовка карты водой после этапа формирования слоев
                        For x = 0 Until fxsize
                                waterize x, 0
                                waterize x, fysize - 1
                        Next
                        For y = 0 Until fysize
                                waterize 0, y
                                waterize fxsize - 1, y
                        Next
                End If
        Next
End Function

' Залитие тайла водой
Function waterize(x, y)
        layer_tiles.frame[x, y] = 0 ' Рисуем тайл воды
        layer_water.collision[x, y] = True ' Kоллизия для тайла водного слоя
End Function

' Создание библиотеки тайлов перехода между текстурами
Function tile_lib_create(bottom_tile:TImage, top_tile:TImage, rowd#, period#, tile_lib:TImage, offset = 0)
        Local dt#[tilesize2] ' Заполнение массива колебаний ровной границы
        For dn = 0 Until tilesize2
                dt#[dn] = (Sin(90 + dn * period# / tilesize2) - 1) * tilesize32
        Next

        bottom_pixmap:TPixmap = LockImage(bottom_tile)
        top_pixmap:TPixmap = LockImage(top_tile)

        For n = 0 To 255 ' Восемь клеток вокруг тайла могут быть такими же либо отличными (2 состояния),
         ' поэтому всего - 2 ^ 8 = 256 вариантов
                loadingbar "Generating transition tiles...", n, 256
                lib_pixmap:TPixmap = LockImage(tile_lib, n + offset)
                For n1 = 0 To 1
                        For n2 = 0 To 1
                                v = biton(n, n1 + n2 * 2)
                                vx = biton(n, n1 + 4)
                                vy = biton(n, n2 + 6)
                                For yy = 0 Until tilesize2
                                        For xx = 0 Until tilesize2
                                                If vx Then
                                                        If vy Then
                                                                If v Then
                                                                        k1# = 1
                                                                Else
                                                                        k1# = rowd# * (Sqr(xx * xx + yy * yy))
                                                                End If
                                                        Else
                                                                k1# = (yy + dt#[xx]) * rowd#
                                                        End If
                                                Else
                                                        If vy Then
                                                                k1# = (xx + dt#[yy]) * rowd#
                                                        Else
                                                                k1# = 2.0 - rowd# * (Sqr((tilesize2 - xx) * (tilesize2 - xx) + (tilesize2 - yy) * (tilesize2 - yy)) + Rand( -1, 1))
                                                        End If
                                                End If
                                                If k1# > 1 Then k1# = 1 ' Ограничиваем коэффициент в пределах интервала [0, 1]
                                                If k1# < 0 Then k1# = 0
                                                k2# = 1.0 - k1# ' Kоэффициент прозрачности для пикселей другого тайла
                                                If n1 Then x = tilesize - 1 - xx Else x = xx ' Отражения отн. осей (если нужно)
                                                If n2 Then y = tilesize - 1 - yy Else y = yy
                                                fromrgba ReadPixel(top_pixmap, x, y), r1, g1, b1, dummy ' Получаем цветовые компоненты пикселей тайлов
                                                fromrgba ReadPixel(bottom_pixmap, x, y), r2, g2, b2, dummy
                                                ' Печатаем пиксел, смешивая цвета с заданными коэффициентами
                                                WritePixel lib_pixmap, x, y, torgba(k1# * r1 + k2# * r2, k1# * g1 + k2# * g2, k1# * b1 + k2# * b2, 255)
                                        Next
                                Next
                        Next
                Next
        Next

        UnlockImage bottom_tile
        UnlockImage top_tile
End Function

' Генерация объектов
Function objects_generate()
        For n = 1 To objq
                If (n Mod 100) = 0 Then loadingbar "Generating objects...", n, objq
                If Rand(0, 2) Then kolobok_obj.create() Else static_obj.create()
        Next
End Function

' Функция изменения координат камеры и увеличения
Function camera_change(x#, y#, scale#)
        ' Приращения координат камеры и увеличения
        sc# = sc# + magn_speed# * (scale# - sc#) * dtim#
        camx# = camx# + cam_speed# * (x# - camx#) * dtim#
        camy# = camy# + cam_speed# * (y# - camy#) * dtim#

        sc# = limit(sc#, Max(1.0 * sxsize / fxsize, 1.0 * sysize / fysize), 256.0) ' Ограничение увеличения
        tilesc# = sc# / tilesize ' Вычисление коэффициента увеличения для тайлов
       
        xsize# = sxsize / sc#   ' Размеры отображаемого прямоугольного куска поля
        ysize# = sysize / sc#
       
        fdx# = limit(camx# - xsize# * 0.5, 0, fxsize - xsize#) ' Ограничения смещения поля (по границам)
        fdy# = limit(camy# - ysize# * 0.5, 0, fysize - ysize#)
End Function

' Функция вырезания изображения из другого изображения
Function new_grab:TImage(image:TImage, x, y, frame)
        pixmap:TPixmap = LockImage(image, frame)
        w:TPixmap = images.window(x, y, ImageWidth(image), ImageHeight(image))
        pixmap.paste w, 0, 0
        UnlockImage image
        Return image
End Function

' Функция вырезания тайла или серии тайлов из изображения
Function tiles_grab:TImage(num, frameq = 1, midhn = True)
        image:TImage = CreateImage(tilesize, tilesize, frameq)
        If midhn Then MidHandleImage image ' флаг midhn означает, что изображение нужно отцентровать
        For n = 0 To frameq - 1
                pos = num + n
                new_grab image, (pos Mod 4) * tilesize, Floor(pos / 4) * tilesize, n ' По умолчанию тайлы располагаются на изображении в 4 столбца
        Next
        Return image
End Function

' Установка цвета - оттенка серого
Function SetGrayColor(col)
        SetColor col, col, col
End Function

' Полоса отображения завершенности процесса
Function loadingbar(txt$, pos, maximum)
        Cls
        SetColor 128, 128, 255
        DrawText txt$, (sxsize - TextWidth(txt$)) / 2, sysize34
        col = 255 * pos / maximum
        SetGrayColor 255
        DrawEmptyRect sxsize4, sysize34 + 20, sxsize2, 30
        SetColor 255 - col, col, 0
        DrawRect sxsize4 + 2, sysize34 + 22, sxsize24 * pos / maximum, 26
        Flip False
        SetGrayColor 255
End Function

' Функция, рисующая пустой прямоугольник
Function DrawEmptyRect(x#, y#, xsize#, ysize#)
        xsize# = xsize# - 1
        ysize# = ysize# - 1
        DrawLine x#, y#, x# + xsize#, y#
        DrawLine x# + xsize#, y#, x# + xsize#,y# + ysize#
        DrawLine x# + xsize#, y# + ysize#, x#, y# + ysize#
        DrawLine x#, y# + ysize#, x#, y#
End Function

' Функция, переводящая Write/ReadPixel-значение в значения цветовых компонент и альфа канала
Function fromRGBa(from, r Var, g Var, b Var, a Var)
        b = from & $FF
        g = (from Shr 8) & $FF
        r = (from Shr 16) & $FF
        a = (from Shr 24) & $FF
        Return
End Function

' Функция, переводящая значения цветовых компонент и альфа канала в Write/ReadPixel-значение
Function toRGBa(r, g, b, a = 255)
        Return b | (g Shl 8) | (r Shl 16) | (a Shl 24)
End Function

' Функция сброса трансформаций
Function reset_transformations()
        SetGrayColor 255
        SetRotation 0
        SetAlpha 1
        SetScale 1.0, 1.0
End Function

' Перевод из экранных координат в тайловые
Function scr2field(sx#, sy#, tx# Var, ty# Var)
        tx# = sx# / sc# + fdx#
        ty# = sy# / sc# + fdy#
End Function

' Перевод из тайловых координат в экранные
Function field2scr(tx#, ty#, sx# Var, sy# Var)
        sx# = (tx# - fdx#) * sc#
        sy# = (ty# - fdy#) * sc#
End Function

' Ограничение переменной минимальным и максимальным значениями
Function limit#(v#, vmin#, vmax#)
        If v# < vmin# Then v = vmin# ElseIf v# > vmax# Then v# = vmax#
        Return v#
End Function

' Функция, возвращающая значение бита под номером bitnum
Function biton(v, bitnum)
        If v & (1 Shl bitnum) Then Return True Else Return False
End Function

' Включение бита под номером bitnum в значении переменной
Function setbit(v Var, bitnum)
        v = v | (1 Shl bitnum)
End Function

' Изменение минимума и максимума на основе переменной
Function setminmax(v#, vmin# Var, vmax# Var)
        If v#<vmin# Then vmin# = v#
        If v#>vmax# Then vmax# = v#

End Function

Игра

Итак, движок готов, теперь перейдем непосредственно к созданию игры. Возможно, некоторые из вас уже проводят аналогии с нашумевшим хитом Crimsonland от Reflexive. Разовьем основную идею (отражение атаки толпы монстров) в несколько ином направлении. У нас уже есть ландшафт и стреляющие колобки. Пусть главный герой - колобок будет отбиваться от нападения диких колобков. Колобки будут двух видов - водные и сухопутные. Разбросаем по полю ящики с бонусами. Цель - собрать некоторое кол-во определенных предметов. Для этого главный герой должен будет искать их, разбивая ящики. Таким образом, он будет вынужден обойти большую часть поля, отбиваясь от врагов. Сделаем это занятие увлекательным и по возможности долгим (но не до такой степени, чтобы игроку осточертело однообразие).

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

Получение повреждений и разрушение

Сделаем для каждого объекта "запас прочности" (назовем кратко - здоровьем) и пусть попадания пуль этот запас расходуют. При этом, чтобы показать, что в объект попали, пусть он на мгновение покраснеет. Когда здоровье упадет до 0, объект будет в течение некоторого времени исчезать и, затем, уничтожится. Для этих состояний введем переменную в тип объекта - damage_end (время окончания покраснения от повреждений) и перенесем death (время смерти, хотя по сюжету - выброса за пределы сектора) из объекта пули в базовый. Если эти переменные больше Millisecs(), то на основе разницы можно определить коэффициент "покраснения" и прозрачность. Добавим в основной тип метод получения повреждений (возможно, приводящий к смерти) - damage.

Игрок

Главный объект в игре, конечно, сам игрок, точнее то, чем он управляет. Создадим для него отдельный тип на основе типа колобка, ведь это колобок, но ведет он себя иначе. Управление - стандартное Квейкеровское - WASD. Причем, кнопки задают угол движения и если нажаты две одновременно, колобок движется по диагонали (как это реализовано, посмотрите в программе). Компьютерные миры - не Солнечная Система, поэтому мир вертится не вокруг Солнца, а вокруг игрока. То есть, камеру нужно привязать к колобку - главному герою (если просто передвигать обычным образом за ним, то перемещение будет дерганым). И границы перемещения прицела тоже будут зависеть от положения игрока. Для удобства обзора, лучше сделать, чтобы камера стремилась в точку, находящуюся на середине отрезка "игрок - прицел". Так можно будет "глядеть по сторонам". Стрелять игрок будет в направлении прицела, как в Crimsonland.

Характеристики колобков

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

  • Цвет
  • Размер (1/4 - 4/4 тайла)
  • Скорость (0.5 - 2.0 тайлов / сек)
  • Максимальное здоровье (50 - 200)
  • Время перезарядки (300 - 1000 мс)
  • Скорость и время жизни пули (1 - 4 с)
  • Повреждения от попадания пули (1 - 5)
  • Повреждение от укуса (4 - 12)
  • Интервал между укусами (200 - 500 мс)

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

Враги: движение к игроку, укусы, стрельба в него

Бесцельное блуждание врагов - это неспортивно (да и игроку неприятно такое игнорирование), поэтому установим им особое отношение к главному герою. А именно - пусть бегут к нему, стреляют и пытаются укусить. Это реализовать несложно - определяем угол движения функцией ATan2(разница y-координат, разница х-координат) и двигаем колобка в эту сторону, он начинает стрелять, если расстояние между ним и игроком меньше определенного предела (чтобы враги не калечили друг друга вдали от игрока, где попасть в него они в принципе не могут).

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

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

С укусами разберемся так - если колобок столкнулся с игроком, он кусает его и отнимает здоровье, после чего в переменную "перезарядки" укуса заносится время, по достижении которого можно будет куснуть снова. Если колобок "присосался" к игроку, то он просто стоит.

Бонусы

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

  • Цвет - белый
  • Размер (3/4 тайла)
  • Скорость (2 - 4 тайлов / сек)
  • Максимальное здоровье (300 - 800)
  • Время перезарядки (450 - 50 мс)
  • Скорость и время жизни пули (2 - 5 с)
  • Повреждения от попадания пули (2.5 - 12.5)

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

Создадим два новых типа - ящик (коллизионная модель - квадрат, размер - 1.0, доп. переменная - тип бонуса внутри) и бонус (коллизионная модель - круг, метод get (когда игрок берет бонус)). Чтобы бонус привлекал внимание, пусть раскачивается и пульсирует. Для этого внесем пару переменных (чтобы бонусы пульсировали по-разному).

Частицы: разрушение ящиков

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

Временные состояния

Но до танка расти долго, а задать зверским колобкам взбучку хочется уже сейчас. Сделаем временный бонус, увеличивающий огневую мощь. Тоже введем переменную (можно глобальную, т. к. игрок - один), определяющую, когда действие бонуса закончится. А до этого времени пусть колобок мигает желтым цветом (для введения периодичности хорошо подходит функция синуса). Чтобы дикие колобки не лезли, как последнее мясо, на мега-пули, пусть во время расцвета ваших сил драпают от вас (просто прибавить к углу наведения на игрока 180 градусов). Так гораздо интереснее - не так-то просто попасть. Аналогично делаем неуязвимость (индицируется полупрозрачностью), суперскорость. Просто вносим условия, проверяющие, действует ли бонус, и если да, то меняющие характеристики (огневая мощь, скорость) / пресекающие повреждения (неуязвимость). Еще веселая штука - бомба: просто генерируем несколько колец из увеличенных сверхповреждающих пуль. Плюс добавим бонус "здоровье" - восстановление 10% от максимума.

Игрок: телепортация, Сила

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

Часть ящиков, в том числе с источниками энергии, могут оказаться на недоступном островке. Поэтому сделаем возможность телепортации. А чтобы игрок не злоупотреблял этим, пусть нужно будет 5 секунд готовиться - стоять и мужественно терпеть укусы и попадания. Что до эффектов, пусть во время подготовки колобок мигает (меняется прозрачность), затем уменьшается до 0 и появляется, увеличиваясь, в новом месте (если к тому времени его еще никто не занял). В итоге - 4 статуса телепортации - нормальная игра, подготовка, уменьшение, увеличение. Вдобавок, зафиксируем камеру на колобке в период телепортации.

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

Заключение

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

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

И еще: если игра покажется вам сложной - используйте несколько приемов:

  • Старайтесь уходить от скоплений колобков
  • Если вас зажали - применяйте Силу и немедленно выбирайтесь из окружения.
  • Если вы взяли неуязвимость, можно смело телепортироваться подальше от толпы
  • Старайтесь брать скорость и огневую мощь одновременно - так вы устраните больше врагов
  • Перед взятием бомбы, подождите, пока колобки ее окружат.
  • Не спешите брать огневую мощь, если колобки далеко, подождите, пока они подойдут поближе.
  • Во время пользования огневой мощью, не забирайтесь глубоко в рассеянную массу колобков - когда закончится бонус, они опять стянутся к вам и вы можете оказаться в ловушке.
  • Ипользуйте большое увеличение, чтобы определить, где больше ящиков, и старайтесь прорваться туда.
Framework brl.glmax2d ' Базовый модуль - движок на основе OpenGL

Import brl.random ' Генератор случайных чисел
Import BRL.Basic ' Из этого модуля используется команда Incbin
Import BRL.PNGLoader ' Загрузка PNG-изображений

Incbin "2DEngine-NewImages.png" ' Сохраняем в exe-файле изображение

Const sxsize = 800, sysize = 600, color_depth = 32 ' Размеры экрана и глубина цвета

Const tilesize = 64 ' Размер тайла / спрайта

' Вспомогательные константы
Const tilesize2 = tilesize / 2, tilesize4 = tilesize / 4, tilesize8 = tilesize / 8
Const tilesize16 = tilesize / 16, tilesize32 = tilesize / 32

Const sxsize2 = sxsize / 2, sysize2 = sysize / 2
Const sxsize4 = sxsize / 4, sxsize34 = sxsize * 3 / 4
Const sysize34 = sysize * 3 / 4, sxsize24 = sxsize / 2 - 4

Const fxsize = 160, fysize = 120 ' Размеры поля в тайлах
Const fblurq = 5 ' Kол-во размытий для временно генерируемой вспомогательной карты высот поля
Const sand_threshold# = 0.4, grass_threshold# = 0.5 ' Пороги высоты для песка и травы
Global fdx#, fdy# ' Сдвиг отображаемой части поля

Const kolobokq = 500 ' Kол-во диких колобков
Global speedpersec# = 1.0 ' Модификатор скорости (тайлов / сек)
Global angpersec# = 90.0 ' Модификатор угловой скорости (градусов / сек)

Global sc# = 1.0, tilesc# ' Увеличение в пикселах и тайлах
Global dtim# ' Время обработки предыдущего кадра
Global timspeed# ' Модификатор для перемещения с учетом прошедшего времени
Global timang# ' Модификатор для поворота с учетом прошедшего времени
Const minms = 100 ' Ограничитель кадров в секунду для действий объектов
Const cam_speed# = 2.0 ' Относительная скорость реакции камеры на движения мышью
Const magn_speed# = 2.0 ' Относительная скорость реакции масштаба на вращение колесика мыши
Global camx#, camy# ' Текущие координаты камеры

Global layer_order:TList = CreateList() ' Список слоев в порядке отображения
Global actingobj:TList = CreateList() ' Список для активных объектов

Const showcollisions = False ' Показ коллизий (отключен)
Global ccnt, objcnt, chcnt ' Счетчики коллизий, объектов, проверок коллизий в секунду

Const force_reload_time = 7000, force_power# = 3.0 ' Время "перезарядки" Силы, ее мощность
Global force_time = 1000, force_radius# = 5.0 ' Время действия Силы, радиус действия
Global force_reload, force_effect ' Время завершения перезарядки и эффекта Силы

Const fireable_percent = 25 ' Процент стрелющих сухопутных колобков
Const min_fire_distance# = 7.0 ' Минимальная дистанция ведения огня
Const min_enemy_distance = 20 ' Минимальное расстояние до врага в начале игры

Const constant_bonustypeq = 7, temporary_bonustypeq = 5 ' Kол-ва постоянных и временных бонусов
Const constant_bonus_crateq = 10 ' Kол-во ящиков с постоянными бонусами (для каждого)
Const temporary_bonus_crateq = 100 ' Kол-во ящиков с временными бонусами
Const empty_crates_percent = 30 ' Процент пустых ящиков
Const crate_bits_packq = 4 ' Kол-во вариантов кусочков ящика
Const bonustypeq = constant_bonustypeq + temporary_bonustypeq

' Постоянные бонусы
Const BONUS_BULLET_DAMAGE = 0 ' Увеличение повреждений от пуль
Const BONUS_BULLET_SPEED = 1 ' Увеличение скорости пуль
Const BONUS_BULLET_LIFETIME = 2 ' Увеличение времени жизни пули
Const BONUS_RELOAD_TIME = 3 ' Уменьшение интервалов между выстрелами
Const BONUS_MAX_HEALTH = 4 ' Увеличение максимального кол-ва здоровья
Const BONUS_SPEED = 5 ' Увеличение скорости колобка
Const BONUS_ESOURCE = 6 ' Источники энергии (необходимо собрать все для завершения игры)
Global esource_collected, light

' Временные бонусы
Const bonus_threshold = constant_bonustypeq
Const BONUS_HEALTH = bonus_threshold + 0 ' Здоровье
Const BONUS_TEMPORARY_FIREPOWER = bonus_threshold + 1 ' Временное повышение огневой мощи
Const BONUS_BOMB = bonus_threshold + 2 ' Бомба!
Const BONUS_TEMPORARY_SPEED = bonus_threshold + 3 ' Временное ускорение
Const BONUS_TEMPORARY_INVULNERABILITY = bonus_threshold + 4 ' Временная неуязвимость

Global temporary_firepower, temporary_speed, temporary_invulnerability ' Время окончания действия бонусов

Const fading_time = 1000, damage_time = 400 ' Время "затухания", "покраснения" от повреждений
Const NOT_YET = 1000000000, INDESTRUCTIBLE = 1000000000 ' Kонстанты "еще не умер", "неразрушимый"

Const TM_IDLE = 0 ' Играем как обычно
Const TM_READY = 1 ' Готовимся к телепортации (ждем)
Const TM_DECREASING = 2 ' Уменьшаемся
Const TM_ENLARGING = 3 ' Вырастаем на новом месте
Global teleport = NOT_YET, teleport_mode = TM_IDLE ' Время окончания цикла телепортации, режим
Const teleport_ready_time = 5000 ' Время подготовки к телепортации
Const max_teleport_radius = 50 ' Максимальное расстояние в тайлах для телепортации

' Слой
Type layer_obj Abstract
        Field collision_with:TList = CreateList() ' Список слоев, с которыми коллизирует данный

        Method collides_with(layer:layer_obj)
                If tile_layer_obj(layer) Then RuntimeError "Tile layers can't collide - use tile collision layer"
                collision_with.addlast layer
        End Method

        Method draw()
        End Method
End Type

' Тайловый слой
Const TILE_DONT_DRAW = -1 ' Kонстанта "Не рисовать тайл"
Type tile_layer_obj Extends layer_obj
        Field image:TImage ' Изображения тайлов
        Field frame[fxsize, fysize] ' Номера тайлов для каждой клетки

        Method collides_with(layer:layer_obj)
                RuntimeError "Tile layers can't collide - use tile collision layer"
        End Method


        Function add:tile_layer_obj(tile_image:TImage, clearing = True) ' Добавление тайлового слоя
                l:tile_layer_obj = New tile_layer_obj
                l.image = tile_image
                If clearing Then
                        For y = 0 Until fysize ' Установка "не рисовать тайл" для всех клеток
                                For x = 0 Until fxsize
                                        l.frame[x, y] = TILE_DONT_DRAW
                                Next
                        Next
                End If
                layer_order.addlast l ' Занесение слоя в список отображения
                Return l
        End Function

        Method draw() ' Прорисовка слоя
                SetScale tilesc#, tilesc#
                scr2field 0, 0, x1#, y1#
                scr2field sxsize - 1, sysize - 1, x2#, y2#

                xx1 = Max(0, Floor(x1#)) ' Определение границ куска поля, попадающего в облась зрения
                xx2 = Min(Ceil(x2#), fxsize - 1)
                yy1 = Max(0, Floor(y1#))
                yy2 = Min(Ceil(y2#), fysize - 1)

                For y = yy1 To yy2
                        For x = xx1 To xx2
                                If frame[x, y] >= TILE_DRAW Then ' Проверка, нужно ли рисовать тайл
                                        field2scr x, y, sx#, sy#
                                        DrawImage image, sx#, sy#, frame[x, y]
                                End If
                        Next
                Next
        End Method
End Type

' Слой тайловой коллизии
Type tile_collision_layer_obj Extends layer_obj
        Field collision[fxsize, fysize] ' Kоллизия с тайлом (да / нет)

        Function add:tile_collision_layer_obj()
                Return New tile_collision_layer_obj
        End Function
End Type

 ' Слой объектов
Type object_layer_obj Extends layer_obj
        Field objects:TList[fxsize, fysize] ' Список объектов для каждой клетки, находящихся на ней

        Function add:object_layer_obj()
                l:object_layer_obj = New object_layer_obj
                For y = 0 Until fysize ' Инициализация списков
                        For x = 0 Until fxsize
                                l.objects[x, y] = CreateList()
                        Next
                Next
                layer_order.addlast l
                Return l
        End Function

        Method draw()
                scr2field 0, 0, x1#, y1#
                scr2field sxsize - 1, sysize - 1, x2#, y2#

                xx1 = Max(0, Floor(x1# - 0.5))
                xx2 = Min(Floor(x2# + 0.5), fxsize - 1)
                yy1 = Max(0, Floor(y1# - 0.5))
                yy2 = Min(Floor(y2# + 0.5), fysize - 1)

                For y = yy1 To yy2
                        For x = xx1 To xx2
                                For o:base_obj = EachIn objects[x, y]
                                        o.draw
                                Next
                        Next
                Next
                reset_transformations
        End Method
End Type

Const CT_IMMATERIAL = 0 ' Тип коллизионной модели - нематериальный
Const CT_CIRCULAR = 1 ' Тип коллизионной модели - круг
Const CT_SQUARE = 2 ' Тип коллизионной модели - квадрат
' Базовый тип для объектов
Type base_obj
        Field x#, y#, size# = 1, angle# ' Kоординаты, размер (в тайлах), угол поворота спрайта объекта
        Field speed# ' Скорость объекта (тайлов / сек)
        Field moving_angle# ' Текущий угол движения
        Field r = 255, g = 255, b = 255 ' Цвет объекта (по умолчанию белый)
        Field image:TImage, frame ' Изображение для объекта, кадр
        Field tilex, tiley ' Kоординаты тайла, на котором находится объект
        Field act_link:TLink, tile_link:TLink ' Ссылки на этот объект из списков активных объектов и объектов клетки
        Field layer:object_layer_obj ' Слой объекта
        Field coll_type = CT_CIRCULAR, radius# = 0.5 ' Тип модели коллизии и ее радиус
        Field health# ' Здоровье объекта
        Field death = NOT_YET, damage_end ' Время смерти (еще не определено), время окончания "покраснения"

        Const ONLY_ON_GROUND = True ' Kонстанта для размещения объекта только на суше
        Method place_find(onlyonground = False) ' Поиск места для размещения объекта
                Repeat
                        x = Rnd(1.0, fxsize - 1.01)
                        y = Rnd(1.0, fysize - 1.01)
                        tilex = Floor(x)
                        tiley = Floor(y)
                        ' Определение сухопутности / подводности колобка
                        If layer_sand.collision(tilex, tiley) Then layer = layer_ground_koloboks Else layer = layer_water_koloboks
                        ' Проверка нахождения на суше (для размещения только на суше) и на отсутствие коллизий
                        If layer = layer_ground_koloboks Or onlyonground = False Then If Not collision(x#, y#) Then Exit
                Forever
        End Method

        Method random_color() ' Задание случайного (но не очень темного) цвета для объекта
                Repeat
                        r = Rand(0, 255)
                        g = Rand(0, 255)
                        b = Rand(0, 255)
                Until r + g + b >= 255
        End Method

        Const ACTIVE = True, INACTIVE = False ' Kонстанты "Активный", "Не активный"
        Method register(acting = ACTIVE) ' Занесение объекта в списки (регистрация)
                tilex = Floor(x#)
                tiley = Floor(y#)
                tile_link = layer.objects(tilex, tiley).addlast(Self) ' Занесение в список объектов клетки
                If acting Then act_link = actingobj.addlast(Self) ' Занесение в список активных объектов
                objcnt:+1
        End Method

        Method draw() ' Рисование объекта
                field2scr x#, y#, sx#, sy#
                SetScale size# * tilesc#, size# * tilesc#
                SetRotation angle#

                dmg = damage_end - MilliSecs() ' "Покраснение" от повреждений
                If dmg > 0 Then
                        k1# = 1.0 * dmg / damage_time; k2# = 1.0 - k1#
                        SetColor k1# * 255 + k2# * r, k2# * g, k2# * b
                Else
                        SetColor r, g, b ' Установка естественного цвета
                End If

                If death = NOT_YET Then
                        SetAlpha 1 ' Если еще не начал исчезать, то непрозрачный
                Else
                        SetAlpha limit(.001 * (death - MilliSecs()), 0, 1) ' Иначе потихоньку исчезает
                        If death<MilliSecs() Then destroy ' И в конце уничтожается совсем
                End If

                If Self = player Then
                        If temporary_firepower > MilliSecs() Then
                                col = 191 + 64 * Sin(MilliSecs()) ' Мерцающий желтый цвет игрока с огневой мощью
                                SetColor col, col, 0
                        End If
                        If temporary_invulnerability > MilliSecs() Then SetAlpha 0.5 ' Полупрозрачность неуязвимого
                        Select teleport_mode
                                Case TM_READY; SetAlpha 0.75 + 0.25 * Sin(MilliSecs()) ' Циклическое изменение прозрачности во время подготовки к телепортации
                                Case TM_DECREASING; s# = sc# * size# / tilesize * Max(0.0, 1.0 * (teleport - MilliSecs()) / fading_time); SetScale s#, s# ' Уменьшение
                                Case TM_ENLARGING; s# = sc# * size# / tilesize * Min(1.0, 1.0 - 1.0 * (teleport - MilliSecs()) / fading_time); SetScale s#, s# ' Появление в новом месте
                        End Select
                End If

                DrawImage image, sx#, sy#, frame
        End Method

        Method move(newx#, newy#) ' Kорректное перемещение
                newtilex = Floor(newx#)
                newtiley = Floor(newy#)
                If tilex <> newtilex Or tiley <> newtiley Then ' Если объект переместился в другую клетку, то
                        removeLink tile_link ' Удаление из списка старой клетки
                        tilex = newtilex
                        tiley = newtiley
                        tile_link = layer.objects[tilex, tiley].addlast(Self) ' Занесение в список новой
                End If
                x# = newx#
                y# = newy#
        End Method

        Method try_move(newx#, newy#)
                If Not collision(newx#, newy#) Then move newx#, newy#; Return True
        End Method

        Method try_move_ang(ang#, spd#, ma_change = False)
                If try_move(x# + timspeed# * Cos(ang#) * spd#, y# + timspeed# * Sin(ang#) * spd#) Then
                        If ma_change Then moving_angle# = ang#
                        Return True
                End If
        End Method

        Method collision2(o:base_obj, newx#, newy#) ' Проверка объекта на столкновение с другим
                Select True
                        Case coll_type = CT_CIRCULAR ' Если модель данного объекта - круг
                                Select True
                                        Case o.coll_type = CT_CIRCULAR ' И модель второго объекта - тоже круг (круг с кругом)
                                                dx# = newx# - o.x#
                                                dy# = newy# - o.y#
                                                ' Проверяем, меньше ли расстояние между объектами, чем сумма их радиусов
                                                If Sqr(dx# * dx# + dy# * dy#) < o.radius# + radius# Then ccnt:+1; Return True
                                        Case o.coll_type = CT_SQUARE ' А если модель второго объекта - квадрат (круг с квадратом)
                                                If (o.x# - o.radius# <= newx# And newx# <= o.x# + o.radius#) Or (o.y# - o.radius# <= newy# And newy# <= o.y# + o.radius#) Then
                                                        dx# = Abs(newx# - o.x#)
                                                        dy# = Abs(newy# - o.y#)
                                                        sumr# = o.radius# + radius#
                                                        If dx# < sumr# And dy# < sumr# Then ccnt:+1; Return True
                                                Else
                                                        dx# = Min(Abs(newx# - o.x# - o.radius#), Abs(newx# - o.x# + o.radius#))
                                                        dy# = Min(Abs(newy# - o.y# - o.radius#), Abs(newy# - o.y# + o.radius#))
                                                        If Sqr(dx# * dx# + dy# * dy#) < radius# Then ccnt:+1; Return True
                                                End If
                                        Default ' Но вот если второй объект нематериален - столкновения нет
                                                Return False
                                End Select
                        Case coll_type = CT_SQUARE ' Если модель данного объекта - квадрат
                                If o.coll_type = CT_SQUARE Then ' И модель второго объекта - тоже квадрат
                                        dx# = Abs(newx# - o.x#)
                                        dy# = Abs(newy# - o.y#)
                                        sumr# = o.radius# + radius#
                                        ' Проверяем, меньше ли модуль разности соотв. координат, чем сумма радиусов
                                        If dx# < sumr# And dy# < sumr# Then ccnt:+1; Return True
                                Else ' Иначе проверяем столкновение второго объекта с данным (меняем местами)
                                        Return o.collision2(Self, newx#, newy#)
                                End If
                        Default ' Нематериальный объект не коллизирует
                                Return False
                End Select
        End Method

        Method collision(newx#, newy#) ' Проверка данного объекта на столкновение с чем бы то ни было
                ' Столкновение с границами поля (это осложнит другие проверки, поэтому выходим)
                If newx# < 1.0 Or newy# < 1.0 Or newx# >= fxsize - 1.0 Or newy# >= fysize - 1.0 Then
                        boundaries_collision_act
                        Return True
                End If
                For l:layer_obj = EachIn layer.collision_with ' Цикл по всем слоям коллизии
                        tl:tile_collision_layer_obj = tile_collision_layer_obj(l)
                        If tl Then ' Если слой - тайлово-коллизионный, то
                                For yy = Floor(newy# - radius#) To Floor(newy# + radius#)
                                        For xx = Floor(newx# - radius#) To Floor(newx# + radius#)
                                                If tl.collision(xx, yy) Then
                                                        tile_object.x# = xx + 0.5
                                                        tile_object.y# = yy + 0.5
                                                        If collision2(tile_object, newx#, newy#) Then collided = True; tile_collision_act xx, yy
                                                End If
                                        Next
                                Next
                        Else ' Иначе слой - объектный, тогда
                                ol:object_layer_obj = object_layer_obj(l)
                                x2 = Floor(newx#)
                                y2 = Floor(newy#)
                                For yy = y2 - 1 To y2 + 1
                                        For xx = x2 - 1 To x2 + 1
                                                For o:base_obj = EachIn ol.objects[xx, yy]
                                                        If Self<>o Then
                                                                chcnt:+1
                                                                If showcollisions Then ' Показ проверок коллизий линиями
                                                                        field2scr o.x#, o.y#, sx1#, sy1#
                                                                        field2scr newx#, newy#, sx2#, sy2#
                                                                        DrawLine sx1#, sy1#, sx2#, sy2#
                                                                End If
                                                                If collision2(o, newx#, newy#) Then collided = True; object_collision_act o
                                                        End If
                                                Next
                                        Next
                                Next
                        End If
                Next
                Return collided
        End Method

        Method act() ' Действия объектов
        End Method

        Method object_collision_act(o:base_obj) ' Действия при столкновении с объектами
        End Method

        Method tile_collision_act(xx, yy) ' Действия при столкновении с тайлами
        End Method

        Method boundaries_collision_act() ' Действия при столкновении с границами карты
        End Method

        Method damage(amount#) ' Получение повреждений
                If death < NOT_YET Then Return ' Если уже исчезает, то с него хватит
                If health# = INDESTRUCTIBLE Then Return ' Если в принципе неуязвим, тогда тоже выходим
                If Self=player And temporary_invulnerability>MilliSecs() Then Return ' Если временно неуязвим - выходим
                health# = health# - amount# ' Уменьшаем здоровье
                damage_end = damage_time + MilliSecs() ' Задаем "покраснение"
                If health <= 0 Then ' Если здоровье на нуле, то
                        death = fading_time + MilliSecs() ' Объект начинает исчезать
                        ' Ящик исчезает сразу, остальные становятся нематериальными
                        If crate_obj(Self)=Null Then coll_type = CT_IMMATERIAL Else death = 0
                End If
        End Method

        Method destroy() ' Kорректное уничтожение объекта
                If act_link<>Null Then removeLink act_link ' Удаление объекта из списка активных
                removeLink tile_link ' Удаление объекта из списка объектов клетки
                objcnt:-1
        End Method
End Type

Global tile_object:base_obj = New base_obj
tile_object.radius# = 0.5
tile_object.coll_type = CT_SQUARE

' Базовый тип для колобков
Type kolobok_obj Extends base_obj
        Field bullet_reload, bullet_reload_time ' Время окончания перезарядки, время перезарядки
        Field bullet_speed#, bullet_lifetime = 2000 ' Скорость и время жизни пули этого колобка
        Field bullet_damage# ' Повреждения от попадания пулей
        Field max_health# = 1 ' Максимальное здоровье
        Field bite_damage#, bite_reload ' Повреждение от укуса и время возможности следующего укуса
        Field bite_reload_time, bite ' Интервал между укусами, вспомогательный флаг

        Function create:kolobok_obj() ' Создание дикого колобка
                o:kolobok_obj = New kolobok_obj
                o.random_color
                o.image = kolobok
                o.moving_angle# = Rnd(0, 360)
                If Rand(1, 100) > fireable_percent And o.frame = 1 Then ' Параметры для не умеющих стрелять
                        o.bullet_reload = 1000000000
                        o.bullet_reload_time = 1000
                        o.bullet_lifetime = 0
                        o.bullet_damage# = 0
                        o.bullet_speed# = 0
                Else ' Параметры для умеющих стрелять
                        o.bullet_reload_time = Rand(300, 1000)
                        o.bullet_lifetime = Rand(1000, 4000)
                        o.bullet_damage# = Rnd(1, 5)
                        o.bullet_speed# = Rnd(0.5, 1.5)
                End If
                o.max_health = Rand(50, 200)
                o.health = o.max_health
                o.bite_damage# = Rnd(4, 12)
                o.bite_reload_time = Rand(200, 500)
                ' Расчет размера и скорости по совокупности параметров
                o.size# = (o.max_health - 50) / 150.0 + o_bullet_speed# / 1.5  + o.bullet_lifetime / 4000.0
                o.size#:+ o.bullet_damage# / 5.0 + (o.bite_damage# - 4.0) / 8.0 + (500 - o.bite_reload_time) / 300.0
                o.size#:+ (1000.0 - o.bullet_reload_time) / 1000.0
                o.size# = limit(o.size / 7.0 * 1.0 + 0.25, 0, 1.0)
                o.speed# = (1.25 - o.size#) * 2
                o.radius# = 0.4 * o.size#
                o.place_find
                o.frame = (o.layer = layer_ground_koloboks) ' Для водных колобков - 0-й кадр, для наземных - 1-й
                o.register
                Return o
        End Function

        Method draw() ' Рисование колобка
                super.draw
                bar_draw
        End Method

        Method bar_draw() ' Рисование полоски здоровья
                field2scr x#, y#, sx#, sy#
                barsize = 1.0 * size# * sc# ' Определение длины (по размеру колобка в пикселах)
                If barsize > 4 And max_health <> health Then
                        barsize2 = barsize / 2
                        barheight = limit(Floor(max_health / 50) + 1, 1, 6) ' Определение высоты по максимуму здоровья
                        SetRotation 0
                        SetScale 1, 1
                        SetGrayColor 255
                        k# = 1.0 * health / max_health
                        DrawEmptyRect sx# - barsize2, sy# - barsize2 - 6, barsize-1, barheight + 2
                        SetColor 255 * (1.0 - k#), 255 * k#, 0 ' Задание цвета: ближе к максимуму - зеленый, ближе к 0 - красный
                        DrawRect sx# - barsize2 + 1, sy# - barsize2 - 5, k# * (barsize - 2), barheight
                End If
        End Method

        Method act() ' Действия колобка
                If death < NOT_YET Then Return ' Если исчезает, то действовать прекращает

                angle# = ATan2(player.y - y#, player.x - x#) ' Угол "наведения" на игрока

                If force_effect > MilliSecs() Then ' Определение расстояния до игрока, если действует Сила
                        rad# = Sqr((player.x# - x#) * (player.x# - x#) + (player.y# - y#) * (player.y# - y#))
                Else
                        rad# = 10000
                End If
                If rad# <= force_radius# Then ' Если расстояние до игрока меньше радиуса действия Силы, то
                        ' Пытаемся удалиться от игрока
                        try_move_ang angle# + 180.0, force_power# * Sin(90.0 * (force_radius# - rad#) / force_radius#)
                Else
                        ' Иначе определяем, в какую сторону вращаться
                        dang# = calc_dangle(moving_angle#, angle# + 180 * (temporary_firepower > MilliSecs()))
                        ' И пробуем переместиться, повернувшись
                        If Not try_move_ang(moving_angle# + timang# * (1 - 2 * (dang# < 0)), speed#, True) Then
                                 ' Если переместиться не удалось, то
                                If bite Then ' Если можно укусить, то стоим и кусаем...
                                        moving_angle# = angle#
                                        bite = False
                                Else ' Если нельзя, то пробуем сместиться в сторону
                                        If Not try_move_ang(moving_angle# + 90.0 * (1 - 2 * Rand(0, 1)), speed#, True) Then
                                                 ' Если не получается, пробуем сместиться в другую
                                                If Not try_move_ang(moving_angle# + 180.0, speed#, True) Then moving_angle# = Rnd(0.0, 360.0)
                                                ' Если совсем нас зажали, в следующий раз попробуем случайный угол
                                        End If
                                End If
                        End If
                End If

                If bullet_reload < MilliSecs() Then ' Если пришло время стрелять
                        ' И расстояние до игрока меньше максимального
                        If Sqr((player.x# - x#) * (player.x# - x#) + (player.y# - y#) * (player.y# - y#)) <= min_fire_distance# Then
                                ' Создаем список и заносим туда всех подыодных колобков
                                near:TList = nearly_objects(CreateList(), tilex, tiley, 2, layer_water_koloboks)
                                ' А также наземных
                                near = nearly_objects(near, tilex, tiley, 2, layer_ground_koloboks)
                                ' Но удаляем себя и игрока
                                near.remove player
                                near.remove Self
                                ' Потому что будем проверять, не находится ли другой колобок на пути пули, выпущенной в игрока
                                For o:base_obj = EachIn near
                                        If kolobok_obj(o) Then
                                                ' Вычисляем угол между вектором выстрела и отрезком, соединяющим центры стреляющего и проверяемого колобка
                                                dang# = Abs(calc_dangle(ATan2(y# - o.y#, x# - o.x#), ATan2(y# - player.y#, x# - player.x#)))
                                                ' Проверяем не меньше ли радиуса колобка длина дуги
                                                If Pi * Sqr((x# - o.x#) * (x# - o.x#) + (y# - o.y#) * (y# - o.y#)) * dang# / 180.0 < o.radius Then Return
                                        End If
                                Next
                                ' Если на пути выстрела нет колобков - смело стреляем
                                fire
                        End If
                End If
        End Method

        Method object_collision_act(o:base_obj)
                If o = player Then ' Проверяем, столкнулись ли с игроком
                        If bite_reload < MilliSecs() Then ' И готовы ли кусать
                                player.damage(bite_damage) ' Если да, то кусаем
                                bite_reload = MilliSecs() + bite_reload_time
                        End If
                        bite = True ' Флаг показывает, что мы вцепились в игрока и можно стоять на месте
                End If
        End Method

        Method fire()
                ' Поправка на скорость при временном ускорении
                If Self=player And temporary_speed > MilliSecs() Then spd# = 6.0 Else spd# = speed#  
                If Self=player And temporary_firepower > MilliSecs() Then ' Стрельба при огневой мощи
                        bullet_obj.create x#, y#, 0.75, angle#, 4.0 + spd#, 2000, 25, Self, 0.5 * 0.3, r, g, b
                        bullet_reload = MilliSecs() + 40
                Else ' Стрельба в обычном режиме
                        bullet_obj.create x#, y#, 0.5 * size#, angle#, bullet_speed# + spd#, bullet_lifetime, bullet_damage, Self, size# * 0.3, r, g, b
                        bullet_reload = MilliSecs() + bullet_reload_time
                End If
        End Method

End Type

' Игрок
Type player_obj Extends kolobok_obj
        Function create:player_obj()
                o:player_obj = New player_obj
                o.x# = 0.5 * fxsize ' Помещаем игрока в центре
                o.y# = 0.5 * fysize
                o.size# = 0.75
                o.radius# = 0.4 * o.size
                o.image = kolobok
                o.frame = 2
                o.speed# = 2.0
                o.bullet_reload_time = 450
                o.bullet_speed# = 1.0
                o.bullet_damage# = 2.5
                o.max_health# = 300
                o.health# = o.max_health#
                o.layer = layer_ground_koloboks
                Repeat ' Двигаем игрока вправо, пока он не окажется полностью на суше
                        o.x:+0.5
                Until Not o.collision(o.x#, o.y#)
                o.register
                Return o
        End Function

        Method act() ' Действия игрока
                If death < NOT_YET Then Return ' Если уже "замочили", то плюем в потолок
                If teleport_mode = TM_IDLE Then ' Если телепортация не в процессе, то
                        If KeyHit(KEY_SPACE) Then ' Если нажат пробел
                                If Sqr(targetx# * targetx# + targety# * targety#) <= max_teleport_radius Then ' И расстояние не больше максимума
                                        If Not collision(player.x# + targetx#, player.y# + targety#) Then ' А также в месте появления нет коллизий
                                                teleport_mode = TM_READY ' То готовимся к телепортации
                                                teleport = MilliSecs() + teleport_ready_time ' В течение заданного времени
                                        End If
                                End If
                        End If
                Else
                        If teleport <= MilliSecs() Then ' Если цикл завершился, то
                                teleport_mode = teleport_mode + 1 ' Переходим к следующему
                                teleport = MilliSecs() + fading_time ' Задаем время цикла
                                If teleport_mode = TM_ENLARGING And Not collision(player.x# + targetx#, player.y# + targety#) Then
                                        move player.x# + targetx#, player.y# + targety# ' Перемещаемся в точку телепортации после уменьшения
                                        fdx2# = fdx2# - targetx#
                                        fdy2# = fdy2# - targety#
                                        targetx# = 0
                                        targety# = 0
                                ElseIf teleport_mode > TM_ENLARGING Then ' Если цикл увеличения завершен
                                        teleport_mode = TM_IDLE ' то сбрасываем режим
                                End If
                        End If
                        Return ' При телепортации нужно стоять смирно, поэтому выходим из метода
                End If

                If bullet_reload < MilliSecs() And MouseDown(1) Then fire ' Если подошло время стрелять и нажат "огонь" - стреляем
               
                If MouseDown(2) And force_reload <= MilliSecs() Then ' Используем Силу если нажата кнопка и колобок готов
                        force_reload = force_reload_time + MilliSecs()
                        force_effect = force_time + MilliSecs()
                End If
                If force_effect > MilliSecs() Then size# = 0.75 + 0.5 * (force_effect - MilliSecs()) / force_time Else size# = 0.75 ' "Пухнем" от Силы

                mov = False ' Определяем угол вектора движения, основываясь на нажатых клавишах
                If KeyDown(KEY_S) Then ang2# = 90.0; mov = True
                If KeyDown(KEY_W) Then ang2# = - 90.0; mov = True
                ' Если одна из предыдущих клавиш нажата - модифицируем угол с учетом этого
                If KeyDown(KEY_A) Then ang2# = 180.0 - 0.5 * ang2#; mov = True
                If KeyDown(KEY_D) Then ang2# = 0.5 * ang2#; mov = True
       
                If Not mov Then Return ' Если стоим, то больше здесь делать нечего
       
                ' Модификатор скорости для временного ускорения
                If temporary_speed > MilliSecs() Then spd# = 6.0 Else spd# = speed#
                 ' Если нет коллизий, перемещаемся
                try_move_ang ang2#, spd#
        End Method

        Method destroy()
                ' Нам задали взбучку - истошно орем
                Notify "AAAAAAAAAAAAAA!!! Whyyyyy???!!!"
                End
        End Method

        Method object_collision_act(o:base_obj)
                ' Если столкнулись с бонусом и он еще не взят - берем
                bo:bonus_obj = bonus_obj(o)
                If bo Then If bo.death = NOT_YET Then bo.get
        End Method
End Type

' Пуля
Type bullet_obj Extends base_obj
        Field parent:base_obj, damage# ' Указатель на стреляющего и коэффициент повреждения

        ' Создаем пулю: начальные координаты, размер, угол, скорость, время жизни, повреждения, стреляющий, отступ от координат
        Function create:bullet_obj(bx#, by#, bsize#, bangle#, bspeed#, blifetime, bdamage#, bparent:base_obj=Null, d#=0, br=255, bg=255, bb=255)
                bul:bullet_obj = New bullet_obj
                bul.layer = layer_bullets
                bul.x# = bx# + Cos(bangle#) * d# ' Смещение отн. данных координат
                bul.y# = by# + Sin(bangle#) * d#
                bul.r = br
                bul.g = bg
                bul.b = bb
                bul.image = bullet
                bul.parent = bparent
                bul.angle# = bangle#
                bul.size# = bsize#
                bul.speed# = bspeed#
                bul.radius = bsize# * 0.25
                bul.death = MilliSecs() + blifetime
                bul.damage# = bdamage#
                bul.register
        End Function

        Method act() ' Действует просто - летит вперед до столкновения
                move x# + timspeed# * Cos(angle#) * speed#, y# + timspeed# * Sin(angle#) * speed#
                collision x#, y#
                If MilliSecs() > death Then destroy ' Время жизни ограничено с появления
        End Method

        Method object_collision_act(o:base_obj) ' Повреждение встреченного объекта
                If o <> parent Then
                        ccnt:+1
                        o.damage(damage)
                        destroy
                End If
        End Method
       
        Method boundaries_collision_act() ' Уничтожается при столкновении с границами
                destroy
        End Method
End Type

' Ящик
Type crate_obj Extends base_obj
        Field bonus_type ' Тип бонуса внутри

        Function create:crate_obj(b_type)
                o:crate_obj = New crate_obj
                o.image = crate
                o.place_find ONLY_ON_GROUND
                o.bonus_type = b_type
                o.coll_type = CT_SQUARE
                o.health = 10
                If o.speed >= bonustypeq Then o.speed = -1
                o.register INACTIVE
        End Function

        Method destroy() ' Взрыв ящика
                If bonus_type >= 0 Then bonus_obj.create x#, y#, bonus_type ' Создание бонуса на его месте

                offset = Rand(0, crate_bits_packq - 1) * 16 ' Случайный выбор пакета кусочков
                For yy = 0 To 3 ' Создание 16 разлетающихся кусочков
                        For xx = 0 To 3
                                o:crate_bits_obj = New crate_bits_obj
                                o.dx# = Rnd(-1.0, 1.0) + xx - 1.5
                                o.dy# = Rnd(-1.0, 1.0) + yy - 1.5
                                o.x# = x# + 0.125 * (xx * 2 - 3)
                                o.y# = y# + 0.125 * (yy * 2 - 3)
                                o.image = crate_bits
                                o.frame = xx + yy * 4 + offset
                                o.layer = layer_top_scenery
                                o.death = 2000 + MilliSecs()
                                o.register
                        Next
                Next
               
                super.destroy ' Уничтожение объекта ящика - вызов процедуры из base_obj
        End Method

End Type

' Kусочки ящика
Type crate_bits_obj Extends base_obj
        Field dx#, dy# ' Приращения для движения

        Method act() ' Kусочки просто летят 2 секунды
                x# = x# + dx# * timspeed#
                y# = y# + dy# * timspeed#
        End Method
End Type

' Бонус
Type bonus_obj Extends base_obj
        Field dangle#, rotation_period!, pulsing_period! ' Переменные для шевеления

        Function create:bonus_obj(x#, y#, b_type)
                o:bonus_obj = New bonus_obj
                o.x=x#
                o.y=y#
                o.image = bonus
                o.frame = b_type
                o.health = INDESTRUCTIBLE ' Бонус неуничтожим
                o.dangle# = Rnd(5, 30)
                o.rotation_period! = Rnd(0.5, 0.1)
                o.pulsing_period! = Rnd(0.5, 0.1)
                o.layer = layer_ground_koloboks
                o.register
        End Function

        Method draw()
                angle# = dangle# * Sin(rotation_period! * MilliSecs()) ' Kолебания угла
                size# = 0.8 + 0.2 * Sin(pulsing_period! * MilliSecs()) ' Kолебания размера
                super.draw
        End Method

        Method get() ' Берем бонус
                ' Постоянные бонусы изменяют характеристики на значение, зависящее
                ' от количества таких бонусов на карте (если собрать все бонусы, то характеристики изменятся
                ' от начального до фиксированного значения, они указаны в комментариях
                Select frame
                        Case BONUS_BULLET_DAMAGE; player.bullet_damage:+ 10.0 / constant_bonus_crateq ' 2.5 - 12.5 ед
                        Case BONUS_BULLET_SPEED; player.bullet_speed:+ 3.0 / constant_bonus_crateq ' 1.0 - 4.0 тайлов / сек
                        Case BONUS_BULLET_LIFETIME; player.bullet_lifetime:+ 3000 / constant_bonus_crateq ' 2 - 5 сек
                        Case BONUS_RELOAD_TIME; player.bullet_reload_time:- 400 / constant_bonus_crateq ' 0.5 - 0.1 сек
                        Case BONUS_MAX_HEALTH; player.max_health:+ 500.0 / constant_bonus_crateq; player.health = player.max_health ' 300 - 800 ед
                        Case BONUS_SPEED; player.speed:+ 2.0 / constant_bonus_crateq ' 2.0 - 4.0 тайлов / сек
                        Case BONUS_HEALTH
                                If player.health = player.max_health Then Return ' Если полное здоровье - бонус не берем
                                player.health = limit(player.health + 0.15 * player.max_health, 0, player.max_health) ' +15% от максимума
                        Case BONUS_TEMPORARY_FIREPOWER; temporary_firepower=MilliSecs() + 10000 ' 10 секунд огневой мощи
                        Case BONUS_TEMPORARY_SPEED; temporary_speed=MilliSecs() + 15000 ' 15 секунд ускорения
                        Case BONUS_TEMPORARY_INVULNERABILITY; temporary_invulnerability=MilliSecs() + 20000 ' 20 секунд неуязвимости
                        Case BONUS_BOMB
                                For n1 = 2 To 4 ' Генерация осколков бомбы
                                        n2 = 0
                                        While n2 < 360
                                                bullet_obj.create x#, y#, 1, n2, n1, (5 - n1) * 800, 35, player, player.size# * 0.4
                                                n2 = n2 + 10 * (n1 - 1)
                                        Wend
                                Next
                        Case BONUS_ESOURCE
                                esource_collected = esource_collected + 1
                                If esource_collected = constant_bonus_crateq Then light = MilliSecs() + fading_time ' Да будет свет, если собрана вся мана
                End Select
                death = fading_time + MilliSecs() ' Исчезновение бонуса
                coll_type = CT_IMMATERIAL ' Бонус становится нематериальным
        End Method
End Type

SeedRnd MilliSecs() ' Для того, чтобы каждый раз получать новую последовательность случайных чисел

SetGraphicsDriver GLMax2DDriver() ' Установка драйвера отображения графики OpenGL
Graphics sxsize, sysize, color_depth
AutoImageFlags FILTEREDIMAGE | MIPMAPPEDIMAGE | DYNAMICIMAGE
SetBlend ALPHABLEND
reset_transformations

' Загружаем изображения с альфа-каналом из exe-файла
Global images:TPixmap = LoadPixmapPNG("incbin::2DEngine-NewImages.png")

' Создаем текстуры для тайлов
tex_water:TImage = tiles_grab(0, 1, False)
tex_sand:TImage = tiles_grab(1, 1, False)
tex_grass:TImage = tiles_grab(2, 1, False)

' Вырезаем изображения
Global kolobok:TImage = tiles_grab(3, 3)
Global bullet:TImage = tiles_grab(6)
Global bonus:TImage = tiles_grab(7, 12)
Global crate:TImage = tiles_grab(19)
Global crate_bits:TImage = CreateImage(tilesize4, tilesize4, crate_bits_packq * 16)
For n = 0 To 3
        For yy = 0 To 3
                For xx = 0 To 3
                        new_grab crate_bits, n * tilesize + xx * tilesize4, yy * tilesize4 + tilesize * 5, n * 16 + yy * 4 + xx
                Next
        Next
Next
Global target:TImage = tiles_grab(24), targetx#, targety#

' Создаем в пакете изображений тайлов текстуру воды
tile_tex:TImage = CreateImage(tilesize, tilesize, 513)
pixmap:TPixmap = LockImage(tile_tex,0)
pixmap.paste(LockImage(tex_water)), 0, 0
UnlockImage tile_tex, 0
UnlockImage tex_water
' И две библиотеки - переход от воды к песку и от песка к траве
tile_lib_create tex_water, tex_sand, 4.0 / tilesize, 360.0, tile_tex, 1
tile_lib_create tex_sand, tex_grass, 4.0 / tilesize, 720.0, tile_tex, 257

' Делаем "пирог" из слоев
Global layer_tiles:tile_layer_obj = tile_layer_obj.add(tile_tex) ' Сначала - тайлы
Global layer_bullets:object_layer_obj = object_layer_obj.add() ' Затем пули и осколки бомб
Global layer_water_koloboks:object_layer_obj = object_layer_obj.add() ' После - водные колобки
Global layer_ground_koloboks:object_layer_obj = object_layer_obj.add() ' Потом - наземные колобки, ящики и бонусы
Global layer_top_scenery:object_layer_obj = object_layer_obj.add() ' Сверху - осколки ящиков

' Создаем слои тайловой коллизии
Global layer_water:tile_collision_layer_obj = tile_collision_layer_obj.add() ' Слой "твердой" воды
Global layer_sand:tile_collision_layer_obj = tile_collision_layer_obj.add() ' Слой "твердого" песка

' Определяем что с чем коллизирует
layer_water_koloboks.collides_with layer_water_koloboks ' Водные колобки - между собой
layer_water_koloboks.collides_with layer_sand ' Водные колобки - с коллизионным слоем песка
layer_ground_koloboks.collides_with layer_ground_koloboks ' Сухопутные колобки - между собой
layer_ground_koloboks.collides_with layer_water ' Сухопутные колобки - с коллизионным слоем воды
layer_bullets.collides_with layer_water_koloboks ' Пули - с сухопутными колобками
layer_bullets.collides_with layer_ground_koloboks ' Пули - с водными колобками

field_generate ' Генерируем поле
Global player:player_obj = player_obj.create() ' Создаем игрока
objects_generate ' Создаем колобков и ящики

HideMouse

sc# = 64.0
fdx# = player.x + sxsize2 / sc#
fdy# = player.y + sysize2 / sc#
Repeat

        tim = MilliSecs() ' Засекаем время
       
        MoveMouse sxsize2, sysize2 ' Установка курсора мыши в центр экрана

        ' Плавное изменение координат камеры (при телепортации камера фиксируется на игроке, иначе - на средней точке между игроком и мишенью)
        camera_change 0.5 * targetx# * (teleport_mode = TM_IDLE), 0.5 * targety# * (teleport_mode = TM_IDLE), 1.1 ^ MouseZ() * 64.0
       
        player.angle = ATan2(targety#, targetx#) ' Нацеливание спрайта игрока на мишень

        timspeed# = speedpersec# * dtim# ' Определение множителя к скорости на основе прошедшего времени
        timang# = angpersec# * dtim# ' То же для угловой скорости

        ' Прорисовка слоев
        For l:layer_obj = EachIn layer_order
                l.draw
        Next

        ' Действия активных объектов
        For o:base_obj = EachIn actingobj
                o.act
        Next

        ' Отображение счетчиков
        DrawText "Frames/sec:" + fps + ", objects:" + objcnt + ", collision checks/frame:" + chcnt + ", collisions/frame:" + ccnt, 0, 0
        ccnt = 0
        chcnt = 0

        ' Отображение мишени
        field2scr targetx# + player.x, targety# + player.y, sx#, sy#
        DrawImage target, sx#, sy#

        ' Осветление экрана при сборе всех источников энергии
        If light > MilliSecs() Then
                ' Устанавливаем прозрачность
                SetAlpha 1.0 - 1.0 * (light - MilliSecs()) / fading_time
                ' И рисуем белый прямоугольник на весь экран
                DrawRect 0, 0, sxsize, sysize
                reset_transformations
        ElseIf light<>0 Then
                ' Поздравляем игрока с победой
                Notify "Congratulations!!!"
                End
        End If

        Flip False

        ' Обновление счетчика кадров в секунду
        If fpstim<= MilliSecs() Then
                fpstim = MilliSecs() + 1000
                fps = cnt
                cnt = 0
        Else
                cnt:+1
        End If

        If teleport_mode = TM_IDLE Then
                targetx#:+(MouseX() - sxsize2) / sc#    ' Изменение координат мишени
                targety#:+(MouseY() - sysize2) / sc#
                ' Ограничения на перемещение цели
                targetx# = limit(targetx#, Max(-sxsize / sc# * 0.75, -player.x), Min(sxsize / sc# * 0.75, fxsize - player.x))
                targety# = limit(targety#, Max(-sysize / sc# * 0.75, -player.y), Min(sysize / sc# * 0.75, fysize - player.y))
        End If

        ' Вычисление времени, затраченного на виток цикла (в секундах) для вычисления множителей скоростей
        dtim# = 0.001 * (Min(MilliSecs() - tim, minms))
        ' Время ограниченно пределом для недопущения слишком больших множителей, отрицательно
        '  сказывающихся на определении столкновений

Until KeyHit(KEY_ESCAPE)

' Генерация поля
Function field_generate()
        Const tile_water = 0
        Const tile_sand = 256
        Const tile_grass = 512
        Local ff#[fxsize, fysize, 2] ' Вспомогательный буферизованный массив высот для тайловой карты
        Local pos2bit[] = [0, 6, 1, 4, 5, 2, 7, 3]
        fmin# = 1.0; fmax# = 0 ' Переменные минимума и максимума значений высот
        For n = 0 To fblurq + 3
                loadingbar "Generating field...", n, fblurq + 4 ' Индикатор завершенности процесса
                maxd# = 0
                For y = 0 Until fysize ' Цикл по всем тайлам
                        For x = 0 Until fxsize
                                Select n
                                        Case 0 ' Сначала заполняем массив высот случайными значениями
                                                ff#[x, y, 1] = Rnd(0, 1)
                                        Case fblurq + 1 ' После этапов сглаживания - этап формирования тайловых слоев
                                                d# = (ff#[x, y, k] - fmin#) / (fmax# - fmin#) ' Kорректируем значение высоты, чтобы минимум соответствовал значению 0.0, максимум - 1.0
                                                If d# < sand_threshold# Then ' До порога песка
                                                        layer_tiles.frame[x, y] = tile_water ' Отображаем чистый тайл воды
                                                        layer_water.collision[x, y] = True ' Установка коллизии с этим тайлом в водном слое
                                                ElseIf d# < grass_threshold# Then ' От порога песка до порога травы
                                                        layer_tiles.frame[x, y] = tile_sand ' Пока отображаем чистый тайл песка
                                                        layer_sand.collision[x, y] = True ' Установка коллизии с этим тайлом в слое песка
                                                Else ' После порога травы
                                                        layer_tiles.frame[x, y] = tile_grass ' Пока отображаем чистый тайл травы
                                                        layer_sand.collision[x, y] = True
                                                End If
                                        Case fblurq + 2 ' Этап устранения травы, примыкающей к воде
                                                If layer_tiles.frame[x,y] = tile_grass Then ' Если тайл - трава, то
                                                        For yy = - 1 To 1 ' Цикл по всем соседним тайлам
                                                                For xx = - 1 To 1
                                                                        x2 = (x + xx + fxsize) Mod fxsize ' Расчет координат соседнего тайла
                                                                        y2 = (y + yy + fysize) Mod fysize '  (поле зациклено)
                                                                        If layer_tiles.frame[x2, y2] = tile_water Then ' Если один из тайлов - вода
                                                                                layer_tiles.frame[x, y] = tile_sand ' То меняем тайл травы на тайл песка
                                                                        End If
                                                                Next
                                                        Next
                                                End If
                                        Case fblurq + 3 ' Этап сглаживания тайлов (выбора кадра из библиотеки)
                                                If layer_tiles.frame[x, y] > tile_water Then ' Если чистая вода, то пропускаем этот тайл
                                                        bitpos = 0; mask = 0
                                                        For yy = - 1 To 1 ' Цикл по всем соседним тайлам
                                                                For xx = - 1 To 1
                                                                        If xx<>0 Or yy<>0 Then
                                                                                x2 = (x + xx + fxsize) Mod fxsize
                                                                                y2 = (y + yy + fysize) Mod fysize
                                                                                If layer_tiles.frame[x, y] > tile_sand Then ' Если данный тайл - трава, то
                                                                                        ' Если соседний тайл - трава, то включаем бит присутствия соседа для данного тайла
                                                                                        If layer_tiles.frame[x2, y2] > tile_sand Then setbit mask, pos2bit[bitpos]
                                                                                Else ' Иначе это тайл песка
                                                                                        ' Если соседний тайл - песок, то включаем бит присутствия соседа для данного тайла
                                                                                        If layer_tiles.frame[x2, y2] > tile_water Then setbit mask, pos2bit[bitpos]
                                                                                End If
                                                                                bitpos:+1 ' Увеличиваем счетчик номера бита
                                                                        End If
                                                                Next
                                                        Next
                                                        layer_tiles.frame[x, y] = 1 + 256  * (layer_tiles.frame[x, y] = tile_grass) + mask
                                                End If
                                        Default ' Этапы сглаживания массива высот
                                                sum# = 0
                                                For yy = - 1 To 1 ' Суммируем значения высот соседних тайлов и высоту данного тайла * 8
                                                        For xx = - 1 To 1
                                                                sum# = sum# + ff#[(x + xx + fxsize) Mod fxsize, (y + yy + fysize) Mod fysize, k] * (1.0 + 7.0 * (xx = 0 And yy = 0))
                                                        Next
                                                Next
                                                sum# = sum# / 16.0 ' Вычисляем среднее значение (центральный тайл имеет такой же вес, что и все 8 соседних в сумме)
                                                If n = fblurq Then setminmax sum#, fmin#, fmax# ' Kорректируем значения максимума и минимума высоты
                                                ff#[x, y, 1 - k] = sum# ' Устанавливаем значение высоты в буфере
                                End Select
                        Next
                Next
                k = 1 - k ' Меняем буфер и текущую карту местами
                If n = fblurq + 1 Then ' Окантовка карты водой после этапа формирования слоев
                        For x = 0 Until fxsize
                                waterize x, 0
                                waterize x, fysize - 1
                        Next
                        For y = 0 Until fysize
                                waterize 0, y
                                waterize fxsize - 1, y
                        Next
                End If
        Next
End Function

' Залитие тайла водой
Function waterize(x, y)
        layer_tiles.frame[x, y] = 0 ' Рисуем тайл воды
        layer_water.collision[x, y] = True ' Kоллизия для тайла водного слоя
        layer_sand.collision[x, y] = False ' Нет коллизии для тайла песка
End Function

' Создание библиотеки тайлов перехода между текстурами
Function tile_lib_create(bottom_tile:TImage, top_tile:TImage, rowd#, period#, tile_lib:TImage, offset = 0)
        Local dt#[tilesize2] ' Заполнение массива колебаний ровной границы
        For dn = 0 Until tilesize2
                dt#[dn] = (Sin(90 + dn * period# / tilesize2) - 1) * tilesize32
        Next

        bottom_pixmap:TPixmap = LockImage(bottom_tile)
        top_pixmap:TPixmap = LockImage(top_tile)

        For n = 0 To 255 ' Восемь клеток вокруг тайла могут быть такими же либо отличными (2 состояния),
         ' поэтому всего - 2 ^ 8 = 256 вариантов
                loadingbar "Generating transition tiles...", n, 256
                lib_pixmap:TPixmap = LockImage(tile_lib, n + offset)
                For n1 = 0 To 1
                        For n2 = 0 To 1
                                v = biton(n, n1 + n2 * 2)
                                vx = biton(n, n1 + 4)
                                vy = biton(n, n2 + 6)
                                For yy = 0 Until tilesize2
                                        For xx = 0 Until tilesize2
                                                If vx Then
                                                        If vy Then
                                                                If v Then
                                                                        k1# = 1
                                                                Else
                                                                        k1# = rowd# * (Sqr(xx * xx + yy * yy))
                                                                End If
                                                        Else
                                                                k1# = (yy + dt#[xx]) * rowd#
                                                        End If
                                                Else
                                                        If vy Then
                                                                k1# = (xx + dt#[yy]) * rowd#
                                                        Else
                                                                k1# = 2.0 - rowd# * (Sqr((tilesize2 - xx) * (tilesize2 - xx) + (tilesize2 - yy) * (tilesize2 - yy)) + Rand( -1, 1))
                                                        End If
                                                End If
                                                If k1# > 1 Then k1# = 1 ' Ограничиваем коэффициент в пределах интервала [0, 1]
                                                If k1# < 0 Then k1# = 0
                                                k2# = 1.0 - k1# ' Kоэффициент прозрачности для пикселей другого тайла
                                                If n1 Then x = tilesize - 1 - xx Else x = xx ' Отражения отн. осей (если нужно)
                                                If n2 Then y = tilesize - 1 - yy Else y = yy
                                                fromrgba ReadPixel(top_pixmap, x, y), r1, g1, b1, dummy ' Получаем цветовые компоненты пикселей тайлов
                                                fromrgba ReadPixel(bottom_pixmap, x, y), r2, g2, b2, dummy
                                                ' Печатаем пиксел, смешивая цвета с заданными коэффициентами
                                                WritePixel lib_pixmap, x, y, torgba(k1# * r1 + k2# * r2, k1# * g1 + k2# * g2, k1# * b1 + k2# * b2, 255)
                                        Next
                                Next
                        Next
                Next
        Next

        UnlockImage bottom_tile
        UnlockImage top_tile
End Function

' Генерация ящиков и колобков
Function objects_generate()
        ' Kолобки
        For n = 1 To kolobokq
                If (n Mod 100) = 0 Then loadingbar "Generating objects...", n, kolobokq * 3
                Repeat
                        o:kolobok_obj = kolobok_obj.create()
                        ' Расстояние до игрока должно быть не меньше минимума
                        If Sqr((o.x - player.x) * (o.x - player.x) + (o.y - player.y) * (o.y - player.y)) >= min_enemy_distance Then Exit
                        o.destroy
                Forever
        Next
       
        ' Ящики с постоянными бонусами
        For n1 = 1 To constant_bonustypeq ' Цикл по всем типам бонусов
                If (n Mod 100) = 0 Then loadingbar "Generating objects...", n1 + constant_bonustypeq, constant_bonustypeq * 3
                For n2 = 1 To constant_bonus_crateq ' Создаем заданное кол-во ящиков каждого типа
                        crate_obj.create(n1 - 1)
                Next
        Next

        ' Ящики с временными бонусами
        For n = 1 To temporary_bonus_crateq
                loadingbar "Generating objects...", n + temporary_bonus_crateq * 2, temporary_bonus_crateq * 3
                If Rand(1,100) > empty_crates_percent Then
                        crate_obj.create Rand(0, temporary_bonustypeq - 1) + bonus_threshold
                Else
                        crate_obj.create -1 ' Часть ящиков пусты
                End If
        Next
End Function

' Занесение в список объектов находящихся в пределах заданного кол-ва тайлов
Function nearly_objects:TList(lst:TList, x, y, radius, layer:object_layer_obj)
        For yy = Max(y - radius, 0) To Min(y + radius, fysize - 1)
                For xx = Max(x - radius, 0) To Min(x + radius, fxsize - 1)
                        For o:base_obj = EachIn(layer.objects[xx, yy])
                                lst.addlast o
                        Next
                Next
        Next
        Return lst
End Function

' Функция вырезания изображения из другого изображения
Function new_grab:TImage(image:TImage, x, y, frame)
        pixmap:TPixmap = LockImage(image, frame)
        w:TPixmap = images.window(x, y, ImageWidth(image), ImageHeight(image))
        pixmap.paste w, 0, 0
        UnlockImage image
        Return image
End Function

' Функция вырезания тайла или серии тайлов из изображения
Function tiles_grab:TImage(num, frameq = 1, midhn = True)
        image:TImage = CreateImage(tilesize, tilesize, frameq)
        If midhn Then MidHandleImage image ' флаг midhn означает, что изображение нужно отцентровать
        For n = 0 To frameq - 1
                pos = num + n
                new_grab image, (pos Mod 4) * tilesize, Floor(pos / 4) * tilesize, n ' По умолчанию тайлы располагаются на изображении в 4 столбца
        Next
        Return image
End Function

' Функция сброса трансформаций
Function reset_transformations()
        SetGrayColor 255
        SetRotation 0
        SetAlpha 1
        SetScale 1.0, 1.0
End Function

Function camera_change(x#, y#, scale#)
        ' Приращения координат камеры и увеличения
        sc# = sc# + magn_speed# * (scale# - sc#) * dtim#
        camx# = camx# + cam_speed# * (x# - camx#) * dtim#
        camy# = camy# + cam_speed# * (y# - camy#) * dtim#

        sc# = limit(sc#, Max(1.0 * sxsize / fxsize, 1.0 * sysize / fysize), 256.0) ' Ограничение увеличения
        tilesc# = sc# / tilesize ' Вычисление коэффициента увеличения для тайлов
       
        xsize# = sxsize / sc#   ' Размеры отображаемого прямоугольного куска поля
        ysize# = sysize / sc#
       
        fdx# = limit(player.x + camx# - xsize# * 0.5, 0, fxsize - xsize#) ' Ограничения смещения поля (по границам)
        fdy# = limit(player.y + camy# - ysize# * 0.5, 0, fysize - ysize#)
End Function

' Установка цвета - оттенка серого
Function SetGrayColor(col)
        SetColor col, col, col
End Function

' Полоса отображения завершенности процесса
Function loadingbar(txt$, pos, maximum)
        Cls
        SetColor 128, 128, 255
        DrawText txt$, (sxsize - TextWidth(txt$)) / 2, sysize34
        col = 255 * pos / maximum
        SetGrayColor 255
        DrawEmptyRect sxsize4, sysize34 + 20, sxsize2, 30
        SetColor 255 - col, col, 0
        DrawRect sxsize4 + 2, sysize34 + 22, sxsize24 * pos / maximum, 26
        Flip False
        SetGrayColor 255
End Function

' Функция, рисующая пустой прямоугольник
Function DrawEmptyRect(x#, y#, xsize#, ysize#)
        xsize# = xsize# - 1
        ysize# = ysize# - 1
        DrawLine x#, y#, x# + xsize#, y#
        DrawLine x# + xsize#, y#, x# + xsize#,y# + ysize#
        DrawLine x# + xsize#, y# + ysize#, x#, y# + ysize#
        DrawLine x#, y# + ysize#, x#, y#
End Function

' Функция, переводящая Write/ReadPixel-значение в значения цветовых компонент и альфа канала
Function fromRGBa(from, r Var, g Var, b Var, a Var)
        b = from & $FF
        g = (from Shr 8) & $FF
        r = (from Shr 16) & $FF
        a = (from Shr 24) & $FF
        Return
End Function

' Функция, переводящая значения цветовых компонент и альфа канала в Write/ReadPixel-значение
Function toRGBa(r, g, b, a = 255)
        Return b | (g Shl 8) | (r Shl 16) | (a Shl 24)
End Function

' Меняем местами значения двух переменных
Function swap(v1 Var, v2 Var)
        z = v2
        v2 = v1
        v1 = z
End Function

' Изменение минимума и максимума на основе переменной
Function setminmax(v#, vmin# Var, vmax# Var)
        If v#<vmin# Then vmin# = v#
        If v#>vmax# Then vmax# = v#
End Function

' Перевод из экранных координат в тайловые
Function scr2field(sx#, sy#, tx# Var, ty# Var)
        tx# = sx# / sc# + fdx#
        ty# = sy# / sc# + fdy#
End Function

' Перевод из тайловых координат в экранные
Function field2scr(tx#, ty#, sx# Var, sy# Var)
        sx# = (tx# - fdx#) * sc#
        sy# = (ty# - fdy#) * sc#
End Function

' Ограничение переменной минимальным и максимальным значениями
Function limit#(v#, vmin#, vmax#)
        If v# < vmin# Then v = vmin# ElseIf v# > vmax# Then v# = vmax#
        Return v#
End Function

' Функция, возвращающая значение бита под номером bitnum
Function biton(v, bitnum)
        If v & (1 Shl bitnum) Then Return True Else Return False
End Function

' Включение бита под номером bitnum в значении переменной
Function setbit(v Var, bitnum)
        v = v | (1 Shl bitnum)
End Function

' Вычисление минимальной разности углов
Function calc_dangle#(ang1#, ang2#)
        dang# = ang2# - ang1#
        Return dang# - Floor(dang# / 360 + 0.5) * 360

End Function

Автор: Матвей Меркулов (E-mail: MattMerkulov_sobaka_gmail.com, ICQ: 392-274-050)

Другие

Друзья