|
|
Создание двумерного движка на примере игры «Зверские колобки»
Материал из 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 gmail.com, ICQ: 392-274-050)
|
|