Родительские объекты
При работе с объектами в GameMaker IDE можно установить иерархию " родитель/ребенок ". Для этого нужно нажать кнопку "Parent" в редакторе объектов, а затем выбрать другой объект из браузера ассетов. который будет "родителем" редактируемого объекта:
Итак, каждый объект в вашем игровом проекте может иметь родительский объект, но что это значит? Когда у объекта есть родительский объект, он может обмениваться кодом, действиями и событиями с этим родителем. Такой обмен называется "наследованием", а объект, у которого есть родитель, называется "дочерним". Дочерний объект не только может делиться кодом со своим родителем, но вы можете выполнять проверки и запускать код на родительских объектах, и он будет автоматически включать и дочерние объекты, что экономит много времени и энергии.
Если это звучит сложно, то другой способ взглянуть на родительский объект — это способ "сгруппировать" объекты под одним зонтиком, чтобы они имели общие черты, не теряя при этом своей уникальной индивидуальности. Возможно, это все еще не проясняет ситуацию, поэтому давайте приведем несколько примеров.
Допустим, у вас есть объект "игрок" и четыре различных объекта "враг". Теперь вы хотите, чтобы игрок умер, если он коснется любого из этих четырех объектов, и это обычно влечет за собой четыре различных события столкновения с четырьмя различными наборами действий или кода, по одному для каждого из вражеских объектов. Но если мы создадим родительский объект для всех врагов, то мы сможем создать одно событие столкновения только с родительским объектом, и оно сработает независимо от того, какой из четырех "дочерних" вражеских объектов коснется игрока. коснется игрока. Удобная штука! В реальном объекте GameMaker вы увидите примерно следующее:
Слева у нас есть четыре отдельных события столкновения, а справа — одно событие столкновения, поскольку мы создали "родительский" объект и назначили ему все вражеские объекты. Обратите внимание, что родительский объект не должен иметь никаких событий или кода в нем.
Другим примером родительского поведения может быть игра, в которой вы хотите создать 10 различных по внешнему виду объектов и заставить их всех вести себя одинаково. Для этого вы создадите один родительский объект и в нем все ваши поведенческие действия или код в нужных событиях, а затем вы создадите десять объектов без действий или кода, но с разными sprites , и назначите им родительский объект. Теперь, когда вы поместите эти экземпляры в комнату, все они будут вести себя одинаково, но выглядеть по-разному, поскольку они "наследуют" события родительского объекта.
Наконец, вы можете использовать воспитание, чтобы "смешивать и сопоставлять" события и поведение. Мы проиллюстрируем это на последнем примере. Допустим, вам нужны два монстра: один двигается вверх и вниз, а другой — влево и вправо. чтобы они имели одинаковое здоровье, стреляли в игрока и ранили игрока при столкновении с ними. В этом случае вы видите, что почти все события должны иметь одинаковые действия, за исключением одного или двух, которые управляют движением. Итак, опять же, мы можем сделать один объект родителем другого, но в этом случае мы также определяем определенные события для дочернего объекта. Эти события "переопределяют" родительские события, то есть всякий раз, когда событие для дочернего объекта содержит действия, они выполняются вместо действий, содержащихся в событии родителя. Если вы также хотите выполнить родительское событие, вы можете вызвать так называемое "унаследованное" событие с помощью функции event_inherited() , или визуальное действие GML Call Parent Event.
Слева вверху находится родительский объект с 5 событиями в нем, а справа вы видите "дочерний" объект. Дочерний объект также имеет 5 событий, но два из них переопределяют унаследованные от родителя события (события Step и Draw ), а три других выделены серым цветом, так как они являются событиями, унаследованными от родителя. Унаследованные события также будут иметь значок "переопределение родителя" рядом с ними в редакторе событий:
Когда вы нажимаете на унаследованное событие, откроется редактор кода, в котором будет показан унаследованный родительский код, но вы не сможете редактировать этот код, так как его можно редактировать только в самом родительском объекте. Вы можете нажать правую кнопку мыши на любом из родительских событий, чтобы открыть следующее меню опций:
Здесь вы можете выбрать два варианта Открыть родительское событие, чтобы просмотреть код, или же вы можете выбрать Наследовать событие событие или переопределить событие. Если вы выберете Наследовать , то редактор кода откроется с функцией event_inherited() уже (или действием Call Parent Event, если вы используете GML Visual). Любой последующий код, который вы поместите в это событие, теперь будет выполняться так же, как и код родительского объекта. имеет. Если вы выберете Переопределить событие, то окно кода также откроется, только теперь функция event_inherited() не будет вызываться, поэтому все, что вы добавите сюда, будет выполняться вместо кода в родительском объекте.
ПРИМЕЧАНИЕ: Из редактора кода вы можете быстро перейти к родительскому объекту, нажав правую кнопку мыши и выбрав Go To Object из всплывающего меню, или (если событие было переопределено) вы можете выбрать Open Inherited Event, чтобы перейти непосредственно в редактор кода с кодом родительского события в нем.
Когда в коде вы нацеливаетесь на родительский объект, код будет применяться и к "детям" родительского объекта. Это происходит, когда в действии вы указываете, что действие должно быть применено к экземплярам определенного объекта, а в коде это происходит, когда вы используете with() оператор. Это будет работать и при вызове функций кода, таких как instance_position() , instance_number() , и т.д., где — если вы предоставите родительский объект — все экземпляры родительского и дочернего экземпляров будут включены в проверку. И наконец, родительские функции работают, когда вы ссылаетесь на переменные в других объектах, как в приведенном выше примере с монстром, если я установлю скорость врага 1 равной 10, то скорость врага 2 также будет равна десяти, поскольку он является дочерним объектом врага 1.
Обычно считается хорошей практикой в большинстве случаев создавать один базовый родительский объект, чтобы этот базовый объект содержал все поведение по умолчанию, но никогда не использовать его экземпляр в игре. Лучше использовать все дочерние объекты и использовать родительский объект только в ситуациях, подобных тем, что я описал выше. ситуациях, подобных тем, которые я описал выше, для столкновений, для ссылок на переменные и т.д. Вы также должны понимать, что у родителей тоже могут быть родители! Очевидно, что вы не можете создать цикл "родитель 1 является ребенком родителя 2 является ребенком родителя 1", но вы можете создать так называемую "иерархию объектов", где "родитель 3 является ребенком родителя 2 является ребенком родителя 1". Это чрезвычайно полезно для структурирования вашей игры, и вам настоятельно рекомендуется научиться использовать этот механизм.
Разработка простой игры в Game Maker. Эпизод 0. Первые строки
Если вы любите игры, несомненно задавались вопросом о том, как их делают. Если у вас есть (или будет) желание делать игры, но нет опыта, в этой статье я расскажу о том, как это лучше начать.
Я хотел бы рассказать об игровом движке Game Maker и разместить несколько публикаций, в которых мы напишем клон не сложной игры, например, Plants vs Zombies. Возможно, добавим поддержку геймпада и сделаем, например, Android-версию.
Исходные коды будут открытыми, а вот графика, если не найдется желающего безвозмездно её нарисовать и поделиться с сообществом, будет куплена на GraphicRiver и распространяться по понятным причинам с игрой не будет. Ну и обилием анимаций игра обладать тоже не будет.
Вступление
Game Maker — это невероятно простой игровой движок, позволяющий создать игры для большого числа платформ — Windows, Mac OS X, Ubuntu, Android, iOS, Tizen, Windows Phone, Windows 8, PlayStation 3, PS 4, PS Vita, Xbox One и HTML 5. Есть поддержка SteamWorks. В случае успеха вашей игры, портирование на другую платформу сложной задачей не будет.
Скорость разработки даже при скромных знаниях и минимальной мотивации субъективно быстрее, чем на других движках. Установка и настройка для начинающих максимально проста и не требует особых знаний. Компиляция под другие платформы не требует смены кода игры и осуществляется одним кликом (ну почти).
YoYoGames — компания, создавшая Game Maker, недавно была приобретена Playtech, что дает уверенность в том, что Game Maker продолжит развиваться. Анонсированный Game Maker 2.0 вероятно будет еще более дружественным и простом, а также логично предположить, что будет обладать еще большими возможностями. Как пишут в пресс-релизе, GM 2.0 — одна из причин покупки компании.
В этой статье я кратко расскажу о Game Maker и мы сделаем простой набросок будущего проекта.
Для кого этот движок и с какой целью его еще можно использовать?
Для всех. Цель — любая 2D игра. Однако для тех, для кого программирование не родная стихия, а так же для быстрого прототипирования и создания игры с минимальными усилиями для любого желающего делать игры и/или заработать на них, Game Maker подойдет идеально.
Плюсы Game Maker
— простое вхождение;
— знакомый всем по Java/C/C#… синтаксис;
— возможность легкой компиляции на разные платформы;
— активное сообщество, которое за многие годы уже решило много проблем и написало код за вас;
— стандартный функционал, благодаря которому не нужно самому писать большое количество кода;
— расширяемость через extension’ы;
— справка (F1) очень простая и удобная с отличными объяснениями и примерами.
Минусы Game Maker
— платность (когда вы дорастете до публикации игры, придется купить лицензию);
— нет автоподстановки пользовательских переменных, только для стандартных и скриптов;
— высокая стоимость максимальной лицензии (впрочем, не всем нужны прямо все модули);
— техподдержка (дважды обращался в техподдержку, быстрее чем через 2 недели мне не отвечали);
— нет возможности авторефекторинга.
Теперь к созданию игры. Я думаю, установить Game Maker и создать пустой проект проблемой не является. Вообще для начала хорошо было бы продумать весь функционал, нарисовать схемки, продумать монетизацию и т.д., но это не является целью статьи, так что я покажу вам способ создания проекта для начинающего разработчика игр.
- Sprites — папка с спрайтами(изображения, анимации);
- Objects — объекты со своими заготовленными событиями (например, создание, отрисовка, клик и т.д.);
- Rooms — игровые комнаты (экраны). Для каждого экрана нужно делать свою комнату. Очень удобно;
- Background — фоны, которыми можно залить комнату. Так же используется как tile set’ы
Что такое спрайт в Game Maker?
Это изображение/анимация, которые используются в игре. Они обладают своей маской, формы и размеры которой можно менять. Маска — это область изображения, которая реагирует на события столкновения объектов (если этот спрайт присвоен какому-то объекту), кликов по нему. Можно задать точку отрисовки (Origin) — например, от центра, угла или любой другой точки.
Так же для спрайта можно можно задать Texture Group. Нужно для оптимизации отрисовки (например, незачем держать в памяти texture pages с изображениями, которые используются на экране меню, когда у нас сейчас игровой экран). Для каждой Texture Group можно задать платформу, на которой они будут действовать. Например, для Android можно иметь менее детальные изображения, чем для Windows 8 планшетов.
Что такое объект (object) в Game Maker?
Это описание некоторой сущности, обладающая своими методами (функциями). Каждый объект рисует себя сам (если не задано иное), реагирует на стандартные события — нажатия клавиши, клика по спрайту и т.д… По аналогии с ООП — это класс (class).
Что такое инстанс (instance) в Game Maker?
Если объект — это просто описание сущности, то инстанс — это экземпляр объекта, его реализация в самой игре. Создав инстанс вы даете ему жизнь и теперь все события, описание которых есть в объекте начнут реагировать.
По аналогии с ООП — это объект (object).
Первое, что необходимо сделать — создать новую комнату (на левой панели правый клик на Rooms — Create Room). Назовем её rm_game. Зададим размеры окна во вкладке Settings — Width — 800, Height — 480, Speed — 60. Т.е. игра у нас будет происходить в окне 800х480, fps будет не превышать и стремиться к 60 кадрам. Сохраняем, закрываем.
Добавим несколько спрайтов. Правой кнопкой по папке Sprites -> Create Sprite. Назовем его spr_unit_shooter, загрузим картинку (есть на гитхабе в конце статьи), например, размера 54х54 и отцентрируем (кнопка Center). Кнопка «OK» и данные сохранились.
Теперь нам нужен первый юнит. Пусть это будет классическое стреляющее растение. Но перед этим нам желательно создать объект, который будет родительским для всех пользовательских юнитов (да, примерно тоже, что и наследование в ООП). Так можно избежать повторяющейся логики для всех юнитов, а также как вы увидите ниже, можно будет обращаться ко всем типам созданных во время игры «детям» этого объекта.
По принципу, аналогичному со спрайтами и комнатами, создаем пустой объект. Назовем его o_unit_parent и больше пока с ним ничего не делаем. Теперь создадим o_unit_shooter и в графе Parent выберем o_unit_parent. Зададим ему спрайт — spr_unit_shooter. Для этого воспользуемся кнопкой, которая находится под именем.
Называть спрайты, объекты, комнаты и т.д. можно как вам удобно, но для того, чтобы потом не путаться, лучше сразу называть вещи своими именами, например, спрайты с приставкой spr_, объекты obj_ или o_, скрипты — scr_ и т.д.
Теперь, каждый раз, когда вы будете создавать объект o_unit_shooter в комнате, он будет сам рисовать выбранный вами спрайт (конечно, если вы не переопределите это кодом).
Спрайт можно задавать и программно, но в таком случае он не будет отображаться в превью Project Structure слева. Теперь добавим событие, которое будет срабатывать при создании инстанса объекта. В этом событии нужно задать начальную инициализацию переменных, если они имеются. Нажмем Add Event. Как видите Game Maker позволяет каждому объекту отлавливать большое число событий. Нас интересует — Create.
Как видите справа в контейнере Actions в нескольких вкладках есть огромное количество drag’n’drop элементов, с помощью которых в теории можно создать совершенно полноценную игру не написав ни строчки кода. Но это для извращенцев и вероятно в Game Maker 2.0 этот ненужный функционал наконец уберут.
Перейдем по вкладку Control перетащим или сделаем правый клик по иконке Execute code. Откроется текстовый редактор, в котором и можно размещать игровую логику.
Как вы помните, юниты должны с определенным периодом стрелять. Сделать это можно очень легко. В событии Create напишем этот код: alarm[0] = room_speed * 2;
Это означает, что мы запускаем alarm под номером 0, который сработает через room_speed*2 шагов(кадров). room_speed у нас равно 60, что примерно равно секунде. Так что alarm[0] сработает через 120 кадров(2 секунды). alarm — это функция, а точнее событие объекта, она срабатывает как только счетчик шагов дойдет до 0. Как и все другие событие оно добавляется через Add event. Теперь нужно прописать логику, которую мы добавим в alarm[0], но прежде давайте создадим то, чем будет стрелять наше растение.
Создаем новый спрайт spr_bullet, размером 16х16 и центрируем. Теперь создаем новый объект o_bullet и задаем ему только что созданный спрайт. В событии Create добавляем код hspeed = 7; точно так же как и с предыдущим объектом. Этой строчкой мы задаем, что объект будет двигаться со скоростью 7 по горизонтали (hspeed = horizontal speed, если кто не понял). Это встроенное свойство каждого объекта. Присвоив ему ненулевое значение, этот объект начнет двигаться на заданное количество пикселей(в нашем случае по 7 вправо) каждый шаг(Step). Если мы зададим hspeed = -7; — объект будет двигаться по -7 пикселей каждый шаг, т.е. будет двигаться справа налево.
Все, возвращаемся к объекту o_unit_shooter и создаем новое событие — Alarm 0. Код, который мы напишем в этом событии как раз и будет срабатывать когда запущенный счетчик, который мы создали в событии Create. В событии Alarm 0 мы и будем создавать «пули» (в оригинале — горох), которыми стреляет растение. Добавим такой код:
Разберем этот код.
/// shoot — это просто комментарий, который будет отображаться при переходе на событие. По-умолчанию показывается — «Execute piece of code», что не очень-то информативно. Так что рекомендуется писать такие комментарии, чтобы не тратить время на переход в редактор кода.
var b = instance_create(x + sprite_width/2, y, o_bullet);
var b — мы объявляем локальную переменную, которая будет доступна исключительно в этом событии. После завершения события память освободится и обратиться к этом переменной вы не сможете.
instance_create(x + sprite_width/2, y, o_bullet); — так мы создаем новый экземпляр объекта и помещаем его в координаты по x: x + sprite_width/2, по y — y. В данном случае x и y — это координаты материнского объекта — o_unit_shooter. o_bullet — это тот объект, который мы создаем.
b.depth = depth + 1; — эта строка означает, что только что созданный экземпляр o_bullet будет находится на 1 слой ниже чем o_unit_shooter.
Последняя строка означает, что мы снова запускаем alarm[0] — растение же должно постоянно стрелять.
Как вы помните, в оригинале можно расставлять растительность только в определенных ячейках. Чтобы визуально было понятно в каких пределах можно поставить растение, создадим фон и зальем им нашу комнату. Правой кнопкой по Backgrounds — Create Background — Load Background, выбираем нужную картинку, скажем, 64х64 пикселя, обзываем bg_grass. Переходим в нашу комнату (rm_game), вкладка Backgrounds, выбираем созданный секунду назад фон. Ставим галочки, если не стоят как на изображении.
Теперь нам нужен какой-то стартовый объект, который будет делать начальную инициализацию. Создаем новый объект и называем его, например, o_game. Пусть этот объект и реагирует на клик по полю. Для этого добавим событие — Mouse -> Global mouse -> Global left released. Обычное mouse-событие означает непосредственный клик по объекту, но так как o_game не имеет своего спрайта+маски и т.к. нам нужно чтобы игрок мог кликнуть по любой точке игрового поля нужно отлавливать все события клика. Именно это и делает Global Mouse. Global left released означает, что где-то внутри игрового окна был сделан клик мышью или тач на сенсорном экране(событие срабатывает когда отпустили палец).
Добавим этому событию такой код:
Точку с запятой после операции можно и не ставить, логика кода от этого не меняется и ошибок не вызовет. Но если можно ставить, почему бы и не сделать это. Да и привычнее.
В первых четырех строках мы объявляем локальные переменные. background_get_width, background_get_height — встроенные функции, возвращающие width и height нашего фона. Как видите эти данные нам понадобятся для того, что бы просчитать iX и iY. iX и iY — это будут координаты, в которых мы создадим экземпляр объекта o_unit_shooter. mouse_x, mouse_y — встроенные в Game Maker глобальные переменные (т.е. те, к которым мы может обратиться из любого места), хранящие текущие координаты курсора мыши (пальца). Т.к. мы работаем в событии Global left released, в них хранятся последние координаты где пользователь отпустил левую кнопку мыши (отпустил палец). Математические операции, результат которых присваиваются переменным iX, iY нужны для просчета координат, в которых экземпляр объекта o_unit_shooter будет находится ровно по средине ячейки фона bg_grass. Т.е. помните, что в Plants Vs Zombies растение нельзя поставить где угодно, только в определенной точке, но при этом кликнуть-то можно где угодно и растение поставится как раз в нужном месте. Этим и занимается весь код выше.
Проверка instance_position (iX, iY, o_unit_parent) != noone означает, что мы смотрим находится ли по координатам iX, iY любой инстанс (экземпляр объекта), родительским объектом которого является o_unit_parent. По скольку у нас сейчас только один наследуемый юнит — o_unit_shooter, то мы проверяем, нет ли экземпляров o_unit_shooter на игровом поле, но пишем o_unit_parent в проверке для того, чтобы код срабатывал и тогда, когда мы добавим новые пользовательские юниты. noone (от «no one») — некий аналог null в других языках.
exit — код, который обрывает выполнение события. Т.е. если в координатах iX, iY какой-то юнит уже есть, срабатывает exit и инстанс o_unit_shooter не создается, т.к. мы прерываем выполнение всего последующего кода. Это нам нужно, чтобы в одной ячейке не могло стоять 2 пользовательских юнита.
Что ж, пришло время добавить первого врага. Создаем новый объект и опять же создадим базовый родительский объект. Назовем o_enemy_zombie и o_enemy_parent, который будет ему родительским. Создадим спрайт spr_enemy_zombie, отцентрируем и присвоим его o_enemy_zombie.
По сколько свойством всех врагов является движение в сторону растений, то создадим в o_enemy_parent в событии Create этот код:
cHspeed — это пользовательская переменная, значение которой мы присваиваем hspeed, с которой мы уже встречались. Почему просто не написать hspeed = -4; — увидите потом.
Ранее мы объявили пользовательские переменные через конструкцию var, но здесь мы этого не делаем. В чем же разница между cHspeed = -4; и var cHspeed = -4;?
Все просто — в первом случае переменная будет доступна из любой точки кода этого объекта и к ней можно будет обратиться из любого другого объекта, но только не забыв упомянуть, к какому именно объекту мы обращаемся. Сейчас вникать в это необязательно. Помним то, что эта переменная существует все время существования инстанса объекта с тех пор, как она объявлена. В случае же с var cHspeed = -4; она будет доступна только на время действия события, в котором она создана.
На самом деле к ней тоже можно обратиться из другого объекта, но в случае если вы обратитесь к ней из другого объекта в момент, когда событие, в котором она создана уже закончилось, это вызовет ошибку — знакомый всем null pointer, ибо из памяти она уже выгружена.
Если сам не нравятся функции hspeed, wspeed вы можете сами их реализовать изменяя значение x или y в событии Step. Эти функции просто делают это за вас.
HP — это еще одна переменная, в которой мы будем хранить количество очков жизни врагов. Этой переменной будет «владеть» каждый инстанс, но ведь максимальное количество очков жизни у разных типов врагов разные, т.е. нужно как-то переопределить/перезадать это значение. Либо можно задать всем врагам одинаковое количество жизней, скажем, 100 и ввести понятие defence от которой будет зависеть получаемый врагом урон, но сейчас нету смысла усложнять, верно? Так что обойдемся только одной переменной — HP.
Запомните, gml — язык, используемый в Game Maker регистрозависимый, HP, hP, Hp и hp — будут разными переменными.
canAttack — просто переменная, которой мы присваиваем значение true(истина). Пока просто напишем и забудем о ней.
Раз у нас значение HP у каждого врага будет разное, нужно как-то переопределить это значение. Это очень-очень просто. Переходим к объекту o_enemy_zombie, создаем реакцию на событие Create и пишем код:
Функция event_inherited(); и занимается наследованием. Т.е. теперь o_enemy_zombie выполнит код:
Который «импортирует» эта функция, а затем значение выполнится строка — HP = 20;
Т.е. по факту на конец события Create объект o_enemy_zombie будет иметь такие свойства:
Если же мы забудем о функции event_inherited(); или забудем объекту o_enemy_zombie указать родительский объект, враг двигаться не будет, при попытке обратиться к переменной cHspeed этого объекта появится ошибка.
Великолепно, если мы захотим создать еще один тип врага, в событии Create мы напишем тоже самое, изменив на нужно количество HP:
Раз у зомби есть очки жизни, они должны быть и у растения. Добавьте самостоятельно в событие Create объекта o_unit_parent код HP = 20; и строку event_inherited(); в событие Create объекта o_unit_shooter.
А вы знаете?
Если вам не нужно ничего переопределять и дописывать в событии Create, добавлять код event_inherited(); без другой логики в событие не нужно — за вас это сделаем сам Game Maker. Тоже касается любых других событий, не только Create.
Отлично, наш зомби теперь идет, однако его не берут пули и растения его не тормозят. Решим сначала первую задачу. Перейдем в o_bullet и создадим новую реакцию на событие — Add Event -> Collision -> o_enemy_zombie. Это событие будет вызываться когда o_bullet и o_enemy_zombie врежутся друг в друга. Коллизия проверяется по маске, о которой вы читали в начале статьи. Добавим код:
Это очень интересный момент. other — это инстанс объекта, с которым в этот момент события происходит коллизия. Естественно, т.к. этот код находится в событии столкновения с экземпляром объекта o_enemy_zombie, то в other и будет только инстанс o_enemy_zombie.
С помощью конструкции with()<> мы обращаемся к этому элементу other. Все, что происходит внутри <> касается исключительно этого экземпляра объекта. Таким образом, HP -= 5; — это вычитание 5 очков жизни из врага. В if (HP <= 0)<> мы сравниваем количество очков жизни тоже именно у этого объекта. Помните я немного выше говорил про разницу между обычным объявлением переменной и с переменной через var. Вот этот пример должен вам окончательно прояснить ситуацию. Т.к. переменная HP у нас объявлена не через var, то она доступна в любой момент времени. Так что с помощью конструкции with мы можем к ней получить доступ. Альтернативный способ обращения к переменной другого объекта выглядел бы так:
Но так обращаться к переменным менее удобно, особенно, если логики будет больше, но тем не менее в некоторых случая применимо.
Не забывайте, если вы объявили переменную не в событии Create, а в коде вы обращаетесь к ней до того, как она объявлена, это вызовет ошибку, если вы попытаетесь считать какие-то данные из нее.
Не нужно обладать большими знаниями английского, что бы понять, что функция instance_destroy(); удаляет этот экземпляр объекта(инстанс).
Таким образом весь этот код означает, что при коллизии мы отнимаем 5 очков жизни у зомби и если у него их становится 0 или меньше, то мы его уничтожаем. Независимо от результата в конце мы удаляем нашу пулю. Проще некуда. Вообще, наверное, лучше было бы заставить зомби самостоятельно следить за своим здоровьем, но пока нас это не интересует. Но это уже другая история вопрос оптимизации.
Было бы неправильно, если бы наши зомби могли только получать урон. Нужно же добавить возможность наносить урон. Прежде всего добавим новую переменную в событие Create объекта o_enemy_parent
Пришло время ознакомится с событием Step, о котором я ранее рассказывал. Данное событие срабатывает каждый кадр. Все просто. Если room_speed равно 60, то данное событие будет срабатывать примерно 60 раз в секунду. Добавим этот код в событие Step -> Step объекта o_enemy_zombie.
Ничего страшного в нем нет почти все конструкции вам уже знакомы.
if (!isActive) exit; — если объект не активен, т.е., скажем, отдыхает/перезаряжается/делает замах, данное событие выполнятся не будет. В следующих двух строках мы получаем координаты центра ячейки, находящейся слева от той, на которой сейчас находится центр нашего instance(помним, что x — возвращает координаты Origin-точки, а она у нас выставлена как раз по центру спрайта). Дальше мы смотрим, находится ли по координатам (leftCellCenterX, y) пользовательский юнит. Если там что-то есть происходит последующая логика, но о ней через секунду, если же там ничего нет, мы присваиваем hspeed значение переменной cHspeed, которую мы, помните, создаем в событии Create. Вот тут она и пригодилась. Смысл за этим скрывается такой — если наш зомби остановился для того, чтобы атаковать и уничтожил растение, нужно чтобы он продолжил свой путь. Можно, конечно, не вводить переменную cHspeed, но тогда нужно будет вспомнить где вы задаете скорость движения, а это забывается.
Это в случае если на пути зомби ничего нет, теперь же возвращаемся к моменту, когда нам предстоит бой. Первые же строки оказываются очень интересными, с подвохом. Дело в том, что объявив локальную переменную frontEnemySprtWidth мы в инстансе frontEnemy присваиваем ей значение. Знакомые с программированием, скажут, но ведь в таком случае мы обращаемся к переменной frontEnemySprtWidth не нашего зомби, а к переменной с таким же именем, но инстанса frontEnemy. Так да не так, дело в том, что локальные переменные(объявленные через var) становятся видимыми внутри этого события везде, даже изнутри инстанса frontEnemy. Таким образом в коде ошибки нет, мы действительно обращаемся именно к той переменной, которая была объявлена локальной внутри зомби. Если вы не поняли этого момента поэкспериментируйте или прочтите справку, там все прекрасно объяснено, а мы идем дальше.
Мы присвоили frontEnemySprtWidth значение длины(width) спрайта юнита пользователя(растения), который находится на ячейку левее нашего зомби. Вы скажете, а зачем нам городить такую сложную для первого понимания конструкцию, если можно обойтись var frontEnemySprtWidth = sprite_get_width(spr_unit_shooter);. Ответ прост — это сейчас у нас одно растение и мы знаем к какому спрайту обратиться, но при добавлении новых типов юнитов(подсолнухи и т.д.), придется городить громоздкую конструкцию switch, чтобы узнать что же за объект впереди нас, а так довольно просто решается эта проблемка.
Дальше мы проверяем, если расстояние между крайней правой точкой пользовательского юнита и крайней левой точкой нашего зомби меньше 12 пикселей, то мы останавливаем нашего зомби, проверяем может ли наш зомби атаковать(проверяем значение ранее созданной в событии Create объекта o_enemy_parent переменной canAttack), продолжается выполняться код, в котором мы говорим, что атаковать теперь уже нельзя и что следующий раз это можно будет сделать через room_speed * 1.2 кадров(через 60*1.2) — это мы делаем в alarm[0](сами добавьте его в соответствующее событие(Alarm 0) объекта o_enemy_parent, где напишите код canAttack = true;). Если атаковать можно, отнимаем у инстанса растения 5 очков жизни и проверяем, живо ли оно еще, если нет — уничтожаем.
Ну вот и отлично враг готов — он двигается, атакует и продолжает движение, если уничтожил растение, но у него есть один недостаток — его не существует. Мы создали только описание нашего врага, теперь нужно же помещать зомби на игровое поле. Возвращаемся в событие Create объекта o_game. Добавим код
alarm[0] = room_speed; // generate enemies
Т.е. через 60 кадров сработает Alarm 0 и будет создан зомби, правильно? Нет. Мы же не создали логики для этого Alarm. А код тут тоже простой:
Все просто — мы не будем усложнять и просто каждые 3 секунды(60 кадра * 3) создаем инстанс o_enemy_zombie по координатам X: room_width + sprite_get_width(spr_enemy_zombie)/2 + 1 т.е. за ровно на один пиксель правее, чем граница экрана, т.е. зомби изначально видно не будет и Y — случайная ячейка. room_width и room_height, как вы уже поняли, это width и height нашей комнаты. Т.е. 800 и 480 соответственно.
Это все отлично, но инстанс объекта o_game тоже кто-то должен создать иначе весь эпизод смысла не имеет. Но наш завершающий шаг очень прост — переходим в комнату rm_game -> Objects -> выбираем в менюшке o_game и помещаем его где попало в комнате. Альтернативный вариант выглядит так — вкладка Settings -> Creation Code( — это код, который будет срабатывать когда мы переходим в эту комнату). Добавляем строку instance_create(0,0, o_game);
Координаты можно любые. Теперь мы можете задать вопрос, а как Game Maker определит, что нужно запускать комнату rm_game или «а что если у нас будет много комнат, с какой Game Maker начнет?». Все как всегда просто — самая верхняя комната запускается первой(их порядок можно менять перетягивая мышкой). Сейчас она у нас одна потому сразу она же и запустится.
Теперь у нас должно получиться что-то такое:
На этом первый эпизод закончен. Поздравляю, мы сделали прототип игры. Осталось совсем немного — сделать из него полноценную игру, чем и займемся в следующих частях.
В этом эпизоде мы ознакомились с базовыми понятиями Game Maker, использовав как можно больше возможностей. Некоторые моменты сделаны не очень рационально, их можно и нужно переделать, но не все сразу. Для начального понимания происходящего, я считаю, лучше все же писать по аматорски.
Как видите по уроку, в некоторых планах текущая версия Game Maker не идеальна, многое приходится держать в голове, зато в Game Maker проще делать все остальное. Небольшое неудобство стоит того.
В следующем эпизоде:
— /теория/ скрипты
— /теория/ отладка
— /практика/ юнит подсолнухи
— /практика/ мана(солнышки)
— /практика/ генерация врагов волнами
— /практика/ газонокосилки
— /практика/ новые юниты зомби и растений
— /теория + практика/ примитивный интерфейс
В принципе, уже сейчас все из раздела практики вы уже можете сделать сами из полученных знаний, но, наверное, в целях увеличения багажа знаний мы с вами реализуем в более усложненном виде.
Наследование
Ключевое слово parent
Наследование — это некий механизм, посредством которого один или несколько классов можно получить из некоторого базового класса.
Класс Product (базовый класс) — должен содержать общие свойста и общие методы (общую логику).
Поэтому удаляем из класса все частные свойства и методы. — Оставляем свойства общие для всех товаров.
— Конструктр будет задавать свойства, общие для всех товаров.
— Получим через гетеры getName() и getPrice() общие свойства товаров.
— Метод getProduct() — будет возвращать инфрмацию о товаре, используя общие свойства.
Файл Product.php
<?php
class Product
<
// общие свойства товаров
public $name ; // наименование товара
public $price ; // цена товара
// конструктр будет задавать свойства, общие для всех
public function __construct ( $name , $price )
<
$this -> name = $name ;
$this -> price = $price ;
>
// метод getProduct() — будет возвращать инфрмацию о товаре, используя общие свойства
public function getProduct ( $type = ‘notebook’ )
<
$out = «<hr><b>О товаре:<b><br>
Наименование: < $this ->name > <br>
Цена: < $this ->price > <br> » ;
>
// общие гетеры:
public function getName ()
<
return $this -> name ;
>
public function getPrice ()
<
return $this -> price ;
>
>
?>
В индексом файле в экземпляры класса передаем по два параметра: название и цена. Получаем общую для всех типов товаров информацию с помощью метода getProduct() .
Файл index.php
<?php
error_reporting (- 1 );
require_once ‘classes/Product.php’ ;
function debug ( $data )
<
echo ‘<pre>’ . print_r ( $data , 1 ) . ‘</pre>’ ;
>
// В экземпляре класса Product (книжка) — передаем только два параметра
$book = new Product (‘ Три мушкетера’ , 20 );
// экземпляр класса Product (ноутбук)
$notebook = new Product ( ‘Dell’ , 1000 );
// распечатаем данные объекты
debug ( $book );
echo ‘<br>’ ;
debug ( $notebook );
// получаем информацию о продукте с помощью метода getProduct()
echo $book -> getProduct ();
echo $notebook -> getProduct ();
?>
Выведет:
Product Object
(
[name] => Три мушкетера
[price] => 20
)
В результате у нас выводится информация, общая для всех типов товаров: названия товаров и их цена. Специфичная информация каждого конкретного товара не выводится.
Поскольку у нас два типа товаров (ноутбук и книги), то есть смысл создать два дочерних класса: NotebookProduct и BookProduct.
Создаем дочерний класс NotebookProduct и добавим в него специфичные свойства, в нашем случае — $cpu .
Для того, что-бы один класс мог наследовать другой класс, используется ключевое слово extends ( extends — расширять), и название класса, который расширяем — Product.
В этом случае все методы и свойства родительского класса Product — наследуются и они нам доступны.
Чтобы получить специфичные свойства и методы — опишем их в дочернем классе NotebookProduct. Объявим свойство $cpu — специфическое свойство для Notebook. В классе Product не было этого свойства, а в классе NotebookProduct мы его добавили — тем самым расширили класс Product.
Также добавим метод getCpu() .
При создании объекта нам необходимо заполнить свойства значением. Для этого напишем конструктор ( __construct ).
Когда мы определяем метод с тем же названием ( __construct ), что и в родительском классе, то мы его перезаписываем ( переопределяем ), то есть, теперь будет работать этот метод, а не метод в родительском классе. Если мы хотим получить ту же самую функциональность, мы должны объявить те же самые параметры из родительского класса плюс добавить наши параметры: $name,$price плюс $cpu .
Ключевое слово parent
Для того, чтобы не дублировать код, используем ключевое слово parent и оператор разрешения области видимости «::«.
С помощью ключевого слова parent ( parent — это слово указывает на родительский класс) и оператора разрешения области видимости «::» мы обращаемся к родительскому классу и вызваем нужный нам метод ( __construct ), который требует два параметра $name и $price :
parent::__construct($name, $price) .
Затем дописываем нужную нам функциональность (добавляем наш параметр):
Теперь наш конструктор (перезаписанный) вызывает сначала родительский конструктор, который отработает, и затем будет отработывать добавленный нами код.
Аналогично переопределяем родительский метод getProduct() :
Вызываем родительскую функциональность:
— и сохраняем ее в переменную $out ,
и далее дописываем переменную $out нашей новой функциональностью (информацией о процессоре)
— и возвращаем переменную $out .
Файл NotebookProduct.php
// создаем дочерний класс NotebookProduct
// добавляем специфичные свойства
// для того, что-бы один класс мог наследовать другой класс,
// ипользуется ключевое слово (extends — расширять),
// и класс, который расширяем — Product
class NotebookProduct extends Product
<
// специфическое свойство для Notebook (мы расширили класс Product)
public $cpu ;
// когда мы определяем метод с тем же названием, что и в родительском классе,
// то мы его перезаписываем (переопределяем),
// то есть, теперь будет работать этод метод, а не метод в родительском классе
// мы должны объявить те же самые параметры из родительского класса
// плюс добавить наши параметры
public function __construct ( $name , $price , $cpu )
<
//$this->name=$name; // параметры из родительского класса
//$this->price=$price;
// с помощью ключевого слова (parent — это слово указывает на
// родительский класс) и оператора (:: — разрешение области видимости)
// мы обратились к родительскому классу и вызвали нужный
// нам метод (__construct), который требует два параметра $name и $price
parent :: __construct ( $name , $price );
// дописываем нужную нам функциональность (добавляем наш параметр)
$this -> cpu = $cpu ;
>
// аналогично переопределяем родительский метод getProduct()
public function getProduct ()
<
// строки из метода (getProduct() класса Product) переносим
// сюда и присваиваем переменной $out
$out = parent :: getProduct ();
// дописываем переменную $out нашей новой функциональностью
$out .= «Процессор: < $this ->cpu > <br>» ;
// возвращаем переменную $out
return $out ;
>
// метод getCpu()
public function getCpu ()
<
return $this -> cpu ;
>
>
?>
Тоже самое сделаем и для дочернего класса BookProduct:
Файл BookProduct.php
class BookProduct extends Product
<
public $numPages ;
public function __construct ( $name , $price , $numPages )
<
parent :: __construct ( $name , $price );
$this -> numPages = $numPages ;
>
public function getProduct ()
<
$out = parent :: getProduct ();
$out .= «Кол-во страниц:: < $this ->cpu > <br>» ;
return $out ;
>
public function getNumPeges ()
<
return $this -> numPages ;
>
>
?>
В итоге мы получили два класса NotebookProduct и BookProduct, которые расширяют родительский класс Product.
В индексном файле подключаем наши классы и создаем объекты уже этих классов и передаем им параметры из соответствующих конструкторов.
Файл index.php
<?php
error_reporting (- 1 );
require_once ‘classes/Product.php’ ;
// подключаем класс NotebookProduct
require_once ‘classes/NotebookProduct.php’ ;
// подключаем класс BookProduct
require_once ‘classes/BookProduct.php’ ;
function debug ( $data )
<
echo ‘<pre>’ . print_r ( $data , 1 ) . ‘</pre>’ ;
>
// создаем объект класса BookProduct и передаем параметры из конструктора BookProduct
$book = new BookProduct (‘ Три мушкетера’ , 20 , 1000 );
// создаем объект класса NotebookProduct и передаем параметры из конструктора NotebookProduct
$notebook = new NotebookProduct ( ‘Dell’ , 1000 , ‘Intel’ );
// распечатаем данные объекты
debug ( $book );
echo ‘<br>’ ;
debug ( $notebook );
// получаем информацию о продукте с помощью метода getProduct()
echo $book -> getProduct ();
echo $notebook -> getProduct ();
?>
Получим:
BookProduct Object
(
[numPages] => 1000
[name] => Три мушкетера
[price] => 20
)
NotebookProduct Object
(
[cpu] => Intel
[name] => Dell
[price] => 1000
)
———————————————————————————
О товаре:
Наименование: Три мушкетера
Цена: 20
Кол-во страниц: 1000
———————————————————————————
О товаре:
Наименование: Dell
Цена: 1000
Процессор: Intel
— В результате получаем вместо двух объектов класса Product, конкретные объекты класса BookProduct и класса NotebookProduct.
Никаких пустых свойств: для книжки — cpu и для ноутбука — numPages здесь нет.
Проблема дублирования кода, проблема каких-то лишних свойств для того или иного объекта — решены. При этом, если у нас появятся новые типы товаров, нам достаточно описать соответствующие классы и при этом наследоваться от суперкласса (родительского класса), в котором уже описана какая-то общая логока. И вся эта логика будет наследоваться дочерними классами. В дочерних классах останется только дописать специфичную логику для данного класса.
Наследовать можно один класс только от одного другого класса. Но PHP поддерживает цепочку наследований: сейчас мы наследуем класс BookProduct от класса Product ( расширяем класс Product), но также мы можем создать какой-нибудь класс, который будет наследоваться от класса BookProduct ( расширять класс BookProduct). Этот класс получит все свойства и методы, которые содержатся в классе BookProduct и в классе Product, то есть, он будет наследовать сразу два класса. Эта цепочка наследований не ограничена.
Родительские объекты
Небольшая статья для новичков о родительских объектах — что это и зачем они могут понадобиться.
Примечание: Поскольку в txt2 нет разметки gml-кода, я взял на себя труд выделить ключевые слова, так, как они выглядят по-умолчанию в самом gml, чтобы код выглядел более-менее читабельным.
Общие сведения
Родительский объект — это обычный объект, который выступает одному или нескольким объектам "родителем". Объекты, которые имеют родителя, называются дочерними, и наследуют события и поведение родителя. В свою очередь, родительские объекты могут иметь своих родителей и так далее. Таким образом, мы получаем очень мощный инструмент по оперированию объектами.
Можно привести пример — в игре есть несколько видов разных монстров, у каждого своё поведение и набор данных, но на другие объекты они реагируют одинаково — умирают при попадании в ловушку; меняют направление при соприкосновении со стенами; убивают игрока, если тот наткнётся на них, и, в свою очередь, погибают от пуль игрока. В таком случае удобно объединить все объекты монстров, под общим родителем "Enemy". Теперь, сам объект "Enemy" никогда в игре не появится, но все события>действия, которые вы зададите в нём, будут выполнятся для всех его дочерних объектов!
Крайне удобно объединить все пули/снаряды под общим родителем "Missle" и задать ему событие столкновения с "Enemy", в результате чего, нанести урон последнему, а сам снаряд уничтожить. Или дать событие столкновения с родителем "Walls", что приведёт к уничтожению снаряда.
Что мы получаем в результате? Родительские объекты определяют общие свойства и поведение дочерних им объектов — теперь любой снаряд, если столкнётся с любой стеной — уничтожится, а если с любым врагом — нанесёт тому урон и тоже уничтожится.
Как видите, родительские объекты избавляют нас от многих хлопот — не нужно, например, отдельно прописывать каждой пуле события столкновения с каждым монстром, описывать их почти одинаковые действия и т.д.
Есть два способа установить одному объекту родителя. Первый — в окне объекта, кликнув на кнопку Parent выбрать из списка нужный объект.
Или непосредственно в коде, задав его с помощью функции object_set_parent (obj,par), где — obj — идентификатор/имя объекта, которому необходимо установить родителя, par — идентификатор/имя объекта будущего родителя.
Также родительские объекты нужны, чтобы объединить разные объекты в одну группу (категорию) и проводить над ними общие действия. Примером может служить RTS-построение игры:
Объекты "солдат", "танк", "самолёт" — объединяются под родительским объектом "Юниты".
Объекты "бараки", "завод", "аэродром" — объединяются под родительским объектом "Здание".
"Юнит" и "здание" объединяются под объектом "Войска игрока". Это позволяет оперировать над всеми войсками одновременно, да и вовсе показывает, что данный объект — игровой юнит, что очень полезно в некоторых случаях.
Родительские объекты крайне удобно использовать с конструкцией with , так как указание в конструкции родительского объекта также действует и на все его дочерние объекты.
Пример: Заставить всех юнитов двигаться в нужном направлении:
with ( obj_unit )
<
motion_set (<параметры движения>)
>
В результате, все танки, пехота и самолёты, родитель которых — obj_unit направятся в нужную сторону.
Однако, это можно сделать и в самом родительском объекте, без конструкции with, лишь прописав в нужное условие действия движения.
Конец
Прикрепляю к статье исходник *.gmk и простенькую мини-игру (для тех, у кого нет гамака -_-), построенную на взаимодействии родительских объектов, чтобы глубже понять возможности этого инструмента.