Всех приветствую! В данном уроке я расскажу вам, как сделать обычный платфтормер с необычным оформлением - в виде псевдо 3D башни. Урок довольно длинный и поделён на несколько глав. Так же данный урок рассчитан именно на отрисовку башни, а не на классную механику платформера. Так что перед началом запаситесь вкусняшками, кофе и чаем. Приятного прочтения!
Глава 1 - Скучная основаДа, как бы это уныло не выглядело, но всё нужно начинать с самой обычной и скучной заготовки. Но тут есть два нюанса. Столкновение с миром мы будем делать через слой тайлов и нам требуется, чтобы игрок зациклено двигался по комнате слева на право. Иными словами - телепортировался с одного края комнаты на другой, но обо всём по порядку.
Создаём комнату размером 640х480. Устанавливаем размер вида и порта такой же.
Создаём слой тайлов в комнате, даём название
Tiles и чертим какой нибудь небольшой уровень. Не забывайте, там где будут расставлены тайлы и будет коллизия с уровнем. Перемещаем данный слой так, чтобы он был между слоем объектов и слоем фона. Слой объектов переименовываем на
Objects.
Создаём спрайт игрока и спрайт тайлсета. В данном уроке, размеры игрока будут 32х32, а размер одного тайла 32х16. Установим центр спрайта игрока в центр. Это важно для дальнейшего кода.
Приступим к написанию скрипта коллизии. Создаём новый скрипт и называем его
place_meeting_tile.
/// @desc Столкновение с тайлами
/// @arg x
/// @arg y
// Запоминаем координаты откуда пришли
var xfrom = x;
var yfrom = y;
// Перемещаемся в новые координаты
x = argument0;
y = argument1;
// Берём id слоя тайлов
var tilemap = layer_tilemap_get_id(layer_get_id("Tiles"))
// Зацикливаем проерку столкновения в пределах комнаты
if bbox_right > room_width x -= room_width;
else if bbox_left < 0 x += room_width;
// Сохраняем результат столкновения в переменную
var result = tilemap_get_at_pixel(tilemap , bbox_left, bbox_top) ||
tilemap_get_at_pixel(tilemap , bbox_left, bbox_bottom) ||
tilemap_get_at_pixel(tilemap , bbox_left, y) ||
tilemap_get_at_pixel(tilemap , bbox_right, bbox_top) ||
tilemap_get_at_pixel(tilemap , bbox_right, bbox_bottom) ||
tilemap_get_at_pixel(tilemap , bbox_right, y);
// Возвращаемся в предыдущие координаты
x = xfrom;
y = yfrom;
// Возвращаем результат столкновения
return result;
Как видно из кода, мы также используем проверку по
y (центру спрайта) для определения коллизии с тайлами.
Создаём объект игрока. Пишем в
create 
:
// Скорость перемещения игрока
spd = 2;
// Горизонтальная и вертикальная скорость игрока
hsp = 0;
vsp = 0;
// Гравитация
grv = 0.5;
Пишем в
step 
:
// Клавиши управления
var key_right = keyboard_check(vk_right);
var key_left = keyboard_check(vk_left);
var key_jump = keyboard_check_pressed(ord("X"));
// Движение в нужную сторону
var move = key_right - key_left;
// Устанавливаем горизонтальную и вертикальную скорость
hsp = move * spd;
vsp += grv;
// Прыжок
if place_meeting_tile(x, y + 1) && key_jump
{
vsp = -8;
}
// Направление движения по горизонтали и вертикали
var xdir = sign(hsp);
var ydir = sign(vsp);
// Горизонтальная коллизия
if place_meeting_tile(x + hsp, y)
{
while !place_meeting_tile(x + xdir, y)
{
x += xdir;
}
hsp = 0;
}
x += hsp;
// Если вышли за пределы комнаты, то телепортируемся
move_wrap(true, false, 0);
// Вертикальная коллизия
if place_meeting_tile(x, y + vsp)
{
while !place_meeting_tile(x, y + ydir)
{
y += ydir;
}
vsp = 0;
}
y += vsp;
Для телепортации с одного края комнаты на другой мы используем функцию
move_wrap. Первый аргумент устанавливаем на
true, так как нам нужно перемещаться лишь по горизонтали. Третий аргумент ставим на
0, потому что спрайт у нас отцентрирован.
Собственно, на этом данная глава заканчивается. Наверное, вы слегка разочарованы, но мы только что сделали базовую наработку для нашей будущей башни, хотя по виду и не скажешь.
Глава 2 - Враги, монетки и их родительИз названия главы должно быть понятно, что здесь мы займёмся сбором монеток и созданием врагов для уровня, а так же назначим им общего родителя.
Но для начала создадим объект-контроллер, который будет хранить количество собранных монеток, жизни игрока, а так же всё это отображать на экране. Создадим объект и назовём его
o_controller. Так же, создадим для него отдельный слой в комнате, назовём его
Main и поместим его самым первым.
В create

пишем:
coins = 0;
В draw_gui

:
draw_text(0, 0, "Coins: " + string(coins));
Нарисуйте спрайты монеток и врагов. Установим им центр спрайта по центру. Создадим для них объекты
o_coin и
o_enemy.
Начнём реализовывать столкновения с монеткой. Возможно, вы скажете, что тут нет ничего сложного, но я сразу отвечу - это платформер с псевдо 3D башней, уровень здесь зациклен по кругу и есть свои особенности столкновения с объектами.
Перейдём в игрока и создадим событие столкновения с
o_coin 
. Там напишем:
// Прибавляем значение переменной и удаляем монетку
o_controller.coins++;
instance_destroy(other);
А теперь самое интересное. Если вы дойдёте до края уровня, то монетки с противоположной стороны не удалятся, что не есть хорошо. Это нужно исправить, иначе, когда у нас будет отображаться уровень в виде башни, то будет казаться, что игрок просто не взаимодействует с монетками. (что собственно логично, если смотреть на это в виде обычного уровня)
Чтобы это исправить, мы создадим скрипт под названием
instance_place_wrap. Хочу так же сказать отдельное спасибо
YellowAfterLife, который помог с пониманием того, как правильно вызывать функцию
event_perform.
/// @desc Столкновение с объектами в комнате
/// @arg obj
// Объект для столкновения
var obj = argument0;
// Запоминаем координаты
var xx = x;
var yy = y;
// Зацикливаем перемещение в комнате
if bbox_right > room_width xx -= room_width;
else if bbox_left < 0 xx += room_width;
// Ищем объект для столкновения
var collision = instance_place(xx, yy, obj);
// До тех пор пока collision != noone
while collision
{
// Принудительно вызываем событие столкновения с объектом
with collision
{
with other
{
event_perform(ev_collision, collision.object_index);
}
}
// Ищем новый такой же объект
var new_collision = instance_place(xx, yy, obj);
// Если нет новых объектов, то завершаем цикл
if collision = new_collision break;
// Присваиваем новый объект переменной
collision = new_collision;
}
Данный скрипт гарантированно пройдётся по всем экземплярам объекта и вызовет события столкновения.
Переходим в событие
step 
игрока и в самом конце дописываем строчку:
// Коллизия с монетками
instance_place_wrap(o_coin);
Теперь, если вы запустите проект и дойдёте до границы комнаты, то монетки с противоположной стороны исчезнут, как и положено.
Перейдём к врагу. Для данного примера я не буду использовать какую-то изощрённую логику. Будет максимально простая.
В
create 
пишем:
hsp = choose(1, -1);
В
step 
пишем:
// Определяем сторону для проверки столкновения с нижним тайлом
var side;
if hsp > 0 side = bbox_right;
else if hsp < 0 side = bbox_left;
// Если нет тайла в стороне куда движемся и сталкиваемся с нижним тайлом, то двигаемся
// Иначе меняем направление
if !place_meeting_tile(x + hsp, y) && place_meeting_tile(side, y + 1)
{
x += hsp;
}
else
{
hsp *= -1;
}
// Если вышли за пределы комнаты, то телепортируемся
move_wrap(true, false, 0);
Собственно, наш новый враг будет ходить из стороны в сторону и менять направление, если столкнулся со стеной или на половину вышел с платформы.
Перейдём к нашему игроку. Ему пропишем взаимодействие с врагом и добавим переменную жизней.
В
create 
допишем:
// Жизни
hp = 3;
hit_time = 0;
Переменная
hit_time будет отвечать за задержку между получением урона.
Создадим событие столкновения с
o_enemy 
. Там пропишем простенький код:
// Если падаем на врага, то подпрыгиваем и уничтожаем его
if vsp > 0
{
instance_destroy(other);
vsp = -4;
}
else
{
// Иначе, если не получили урон
if hit_time = 0
{
// Устанавливаем задержку в 60 шагов (1 секунду)
hit_time = 60;
// Отнимкем жизнь. Если жизней нет, то перезапускаем комнату
hp--;
if hp = 0 room_restart();
}
}
Допишем код в
step 
, чтобы игрок мигал при получении урона:
// Если получили урон
if hit_time > 0
{
// Мигаем
visible = !visible;
// Если задержка между уроном прошла, то не мигаем
hit_time--;
if hit_time = 0 visible = true;
}
Можно подумать, что вроде всё и готово. Игрок будет уничтожать врагов прыжком на голову и получать урон, если он этого не делает, но не стоит забывать о границах комнаты. Там тоже должна срабатывать проверка столкновения с врагами, чтобы небыло в дальнейшем странных багов.
Конечно можно дополнительно дописать скрипт в
step 
:
instance_place_wrap(o_enemy);
Но, а что если в будущем будет не 2 интерактивных объекта, а 202? Да, я немного преувеличил. Лучшем решением будет переписать наш скрипт на более универсальный.
Сначала создадим новый объект и назовём его
o_objects. Это будет наш общий родитель для всех интерактивных объектов. Назначьте его родителем для
o_coin и
o_enemy.
Перейдём в наш скрипт
instance_place_wrap. Сотрём строчку
/// @arg obj, так как наш скрипт больше не будет принимать аргументов. Заменим
argument0 на
o_objects.
Вернёмся к событию
step 
игрока и просто уберём аргумент из скрипта столкновения с объектами:
// Коллизия с объектами
instance_place_wrap();
Теперь всё работает как надо! Осталась лишь одна мелочь - отображение количества жизней.
Сделаем это довольно простым способом. Перейдём в событие
draw gui 
объекта
o_controller и допишем строчку:
draw_text(0, 16, "Health: " + string(o_player.hp));
Глава 3 - Рисуем башнюНаконец мы перешли к самой интересной главе данной статьи! Здесь я расскажу, как же отображать уровень в виде псевдо трёхмерной башни. Но сначала начнём с небольшой скучной теории.
Представим башню с видом сверху.

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

И как возможно вы догадались, мы для этих целей будем использовать
lengthdir_x по координате
x, а координату
y не будем трогать. Тем самым добьёмся такого результата.
Для удобства создадим отдельный объект и назовём его
o_tower. Чтобы правильно отобразить все тайлы, нам нужно сосчитать количество ячеек по горизонтали и узнать шаг угла в градусах, чтобы рисовать тайл в правильной части "окружности".
Добавим код в
create 
:
// Берём id слоя с тайлами
var layer_id = layer_get_id("Tiles");
// Выключаем отображения слоя с тайлами
layer_set_visible(layer_id, false);
// Запоминаем id слоя с тайлами
tilemap = layer_tilemap_get_id(layer_id);
// Считаем количество ячеек по ширине и высоте
cell_w = 32;
cell_h = 16;
tower_w = room_width div cell_w;
tower_h = room_height div cell_h;
// Определяем шаг угла
angle_step = 360 / tower_w;
// Смещение башни по координате x
offset_x = camera_get_view_width(view_camera[0]) * 0.5;
Сначала мы записываем
id слоя тайлов в переменную, а так же выключаем отображение этого слоя в комнате. Далее, мы считаем размер комнаты в ячейках по горизонтали и вертикали. Получаем размер шага угла для рисования тайлов, а так же считаем смещение по координате
x, чтобы рисовать башню по центру вида.
Создадим событие
draw 
и напишем код:
// Локальные переменные
var local_offset_x = offset_x;
// Проходим циклом все горизонтальные ячейки
for (var i=0; i<tower_w; i++)
{
// Считаем угол (координату x) где будет отображён тайл
var lx = lengthdir_x(128, i * angle_step);
// Проходим циклом все вертикальные ячейки
for (var j=0; j<tower_h; j++)
{
// Берём информацию о тайле и рисуем тайл по соответвующим координатам
var tile = tilemap_get(tilemap, i, j);
draw_tile(t_tiles, tile, 0, lx + local_offset_x, j * cell_h);
}
}
Если сейчас запустить проект, то можно увидеть первый "прототип" нашей башни. Все объекты пока не рисуются так как надо, да и видны зазоры между тайлами башни. Да и проблемы с глубиной тоже есть.

Для начала исправим зазоры между тайлами башни. Они у нас есть, потому что в коде отображения я пока использовал "магическое число" 128 для определения радиуса башни. Чтобы всё рисовалось как надо, нужно написать формулу.
Перейдём в
create 
объекта
o_tower и допишем код:
// Радиус башни
tower_r = ((room_width / 10) / 2 * pi);
Если совсем честно, даже я не совсем понимаю, как это работает. Уже точно не помню, какую формулу я находил, но плясал от неё. Заработала она как надо только тогда, когда я поделил ширину комнаты на волшебную цифру 10... Данная формула работает с любым размером комнаты и с любым размером тайлов. Если кто нибудь объяснит, как это работает, то буду примного благодарен.
Собственно, в событии
draw 
создаём ещё одну локальную переменную и просто меняем 128 на неё.
// Локальные переменные
var local_offset_x = offset_x;
var local_tower_r = tower_r;
// Считаем угол (координату x) где будет отображён тайл
var lx = lengthdir_x(local_tower_r , i * angle_step);
Теперь нужно сделать правильное отображение объектов, чтобы они рисовались в правильных координатах.
Для начала перейдём к объекту
o_objects и добавим ему пустое событие
draw 
с каким либо комментарием.
/// Рисованием занимается другой объект
Тоже самое сделаем и для объекта
o_player.
Это нам позволит использовать объект
o_tower для рисования всех объектов в нужных координатах.
Вернёмся к объекту
o_tower. Для начала будем считать координаты игрока. Для этого, перейдём в событие
draw 
и в самом начале напишем:
// Позиция игрока в башне
var player_position = (o_player.x * 360) / room_width + 90;
Формула довольно простая. Умножаем текущию координату на 360 и делим на ширину комнаты. В итоге будем получать значения от 0 до 360. Объясняю зачем прибавлять 90 градусов. Это нужно потому, что мы будем вычитать свои координаты с координатами игрока и нам требуется отнять 90 градусов, чтобы начало уровня отображалось по центру экрана. Если этого не сделать, то начало уровня будет отображаться в правой части экрана. То есть в 0 градусе башни.
Далее, дописываем код внутри цикла for. Нам требуется смещать тайлы отностильно игрока, значит нужно переписать строку:
var lx = lengthdir_x(local_tower_r, i * angle_step);
На эту:
var lx = lengthdir_x(local_tower_r, i * angle_step - player_position);
Затем отобразим все объекты с учётом координат игрока:
// Рисуем все объект
with o_objects
{
var my_position = (x * 360) / room_width;
var my_angle = my_position - player_position;
var lx = lengthdir_x(local_tower_r, my_angle) + local_offset_x;
draw_sprite(sprite_index, image_index, lx, y);
}
А потом рисуем игрока по центру экрана:
// Рисуем игрока
with o_player
{
if visible draw_sprite(sprite_index, image_index, global.offset_x, y);
}
Как я и писал ранее, здесь мы вычитаем свой угол в башне с углом игрока, чтобы получить правильное местоположение в башне.
Осталось сделать слежение камеры по вертикали в пределах высоты комнаты. Для этого перейдём в событие
create 
объекта
o_tower и добавим новую переменную:
// Размер вида по вертикали
camera_h = camera_get_view_height(view_camera[0]);
Далее создадим событие
step 
и напишем код слежения за игроком:
// Ограничиваем координаты камеры в пределах комнаты
var camera_y = clamp(o_player.y - offset_x, 0, room_height - camera_h);
// Следим за игроком
camera_set_view_pos(view_camera[0], 0, floor(camera_y));
Координаты камеры по
y мы округялем, чтобы избежать странных смещений пикселей, когда камера двигается по вертикали.
Собственно уже можно запускать проект! Теперь башня должна вращаться вслед за игроком. Но как вы можете заметить, у нас есть большая проблема с глубиной рисования... Данную проблему мы будем решать в следующей главе.
Так же оставлю полный код события
draw 
объекта
o_tower:
// Позиция игрока в башне
var player_position = (o_player.x * 360) / room_width + 90;
// Локальные переменные
var local_offset_x = offset_x;
var local_tower_r = tower_r;
// Проходим циклом все горизонтальные ячейки
for (var i=0; i<tower_w; i++)
{
// Считаем угол (координату x) где будет отображён тайл
var lx = lengthdir_x(local_tower_r, i * angle_step - player_position);
// Проходим циклом все вертикальные ячейки
for (var j=0; j<tower_h; j++)
{
// Берём информацию о тайле и рисуем тайл по соответвующим координатам
var tile = tilemap_get(tilemap, i, j);
draw_tile(t_tiles, tile, 0, lx + local_offset_x, j * cell_h);
}
}
// Рисуем игрока
with o_player
{
if visible draw_sprite(sprite_index, image_index, local_offset_x, y);
}
// Рисуем все объект
with o_objects
{
var my_position = (x * 360) / room_width;
var my_angle = my_position - player_position;
var lx = lengthdir_x(local_tower_r, my_angle) + local_offset_x;
draw_sprite(sprite_index, image_index, lx, y);
}
Глава 4 - Глубина и то, что не должно быть видноСортировать по глубине мы будем с помощью небольшого шейдера. На мой взгляд это лучшее решение для наших целей, так как помимо сортировки по глубине, мы можем ещё добавить затенение нашей башне.
Создадим новый шейдер, назовём его
sh_tower. Пишем вертексную часть шейдера:
attribute vec3 in_Position;
attribute vec4 in_Colour;
attribute vec2 in_TextureCoord;
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
uniform float Depth;
void main()
{
vec4 object_space_pos = vec4( in_Position.x, in_Position.y, Depth, 1.0);
gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * object_space_pos;
v_vColour = in_Colour;
v_vTexcoord = in_TextureCoord;
}
По большей части он уже и так был готов. Мы лишь дописали униформу
Depth, которую шейдер будет принимать, а так же заменили
in_Position.z на
Depth, чтобы сделать нашу сортировку по глубине.
Перейдём в объект
o_tower в событие
create 
. Допишем немного кода:
// Половина ширины ячейки
cell_w_half = cell_w * 0.5;
// Половина шага угла
angle_step_half = angle_step * 0.5;
// Включаем z - буфер и определяем порог рисования
gpu_set_ztestenable(true);
angle_discard = 0.5 + 1 / angle_step_half;
Поясняю.
Половина ширины ячейки и половина шага угла нам потребуется для того, чтобы подвинуть и повернуть нашу башню на половину ширины платформы. Это связано с тем, что башня нарисована не совсем по центру экрана, но об этом чуть позже.
gpu_set_ztestenable просто включает буфер глубины.
angle_discard - порог после которого платформы не будут рисоваться. Зачем это нужно? Это нужно для того, чтобы не рисовать платформы за башней, где их всё равно не будет видно. Тем самым, мы немного снизим нагрузку на рисование. Как найти этот самый порог? Всё просто. Давайте взглянем на этот рисунок:

Снова представляем башню с видом сверху. Для определения глубины платформ, мы будем использовать
dsin со значением угла в градусах. Как видно из рисунка, левая и правая часть равны 0. От этого и начинаем писать формулу.
Сначала пишем 0.5. Это означает, что на углах в 45 и 135 градусов платформы не будут уже рисоваться, но это не совсем точно, потому что они будут пропадать тогда, когда не зашли полностью за башню. Чтобы это исправить мы считаем половину шага угла
(1 / angle_step_half), то есть это равносильно половины ширины платформы но в "координатах" башни. Далее мы просто прибавляем это значение к 0.5. Теперь наши платформы будут пропадать, когда полностью скроются за башню.
Переходим в событие
draw 
. Сначала исправим расположение башни. Изменим эту строку:
var z = i * angle_step - player_position;
На эту:
var z = i * angle_step + angle_step_half - player_position;
Здесь мы просто прибавили половину шага угла, чтобы повернуть нашу башню немного вправо.
Далее сместим все наши тайлы влево на половину ширины платформы. Снова исправляем строчку:
draw_tile(t_tiles, tile, 0, lx + local_offset_x, j * cell_h);
На эту:
draw_tile(t_tiles, tile, 0, lx + local_offset_x - cell_w_half, j * cell_h);
Таким образом наша башня теперь нарисованна по центру. Обратите внимание, что мы исправляем координаты только башни!
А теперь займёмся глубиной башни. Добавим к локальным переменным униформу шейдера:
var uniform_depth = shader_get_uniform(sh_tutorial, "Depth");
Далее установим шейдер:
/* Локальные переменные */
shader_set(sh_tower);
/* Весь остальной код */
shader_reset();
Перейдём к первому циклу
for. После того, как мы находим позицию в башне, сразу же будем находить глубину, на которой следует нарисовать объекты:
var z_depth = dsin(z);
Далее будем отключать рисование платформ, если они зашли за башню:
// Если глубина привысила порог, то не рисуем платформы
if z_depth > angle_discard continue;
Наконец приступим к рисованию стен нашей башни. Сделаем это через примитивы. Для начала найдём координаты левой и правой части стены:
// Координаты стен башни
var wall_l = lengthdir_x(local_tower_r - cell_w_half, z) + local_offset_x;
var wall_r = lengthdir_x(local_tower_r - cell_w_half, z + angle_step) + local_offset_x;
Тут всё довольно просто.
len у нас равняется радиусу башни минус половина ширины платформы,
dir это значение от 0 до 360, то есть. подставляем
z. И не забываем прибавить смещение башни. Стоит отметить, что для
wall_r мы также прибавляем
angle_step, что является следующей точкой в нашей башне. Зная эти 2 точки - левую и правую, мы теперь можем построить примитив. Создайте новый спрайт и назовите его
s_tower. Поставьте галочку
Separate Texture Page. Следует учитывать, что данный спрайт должен быть кратен степени двойки. То есть размер должен быть 2х2, 4х4, 8х8, 16х16, 32х32 итд. Далее продолжаем писать код в
draw 
:
// Устанавливаем глубину рисования
shader_set_uniform_f(uniform_depth, z_depth + 0.5);
draw_primitive_begin_texture(pr_trianglestrip, sprite_get_texture(s_tower, 0));
draw_vertex_texture(wall_l, 0, 0, 0);
draw_vertex_texture(wall_r, 0, 1, 0);
draw_vertex_texture(wall_l, room_height, 0, 32);
draw_vertex_texture(wall_r, room_height, 1, 32);
draw_primitive_end();
Сначала передаём в униформу глубину рисования. Она должна быть больше чем глубина рисования платформ, иначе объект и центральная часть башни будут перекрывать друг друга. Затем просто рисуем примитив по нашим найденым координатам.
Собственно перед вторым циклом, тот который рисует платформы, мы вновь передаём параметры глубины в униформу:
// Устанавливаем глубину рисования
shader_set_uniform_f(uniform_depth, z_depth);
Здесь мы уже не прибавляем 0.5.
Перейдём к части кода
with o_objects. Здесь нужно тоже передать в униформу глубину перед рисованием спрайта:
shader_set_uniform_f(uniform_depth, dsin(my_angle));
draw_sprite(sprite_index, image_index, lx, y);
В качестве глубины мы уже используем свой угол (положение в башне).
Перед рисованием игрока тоже нужно передать глубину в униформу шейдера. Так как игрок находится всегда впереди, то передадим глубину со значением -2.
shader_set_uniform_f(uniform_depth, -2);
Собственно вот мы и закончили с отображением башни! Можно смело проверять проект! Весь код
draw 
выглядит так:
// Позиция игрока в башне
var player_position = (o_player.x * 360) / room_width + 90;
// Локальные переменные
var local_offset_x = offset_x;
var local_tower_r = tower_r;
var uniform_depth = shader_get_uniform(sh_tutorial, "Depth");
shader_set(sh_tower);
// Проходим циклом все горизонтальные ячейки
for (var i=0; i<tower_w; i++)
{
// Позиция в башне (0 - 360)
var z = i * angle_step + angle_step_half - player_position;
var z_depth = dsin(z);
// Если глубина привысила порог, то не рисуем платформы
if z_depth > angle_discard continue;
// Координаты стен башни
var wall_l = lengthdir_x(local_tower_r - cell_w_half, z) + local_offset_x;
var wall_r = lengthdir_x(local_tower_r - cell_w_half, z + angle_step) + local_offset_x;
// Устанавливаем глубину рисования
shader_set_uniform_f(uniform_depth, z_depth + 0.5);
draw_primitive_begin_texture(pr_trianglestrip, sprite_get_texture(s_tower, 0));
draw_vertex_texture(wall_l, 0, 0, 0);
draw_vertex_texture(wall_r, 0, 1, 0);
draw_vertex_texture(wall_l, room_height, 0, 32);
draw_vertex_texture(wall_r, room_height, 1, 32);
draw_primitive_end();
// Считаем угол (координату x) где будет отображён тайл
var lx = lengthdir_x(local_tower_r, z);
// Устанавливаем глубину рисования
shader_set_uniform_f(uniform_depth, z_depth);
// Проходим циклом все вертикальные ячейки
for (var j=0; j<tower_h; j++)
{
// Берём информацию о тайле и рисуем тайл по соответвующим координатам
var tile = tilemap_get(tilemap, i, j);
draw_tile(t_tiles, tile, 0, lx + local_offset_x - cell_w_half, j * cell_h);
}
}
// Рисуем все объект
with o_objects
{
var my_position = (x * 360) / room_width;
var my_angle = my_position - player_position;
var lx = lengthdir_x(local_tower_r, my_angle) + local_offset_x;
shader_set_uniform_f(uniform_depth, dsin(my_angle));
draw_sprite(sprite_index, image_index, lx, y);
}
shader_set_uniform_f(uniform_depth, -2);
// Рисуем игрока
with o_player
{
if visible draw_sprite(sprite_index, image_index, local_offset_x, y);
}
shader_reset();
Теперь не должно быть никаких багов с отображением спрайтов по глубине. Но остануться небольшие баги с альфой, если вы достаточно внимательны. Она будет перезаписывать любой цвет на прозрачный, так что стоит дописать наш шейдер, чтобы исправить это, а так же добавить объёма башне.
Начнём с вертексного шейдера:
attribute vec3 in_Position;
attribute vec4 in_Colour;
attribute vec2 in_TextureCoord;
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
varying vec2 v_vDepth;
uniform vec2 Depth;
void main()
{
vec4 object_space_pos = vec4( in_Position.x, in_Position.y, Depth.x, 1.0);
gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * object_space_pos;
v_vColour = in_Colour;
v_vTexcoord = in_TextureCoord;
v_vDepth = Depth;
}
Тут мы поменяли тип
Depth с
float на
vec2. Глубина теперь у нас будет содержаться в
Depth.x, а дополнительный параметр для цвета в
Depth.y.
Так же мы добавили
varying vec2 v_vDepth и передали в него
Depth для дальнейшего использования во фрагментном шейдере.
Перейдём во фрагментный шейдер:
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
varying vec2 v_vDepth;
void main()
{
vec4 ColorBlend = vec4(0.0, 0.0, 0.0, 1.0);
ColorBlend.rgb -= (v_vDepth.x - v_vDepth.y);
vec4 Result = texture2D( gm_BaseTexture, v_vTexcoord ) * v_vColour * ColorBlend;
if (Result.a == 0.0) discard;
gl_FragColor = Result;
}
Тут всё чуть более интересно. Сначала объявляем цвет для смешивания. Он у нас чёрный. Вычитаем из него разницу между
v_vDepth.x и
v_vDepth.y. Умножаем это всё со стандартной текстурой и цветом смешивания спрайта (image_blend). Далее, если данный пиксель полностю прозрачный, то пропускаем его, чтобы не затирать другие пиксели прозрачным цветом.
Да, выглядит довольно странно, но я придумал лишь такой выход, чтобы избежать искажения цвета. Искажается он потому, что напрямую зависит от глубины.
Собственно, возвращаемся в
draw 
нашей башни. Начнём дописывать второй параметр в наши униформы.
Когда рисуем примитивы, то дописываем 0.5 в униформу:
shader_set_uniform_f(uniform_depth, z_depth + 0.5, 0.5);
Перед рисование платформ и перед рисованием объектов пишем 0. (можно ничего не писать, но мне спокойнее, когда есть 0)
shader_set_uniform_f(uniform_depth, z_depth, 0);
И перед рисованием игрока дописываем -1.
shader_set_uniform_f(uniform_depth, -2, -1);
Можно смело проверять проект, но а этом моя статья подходит к концу. Надеюсь, она вам понравилась и вдохновила на новые проекты. Всем спасибо за чтение!
Возможно, что в статье недостаточно картинок. Возможно, что где-то не очень понятно всё расписано. Любые замечания и пожелания приветствую!