Продолжаем реализовывать клон Plants vs Zombies в Game Maker, изучая основные особенности разработки игр в этой среде. В этом эпизоде мы затронем такие понятия как скрипты, таймлайны, ключевое слово other, depth, переопределим событие Draw, изучим несколько полезных функций и поговорим об отладке игр.


Сейчас мы может расставлять юнитов сколько угодно, но в этом уроке мы это исправим. В объекте o_game в событии Create добавим новую переменную pEnergy, которая будет отвечать за хранение количества энергии(в Plants vs Zombies — это солнышки). За энергию можно будет покупать юниты. Так же добавим новый alarm, который будет отвечать за генерацию новых единиц энергии(в оригинале солнышки падали сверху). Так что событие будет выглядеть теперь так:

pEnergy = 50; // player energy
alarm[0] = room_speed; // generate enemies
alarm[1] = room_speed * 3; // generate energy units


Событие Alarm 1:
/// generate energy elements
instance_create(irandom(room_width - 40) + 40, irandom(room_height - 40) + 40, o_elem_energy);
alarm[1] = room_speed * 6.5;


Теперь сделаем сами «элементы» энергии, которые будут появляться в случайном месте на игровом экране, а затем двигаться. Создаем новый объект, задаем спрайт, называем o_elem_energy. В событии Create пишем такой код

image_alpha = 0;
eAmount = 25; // amount of energy to add
alarm[0] = 1; // incr alpha


image_alpha = 0 — встроенная переменная, которая делает спрайт полностью невидимым, когда исполняется код отрисовки. image_alpha принимает любое значение от 0(невидимый) до 1(видимый). 0.5 — это 50% видимости. С помощью Alarm 0 мы будем увеличивать альфу, т.е. делать спрайт видимым.

eAmount — пользовательская переменная, которая будет хранить количество энергии, получаемой игроком при клике.

Код для Alarm 0:

/// Incr alpha
if (image_alpha + 0.05 <= 1){
  image_alpha += 0.05;
  alarm[0] = 1;
}


[Уголок оптимизации]

В прошлом эпизоде наши «пули», которыми стреляют растения, просто летели горизонтально и делали это бесконечно. Когда там таких пуль будет, скажем, 100, это не проблема, но при 1000 могут возникнуть просадки в производительности. Но в любом случае от лишних элементов нужно избавляться, как только они перестали быть нужными. Поскольку как только пули вылетают за границы экрана, они нас больше не интересуют, их сразу же нужно удалять. Сделать это можно 2 путями — через стандартное событие Other -> Outside View(или Outside Room, зависит от вашей игры) или же просто проверять, не находится ли объект за границами экрана самостоятельно. Разницы между ними никакой, стандартные события делают ровно то же самое, что и второй способ, просто делают это за вас. Так что добавляем событие Step объекту o_bullet и добавим такой код:

if (x - sprite_width / 2 > room_width){
   instance_destroy();
}


Тут, надеюсь, все понятно — как только левая граница спрайта выходит за рамки комнаты(room), мы уничтожаем инстанс.
[/Уголок оптимизации]

Теперь вернемся к объекту o_elem_energy и создадим событие Step. Напишем такое:

if (y + sprite_height/2 < 0){
   instance_destroy();
}else{
   y -= 2;
}


Здесь мы двигаем наш инстанс каждый шаг на 2 пикселя вверх и, если нижняя граница спрайта находится над комнатой, то мы удаляем инстанс.

Игроку нужно будет кликать по элементу энергии, чтобы получить её. Для этого создадим событие Left Button. Это событие срабатывает не только, когда мы нажимаем(Left pressed) или отпускаем мышку или палец на сенсорных устройствах(Left released), но и когда мышка просто проходит через наш инстанс, что нам подходит идеально. Код для этого события мог бы быть таким:

/// grab energy
with(o_game){
   pEnergy += other.eAmount;
}
instance_destroy();


Однако, в первом эпизоде я обещал, что мы добавим поддержку геймпада, и очевидно, что у геймпада нет события Left Button(или Left Pressed, или Left Released) по инстансу. И соответственно при управлении геймпадом будет другой способ подбора энергии. А раз так код выше будет дублироваться. Однако, дублирование, по понятным причинам, это плохо, так что пришло время познакомиться с важным элементом Game Maker — скриптами(папка Scripts). По сути они ничем не отличаются от кода, который мы пишем внутри событий, однако способ его срабатывания отличается — его нужно самостоятельно вызвать. Скрипты очень удобны при дублирующемся коде, при этом скрипты еще могут принимать параметры, так как это делают функции(методы) в любых других языка программирования.

У нас как раз возникнет случай необходимости дублирования кода в дальнейшем, так что вместо кода выше в событии Left Button мы напишем:

scr_elem_energy_grab();


Так выглядит вызов функции. В скобках можно передавать параметры, но сейчас они нам не нужны. Game Maker сейчас показывает ошибку, т.к. этой функции пока еще не существует. Исправим это — создадим скрипт, точно также как и создают объекты/спрайты/и т.д., переименуем на scr_elem_energy_grab и поместим в него вот тот код выше(который начинается с комментария /// grab energy). Сохраняем, закрываем и теперь никаких ошибок и дальнейшего повторения кода, когда мы разработаем механизм ловли энергии геймпадом.
Теперь когда у инстанса объекта o_elem_energy будет срабатывать событие Left Button, мы запускаем скрипт scr_elem_energy_grab, который делает следующее — обращается к переменной pEnergy инстанса объекта o_game(он у нас один, так что можно обращаться к нему и o_game.pEnergy) и присваиваем его значение переменной other.eAmount.

С ключевым словом other мы уже сталкивались, однако здесь его значение несколько другое. other.eAmount — это обращение к переменной инстанса объекта, который и запустил цикл with. Вы спросили, а кто его запустил? Отлично! Давайте подумаем? Цикл with принадлежит тому объекту, кто его вызвал. А кто его вызвал, он ведь находится в скрипте? Все просто — скрипт запущен от лица инстанса объекта o_elem_energy и соответственно получает доступ к его переменным. Так что other.eAmount — это обращение к переменной eAmount, которую мы объявили в событии Create объекта o_elem_energy. В Game Maker все просто. Если мы напишем просто pEnergy = eAmount; то мы будем обращаться к переменной eAmount инстанса o_game, которой не существует, что вызовет ошибку.

Еще раз для закрепления, писать подобные конструкции придется часто независимо от сложности игры. У o_elem_energy есть переменная eAmount. В событии Left Button запускается от лица этого объекта scr_elem_energy_grab, т.е. в скрипте мы можем обращаться напрямую к переменной eAmount, однако мы добавляем энергию в цикле with. События внутри with происходят от лица объекта o_game и мы уже не может напрямую обратиться к eAmount. Но у нас есть ключевое слово other, с помощью которого мы как бы выходим за пределы цикла with и снова получаем доступ к eAmount, т.к. мы находимся внутри скрипта, запущенного от лица o_elem_energy. Это очень просто и, я надеюсь, что это вам понятно.

Помните, мы в первом эпизоде прописывали событие Global Left Released, которое срабатывает всегда и на всем «полотне» игры. Когда мы добавили новое событие Left Button объекту o_elem_energy образовался косяк. Ведь получается, что когда мы будем подбирать энергию, будет запускаться событие Global Left Released объекта o_game и, соответственно, будет ставиться юнит на игровое поле. Странное поведение, не так ли? Так что нужно быть очень аккуратным с событиями Global. Исправить это очень легко, но мы все равно будем переделывать механизм размещения юнитов, так что оставим это на потом, а в исходниках на github пока временно эта проблема исправлена.

Реализуем эквивалент подсолнухов. Создаем новый объект и называем его o_unit_enery_generator, задаем ему спрайт и выставляем родительский объект o_unit_parent. Чтобы не было хаоса среди объектов/спрайтов/скриптов, в Game Maker есть группы. Правой кнопкой по папке Objects -> Create new group. Дадим ей любое осмысленное название, например, grp_units. В ней мы будет хранить все пользовательские юниты. Перетащим их в эту группу. Сделайте тоже самое для вражеских объектов. Название групп и сортировка объектов по ним создано исключительно для вашего удобства и ни на что больше не влияет.

В коде объекта o_unit_enery_generator ничего нового нет, так что просто размещаю его содержимое.

Create:
event_inherited();

HP = 15;
genElemEnergyTime = room_speed * 2;
alarm[0] = genElemEnergyTime;



Alarm 0:
/// generate energy elements

instance_create(x,y,o_elem_energy);
alarm[0] = genElemEnergyTime;


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

Настало время познакомиться с событием Draw. Раньше оно нас не интересовало, т.к. устраивали стандартные возможности по отрисовке объектами своих спрайтов. Сейчас же нам нужно нечто нестандартное. Это довольно важный элемент в изучении Game Maker.

Итак, добавим событие Draw объекту o_game.

draw_set_alpha(0.68);
draw_rectangle_colour(0,0, elemGUI_width, elemGUI_height, c_blue, c_blue, c_blue, c_blue, false);
draw_set_alpha(1);
draw_text_colour(40,25, string(pEnergy), c_black, c_black, c_black, c_black, 1);


draw_set_alpha — стандартная функция, которая задает прозрачность(альфа-канал) всего рисуемого после нее. По непонятным мне причинам у функции draw_rectangle_colour нет возможности напрямую задать прозрачность, так что приходится использовать дополнительно метод draw_set_alpha. Будьте внимательны, изменив альфа канал вы меняете прозрачность всего рисуемого, а не только того, что прописано в конкретно этом событии Draw. Эта функция влияет и на другие объекты, так что как только вы поменяли альфа, нарисовали все, что нужно, верните прозрачность в её исходное положение, т.е. в 1.

draw_rectangle_colour занимается рисовкой закрашенного в синий прямоугольника с координатами верхнего левого угла (0;0) и координатами нижнего правого (elemGUI_width; elemGUI_height) соответственно. Не забудьте объявить эти переменные в событии Create со значениями, например 200 и 50 соответственно.

draw_text_colour рисует текст заданного цвета, по заданным координатам. Т.к. переменная pEnergy хранит число, нужно перевести его в строку с помощью string(). Хоть типы и не задаются напрямую в Game Maker и к строке можно без проблем прибавить число, но для события Draw конвертировать числа в строки обязательно.

[Внимание]
А теперь момент, если у объекта задан спрайт, то при таком коде как выше он рисоваться не будет. Помните, в первом эпизоде я упоминал, что если мы не переопределяем событие Draw, то Game Maker сам будет рисовать заданный объекту спрайт. Так вот, кодом выше мы переопределяем событие Draw и если бы объекту o_game был задан спрайт, то он бы перестал рисоваться. Чтобы спрайт снова стал рисоваться необходимо в нужном вам месте переопределенного события Draw дописать draw_self(); Эта строка и отвечает за отрисовку заданного объекту спрайта.

Теперь рассмотрим еще один важный момент в Game Maker — depth. В прошлом эпизоде мы его уже 1 раз видели. Настало время более подробно остановиться на этом моменте. Depth определяет порядок отрисовки. Чем выше значение Depth тем в более нижнем слое будет рисоваться объект. Принимать значения Depth может и положительные, и отрицательные. Пример — есть 3 объекта obj0 с depth 0, obj1 с depth 100, obj2 с depth -20. Порядок отрисовки — obj1 -> obj0 -> obj2. Т.е. если все 3 объекта будут находится на экране в одних координатах, поверх всех окажется obj2.

По скольку o_game рисует GUI, оно должно быть над всеми остальными слоями. Так что поставим ему depth равный -100. Сделать это можно в событии Create или на странице самого объекта. Менять же depth можно из любой точки кода.

Так, мы рисуем сейчас исключительно синий прямоугольник с надписью количества доступной энергии. Но нужно же рисовать выбор юнита. Это можно сделать двумя способами — сделать кнопки выбора юнита отдельными объектами и просто помещать их по координатам на этот прямоугольник, а можно прямо в o_game рисовать иконки и в событии Step отлавливать события клика(да, можно и так делать, вместо отдельных событий). Второй способ более навороченный, но первый более простой и конкретно в этом случае более правильный.

Значит, нам нужны объекты, активировав которые мы сможем выбирать, какой конкретно юнит сейчас мы будем ставить. На самом деле, для этой цели нам подойдет и 1 объект. Создадим его и назовем o_gui_unit.

Событие Create:
unitID = -1;
isActivated = false;


Событие Left Pressed:

with(o_game){
   isPlaceUnitClick = false;
}

with(o_gui_unit){
   isActivated = false;
}

isActivated = true;


Первые 3 строки — как раз та защита от неверных кликов, чтобы при клике на ГУИ, не ставился объект на игровое поле. Можете удалить это, либо добавьте строку isPlaceUnitClick = true; в Create объекта o_game.

Событие Draw:

if (isActivated){
   draw_rectangle_colour(x - sprite_width/2 - 3, y - sprite_height/2 - 3, x + sprite_width/2 - 3, 
                         y + sprite_height/2 - 3, c_yellow, c_yellow, c_yellow, c_yellow, true);
}

draw_self();

if (unitID >= 0){
   draw_text(x, y + sprite_height/2 + 5, string(o_game.unitCost[unitID]));
}


Происходящее в этом объекте со стороны кода должно быт очевидным — в событии Left Pressed мы проходим по всем инстансам объекта o_gui_unit и меняем значение пользовательской переменной isActivated на false, что делает его для нас не текущим выделенным, а текущий инстанс делаем активным. Если непонятно, задавайте комментарии.

Также поставьте depth объекту на -110. В общем, любое число, но меньше, чем o_game так как синяя подложка должна быть под юнитами, а не над. Либо вручную поменяйте, либо в Create можно написать depth = o_game.depth — 10;
Никогда не добавляйте 1-2 единицы depth относительного другого объекта. Всегда оставляйте немного (5-10 единиц) на случай, если придется что-то поменять.

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

Теперь нужно поменять немного o_game. Событие Create теперь будет выглядеть так:

pEnergy = 50; // player energy
alarm[0] = room_speed; // generate enemies
alarm[1] = room_speed * 3; // generate energy units

elemGUI_width = 200;
elemGUI_height = 50;
isPlaceUnitClick = true;


enum units{
   shooter = 0,
   energyGenerator = 1
}

unitCost[units.shooter] = 50;
unitCost[units.energyGenerator] = 25;

var unitSprite;
unitSprite[units.shooter] = spr_unit_shooter;
unitSprite[units.energyGenerator] = spr_unit_energy_generator;


var u = instance_create(100, 25, o_gui_unit);
u.unitID = units.shooter;
u.sprite_index = unitSprite[u.unitID];
u.isActivated = true;

u = instance_create(100 + sprite_get_width(unitSprite[units.shooter]) + 10, 25, o_gui_unit);
u.unitID = units.energyGenerator;
u.sprite_index = unitSprite[u.unitID];


Много кода и часть его явно лишняя, но зато смотрится ничего и для учебных целей отлично.
enum — ключевое слово. С его помощью просто создаем глобальный массив, к элементам которого можно обращаться так units.shooter, что выдаст нам 0 — это значение этой переменной. Дальше мы объявляем пользовательский массив с ценами на юниты. Можно было бы не использовать enum, тогда эти 2 строки выглядели бы так:

unitCost[0] = 50;
unitCost[1] = 25;


Но потом искать или вспоминать, что это за юнит 0, а что за юнит 1 может быть сложно и лениво.

Дальше мы создаем условно говоря кнопки(точнее объект o_gui_unit) и задаем им некоторые свойства.
Раньше мы задавали спрайт объекту через интерфейс Game Maker, но это можно сделать и вручную через sprite_index. Что мы, собственно, и сделали.

Теперь раз у нас есть цены на объекты и есть некое подобие GUI, с помощью которого мы можем выбрать какой юнит теперь нужно поставить, то пришла пора изменить код Global Left Released на

/// place user unit
if (!isPlaceUnitClick){
   isPlaceUnitClick = true;
   exit;
}

var tBgWidth = background_get_width(bg_grass);
var tBgHeight = background_get_height(bg_grass);
var iX = mouse_x - mouse_x % tBgWidth + tBgWidth/2;
var iY = mouse_y - mouse_y % tBgHeight + tBgHeight/2;

if (instance_position(iX, iY, o_unit_parent) != noone){
   exit;
}

var currID = -1;
with(o_gui_unit){
   if (isActivated){
      currID = unitID;
      break;
   }
}

if (pEnergy >= unitCost[currID]){
    pEnergy -= unitCost[currID];
    switch (currID)
    {
        case units.shooter: 
             instance_create(iX, iY, o_unit_shooter);    
             break;
        case units.energyGenerator: 
             instance_create(iX, iY, o_unit_energy_generator);   
             break;
    }
}


В первой проверке мы смотрим это клик по полю, или по нашему свежесозданному GUI — т.е. при клике по объекту o_gui_unit. Следующие 8 строк нам знакомы с прошлого эпизода, а дальше разберем.

В цикле with мы проходимся по всем инстансам объекта o_gui_unit и смотрим, какой из них сейчас активен. Как только натыкаемся на активный, записываем в переменную currID значение unitID этого инстанса. unitID нам нужно для знаний цены и того, какой объект будем ставить. В следующей проверке мы смотрим достаточно ли у игрока энергии для покупки этого юнита, если достаточно — снимаем соответствующее количество энергии и смотрим какой же юнит нужно поставить. Все как всегда просто.

Если вам, как и мне, не терпится проверить, что получилось, запускаем и как ни странно — работает. А я так хотел рассказать об отладке.

В дальнейшем мне придется прибегнуть к методу «Как нарисовать сову», потому, что ничего нового из теории не будет, а если расписывать элементарные вещи серия этих публикаций затянется надолго. Код Github будет доступен, так что я буду акцентировать внимание только на новых вещах, если вы не против.

Неплохо было бы сделать газонокосилки как в оригинале, которые срабатывают при подходе к ним зомби. После чего начинают двигаться по прямой, уничтожая всех врагов на своем пути. Газонокосилки не вечные, после первого срабатывания исчезают. Ничего нового из теории в реализации газонокосилок нету. Реализуйте сами и проверьте. Логика будет такая — добавляем газонокосилке событие коллизии с o_enemy_parent и добавляем код, который начинает двигать нашу газонокосилку вправо и уничтожает инстанс, с которым у нее коллизия. Также нужно не забыть поставить проверку выхода за рамки экрана(в самом объекте) и в o_game из автоматически расставить.

Пришло время познакомиться с Timeline. Данное понятие похоже на знакомые вам alarm'ы, но выступают в качестве отдельных сущностей. Таймлайны позволяют выполнять код на определенных, заданных вами шагах(кадрах). Не самая часто используемая функция в Game Maker, но для каких-то целей подойдет. С ними не очень просто работать в текущей версии Game Maker — сложно подбирать нужный шаг, это занимает время. Для удобства нужно придумывать собственный костыль — писать систему создания уровня прямо из игры. Но так как костыль для каждой игры разный, то винить создателей Game Maker не в чем. В случае конкретно этой игры, я бы сделал как-то так — создаем отельную комнату, где можно расставлять врагов. Потом относительно их x-координаты на экране генерировать код для таймлайна. y-координату вычислять будем потом(или random). Это если уж совсем по быстрому, хотя правильнее написать собственный полноценный редактор уровней.

В целях обучения, сделаем генерацию врагов в игре волнами через Timeline. Нам понадобится основной таймлайн, который будет запускать волны. Вероятно, нужно будет делать каждый уровень отдельным таймлайном.
Создаем пустой таймлайн, назовем tl_level0 и нажмем на кнопку Add. Вписываем нужный шаг, например, 120, и подтверждаем. Дальше нам предоставляется возможность использовать все те же возможности, как и внутри любого события любого объекта. В коде нужно будет создавать врагов, чтобы не дублировать код создания, будем использовать скрипты. Например, добавим в событие шагов 120, 240, 600, 1200 код — scr_generate_enemy_wave(3);. А для нескольких следующих шагов(волн), будем передавать в качестве аргумента большее число.

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

/// scr_generate_enemy_wave(maxEnem);

var maxEnem = argument0;

var enCount = 1 + irandom(maxEnem);

while(--enCount >= 0){

    var tBgHeight = background_get_height(bg_grass);
    var cycleCnt = 0;
    var eY, eX = room_width + sprite_get_width(spr_enemy_zombie)/2 + 1;
    do{
      if (++cycleCnt > 200) break;
      
      eY = irandom(room_height - room_height % tBgHeight);
      eY = eY - eY % tBgHeight + tBgHeight/2;  
      
    }until(instance_position(eX, eY, o_enemy_parent) == noone)
    
    instance_create(eX, eY, o_enemy_zombie);
    show_debug_message("enemy created");
}


Как и раньше, комментарии с /// используются для чистого удобства, но конкретно такой синтаксис, когда название скрипта совпадает с таким комментарием позволяет отобразить подсказку о количестве принимаемых аргументов скриптом, когда вы его вызываете.

Строкой var maxEnem = argument0; мы присваиваем значение аргумента под номером 0, передаваемое в скрипт. argument0, argument1, argument2 и т.д. — это стандартны ключевые слова. Т.е. вызывая scr_generate_enemy_wave(3); мы помещаем 3 в переменную argument0.
Дальше мы просто помещаем врага на игровое поле, предварительно проверив, не находится ли уже какой-то юнит под ним.

[А вы знаете?]

В Game Maker нет возможности не передавать аргумент, если в скрипте он используется. Если забудете — ничего страшного, Game Maker не скомпилирует игру и напомнит, где вы забыли передать аргумент.

Обратили внимание на show_debug_message(«enemy created»);? Это самый примитивный, но самый часто используемый способ отладки. Данная стандартная функция позволяет выдать сообщение в консоль Game Maker'а. Отображаться будет строка, которую вы передали этой функции. В нашем случае будет — «enemy created».

[А вы знаете?]

Хотя справка Game Maker'а пишет, что при создании релиз-версии пакета функции show_debug_message игнорируются, по факту перед релизом лучше комментировать эту функцию, особенно если вы передаете большие объемы данных. В какой-то из старых версий GM я передавал большое количество информации через show_debug_message каждый шаг из-за чего значительно проседала производительность даже в релиз-версии игры. Может это был баг, но лучше комментируйте эти строки перед релизом.

Вернемся к информации об отладке в Game Maker после того, как разберемся как запускать timeline'ы. Добавим в событие Create объекта o_game строку alarm[2] = room_speed * 6;// set timeline for enemy generation

В событии Alarm 2 напишем такой код:

timeline_index = tl_level0;
timeline_position = 0;
timeline_running = true;


Мы задаем текущий таймлайн — tl_level0, устанавливаем позицию на 0(шаг 0) и запускаем. Позиция может быть любая, только нужно помнить, что она задается в шагах(кадрах), а не по номеру или времени. В Game Maker много функций, позволяющих работать с таймлайнами, в том числе и «налету» во время самой игры, так что создание редактора уровней сильно упрощается с их помощью.

Раз мы уже заговорили об отладке, то давайте остановимся подробнее. Чаще всего все же достаточно show_debug_message, а для поиска узких мест в производительности с помощью get_timer(); вычислять что просчитывается дольше всего. Но можно использовать инструменты предоставляемые самим Game Maker. Они появились в версии 1.4, до этого функционал дебаггера был более скромным. Вот скриншот текущей версии.



Пользоваться примерно так же как и любым другим отладчиком. Есть прогон по шагам, пауза, просмотр локальных переменных объектов, глобальных переменных и т.д. Запускается дебаггер через кнопку F6. Но опять же, дебаггер — более профессиональный инструмент и часто проще обойтись без него.

Рассмотрим еще несколько полезных функций для отладки/оптимизации. Несомненно полезная функция show_debug_overlay отображающая поверх самой игры использование ею CPU/GPU. Отображается сколько времени нужно на отрисовку(события Draw), сколько уходит на обработку ввода, сколько на очистку экрана, сколько уходит на очистку памяти и еще много разной информации, о которой можно прочесть в справке.

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

В общем, в YoYoGames(компания разработчик Game Maker) позаботились о том, что бы возникало как можно меньше проблем и чаще всего вам отладка помимо show_debug_message в связке с get_timer и не понадобится.

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

Найдя нужные изображения(например эти _1_, _2_), скачаем их и добавим в нашу игру. Заменим спрайт зомби из предыдущего эпизода на нормальный, анимированный. Сделать это можно так, перейдем в наш спрайт, далее Edit Sprite -> File -> Create from Stip. Дальше откроется вот такое окошко, где можно прямо сразу нарезать спрайты.



Мне комфортнее делать это в стороннем графическом редакторе. Увеличим их скажем до 64 пикселей по высоте(Edit -> Transform -> Stretch). К слову, во встроенном графическом редакторе Game Maker довольно много полезных функций, можно поиграться.

Теперь вроде бы все хорошо, но если вы запустите игру, скорость смены кадров анимации очень высока. Это потому, что стандартная скорость смены кадров анимации — 1 кадр в шаг. Изменить это очень просто. В событии Create нужного объекта меняем значение переменной: image_speed = 0.2; Теперь кадры будут меняться раз в 5 шагов.

Также в Game Maker есть поддержка SWF и Spine анимаций. К слову, поделился бы кто относительно простой Spine-анимацией со скелетом для тестов на производительность.

Ну и под конец добавим еще один юнит, чтобы еще немного изучить часто используемых функций Game Maker. Найти его можно на Github, он называется o_enemy_zombie_fast. В нем есть несколько интересных моментов. Рассмотрим событие Step:

if (isActive){

    with(o_unit_parent){
       if (distance_to_point(other.x, other.y) <= 220){
          other.cHspeed = other.cHspeed2;
          break;
       }
    }
}

event_inherited();


Первое, что бросается в глаза — это то, что функцию event_inherited можно вызывать в любом месте кода, не обязательно в начале. Порой это очень полезно. И еще один момент конкретно в этом случае спорный по критерию оптимальности, но тем не менее данная функция очень полезна — distance_to_point. Она вычисляет расстояние между (x;y) объекта, который её вызвал и какой-то точкой, координаты которой мы передаем. Так же полезной является функция distance_to_object, вычисляет расстояние между (x;y) вашего объекта и ближайшим инстансом передаваемого в параметрах объекта.

Вообще в Game Maker, уже приготовлено много стандартных полезных функций, которые экономят время на написание собственного кода.

Итак, игра постепенно принимает нужную форму. Выглядеть она сейчас будет примерно так:



Изучив содержание этих 2 эпизодов, вы уже знаете примерно 70% того, что необходимо для создания игры любой сложности в Game Maker. Как видите, делать игры в этой среде действительно очень легко, просто нужно немного желания.

Примерный план следующего эпизода:

— переделаем способ расстановки юнитов
— добавим поддержку геймпада
— несколько новых юнитов
— переделаем игровое поле
— реализуем победы/поражения
— еще раз поговорим о комнатах и том как адаптировать игры для разных экранов
— научимся работать с View

Проект на Github

Некоторые возможности Game Maker с уклоном в мобильную разработку, о которых я упоминать в этой серии публикаций не буду
— встроенная поддержка Google Play Services, Game Circle(Amazon) и Game Center (ачивки и лидерборды)
— встроенная поддержка ин-аппов для мобильных устройств
— поддержка Facebook API
— рынок расширений для Game Maker
— физический движок Box2D
— шейдеры
Как стоит писать дальше?

Проголосовало 99 человек. Воздержался 41 человек.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Комментарии (0)