Серия «Gamemaker Studio 2: серия гайдов»

32

GameMaker Studio 2. Урок 8. Звук

Это воскресенье, мои чуваки.
Всем привет. Сегодня поговорим с вами о том, как подключить звук к своей игре. Как обычно: немного теории, немного практики.
Покажу вам сайт, где большинство звуков можно взять бесплатно и как его настраивать.
Ну и небольшие бонусы, само собой.

Ссылки на предыдущие гайды:

Первый гайд - Знакомство.

Второй гайд - События отрисовки, коллизия, скрипты.

Третий гайд - Камера и разрешение экрана.

Четвертый гайд - Иерархия объектов. Глобальные переменные.

Пятый гайд - Структуры данных. Сетка комнаты и размещение объектов по сетке.

Шестой гайд - Алгоритмы поиска путей.

Седьмой гайд - Сохранения и их загрузка. Бонус в конце!

План:
- Создание звука
- Подключим звук к кнопкам
- Глобальная настройка звука
- Заставим человечков издавать звук
- Музыка
- Тест звука
- Немного бонусов

Тема сегодня небольшая, поэтому сразу перейдём к делу.

Создание звука.


Прежде, чем приступать к работе, нам нужно подготовить материал. Вы можете записать звук самостоятельно, либо использовать свой: найденный в интернете или записанный самостоятельно.
Я предлагаю скачать уже готовый набор звуков для меню по ссылке:
Ссылка

Как видно, там несколько форматов: OGG, WAV и MP3. Использовать мы можем все из них.
Сохраните архив, распакуйте в удобное место. Можете сразу скопировать путь до него. Дальше нам нужно создать звук и загрузить его. Для этого найдём папочку Sound и там создадим, собственно, Sound.

GameMaker Studio 2. Урок 8. Звук Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок, Игры, Стратегия, Обучение, Видео

Звук назовём sndButton.
Итак. Если сейчас мы нажмём на появившийся звук, то увидим небольшое меню настроек для нашего звука. Пока нам это не интересно. Чтобы загрузить звук, нужно нажать на три точки справа от названия звука.

GameMaker Studio 2. Урок 8. Звук Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок, Игры, Стратегия, Обучение, Видео

Там всё просто: указываем путь до нужного нам файла. У меня это "modern2.wav". Можете выбирать любой формат, пока это не столь важно. Стоит лишь отметить, что форматы отличаются не только весом, но и качеством звука и возможностью на него влиять внутри движка.

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

Обратите внимание: внизу указанного меню у вас есть вкладка Audio Group. Это ничто иное, как группа звуков. Её можно (и нужно) использовать в тех случаях, когда нам нужно уменьшить или увеличить только определённые звуки. Например, сделать три настройки звука: общий, музыка, игра. С-но, здесь будет три аудио группы: стандарт, музыка, игра.
Чтобы добавить свою аудио группу нужно в верхней части экрана найти вкладку Tools, там - Audio Groups.

GameMaker Studio 2. Урок 8. Звук Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок, Игры, Стратегия, Обучение, Видео

Подключим звук к кнопкам.

Для удобства используем объект oMMenu, который сделаем родительским объектом для всех наших кнопок в меню, в т.ч. в настройках.
Не забудьте прописать event_inherited(); у дочерних объектов в тех событиях, которые нужно будет унаследовать.

Для начала, в Create объявим одну переменную:

collided = false;
Она позволит нам проигрывать звук при наведении только один раз.

Теперь в Step'e сделаем простую конструкцию: если наведены на текущий объект и раньше его не касались, то проигрываем звук и говорим, что мы его коснулись.
var dmxg = device_mouse_x_to_gui(0);
var dmyg = device_mouse_y_to_gui(0);
if instance_position(dmxg, dmyg, self) != noone
{
if !collided
{
audio_play_sound(sndButton, 10, false);
collided = true;
}
}
else
{
collided = false;
}
Вот так просто мы сделали следующее:

Соответственно, чтобы проиграть звук нам была нужна команда:
audio_play_sound(Звук, Приоритет,  Зациклить)

Приоритет - то, в каком порядке звук обрабатывается. Значения либо от 0 до 100, либо от 0 до 1. Чем больше число - тем выше приоритет.

Глобальная настройка звука.

Раз есть звук - значит его можно настроить.
Для этого в комнате настроек создадим ещё одну кнопку: oSSound.
Как ни странно, но с помощью этой кнопки мы будем устанавливать значение громкости для всех звуков, в т.ч. музыки.

Для этого нужно в событии step будет прописать следующий код:

var dmxg = device_mouse_x_to_gui(0)
var dmyg = device_mouse_y_to_gui(0)
var sound = audio_get_master_gain(0);
if instance_position(dmxg, dmyg, self) != noone
{
if mouse_check_button_pressed(mb_left)
or mouse_check_button(mb_right)
{
if dmxg > x + 260
{
if sound < 1
{
sound += 0.01;
}
}
else if dmxg < x + 60
{
if sound > 0
{
sound -= 0.01;
}
}
audio_master_gain(sound) // Устанавливаем глобальный звук.
}
}
audio_master_gain(x) принимает на вход значение от 0 до 1 (0% - 100%) и устанавливает громкость звуков.

Есть похожая функция, но которая называется audio_set_master_gain(ID слушателя, Громкость). Соответственно, она необходима нам в том случае, если у нас несколько "слушателей". Полезно при создании мультиплеера, либо для того, чтобы позволить нашему ИИ ориентироваться на звук.
Результат:

Заставим человечков издавать звук.

Прежде, чем приступим, немного теории.
Мы ведь не хотим, чтобы те звуки, которые есть в комнате, были слышны ото всюду, верно? Следовательно, нам нужно использовать немного другой подход к созданию звуков.
Чтобы в дальнейшем иметь возможность на эти звуки влиять, предлагаю воспользоваться эмиттером - или излучателем.
Делать он будет ровно то, о чем говорится в его названии: излучать звук.
А ещё мы сможем его настроить таким образом, чтобы звук был слышен только на определённом расстоянии от излучателя, а также начинал затухать спустя определённое расстояние.

Здесь всё предельно просто.
Перейдём к oCharPlayer и создадим у него событие - alarm 0. Это событие - "будильник", или, если по нормальному, таймер, в котором будет работать код раз в определённое количество кадров. А теперь пошагово:

Create:

s_emit = audio_emitter_create(); // Создаём эмиттер.
audio_max_dist = CellWidth * 6; // Макс. расстояние, на котором будет слышно звук
audio_fall_at = CellWidth * 2; // Расстояние, на котором звук начнёт "падать"
audio_falloff_set_model(audio_falloff_linear_distance); // Указываем модель, по которой будет происходить затухание звука
audio_emitter_position(s_emit, x, y, 0); // Устанавливаем стартовую позицию эмиттера.
audio_emitter_falloff(s_emit, audio_fall_at, audio_max_dist, 1); // Устанавливаем затухание.
var len = audio_sound_length(sndButton); // Узнаём длину звука в секундах.
audio_play_sound_on(s_emit, sndButton, false, 1); // Говорим звуку играть.
alarm[0] = room_speed * len; // Запускаем бесконечный таймер, где будет проигрываться звук.
Alarm 0:
var len = audio_sound_length(sndButton);
audio_play_sound_on(s_emit, sndButton, false, 1);
alarm[0] = room_speed * len;
Step:
audio_emitter_position(s_emit, x, y, 0);
Весь код нужно добавлять в конец существующих событий соответственно.
Итак, что у нас получилось? Сейчас увидим.

Как видно, звук перемещается вместе с нашими человечками. Накладывается друг на друга, но играет строго раз в определённый отрезок времени, успевая полностью "проиграться".

Музыка.

По аналогии с oSSound, создадим oSMusic. Разница в том, что в oSMusic мы в Create сразу пропишем переменную music, равную 0.1. Она и будет отвечать у нас за громкость музыки. Соответственно, нужно sound заменить на music в коде и всё.

Создаём файл для музыки: sndMusic
Заходим в Tools -> Audio Groups. Создаём аудио группу под названием audio_music.
Жмакаем "add asset" и выбираем sndMusic.

Теперь перейдём к oGameManager.
В Create допишем:

if !audio_group_is_loaded(audio_music)
{
audio_group_load(audio_music);
}
audio_group_set_gain(audio_music, 0.1, 0)
Следом в Step:
if !audio_is_playing(sndMusic)
{
audio_play_sound(sndMusic, 10, true)
}
По сути, обозначенный выше код будет загружать нашу аудио группу музыки в память и начнёт её проигрывать.

Тест звука.

Немного бонусов.

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

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

Там так-то всё просто. Общая логика работы следующая:
Когда нажали (разово) левую кнопку мыши, мы запоминаем координаты клетки, на которую были наведены.
Далее, отдельно проверяем: если кнопка мыши зажата и координаты первой клетки запомнены, на постоянной основе обновляем координаты второй. Заодно создаём прямоугольник с заполнением или без.
Ну и при отпускании ЛКМ - проверяем, что первая координата у нас задана, обновляем картинку и обнуляем все использованные переменные.

Соответственно, как это выглядит при работе:

Ссылка на проект

Сайт, откуда можно брать бесплатные ассеты: звуки, музыку, спрайты и прочее.
Либо из маркета YoYo. На ваше усмотрение.

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

Спасибо всем за тёплые слова и поддержку.
До скорых встреч.

Показать полностью 3 5
29

GameMaker Studio 2. Урок 7. Сохранения и их загрузка. Бонус в конце!

Привет!
Сегодня поговорим о реализации сохранений в GMS, их разницу со встроенными сохранениями и немного затронем тему работы с файлами.

Ссылки на предыдущие гайды:

Первый гайд - Знакомство.

Второй гайд - События отрисовки, коллизия, скрипты.

Третий гайд - Камера и разрешение экрана.

Четвертый гайд - Иерархия объектов. Глобальные переменные.

Пятый гайд - Структуры данных. Сетка комнаты и размещение объектов по сетке.

Шестой гайд - Алгоритмы поиска путей.

Краткий план на сегодня. Интересна конкретная тема - поиск по странице вам в помощь.
- Теория. Как работают сохранения?
- Встроенный функционал GMS. Как и когда использовать? Плюсы и минусы.
- Реализация собственной системы сохранений.
- Бонус! 

Теория. Как работают сохранения?

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

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

После записи информации в файл, её необходимо считать и провести с ней какие-либо действия. Поэтому важна системность - хаотичный набор строк разбирать сложно. :)

Встроенный функционал GMS. Как и когда использовать? Плюсы и минусы.

В GMS'е имеется две функции с лаконичными названиями, которые вы можете использовать в своём проекте.
Сохранение игры:

game_save("Название_Файла.Формат") - где ".Формат" официальный мануал предлагает использовать .dat

Загрузка игры:

game_load("Название_Файла.Формат")

Соответственно, самая простая система загрузки сохранений, которую вы можете реализовать, выглядит следующим образом.
Перейдём в объект oGameManager и создадим у него два события: нажатие на F5 и нажатие на F6. Первое будет сохранять файл, второе - загружать.
Важно: везде добавим проверку, что мы находимся в основной комнате. В противном случае, будут вылетать ошибки при попытке загрузки из любой другой комнаты.

В событии нажатия F5:

if room == Room1

{

game_save("save.dat");

}

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

if file_exists("save.dat") and room == Room1
{
game_load("save.dat");
}

Всё. Самая простая система сохранений готова и работает. Как видно, нам хватило буквально четырёх строк кода (двух, если убрать проверки), чтобы её сделать.

Теперь поговорим о тонкостях работы.

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

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

А ещё эта система сохранений в будущем, скорее всего, окончательно станет нерабочей.

Итак, из плюсов у нас только скорость установки. Минусы перечислены выше.
Как итог, скажу следующее: использовать данную систему следует на свой страх и риск. Если вы знаете, что ваша игра не нуждается в нормальных сохранениях и носит исключительно экспериментальный характер, то сойдёт и так. Если же вы игрой заняты серьёзно, то вам нужно будет создать свою систему сохранений.
Благо, что это просто! :)
И плюсов у неё много. Например, устойчивость к обновлениям как игры, так и движка ;)

Реализация собственной системы сохранений.

Для ЛЛ.
Общий алгоритм:
Сохранение -> Заносим всю информацию о каждом изменяемом объекте (в т.ч. о тайлах) комнаты в файл.
Загрузка:
- Если файл сохранения есть -> Удаляем все объекты из комнаты -> Читаем информацию из файла -> Создаём объекты заново и вносим в них правки.

Прежде, чем что-то писать, для начала изменим немного то, что уже есть.
Перейдём к объекту oGrid и создадим у него события: User Event 0, 1.

GameMaker Studio 2. Урок 7. Сохранения и их загрузка. Бонус в конце! Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок, Игры, Стратегия, Обучение, Видео, Без звука

Это, как понятно из названия, пользовательское событие. Мы можем его вызывать только тогда, когда нам нужно, используя event_user(0) (где 0 - это номер события).

Итак.
В событие №0 мы переносим весь код из Create, кроме объявления cells_x/y, а также заполнения тайлмапа и создания grid_array.
В событие №1 перенесём код, который у нас создаёт карту.

var lay_id_blocks = layer_get_id("BlockLayer");
var map_id_blocks = layer_tilemap_get_id(lay_id_blocks);
var lay_id_shadows = layer_get_id("ShadowLayer");
var map_id_shadows = layer_tilemap_get_id(lay_id_shadows);
for (var _y = 0; _y < cells_y; ++_y)
{
for (var _x = 0; _x < cells_x; ++_x)
{
ds_grid_set(global.grid, _x, _y, 0) // 0 - это стоимость клетки. Ноль - значит непроходима.
tilemap_set(map_id_blocks, Tile.ground, _x, _y)
tilemap_set(map_id_shadows, 47, _x, _y)
}
}
Да, всё правильно: grid_array нам больше не нужен. Его наличие будет создавать больше неудобств, чем пользы. Соответственно, нужно будет изменить все места, где он ранее использовался, удалив его. У меня это уже проделано, поэтому рекомендую в последствии скачать проект, если возникнут трудности. Затронуты, в основном, скрипты поиска пути и код у игрока.

P.S. Хоть от grid_array мы и избавились из-за того, что он больше не нужен, нам нужны некоторые формулы.
Получение ID клетки:
id = cell_x + cell_y * max_x;
Соответственно, клетки по x и по y можно получить следующим образом:
cell_x = id mod max_x;
cell_y = id div max_x;

max_x - максимальное количество клеток по оси x, можно получить с помощью функции ds_grid_width(global.grid);


Теперь перейдём в oGameManager. У нас там было событие Room Start, где мы активировали все инстансы. Его нам нужно будет дополнить.

if room == Room1

{

instance_activate_all()

if !instance_exists(oGrid)

{

var inst = instance_create_layer(0, 0, "Instances", oGrid)

inst.cells_x = 65;

inst.cells_y = 55;

with (inst)

{

event_user(0);

event_user(1);

}

}

}

Что мы сделали: если в комнате нет сетки, то мы создаём oGrid, устанавливаем свои значения клеток по x, y и запускаем событие создания сетки. Это нас убережёт от постоянного пересоздания сетки при заходе в комнату.

Наконец, приступаем к созданию скриптов.

Всего их два. scrSave и scrSaveLoad - для сохранения и для загрузки сохранений соответственно.

scrSave - скрипт сохранения
Алгоритм:
Создаём пустой массив. Затем - проходимся по всем изменяемым объектам, сохраняя информацию из них в виде структур в массив.
Затем переводим получившийся массив структур в формат json. Чтобы минимизировать расходы памяти, мы заносим эту информацию в буффер, сохраняем буффер в файл и удаляем буффер.

Кода много, поэтому ссылкой:
https://pastebin.com/YBYejqe0

scrSaveLoad - скрипт загрузки сохранения.
Алгоритм
:
Уничтожаем всё, что есть в нашей комнате.
Получаем айди слоев для тайлов.
Если существует файл сохранения, то загружаем его в буффер, "читаем" в переменную (читай - сохраняем), а буффер удаляем.
Помните, что сохраняли файл мы в json? Теперь мы его "читаем" с помощью команды json_parse. Таким образом, мы получаем массив структур, с которым теперь можем работать.
Проходимся по каждому элементу массива через цикл, проверяя тип объекта, создавая их при необходимости и применяя настройки при необходимости.

Оговорюсь, что в готовом скрипте у нас применяются настройки для oGameManager. Это временная мера, так как настройки игры должны лежать отдельно от файлов сохранения.

Ссылка:
https://pastebin.com/kCNT0zcW

Чтобы проверить работоспособность, на тех же F5 и F6 пропишите эти скрипты.

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

Бонус.

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

Это будет скрипт для прямого управления человечком.
Кратко, алгоритм выглядит так.
На стороне игрока:
- Выделяем человечка.
- Нажимая на любую свободную клетку (или ту, что занята, но рядом имеет свободные), ставим конечную точку пути.
- Рассчитываем путь и передаём его человечку.
- Используя массив с точками, которые нужно пройти, создаём встроенный путь (path) и наполняем его, затем - запускаем. Profit!

Для начала, у oPlayer, в Step'е, где мы ранее создавали пути, всё немного допишем и перепишем. Когда устанавливаем первую точку пути, если у нас в этой же клетке есть подконтрольный персонаж - запоминаем его ID.
Когда будем устанавливать вторую точку пути - если ID персонажа существует, то передаём ему получившийся массив с точками пути, "перевернув" его. Нужно это, чтобы ближайшая к персонажу на данный момент точка шла "нулевой".

Скрипт на переворачивание массива.
https://pastebin.com/uL5MhpmX

Новый код для создания пути:
https://pastebin.com/dWJVuFuB
П.С. Переменную char нужно предварительно создать в Create.

Create у oCharPlayer:

path = noone;
bpath = noone;
spd = 4;
User Event(0) у oCharPlayer:
if path_exists(bpath)
{
path_delete(bpath)
}
bpath = path_add();
var max_x = ds_grid_width(global.grid);
path_set_closed(bpath, false);
for (var i = 0; i < array_length(path); ++i)
{
var path_x = (path[i] mod max_x) * CellWidth + 16;
var path_y = (path[i] div max_x) * CellWidth + 16;
// Стоимость пути. Выше стоимость клетки в DS grid = ниже скорость.
var cost = 100 div ds_grid_get(global.grid, path[i] mod max_x, path[i] div max_x);
path_add_point(bpath, path_x, path_y, cost);
}
path_start(bpath, spd, path_action_stop, false)
Вкратце: если путь уже существует, то мы его удаляем. Затем создаём новый.
Делаем путь "открытым". Соль в чём: "закрытый" путь = зацикленный, то есть достигнув конечной точки пути, наш человечек затем вернётся в самое его начало. Соответственно, открытый путь эту проблему решает.
Затем мы добавляем точки в путь. Важно, передаём мы их в виде координат комнаты, а не сетки. Как бонус - код выше учитывает стоимость клетки, поэтому "замедление" человечка уже реализовано.
Хотите сделать возможность ещё и ускорения - меняйте базовое значение пустой клетки.

Draw у oCharPlayer:
draw_self();
if path_exists(bpath)
{
draw_path(bpath, x, y, false)
}
Задача: просто отрисовываем путь в технических целях. :)

Step у oCharPlayer:
if path_exists(bpath)
{
if x == path_get_x(bpath, 1) and y == path_get_y(bpath, 1)
{
path_delete(bpath);
}
}
Задача: удалить путь, когда он нам больше не нужен.

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

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

P.S. юниты не считаются за "препятствие", а потому спокойно могут проходить сквозь друг друга.

Ссылка на исходник:
https://disk.yandex.ru/d/HjiC4fmpLvsYLw

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

И да. В следующем гайде, скорее всего, без бонусов тоже не обойдётся. Я немного разленился, но постараюсь это исправить. :)

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

Спасибо всем, кто читал и удачи тем, кто будет пытаться повторить. Вы зайки и умницы!
Будут вопросы - задавайте, постараюсь на все ответить максимально подробно.
Есть пожелания или замечания - буду рад выслушать.

Показать полностью 1 1
22

GameMaker Studio 2. Урок 6. Алгоритмы поиска путей. Оптимизация игры

Привет!
Сегодня будет много теории и много практики. Поговорим о том, какой инструментарий нам предлагает GMS 2 для создания путей и как делать делать их самостоятельно. Поговорим о том, почему и когда стоит использовать базовые инструменты или написанные самостоятельно, а также поговорим о том, как сделать игру быстрее.

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


Ссылки на предыдущие гайды:

Первый гайд - Знакомство.

Второй гайд - События отрисовки, коллизия, скрипты.

Третий гайд - Камера и разрешение экрана.

Четвертый гайд - Иерархия объектов. Глобальные переменные.
Пятый гайд - Структуры данных. Сетка комнаты и размещение объектов по сетке.

Оглавление:
- Поиск путей. Что это и как работает?
- Встроенные средства для работы с путями.
- Алгоритмы поиска путей. Какие существуют?
- Алгоритмы поиска путей. Скрипты.
- Оптимизация. Что это и зачем?
- Оптимизация. Исправляем ошибки и учимся их избегать.

Поиск путей. Что это и как работает?

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

Итак, представим сетку комнаты, которую мы делали ранее. Это набор точек, каждая из которых имеет собственные координаты.
Так как сетка у нас квадратная, то каждая точка, кроме крайних, имеет четыре (восемь, если учитывать диагонали) "соседей". Таким образом, из одной точки мы можем попасть в любую соседнюю.
Любой алгоритм поиска путей занимается тем, что пытается попасть из точки А в точку Б, перебирая соседние клетки. Занимая соседнюю клетку, он проходится по её "соседям" и так до тех пор, пока не будет достигнута точка конца или не кончатся "соседи".
На самом деле, необязательно иметь именно квадратную сетку, чтобы поиск путей работал, ведь поиск путей, по своей сути, работает на более высоком уровне: графах.

Граф - это модель, состоящая из множества вершин и множества соединяющих их рёбер.
Ниже - пример графа из заданий по информатике на экзаменах.

GameMaker Studio 2. Урок 6. Алгоритмы поиска путей. Оптимизация игры Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок, Игры, Стратегия

Таким образом, любая сетка - это лишь определённое представление графа.

Встроенные средства для работы с путями.

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

global.grid_mp = mp_grid_create(0, 0, cells_x, cells_y, CellWidth, CellHeight)
Где cells_x, cells_y - количество сеток по осям x, y, а CellWidth и CellHeight - ширина и высота одной клетки.

После создания сетки комнаты, вам остаётся только задать две точки и вы уже сможете строить между ними пути. В step событии любого объекта создайте скрипт, чтобы устанавливать эти точки, а затем в draw событии напишите следующий код:
var new_path = path_add()
var mp_path = mp_grid_path(global.grid_mp, new_path, fcell_coords[0] * CellWidth + 16, fcell_coords[1] * CellHeight + 16, scell_coords[0] * CellWidth + 16, scell_coords[1] * CellHeight + 16, 1)
if mp_path
{
draw_path(new_path, fcell_coords[0] * CellWidth + 16, fcell_coords[1] * CellHeight + 16, false)
}
Где fcell_coords и scell_coords - два массива, хранящие клетки по x,y старта и конца соответственно.
Функция path_add() - создаёт путь.
Функция mp_grid_path() строит путь между указанными координатами по сетке. Если путь существует, то возвращает true, иначе - false. Если путь существует, то сохраняет в переменную пути, указанную в скобках, точки, которые предстоит пройти.

Таким образом у вас будет отрисован путь по клеткам.
Чтобы увидеть сетку, нужно написать:
draw_set_alpha(0.25)
mp_grid_draw(переменная_которая_хранит_сетку)
draw_set_alpha(1)
Чтобы добавить на неё препятствия существуют команды:
mp_grid_add_cell() // Закрашивает одну определённую клетку, делая непроходимой.
mp_grid_add_instances()  // Закрашивает все клетки, на которых расположены определённые объекты.
mp_grid_add_rectangle() // Закрашивает определённую прямоугольную область.
Создать путь - это только первый шаг. Второй - это запустить по нему объект двигаться. Для этого есть функция:
path_start(путь, скорость, что_сделать_в_конце_пути, абсолютный_путь_или_к_текущей_позиции)
Всё. Так легко и просто можно создать сетку, расположить на ней непроходимые объекты и строить пути между точками, в последствии их запуская.

Но у такого подхода есть и минусы. Главный из них - ограниченность инструментария. Он позволяет нормально работать только с квадратными сетками и не учитывает "стоимость" клеток, по которым проложен путь.
Рекомендую использовать встроенные средства в тех случаях, когда вы создаёте простую игру, где о подобных мелочах можно не заботиться. Они достаточно хорошо оптимизированы. А мы идём дальше.

Алгоритмы поиска путей. Какие существуют?

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

Жадный алгоритм.
Суть данного алгоритма отражена в его названии. На каждом шаге он делает наилучший на данный момент выбор. То есть, всегда двигается по самым "дешёвым" вершинам графа, пока в итоге не будет достигнута точка конца.

Поиск в ширину.

Это рекурсивный алгоритм поиска всех вершин графа или дерева. Обход начинается с корневого узла и затем алгоритм исследует все соседние узлы. Затем выбирается ближайший узел и исследуются все неисследованные узлы.
Сам алгоритм:
1. Перейдите на соседнюю нерассмотренную вершину. Отметьте как рассмотренную. Отобразите это. Вставьте ее в очередь.
2. Если смежная вершина не найдена, удалите первую вершину из очереди.
3. Повторяйте шаг 1 и шаг 2, пока очередь не станет пустой.
Если же говорить своими словами, то данный алгоритм равномерно исследует все доступные точки. Такой алгоритм часто применяется при генерации карт, либо для их исследования. Правда, он не строит пути как таковые. Он просто показывает, как мы можем посетить все точки на карте.
Источник

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

Алгоритм A* (A star).
Данный алгоритм является вариацией алгоритма Дейкстры, скрещённого с жадным поиском и эвристическим поиском. То есть, он старается выбирать и самые дешёвые пути, и самые "близкие" к конечной точке.

Что за эвристический поиск?
Эвристический поиск находит расстояние между двумя точками и расширяет границы к конечной точке больше, чем в другие стороны. В зависимости от типа сетки, существует два способа определения расстояния.

Квадратная сетка.
abs(a[0] - b[0]) + abs(a[1] - b[1]) // a[0],b[0] - координаты по оси x, a[1], b[1] - по оси y. abs возвращает абсолютное (всегда положительное независимо от результата) значение.
Гексогональная сетка.
(abs(qa - qb) + abs(ra - rb) + abs(sa - sb)) / 2 // где q, r, s - это координаты в кубической системе.

Источник для гексов
Статья с Хабра с подробным разбором, взятая за основу.

Алгоритмы поиска путей. Скрипты.

Переходим ко вкусному.
Поиск соседей на квадратной сетке.
***
Алгоритм А* на GML.
Небольшая особенность данного скрипта. Если клетка, в которую мы хотим прийти, является непроходимой, то мы всё равно строим до неё путь. Встать на неё мы всё равно не сможем, но получим ближайший к данной точке путь.
Важно. Работает это только для соседних к конечной клеток.
Как работает:
Если найден, возвращает путь в виде массива между двумя точками. Точки старта и конца должны быть переданы в виде их порядкового номера. Если путь не найден, возвращает noone.
Соответственно, чтобы отрисовать путь - нужно будет пройтись по массиву из точек.

Поиск соседей на гексагональной сетке отличается только координатами. Вместо x и y мы используем q, r, s, которые являются трёхмерным представлением двумерных координат.
col - это столбец, row- это строка. x и y по сути.
q = col
r = row - (col - (col & 1)) / 2
s = -q - r

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

По сути - всё. Дальше стандартно: проверяем, что мы не выходим за границы сетки и если стоимость гекса != 0 (или больше 0), то добавляем его.

Оптимизация. Что это и зачем?

Я думаю, каждый знаком с термином "оптимизация". Если же нет, то вики говорит нам:

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

Конкретно в случае с GMS, есть некоторые моменты, которые стоит запомнить.
- Используйте локальные переменные.
Вместо того, чтобы постоянно получать результат функции, сохраните его один раз в переменную и используйте её в дальнейшем.
Вместо постоянного обращения к глобальным переменным, сохраните глобальную переменную в локальную и обращайтесь к ней. И так далее. Это быстрее.

- Старайтесь использовать объекты по минимуму.
На фоне предыдущих гайдов это прозвучит несколько странно, ведь мы заполняли комнату объектами полностью. Делалось это с целью теста, не более того.
В нашем с вами случаем, ребята, гораздо быстрее хранить информацию о каждой клетке в массиве, а клетки отрисовывать через тайлмап.
Объект, даже если он занимается только отрисовкой собственного спрайта, нагружает систему больше. Поэтому проще отрисовать спрайт отдельно. Прирост ФПС составит до 6 раз. ( Замерял, выросло с 500 до 2500 в среднем :) )

О том, как это работает и реализацию покажу ниже. :)

- Не рисуйте объекты, которые находятся за границами камеры.
Очевидный, но важный совет, так как draw event является крайне ресурсоёмким, как и step.

- Не делайте проверку коллизий там, где это не нужно. Если делаете, то сохраняйте результат в локальную переменную.

Оптимизация. Исправляем ошибки и учимся их избегать.

Первое, что мы сделаем - это создадим один большой спрайт, на котором разместим все наши игровые спрайты. Важно, чтобы первая клетка была пустой!
Затем - создайте объект Tileset и назначьте ему этот спрайт.
На скриншоте ниже показано, почему первая клетка должна быть пустой.

GameMaker Studio 2. Урок 6. Алгоритмы поиска путей. Оптимизация игры Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок, Игры, Стратегия

Дальше - создайте слой комнаты типа "Тайл" и назначьте ему тайлсет.

GameMaker Studio 2. Урок 6. Алгоритмы поиска путей. Оптимизация игры Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок, Игры, Стратегия

Всё, теперь мы готовы использовать вместо объектов - тайлы. Причём сразу для всех видов: и для фона, и для непосредственно отрисовки объектов, и для украшательств: спрайтовых теней.
Теперь в oGrid нужно будет заменить старый код на новый.
Ссылка на код.

Кратко пробежимся по тому, что мы используем и как.
lay_id_blocks = layer_get_id("Название_Слоя") - Здесь мы получаем ID нашего слоя.

var map_id_blocks = layer_tilemap_get_id(lay_id_blocks) - Здесь мы получаем слой тайлов, используя ID слоя.
tilemap_set(map_id_blocks, Tile.ground, _x, _y) - Здесь мы устанавливаем у слоя тайлов, по определённым координатам, определённую картинку. Tile.ground возвращает 1, следовательно будет отрисована первая клетка из нашего тайлсета. Нулевая клетка - пустая, используется для удаления тайлов.
С-но, для получения текущего тайла существует команда tilemap_get(map_id_blocks, клетка_x, клетка_y). Если по указанной позиции установлен тайл, вернёт его номер. Если нет - вернёт ноль. Вернёт -1, если есть ошибка.

Специально не обращаю внимание на то, как пришлось переписывать код, так как по сути изменилось лишь взаимодействие, которое вкратце - описано выше.

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

Ссылка на оригинальный скрипт для интересующихся:
https://github.com/iAmMortos/autotile

Как ускорить проекты, сделанные на Gamemaker Studio 2 под Windows

Здесь есть вся нужная информация. Делается с помощью компиляции под С++.
https://help.yoyogames.com/hc/en-us/articles/235186048-Setti...

Будем прощаться, ребята.

Ссылка на исходник
Там два файла. YYZ - для тех, кто хочет повтыкать в код. В папке - уже скомпилированный проект, для тех, кто просто хочет потыкать и посмотреть, как это всё добро работает.

Темы, которые осталось разобрать:
- Сохранение. Встроенное VS самописное. Работа с файлами.

- Звуки.

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

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

Показать полностью 3
28

GameMaker Studio 2. Урок 5. Структуры данных и с чем их едят, а также grid (сетка комнаты), размещение объектов по сетке

Привет! Сегодня разберёмся, что такое массивы и структуры данных. Создадим сетку комнаты, научимся размещать объекты по этой сетке.

P.S. Передаю привет тому человеку, который ставит минусы на всё, что я пишу. Счастья тебе, здоровья.


Ссылки на предыдущие гайды:

Первый гайд

Второй гайд

Третий гайд

Четвертый гайд

Сегодня мы поговорим о следующем:

- Что такое массивы и для чего они нужны? Что такое структуры данных?

- Какие виды структур данных существуют в GMS? Их особенности.
- Реализуем сетку карты.

Что такое массивы и для чего они нужны?

Массив - это структура данных, которая хранит набор значений. Мы можем "взять" каждое такое значение, обратившись к массиву по индексу.

Индекс - это порядковый номер элемента в массиве.

Важно знать:

1. Индексы в массивах начинаются с нуля.

2. В массив можно запихнуть массив, в него запихнуть массив и так до бесконечности. Отсюда следует:

2.1. Массив называется одномерным, если в него не вложены другие массивы. Если в массив вложен другой массив, то это уже двумерный массив. Если в массиве есть массив с массивом - трёхмерный. И так далее.

Массив - структура данных. Что такое структура данных?
Структура данных - это "контейнер", который хранит данные в определённом формате. Соответственно, массив - один форматов хранения данных.

Какие виды структур данных существуют в GMS? Их особенности.


Первый структура данных, которую мы разберём называется Array (массив). Его можно создать несколькими способами:

1.array = array_create(10, noone)
2. array = [1, 2, 3, 4] // внутри пишем любые свои значения
3.
array[0] = 1
array[1] = 2

И так далее. Также можно объявить сколько угодно мерный массив:

array[0][0][0][0] = 1.

Такие массивы преспокойно встраиваются в сохранение стандартными средствами GMS. Также здесь в автономном режиме работает "сборщик мусора". И это важно, поскольку в структурах данных (приписка DS) этого нет. Соответственно, их придётся удалять вручную.

Часто в мануале говорится, что лучше использовать именно array, а не другие аналогичные структуры, так как меньше шанс "выстрелить себе в ногу". Впрочем, если покурить тот же мануал, то использовать DS можно вполне спокойно, выигрывая при этом в скорости работы программы.

Structs или структуры.

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

// Create event
mystruct =
{
pos_x : x,
pos_y : y,
count : 1000
};
// Clean Up event
delete mystruct;

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

Обращение к struct для вычленения из него данных похоже на таковое для объектов. Нам нужно назвать структуру, а затем через точку - нужную нам "переменную".

show_debug_message(mystruct.pos_x)

С-но, в каждом значении структуры можно хранить другое значение. Напоминает HTML-код.

mystruct =
{
a :
{
aa : "Example"
},
b :
{
bb : "Another"
},
};

В таком случае, нам нужно будет обратиться к структуре, к значению, а затем к значению внутри значения.

show_debug_message(mystruct.a.aa)

Также для простоты к структуре можно обращаться через with

with(mystruct)
{
a += other.x;
}

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

// Функция-конструктор.
function Vector2(_x, _y) constructor
{
x = _x;
y = _y;
static Add = function(_vec2)
{
x += _vec2.x;
y += _vec2.y;
}
}
// создаем новую структуру
v2 = new Vector2(10, 10);

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

DS Grids или сетки.

Формально - это двумерный список. Позволяет, как ни странно, создавать сетку и назначать каждой клетке какое-либо числовое значение.

Для хранения позиций клетки всегда следует использовать целые значения. Нецелые будут автоматически округлены движком, что может быть непредсказуемо.

Как создать:

переменная = ds_grid_create(ширина в клетках, высота в клетках)
Ещё раз обращаю внимание, что с помощью DS Grid мы можем назначить каждой клетке отдельное значение. В случае с созданием своего алгоритма поиска путей это означает, что в зависимости от типа местности, алгоритм будет находить наиболее оптимальный путь опираясь на итоговую стоимость пути. Просто как пример.
А ещё сетка используется для создания тех самых "клеток", по которым идёт расположение построек, к примеру.

DS Lists или списки.

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

Применять нужно в тех местах, где важна скорость работы и неизвестно конечное число элементов. В ином случае лучше применять array.

переменная = ds_list_create();
Вставить новое значение:
ds_list_add(переменная, значение)

DS Maps или словари.

Словарь по своей сути похож на struct: он хранит данные в паре "ключ : значение". Если представить это образно, то это две колонки. Первая - это ключ. Второе - значение. Обращаясь к первой колонке, к ключу, мы можем спокойно получить значение. Или получить все ключи по определённому значению.
Мы будем их использовать в следующем гайде, когда будем разбирать пути и будем писать свои функции для их создания.
Способ создания:

переменная = ds_map_create()
Вставить новое значение:
ds_map_add(переменная, ключ, значение)

Мануал рекомендует использовать вместо DS Maps - структуры. Собственно, с ними нам тоже придётся поработать.

DS Queues, DS Priority Queues, DS Stacks

Всё это - "очереди", только по разному работающие. Разберём сначала Stacks и Queues.


Stacks
это структура типа LIFO (last-in-first-out / последним пришёл - первым ушёл). Как и со списками, мы можем "вычленять" последние значения из стаков с помощь команды pop. Особенность LIFO в том, что в таком случае мы получим последнее значение, которое загружали в список.

С Queue ситуация обратная. Там используется FIFO (first-in-first-out / первым пришел - первым ушёл). То есть, забирая значение из queue (очередь), мы получим первое значение, которое вставили.

Важная оговорка. pop - берёт значение из массива, при этом удаляя его из него.

Priority Queue или приоритетная очередь - это та же очередь, но в которой каждое значение имеет свой "вес" - приоритет, что влияет на расположение данных в структуре и позволяет отбирать, скажем, наиболее дешёвые пути. Как говорит мануал, полезно для создания таблиц лидеров или информационных списков. Мы же будем это использовать для написания алгоритма поиска путей А* (A star, А звездочка).

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

Создаём сетку комнаты.

1. У объекта oGameManager пропишем две дополнительные макро переменные, которые будут хранить ширину и высоту одной клетки.

#macro CellWidth 32

#macro CellHeight 32

2. Создадим объект oGrid, который расположим в нашей основной комнате.

3. У этого объекта создадим событие Create, где будем создавать нашу сетку.

3.1. Для наглядности, создадим сетку с помощью команды mp_grid_create
mp в данном случае - это "motion planning" - планирование движения. Система, которая позволяет создать сетку комнаты и сразу же по ней создавать пути, используя встроенные алгоритмы. Они не дают всего необходимого функционала, поэтому этот пункт выполняем исключительно для наглядности.

global.grid = mp_grid_create(0, 0, ceil(room_width / CellWidth), ceil(room_height / CellHeight), CellWidth, CellHeight)

3.1.1. Перейдём в событие Draw, где отрисуем сетку.

draw_set_alpha(0.1)
mp_grid_draw(global.grid)
draw_set_alpha(1)

3.1.2. Зайдём в игру и посмотрим, что получилось.

GameMaker Studio 2. Урок 5. Структуры данных и с чем их едят, а также grid (сетка комнаты), размещение объектов по сетке Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок

Как видно, у нас получилась квадратная сетка. Используя функции mp_grid_* мы можем закрашивать нашу сетку, делая определённые квадраты непроходимыми. Но мы не можем назначать таким клеткам свою стоимость.
Иными словами, если будет два одинаковых по длине пути, но один через условные зыбучие пески, а второй по асфальту, то получим 50/50, что персонаж выберет медленный и потенциально опасный путь, просто потому что мы не можем указать, какой из путей будет приоритетнее.

Подробнее о том, когда и какие функции поиска путей использовать, мы будем говорить в следующем гайде. Продолжим.

3.2. Удаляем или комментируем то, что мы написали. Сейчас будем работать со структурами данных.
3.2.1. Создаём сетку через ds_grid_create в событии create

var cells_x = ceil(room_width / CellWidth)
var cells_y = ceil(room_height / CellHeight)
global.grid = ds_grid_create(cells_x, cells_y)

Ок. По сути, сетка готова. Теперь по ней мы можем создавать объекты. Давайте же заполним всю нашу комнату oDirt, а дальше уже будем разбираться.
3.2.2. Всё там же в Create нам нужно пройтись циклом по осям x, y, создавая объект oDirt по указанным координатам. Затем мы передадим получившиеся координаты, в клетках, в переменные внутри объектов, для удобства.

for (var _y = 0; _y < cells_y; ++_y)

{

for (var _x = 0; _x < cells_x; ++_x)

{

ds_grid_set(global.grid, c, u, 0) // 0 - это стоимость клетки. Ноль - значит непроходима.

var inst = instance_create_layer(_x * CellWidth + 16, _y * CellHeight + 16, "Instances", oDirt)

inst.coordx = _x

inst.coordy = _y

}

}

Зачем при создании объекта мы плюсовали 16 пикселей?
Напомню, что точка origin - отсчёта - у наших объектов находится в центре их спрайтов. Следовательно, располагая объект по координатам (предположим, (0, 0)), часть объекта будет выходить за границы комнаты.

3.2.3. После запуска всё работает. Проверим координаты каждой клетки. Для этого вернёмся к игроку и в событии Draw GUI сделаем проверку: если позиции мыши по x,y (относительно комнаты) содержит в себе oObject, то выводим на экран coordx и coordy.
if instance_position(mouse_x, mouse_y, oObject)
{
var inst = instance_position(mouse_x, mouse_y, oObject)
scrOutlinedText(dmxg + 10, dmyg + 10, c_white, c_black, string(inst.coordx) + " " + string(inst.coordy), depth, fontArialRusSmall, fa_left, fa_top)
}
Проверяем.
GameMaker Studio 2. Урок 5. Структуры данных и с чем их едят, а также grid (сетка комнаты), размещение объектов по сетке Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок

Работает! Впрочем, это только первая часть того, что нам нужно, не так ли? Помимо создания карты, мы должны иметь возможность располагать объекты самостоятельно, удалять их при необходимости. Да и с расположением земли не всё так гладко, как хотелось бы. Мы ведь совсем не добавили фон!

Начнём с конца, ибо это, на самом-то деле, достаточно просто.
У нас с вами есть два пути.
1. Мы создаём слой для спрайтов, на котором у нас будут спрайты фона.
2. Мы создаём для каждого нашего объекта "переднего плана" дубликат для заднего фона. По сути, увеличиваем количество объектов в комнате в два раза, соответственно увеличивая и нагрузку.

Работать мы будем с первым вариантом по той причине, что он будет работать быстрее, а также предоставляет нам весь необходимый функционал, потому и смысла во втором варианте нет.

Первый вариант, несмотря на преимущество в скорости работы, имеет существенный минус. Мы не сможем обращаться к спрайтам по их координатам, да и в целом будем сильно ограничены в работе с ними. Придётся хранить ID каждого отрисованного нами спрайта. Для этого нам придётся создать вторую переменную, которая будет хранить координаты клеток (по x, y), а также id спрайта.
Так как мы знаем размер одной клетки, мы можем легко получить индекс той клетки, на которую сейчас наведены. Следовательно, можем получить и ID текущего спрайта - удалить его или назначить новый.

Получаем порядковый номер клетки. Для этого берём координату x текущей клетки, к ней плюсуем произведение текущей координаты y и максимального размера сетки по x. Т.Е. формула:

cell_id = x + y * max_x

В коде же это будет выглядеть следующим образом:

var cell_id = mouse_x div CellWidth + mouse_y div CellHeight * ceil(room_width / CellWidth)

Итак, работаем.

1. Создаём новый слой - Asset Layer, который назовём Backs.
2. Перейдём к объекту oGrid.
3. Добавим ещё одну глобальную переменную сразу после объявления сетки - global.grid_array = []
4. Чуток допишем цикл. Сделаем так, чтобы в список у нас добавлялись значения x и y.

var cells_x = ceil(room_width / CellWidth)

var cells_y = ceil(room_height / CellHeight)

global.grid = ds_grid_create(cells_x, cells_y)

global.grid_array = []

for (var _y = 0; _y < cells_y; ++_y)

{

for (var _x = 0; _x < cells_x; ++_x)

{

ds_grid_set(global.grid, _x, _y, 0) // 0 - это стоимость клетки. Ноль - значит непроходима.

var inst = instance_create_layer(_x * CellWidth + 16, _y * CellHeight + 16, "Instances", oDirt)

inst.coordx = _x

inst.coordy = _y

array_push(global.grid_array, [_x, _y])

}

}

5. Ок. Осталось ставить спрайты на "фон". Мы могли бы делать это и в цикле, при создании объектов, но зачем нам лишняя нагрузка? Правильно, незачем. Перейдём к объекту oDirt и у него создадим событие Destroy. Этот код будет выполняться перед уничтожением объекта.
С-но, алгоритм прост. Рисуем спрайт, сохраняя его ID. Узнаём индекс нужного нам массива и в него вставляем ID нарисованного спрайта.

var back = layer_sprite_create("Backs", x - 16, y - 16, sDirtBG)
var cell_id = coordx + coordy * ceil(room_width / CellWidth)
array_push(global.grid_array[cell_id], back)
ds_grid_set(global.grid, coordx, coordy, 1) // Делаем проходимым
6. Чтобы проверить, что всё работает, перейдём к игроку и сделаем так, чтобы по нажатию на кнопку мыши, у нас удалялся объект. Это пишется в step.
if instance_position(mouse_x, mouse_y, oObject)
and mouse_check_button_pressed(mb_right)
{
var inst = instance_position(mouse_x, mouse_y, oObject)
with(inst)
{
instance_destroy();
}
}
GameMaker Studio 2. Урок 5. Структуры данных и с чем их едят, а также grid (сетка комнаты), размещение объектов по сетке Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок

Как видим, фон рисуется исправно. Так как мы сохранили ID спрайта, мы можем к нему обратиться и в любой момент можем его удалить. Впрочем, последнее сейчас нам нужно исключительно в целях отладки нашего проекта.
По аналогии, мы можем размещать объекты, но я бы предпочёл сразу сделать систему, которая позволит нам ставить любые объекты, которые мы захотим.

Если упрощать, то нам нужно сделать следующие шаги, чтобы данную систему реализовать в полной мере.
1) Реализовать глобальную переменную, чтобы мы могли отслеживать событие: открыто сейчас какое-либо меню или нет. Простой переключатель, который будет закрывать старое меню при переключении на новое.
2) Сделать родительский объект или функцию для всех меню, так как логика взаимодействия у нас не отличается.
3) Расписать отдельно логику для каждого пункта. Конкретно сейчас, нам нужно таким образом реализовать меню строительства.

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

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

Непосредственно реализация.

Начнём с того, что заблокируем наш пользовательский интерфейс на нужных нам значениях. Пусть это будет FHD. Тогда, в объекте oGameManager, в Create, нужно будет добавить одну строку кода:

display_set_gui_size(1920, 1080);
Её же нужно будет убрать в oSSize.
Всё, теперь наш GUI заблокирован на FHD. Даже если размер окна меньше или больше, координаты мыши будут подстраиваться под данные значения.
Также это означает, что наш пользовательский интерфейс не будет скакать при изменении размеров окна, следовательно - не нужно париться над его перерисовкой.

Дальше. Сделаем само меню и разместим его.
GameMaker Studio 2. Урок 5. Структуры данных и с чем их едят, а также grid (сетка комнаты), размещение объектов по сетке Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок

Нового здесь ничего нет, поэтому повторяться не буду. Всего это шесть новых объектов.
В oGameManager пропишем переменную-тумблер:

global.placing = noone
Переменную "активности" кнопки нам следует прописать у каждого объекта-меню, чтобы мы могли отслеживать, какой именно из объектов сейчас рисуется.

Теперь логика меню. Возьмём за основу кнопку "Строительство".
Сделаем список из нескольких объектов, которые мы бы хотели выводить в списке. Дополним их названиями, так как они нам пригодятся позже.
object_list = [["Земля", oDirt], ["Камень", oRock], ["Металл", oMetal], ["Уголь", oCoal]]
Реализовывать кнопки мы будем через отдельный объект - oCell.
Для этого, его нужно создать. Задать ему спрайт в виде квадрата. Сделать так, чтобы он отрисовывался в событии GUI и при этом хранил следующие значения:
Объект, который он должен рисовать, название этого объекта, какое сейчас меню открыто.
object_name = noone
object_to_draw = noone
menu = noone
Вернёмся к объекту меню строительства.

Код в step:
dmxg = device_mouse_x_to_gui(0)
dmyg = device_mouse_y_to_gui(0)
if instance_position(dmxg, dmyg, self) and mouse_check_button_pressed(mb_left)
{
// Если не активно - делаем активным
if !active
{
active = !active
global.placing = "Building";
var startx = x + sprite_width * 2 + 5
var starty = y - sprite_height
for (var i = 0; i < array_length(object_list); ++i)
{
var inst = instance_create_layer(startx + 64 * i + 5, starty, "Instances", oCell)
inst.object_name = object_list[i][0]
inst.object_to_draw = object_list[i][1]
inst.menu = "Building"
}
}
else
{
active = !active
global.placing = noone
}
}
Теперь у oCell:
step:
if menu != noone and global.placing != menu
{
instance_destroy();
}
Draw GUI:
draw_self()
if object_to_draw != noone
{
draw_sprite(object_get_sprite(object_to_draw), 0, x + 32, y + 32)
}
Результат:
GameMaker Studio 2. Урок 5. Структуры данных и с чем их едят, а также grid (сетка комнаты), размещение объектов по сетке Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок

Это только часть того, что нам нужно, верно?
Во-первых, мы не выводим название. Делать мы это будем отдельно, конечно же.
Во-вторых, у нас нет ограничения для объектов по оси X, а по оси Y они не двигаются вообще.

Начнём с первого, так как это просто. Выводить название объекта мы будем при наведении на него.
Для этого дополним Draw GUI у oCell. Если мы наведены на объект - нарисовать чёрный полупрозрачный прямоугольник и поверх него текст для лучшей отчётливости. И немного настроим "глубину", чтобы не было перекрытия этого текста.

if object_to_draw != noone

{

draw_sprite(object_get_sprite(object_to_draw), 0, x + 32, y + 32)

if instance_position(device_mouse_x_to_gui(0), device_mouse_y_to_gui(0), self)

{

depth = -1000

draw_set_font(fontButtonText20)

var width = string_width(object_name)

var height = string_height(object_name)

draw_set_alpha(0.33)

draw_set_color(c_black)

draw_rectangle(x, y - height, x + width, y, false)

draw_set_color(c_white)

draw_set_alpha(1)

scrOutlinedText(x, y - height, c_white, c_black, object_name, depth, fontButtonText20, fa_left, fa_top)

}

else

{

depth = 0

}

}


Второе.
Вернёмся к объекту меню строительства.
Так как мы знаем конечное количество объектов, мы можем посчитать, какое количество пикселей они будут занимать. Всё, что нам остаётся - определиться с количеством объектов на одну строку. Я предлагаю 10.
Мы не хотим, чтобы эти объекты создавались ниже определённой границы, потому точку старта по Y будем считать относительно неё. В общем, нужно будет заменить всего пару строк при создании объектов.
var starty = display_get_gui_height() - 64 - 64 * (array_length(object_list) div 10)
И
var inst = instance_create_layer(startx + 64 * (i mod 10) + 5, starty + (i div 10) * 64, "Instances", oCell)

Конечный результат меню:

GameMaker Studio 2. Урок 5. Структуры данных и с чем их едят, а также grid (сетка комнаты), размещение объектов по сетке Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок

Теперь логика взаимодействия и размещения.

Нам нужно завести ещё одну переменную, которая будет хранить объект, который мы хотим использовать. Иными словами, при клике на объект oCell, мы должны из него "вычленять" тот объект, который он хранит. Как следствие, получим спрайт, который будем отрисовывать в качестве "призрака".

Сделаем же это. Для этого у объекта oPlayer пропишем переменную:

place_object = noone;
Дальше - просто. В oCell в Step делаем проверку. Если нажали на себя - oPlayer.place_object = object_to_draw;
Код:
if instance_position(device_mouse_x_to_gui(0), device_mouse_y_to_gui(0), self)
and mouse_check_button_pressed(mb_left)
{
oPlayer.place_object = object_to_draw;
mouse_clear(mb_left)
}
Теперь у oPlayer настроим отрисовку объекта. Для этого в draw пропишем код:

if place_object != noone

{

var cell_x = mouse_x div CellWidth * CellWidth

var cell_y = mouse_y div CellHeight * CellHeight

draw_sprite(object_get_sprite(place_object), 0, cell_x + 16, cell_y + 16)

// Обводочка по границам объекта, для удобства.

draw_set_color(c_white)

draw_rectangle(cell_x, cell_y, cell_x + 32, cell_y + 32, true)

}

Результат:
GameMaker Studio 2. Урок 5. Структуры данных и с чем их едят, а также grid (сетка комнаты), размещение объектов по сетке Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок

Может выглядеть кривовато, но это из-за спрайта земли. Проверял на простых квадратах - всё встаёт четко в границы. :)

Всё, что осталось - настройка логики.
Нажали ЛКМ - разместили объект. При этом, фон нам нужно очистить в любом случае. Нажали ПКМ - удалили объект, на который наведены. Если объекта нет, то удаляем фон.
Естественно, всё это сейчас - в целях теста. В дальнейшем мы будем ставить не сами объекты, а их призраки, которые человечки будут строить. В общем, целиком код выглядит так:

Step у oPlayer:

var cell_x = mouse_x div CellWidth
var cell_y = mouse_y div CellHeight
var cell = cell_x + cell_y * ceil(room_width / CellWidth)
if mouse_check_button_pressed(mb_right)
{
// Сбрасываем объект или удаляем объект/фон
if place_object != noone
{
place_object = noone
}
else
{
// Если в текущей позиции нет объекта - удаляем фон, иначе - объект
var inst = instance_position(cell_x * CellWidth, cell_y * CellHeight, oObject)
if inst
{
with(inst)
{
instance_destroy();
}
}
else
{
if array_length(global.grid_array[cell]) > 2
{
layer_sprite_destroy(global.grid_array[cell][2])
array_delete(global.grid_array[cell], 2, 1)
}
}
}
mouse_clear(mb_right)
}
if mouse_check_button_pressed(mb_left)
{
var place_x = mouse_x div CellWidth * CellWidth + 16
var place_y = mouse_y div CellHeight * CellHeight + 16
if place_object != noone
and !instance_position(device_mouse_x_to_gui(0), device_mouse_y_to_gui(0), oGBuild)
{
// Удаляем объект, если находим.
var inst = instance_position(cell_x * CellWidth, cell_y * CellHeight, oObject)
if inst
{
with(inst)
{
instance_destroy()
}
}
// Удаляем фон
if array_length(global.grid_array[cell]) > 2
{
layer_sprite_destroy(global.grid_array[cell][2])
array_delete(global.grid_array[cell], 2, 1)
ds_grid_set(global.grid, cell_x, cell_y, 0)
}
// Ставим новый объект
var new_inst = instance_create_layer(place_x, place_y, "Instances", place_object)
new_inst.coordx = mouse_x div CellWidth
new_inst.coordy = mouse_y div CellHeight
mouse_clear(mb_left)
}
}

Собственно говоря, система размещения объектов сделана. Осталось её дорабатывать напильником.
Результат:

GameMaker Studio 2. Урок 5. Структуры данных и с чем их едят, а также grid (сетка комнаты), размещение объектов по сетке Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок

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

GameMaker Studio 2. Урок 5. Структуры данных и с чем их едят, а также grid (сетка комнаты), размещение объектов по сетке Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок

Собственно, OChar - родитель oCharPlayer и oCharNoControl.
oCharNoControl - родитель oCharNeutral и oCharEnemy.

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

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

- Сохранение. Встроенное VS самописное.

- Звуки.

Небольшое объявление. В формате гайда я объясняю, как сам делаю те или иные вещи, как их понимаю. Когда гайды подойдут к концу - останется только заполнить игру механиками, спрайтами, звуками, музыкой. Это я планирую делать в дальнейшем в виде блога или вроде того.

Есть вопросы или что-то не получается - обращайся, помогу.

Есть пожелания или замечания - буду рад выслушать.

Ссылка на скачивание файла проекта для ЛЛ:
https://disk.yandex.ru/d/w31aDLE9ClPy9Q

Показать полностью 9
15

GameMaker Studio 2. Урок 4. - Иерархия объектов. «Объекты-родители» и их «дети». Глобальные переменные

Привет!

Приношу свои извинения за долгое отсутствие. Лето, выходные, ну вы понимаете. :)

Ссылки на предыдущие гайды:
Первый гайд
Второй гайд
Третий гайд

Недавно проводил один из стримов, на котором частично воссоздал старые добрые "танчики" с Денди (Battle City). Исходники, которые можно будет допилить самому, приложу к посту ниже. Там тоже есть что потыкать.

Иерархия объектов

Кратко:
Тоже самое, что и с классами. Есть родительский класс и дочерний, который наследует его свойства. В нашем случае, слово "класс" заменяется на "объект, но смысл не меняется.
Важно: свойства эти можно "переписать", создав событие заново. В нём же можно дополнительно указать, если мы хотим сохранить свойства, при этом дополнив их.

Подробно:
Перед созданием любого объекта, мы должны чётко представлять его предназначение. Часто множество объектов исполняют схожие функции, но с небольшими отклонениями. Таким образом, вырисовывается проблема: создавать стопицот объектов с одинаковым кодом - бред, который захламляет код и делает его нечитаемым.
Решение этой проблемы - создание универсального "шаблона", на основе которого будут строиться другие объекты с нужными дополнениями.
Таким образом, наш шаблон становится родителем. А все, кто создан по этому шаблону - его "детьми" (дочерними объектами). Они перенимают его свойства, но могут их в нужной степени корректировать.

Работа иерархической системы в GML (GameMaker Language) имеет особенность, с которой быстро придётся познакомиться на практике.
Все "дочерние" объекты по умолчанию считаются частью "родительского".
У нас есть общий родительский объект для всех блоков. При этом, в комнате его у нас нет. Но есть его дочерние объекты.
Следовательно, делая проверку на наличие родительского объекта, у нас высветится "истина".
Делайте проверки на наличие тех или иных объектов в комнате обдуманно.

Как это использовать - можете придумать сами. Ниже примеры, которые использовал я.
- Для создания систем юнитов, зданий
- ИИ, который выбирает приоритетные цели на основе их родительского объекта.
- Для создания инвентарной системы.
- Для создания меню.

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

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

Собственно, по этой причине для данного гайда была выбрана относительно простая в реализации игра.
Основа в упрощённом виде выглядит так:

GameMaker Studio 2. Урок 4. - Иерархия объектов. «Объекты-родители» и их «дети». Глобальные переменные Разработка, Gamedev, Программирование, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок, Обучение

Небольшие пояснения, слева направо.

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

Аналогично с объектами.
- Всё, что есть в игре, кроме персонажей - у нас будет объектом, с которым мы можем взаимодействовать.
- Каждый такой объект мы делим по двум типам: постройки, которые что-то делают (или не делают), но суть - должны быть построены. И природные объекты, которые игрок ставить не сможет.
Дальше, соответственно, они снова делятся.

Как это выглядит в движке.

Создадим два объекта: oObject и oNature.
Первый - родительский объект для всех типов объектов, второй - только для "природных". При этом, у нас также есть объект для земли - oDirt.
Перейдём к oObject.
Нажмём у него на Parent. Далее - на плюсик, где поставим ему дочерним объект oNature.

GameMaker Studio 2. Урок 4. - Иерархия объектов. «Объекты-родители» и их «дети». Глобальные переменные Разработка, Gamedev, Программирование, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок, Обучение

Аналогично проделаем с oNature для oDirt. Как видно, родитель у него уже выставился.

GameMaker Studio 2. Урок 4. - Иерархия объектов. «Объекты-родители» и их «дети». Глобальные переменные Разработка, Gamedev, Программирование, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок, Обучение

Ок. Родительские объекты сделали.

Перейдём обратно к oObject и создадим у него событие Create.
Теперь немного мыслей в слух.

Базово, каждый наш объект должен иметь следующий перечень "настроек":
- Количество ХП. Так как каждый объект интерактивен, следовательно, каждый объект можно атаковать. С-но, вводим переменную hp и приравниваем её к 0 по умолчанию.
- Координаты по сетке. coordx, coordy. Не столько необходимость, сколько просто упрощение. Делаем их равными -1 по умолчанию, так как мы в любом случае будем их переназначать при размещении объектов.
- Может объект гореть или нет - flammable. False по умолчанию.

Пока остановимся на этом. Перейдём к объекту oNature.

GameMaker Studio 2. Урок 4. - Иерархия объектов. «Объекты-родители» и их «дети». Глобальные переменные Разработка, Gamedev, Программирование, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок, Обучение

Как видно, событие "Create" у нас здесь уже есть. Оно унаследовалось от нашего родительского объекта oObject.
Если создать ещё одно событие Create, то мы перепишем родительское. Но, можно предоставить себе выбор, что мы хотим сделать.
Для этого, по существующему событию мы нажмём правой кнопкой мыши.

У нас появится выпадающее меню из трёх опций:
- Open Parent Event - открыть это же событие в родительском объекте.
- Inherit Event - унаследовать событие
- Override Event - переписать событие.

GameMaker Studio 2. Урок 4. - Иерархия объектов. «Объекты-родители» и их «дети». Глобальные переменные Разработка, Gamedev, Программирование, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок, Обучение

С-но, нам нужно выбрать второй пункт: унаследовать событие.

Тогда у нас откроется окно кода с готовой строкой:

event_inherited();

Это значит, что то, что мы прописали в oObject, для oNature у нас унаследуется и будет активно для этого объекта. И при этом, ниже этой строки кода мы можем добавить то, что нам нужно.

Следуя плану, сейчас нам нужно убрать связь между oNature и oDirt, создать ещё два объекта (oResource и oNotResource) и уже затем выставить связь между oNotResource и oDirt.

Собственно говоря, для не-ресурсных объектов - это всё.
Для ресурсных - нам нужно объявить, какие ресурсы он может содержать.

Открыв объект oResource (родительский для всех блоков, с которых что-то будет добываться), создадим его с наследованием от родителя.

Далее - обозначим ресурсы, приравняв их все к нулю.

Metal = 0;
Coal = 0;
Wood = 0;
Stone = 0;
Gem = 0;

По сути - всё.

Теперь, когда будем создавать ресурсные объекты, в качестве родителя будем у них выставлять oResource. Создавая объект, будем наследовать событие создания у oResource, переписывая нужные нам строки кода.

Проверим, как это всё работает.

Создадим объект oRock.
Поставим ему в качестве родителя oResource.
Создадим у него событие Create, унаследовав код.
Пишем код:

event_inherited();
Stone = irandom_range(5, 15)
Далее, чтобы проверить, что всё работает как мы и рассчитывали, в Draw сделаем вывод текста на экран. Выводим текущее количество камня. Не забываем про отрисовку!
GameMaker Studio 2. Урок 4. - Иерархия объектов. «Объекты-родители» и их «дети». Глобальные переменные Разработка, Gamedev, Программирование, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок, Обучение

Как видно, при спавне у каждого блока получилось разное количество камня. При этом, если вывести количество других ресурсов, которые мы не переписывали, мы получим нули. Следовательно, другие переменные также подтянулись от родителя.

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

Глобальные переменные

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

1. Локальные переменные. Объявляются с помощью ключевого слова var

var название = значение

Эти переменные нельзя использовать где-либо, кроме текущего события. С-но, в другом событии они будут недоступны, также будут недоступны и для изменения из посторонних объектов.

2. Обычные переменные (не знаю, как правильно их обозвать:D)
Мы с ними уже активно работали, объявляются они просто:

название = значение

В дальнейшем, их можно вызывать из других объектов, используя with или обращаясь к объекту через переменную, хранящую его id. К примеру:

inst.coordx = 1;
3. Глобальные переменные. Данный вид переменных может быть вызван в любом событии любого объекта и изменён там. Объявляются следующим образом:

global.название = значение

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

4. Макро переменные. По сути, те же глобальные переменные, но за одним исключением. Они не могут быть изменены. Также используются для хранения значений, которые являются константами и могут пригодиться в нескольких местах программы. Объявляются следующим образом:

#macro Название Значение

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

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

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

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

- Иные способы хранения информации в GMS2, когда их стоит или не стоит использовать.

- Сохранение. Встроенное VS самописное.

- Звуки.


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

Ссылки на скачивание:
Исходник

Полуготовые танчики
Показать полностью 6
18

GameMaker Studio 2. Урок 3. Камера. Разрешение экрана

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

Скажу сразу, что будет много кода.
Также немного затронем тему следующего гайда - глобальные переменные.

Также обозначу, что у объекта oPlayer можно выключить свойство Persistent.

Ссылка на первый гайд
Ссылка на второй гайд

P.S.
За спрайты огромное спасибо моему другу
https://vk.com/parijer

Ссылка на мою группу ВК
https://vk.com/club209675869

Камера.

Включение камеры.
Перейдём в комнату с игроком.

Во вкладке слева-снизу (Properties - Room1) открываем вкладку "Viewports and Cameras"

Ставим галочку напротив "Enable Viewports" и "Clear Viewport Background"


Немного теории.

Камера - объект, который содержит информацию о том, что камера видит и что нужно отображать на экране. При этом, камера имеет два параметра:

View (Вид) - то, что камера видит, основываясь на позиции, проекции и повороте камеры.

View Port - область дисплея, на которой будет отображаться то, что камера видит.

GameMaker Studio 2. Урок 3. Камера. Разрешение экрана Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок, Видео, Без звука

Первый способ создания камеры. Простой.

1. Открываем "Viewport 0"

2. Ставим галочку напротив "Visible" для себя.

3. В "Camera Properties" выставляем нужные нам настройки камеры, меняя параметры "Width" и "Height". Поиграйтесь сами, посмотрите, как меняется область камеры и выберите нужные себе значения.

4. Чуть ниже есть надпись "Object Following". Здесь выбираем объект, за которым камера будет следовать. Выбираем игрока.

5. По сути, всё. Камера готова. Но, как вы можете заметить, игрок её двигает только тогда, когда подойдёт к границе. Чтобы это исправить, в "Horizontal Border" и "Vertical Border" выставьте те же параметры, что выставляли в "Camera Properties".

6. Изменяя "Horizontal Speed" и "Vertical Speed" вы сможете настроить "плавность" слежения камеры.

Всё. Камера готова и будет следовать за персонажем - игроком.

GameMaker Studio 2. Урок 3. Камера. Разрешение экрана Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок, Видео, Без звука
GameMaker Studio 2. Урок 3. Камера. Разрешение экрана Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок, Видео, Без звука

Второй способ создания камеры. Сложный.

Сложный он по той причине, что делать мы это всё будем непосредственно через код. :)

Для начала - просто включите Viewports и Clear Viewport Background

Приступим
1. Создаём объект oCamera.

2. Теория.

2. Сделаем нашу комнату с главным меню (rmMainMenu) "Persistent" - постоянной.

3. Перейдём в объект oGameManager. Создаём у него событие "Create". В нём пропишем код:

global.CameraSizes = [[320, 180],[640, 360], [960, 540]] // Разрешённые размеры экрана.
global.CameraNum = array_length(global.CameraSizes) - 1 // Переменная, которая будет отображать, какой из массивов мы используем, допустимые значения от 0 до (длина списка - 1)
global.CameraWidth = global.CameraSizes[global.CameraNum][0] // Ширина камеры.
global.CameraHeight = global.CameraSizes[global.CameraNum][1] // Высота камеры.
#macro CameraScale 2 // Масштаб камеры. Константа.
#macro CameraSpeed 0.1 // Скорость камеры. Константа.
window_set_fullscreen(false) // Выключаем полноэкранный режим при запуске :)
var windowWidth = global.CameraWidth * CameraScale // Ширина окна = ширина камеры * масштаб
var windowHeight = global.CameraHeight * CameraScale // Высота окна = высота камеры * масштаб
surface_resize(application_surface, global.CameraWidth * CameraScale, global.CameraHeight * CameraScale); // Переопределяем "поверхность", чтобы соотношение сторон спрайтов соответствовало нашему экрану.
window_set_size(global.CameraWidth * CameraScale, global.CameraHeight * CameraScale); // Устанавливаем размер окна (Необязательно, но желательно)
window_set_position(display_get_width() / 2 - windowWidth / 2, display_get_height() / 2 - windowHeight / 2); // Располагаем наше окно по центру дисплея

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

Что происходит в данном коде: мы задаём базовые размеры камеры и её масштаб. Говорим, что хотим запускаться в оконном режиме, после чего устанавливаем ширину окна равную размеру камеры, помноженному на масштаб. Масштаб необходим для корректного отображения, но вообще - с этими настройками можно поиграться самому.

Любые переменные, где используется число, а не переменная, можно спокойно взять и настроить под себя.
Однако, нужно не только изменить размер окна - этого недостаточно для корректного отображения. Нужно также изменить размер нашего "холста".
Gamemaker Studio 2 не отрисовывает ничего непосредственно на экран. Для отрисовки наш движок использует "холст" - surface, иначе - application_surface. Таким образом, при изменении размеров окна, нужно менять и размеры холста, как следствие - проводить подгонку UI под новые размеры.
4. Перейдём к объекту oCamera. Разместим его в комнате, где есть игрок. Создадим событие "Create", где создадим саму камеру, скажем, за кем ей следить и настроим её.

Код:

// Если в комнате есть игрок - следим за ним, иначе - за комнатой.
if instance_exists(oPlayer)
{
cameraTarget = oPlayer;
}
else
{
cameraTarget = room;
}
global.Camera = camera_create_view(0, 0, global.CameraWidth, global.CameraHeight); // Создаём камеру по нужной нам ширине и высоте.
// Включаем и устанавливаем камеру.
view_enabled = true;
view_visible[0] = true;
view_set_camera(0, global.Camera);

5. Создадим step событие у камеры, где сделаем так, чтобы она плавно двигалась за игроком и не показывала то, что находится вне комнаты. Также сделаем возможность приближать и отдалять камеру на колёсико мыши - зум.

Код:

var cameraX = camera_get_view_x(global.Camera);
var cameraY = camera_get_view_y(global.Camera);
// Делаем переменные стабильными для зума
cameraWidth = camera_get_view_width(global.Camera);
cameraHeight = camera_get_view_height(global.Camera);
var targetX = cameraTarget.x - cameraWidth / 2;
var targetY = cameraTarget.y - cameraHeight / 2;
// Поддерживаем входные значения в диапазоне от 0 до размеров комнаты
targetX = clamp(targetX, 0, room_width - cameraWidth);
targetY = clamp(targetY, 0, room_height - cameraHeight);
// Возвращаем позицию камеры. Делает передвижение плавным
cameraX = lerp(cameraX, targetX, CameraSpeed);
cameraY = lerp(cameraY, targetY, CameraSpeed);
// Автоподгон размера камеры, если вдруг вышли за рамки
if cameraWidth > 1440 or cameraWidth < 320
{
cameraWidth = global.CameraWidth
cameraHeight = global.CameraHeight
}
// Зум
var wheel = mouse_wheel_down() - mouse_wheel_up(); // Возвращает true/false (-1 / 0 / 1)
if (wheel != 0)
{
wheel *= 0.1; // * 10%
// Определяем, сколько добавить к ширине / высоте
var addWidth = cameraWidth * wheel;
var addHeight = cameraHeight * wheel;
// Где 1440 и 320 - можете вписать свои значения. Это область, которую мы будем показывать.
// Не советую делать привязку к текущему экрана, т.к. тогда чем меньше будет разрешение, тем меньше будет максимальная видимая область.
// Для большого разрешения тоже будет минус - ничего не будет видно.
if (cameraWidth + addWidth < 1440) and (cameraWidth + addWidth > 320)
{
cameraWidth += addWidth;
cameraHeight += addHeight;
// Делаем входные значения стабильными
var prevWidth = cameraWidth;
var prevHeight = cameraHeight;
cameraWidth = clamp(cameraWidth, CameraWidth / 2, room_width);
cameraHeight = clamp(cameraHeight, CameraHeight / 2, room_height);
// Исправляем искривление при зуме. Если разрешение 16:9 - все ок, иначе - исправляем.
if (cameraWidth / cameraHeight == 1.777777777777778) &&
(prevWidth == cameraWidth || prevHeight == cameraHeight) {
// Фиксим позицию камеры
cameraX -= addWidth / 2;
cameraY -= addHeight / 2;
// Опять делаем значения стабильными
cameraX = clamp(cameraX, 0, room_width - cameraWidth);
cameraY = clamp(cameraY, 0, room_height - cameraHeight);
}
else {
cameraWidth = prevWidth - addWidth;
cameraHeight = prevHeight - addHeight;
}
}
}
camera_set_view_pos(global.Camera, cameraX, cameraY);
camera_set_view_size(global.Camera, cameraWidth, cameraHeight);

Всё. Камера готова, её можно отдалять-приближать и она будет следить за игроком!

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

1. Немного изменим расположение нашего меню: так как размер окна у нас может динамически изменяться, неизменяемые значения в расположении кнопок нам сделают только хуже. С-но, нам нужно перейти к объекту oMMenu и в "Create" изменить код следующим образом:


buttons = [oMStart, oMLoad, oMSettings, oMExit]
var wwidth = global.CameraWidth * CameraScale
var wheight = global.CameraHeight * CameraScale
var bwidth = wwidth * 0.25
var bheight = wheight * 0.1
for (var i = 0; i < array_length(buttons); ++i)
{
//instance_create_layer(room_width / 2, room_height / 2 - 50 + 100 * i, "Instances", buttons[i], {width : bwidth, height : bheight})
inst = instance_create_layer(wwidth / 2, wheight * 0.5 + bheight * i, "Instances", buttons[i])
inst.width = bwidth
inst.height = bheight
}

5. Так как код для отрисовки обводки кнопок у нас везде одинаковый, сделаем из него функцию, которую везде будем вызывать. Назовём её: scrOutlinedBox.

Код там будет следующий:

function scrOutlinedBox()
{
if point_in_rectangle(dmxg, dmyg, x - width / 2, y - height / 2, x + width / 2, y + height / 2)
{
draw_set_color(c_white)
draw_set_alpha(0.15)
draw_rectangle(x - width / 2, y - height / 2, x + width / 2, y + height / 2, false)
draw_set_alpha(1)
}
}

dmxg и dmyg мы не объявляем, так как мы их уже используем в нашем коде, это важно. По сути, это просто сокращение кода.


Не забудьте добавить отрисовку спрайта через draw_sprite_streched() :)

Код для текста кнопки выбора размеров:

scrOutlinedText(x, y, c_white, c_black, "Разрешение экрана: " + string(global.CameraSizes[global.CameraNum][0] * CameraScale) + "x" + string(global.CameraSizes[global.CameraNum][1] * CameraScale), depth, fontArialRusSmall, fa_center, fa_middle)

Код для кнопки смены режима экрана:

var fullscreen = window_get_fullscreen()
if fullscreen == 0
{
fullscreen = "Выкл."
}
else
{
fullscreen = "Вкл."
}
scrOutlinedText(x, y, c_white, c_black, "Полноэкранный режим: " + fullscreen, depth, fontArialRusSmall, fa_center, fa_middle)

Теперь немного настроим логику.

step у oSFullscreen:

dmxg = device_mouse_x_to_gui(0)
dmyg = device_mouse_y_to_gui(0)
if point_in_rectangle(dmxg, dmyg, x - width / 2, y - height / 2, x + width / 2, y + height / 2)
and mouse_check_button_pressed(mb_left)
{
window_set_fullscreen(!window_get_fullscreen())
}

step у oSBack

dmxg = device_mouse_x_to_gui(0)
dmyg = device_mouse_y_to_gui(0)
if point_in_rectangle(dmxg, dmyg, x - width / 2, y - height / 2, x + width / 2, y + height / 2)
and mouse_check_button_pressed(mb_left)
{
room_goto(rmMainMenu)
}

step у oSSize

dmxg = device_mouse_x_to_gui(0)
dmyg = device_mouse_y_to_gui(0)
if point_in_rectangle(dmxg, dmyg, x - width / 2, y - height / 2, x + width / 2, y + height / 2)
and mouse_check_button_pressed(mb_left)
{
if global.CameraNum < array_length(global.CameraSizes) - 1
{
global.CameraNum += 1
}
else
{
global.CameraNum = 0
}
global.CameraWidth = global.CameraSizes[global.CameraNum][0]
global.CameraHeight = global.CameraSizes[global.CameraNum][1]
surface_resize(application_surface, global.CameraWidth * CameraScale,global.CameraHeight * CameraScale) // Пересобираем surface
window_set_size(global.CameraWidth * CameraScale,global.CameraHeight * CameraScale);
oSSettings.surface_changed = true;
}

С-но, что мы здесь делаем:

1) Меняем размер окна

2) Меняем размер холста

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

4) Чтобы oSSettings это поняла, в Create нам нужно добавить surface_changed = false.

Здесь же создадим пустой список: all_buttons = []

Затем добавить в цикл for, в конец, строку:

array_push(all_buttons, inst)

Таким образом, мы запоминаем ID тех кнопок, что мы уже поставили.

Затем в step написать следующий код:

if surface_changed
{
var windowWidth = global.CameraWidth * CameraScale
var windowHeight = global.CameraHeight * CameraScale
var bwidth = windowWidth * 0.4
var bheight = windowHeight * 0.1
for (var i = 0; i < array_length(all_buttons); ++i)
{
all_buttons[i].x = windowWidth / 2
all_buttons[i].y = windowHeight / 2 + (windowHeight * 0.1) * i
all_buttons[i].width = bwidth
all_buttons[i].height = bheight
}
surface_changed = false
}

5) Расположение интерфейса нам нужно поменять и в другом меню - oMMenu.

Здесь нужно будет добавить два ивента: Room Start и Room End, а в Create добавить две переменные:

old_size = global.CameraWidth * CameraScale
new_size = global.CameraWidth * CameraScale

Теперь, что мы делаем.

Когда происходит событие "конец комнаты" (смена комнаты) - мы запоминаем текущий размер экрана.

Когда происходит событие "начало комнаты" - мы сравниваем старый размер с новым и если они разные - меняем расположение всех кнопок.

Код Room End

old_size = global.CameraWidth * CameraScale

Код Room Start

new_size = global.CameraWidth * CameraScale
if new_size != old_size
{
var windowWidth = global.CameraWidth * CameraScale
var windowHeight = global.CameraHeight * CameraScale
var bwidth = windowWidth * 0.25
var bheight = windowHeight * 0.1
for (var i = 0; i < array_length(all_buttons); ++i)
{
all_buttons[i].x = windowWidth / 2
all_buttons[i].y = windowHeight * 0.5 + bheight * i
all_buttons[i].width = bwidth
all_buttons[i].height = bheight
}
}

Подобный код нам теперь придётся писать везде, где есть GUI, так как наши элементы интерфейса мы закрепляем лишь единожды, а здесь - располагаем их заново.

Осталось подправить одну недоработку в коде у камеры, а именно - Зум.

В step у камеры, перед зумом добавим проверку, которая будет принудительно изменять размер камеры допустимый, если мы выходим за границы.

if cameraWidth > global.CameraWidth * 1.5 or cameraWidth < global.CameraWidth * 0.5
{
cameraWidth = global.CameraWidth
cameraHeight = global.CameraHeight
}

Теперь немного про скрипты.

Подправим передвижение. Сделаем его немного удобнее для нашей игры.

Сделаем следующее: если игрок подойдёт к краю комнаты и продолжит идти, то окажется на противоположной её стороне.

Для этого в "step" событии напишем следующий код после вызова функции для передвижения:

move_wrap(true, true, sprite_width);

Если наш объект зайдёт за границы экрана на ширину своего спрайта - мы его развернём. Используем только ширину, т.к. грани спрайта у нас одинаковые. Если бы ширина и высота разнились, пришлось бы писать этот код дважды: для ширины и для высоты соответственно, причём вместо "true" выставляя "false" в нужных местах соответственно.

Чтобы почитать о функции подробно - наведитесь на неё и нажмите на колёсико мыши.


Сделаем возможность передвижения камеры с помощью мыши: нажав ПКМ, мы перетаскиваем камеру туда, куда нам нужно.

Так как камера у нас привязана к игроку, передвигать мы будем именно игрока.

Создадим скрипт, который назовём: "scrMoveToMouse"

Код, который в нём будет написан, будет проверять, нажата ли ПКМ и если да - передвигаться игрока к координатам мыши в комнате, делая это плавно.

function scrMoveToMouse()
{
// Перемещение игрока к мыши (и камеры к игроку как следствие)
if mouse_check_button(mb_right)
{
x = lerp(x, mouse_x, 0.05)
y = lerp(y, mouse_y, 0.05)
}
}

Всё. Достаточно вызвать функцию. Теперь, нажимая ПКМ, мы будем двигаться в нужную нам точку.

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

Создадим ещё один скрипт и назовём его: "scrMoveToEdge"

Нам нужно проверять позицию мыши относительно GUI, после чего проверять X и Y:

- Находится ли мышь у левой или правой границы. Для этого возьмём 10% от текущего разрешения за основу. Соответственно, в одном случае - умножим итоговую ширину экрана на 0.1 - если мышь слева и на 0.9 - если справа.

- Аналогичные действия для верха и низа, но с высотой экрана.

Код:

function scrMoveToEdge()
{
var dmxg = device_mouse_x_to_gui(0)
var dmyg = device_mouse_y_to_gui(0)
if dmxg < (CameraScale * CameraWidth) * 0.1
{
x -= player_speed
}
else if dmxg > (CameraScale * CameraWidth) * 0.9
{
x += player_speed
}
if dmyg < (CameraScale * CameraHeight) * 0.1
{
y -= player_speed
}
else if dmyg > (CameraScale * CameraHeight) * 0.9
{
y += player_speed
}
}

Как вы могли заметить, у нас здесь стоит две связи: if else if. Сделано это для того, чтобы игрок мог передвигаться по диагонали, наводясь в углы.

Чтобы не происходило наслоение друг на друга, сделаем так, чтобы каждая функция возвращала true в случае использования и false, если нет.

Код.

scrMove

function scrMove(spd)
{
var key_up = keyboard_check(ord("W")) or keyboard_check(vk_up);
var key_left = keyboard_check(ord("A")) or keyboard_check(vk_left);
var key_right = keyboard_check(ord("D")) or keyboard_check(vk_right);
var key_down = keyboard_check(ord("S")) or keyboard_check(vk_down);
var movement_dir = point_direction(0, 0, key_right - key_left, key_down - key_up);
var movement_input = (key_right - key_left != 0) or (key_down - key_up != 0);
if (movement_input)
{
var h_speed = lengthdir_x(spd, movement_dir);
var v_speed = lengthdir_y(spd, movement_dir);
x += h_speed;
y += v_speed
return true
}
else
{
return false
}
}

scrMoveToEdge

function scrMoveToEdge(pspeed)
{
var dmxg = device_mouse_x_to_gui(0)
var dmyg = device_mouse_y_to_gui(0)
if dmxg > (CameraScale * global.CameraWidth) * 0.1
and dmxg < (CameraScale * global.CameraWidth) * 0.9
and dmyg > (CameraScale * global.CameraHeight) * 0.1
and dmyg < (CameraScale * global.CameraHeight) * 0.9
{
return false
}
if dmxg < (CameraScale * global.CameraWidth) * 0.1
{
x -= pspeed
}
else if dmxg > (CameraScale * global.CameraWidth) * 0.9
{
x += pspeed
}
if dmyg < (CameraScale * global.CameraHeight) * 0.1
{
y -= pspeed
}
else if dmyg > (CameraScale * global.CameraHeight) * 0.9
{
y += pspeed
}
return true
}

scrMoveToMouse

function scrMoveToMouse()
{
// Перемещение игрока к мыши (и камеры к игроку как следствие)
if mouse_check_button(mb_right)
{
x = lerp(x, mouse_x, 0.05)
y = lerp(y, mouse_y, 0.05)
return true
}
else
{
return false
}
}

Осталось настроить вызов функций.

Функции выполняются в любом случае. И в любом случае возвращают какой-то результат. Если мы пропишем функцию в проверку - она будет выполняться и возвращать результат. Но, так как мы возвращаем и true, и false, нам, соответственно, нужно сделать проверку на false:

Если мы не двигаемся на клавиши -> Если мы не двигаемся к мыши -> Если мы не двигаемся к краю.

Таким образом, если мы двигаемся на клавиши - мы не сможем двигаться к мыши и к краю.

Если мы двигаемся к мыши - мы не сможем двигаться к краю.

Код в Step у oPlayer:

if !(scrMove(player_speed))
{
if !(scrMoveToMouse())
{
if !(scrMoveToEdge(player_speed))
{
}
}
}
move_wrap(true, true, sprite_width); // Разворот

Реализуем это именно через лесенку, так как нам нужно, чтобы одновременно выполнялся только один скрипт.

Как растянуть один спрайт на весь фон


1. Выбираем наш фон в "Layers - Room1"
2. Выбираем нужный спрайт.
3. Ставим галочки:
3.1. Horizontal Tile
3.2. Vertical Tile

GameMaker Studio 2. Урок 3. Камера. Разрешение экрана Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок, Видео, Без звука

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

Видео с тем, как это всё выглядит по факту.

Насчёт кнопок: как бы вы их реализовали?
Также, через Nine Slice и отрисовку текста отдельно или сделав готовые кнопки с текстом?

Я склоняюсь ко второму варианту, но для тестирования предпочту использовать первый :)

Оставшиеся гайды:
- Иерархия объектов. «Объекты-родители» и их «дети». Решение часто встречающихся проблем и немного про то, как удобно выстраивать взаимодействие с объектами. Глобальные переменные.

- Массивы и с чем их едят, а также grid (сетка комнаты), размещение объектов по сетке. Включая объяснение, в каких случаях лучше использовать встроенные функции, в каких – писать свои с нуля.

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

- Иные способы хранения информации в GMS2, когда их стоит или не стоит использовать.

- Сохранение. Встроенное VS самописное.

- Звуки.

Ссылка на файл, чтобы всё можно было потыкать самому:
https://disk.yandex.ru/d/A3TRUz39ByvjOQ

Есть вопрос - задавай, постараюсь ответить.

Что-то не получилось, вылезла ошибка и так далее - тоже пиши, помогу или поможем.
Ну и я всегда рад критике.

Показать полностью 4 1
49

GameMaker Studio 2. Урок 2. События отрисовки. Коллизия. Как работают скрипты. Как подключить русский шрифт. Переходы между комнатами

Привет! Это вторая часть урока.
Первая:
GameMaker Studio 2. Урок 1. Знакомство

Сегодня у нас много матчасти и не очень много кода.

Конкретизация разработки.

Как было сказано в прошлом посте, игра, которая будет сделана в ходе данных гайдов, будет относиться к жанру стратегий, а в качестве источников для "вдохновения" у нас - "Oxygen Not Included" и "Rimworld". Теперь же конкретно поговорим с вами о том, что в игре должно быть реализовано, по пунктам.

-) Персонажи.

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

--) Каждый персонаж должен иметь или уметь следующее:

---) Взаимодействовать с предметами, расположенными на карте: подбирать их в свой инвентарь или выкладывать; взаимодействовать с рабочими станциями; сражаться (наносить, получать урон).

---) Каждый персонаж смертен. Каждый персонаж должен иметь ряд характеристик, которые он будет пытаться "поддерживать" на должном уровне, чтобы не умереть и продолжать быть эффективным. Эти характеристики:

----) Сон

----) Голод

----) Настроение

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

-) Интерактивные объекты.

--) Объекты, с которыми можно взаимодействовать через интерфейс игры и с которыми могут взаимодействовать персонажи или иные игровые сущности с какой-то целью.

---) Пример: верстак для создания вещей; исследовательский стол для открытия новых вещей и пр.

-) Интерфейс.
--) Простой, понятный. Желательно, на русском языке.
--) Хотя бы минимально настраиваемый.


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

Сейчас - продолжаем изучать матчасть.

События отрисовки.

Матчасть.

Событие отрисовки добавляется на объект как и любое другое, через кнопку "Добавить событие (Add Event)".

Имеется два события:

-) Draw

--) Событие, которое отрисовывает объект в комнате.

-) Draw GUI.

--) Событие, которое отрисовывает объект на экране.

GameMaker Studio 2. Урок 2. События отрисовки. Коллизия. Как работают скрипты. Как подключить русский шрифт. Переходы между комнатами Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок

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

Возникает вопрос: зачем нам отдельно добавлять событие отрисовки, если объект спокойно отрисовывается и без него?

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

ВАЖНО!

Если вы добавили событие отрисовки, то вам необходимо написать в коде следующую строку:

draw_self();

В противном случае, объект не будет отрисован.
Проделайте это сами. :)

Итак. Что мы можем делать в событиях отрисовки:
-) Отрисовка спрайтов:
--) draw_self() - отрисовывает спрайт текущего объекта с настройками по умолчанию.
--) draw_sprite_ext( название спрайта в обозревателе, номер изображения, x, y, x-масштабирование, y-масштабирование, поворот, цвет, прозрачность) - отрисовка спрайта "расширенная" - т.е. с настройками. Можно отрисовать любой спрайт, как и везде, где его нужно отдельно указывать.

--) draw_sprite_part(название спрайта в обозревателе, номер изображения, x координата левой верхней точки спрайта, y координата левой верхней точки спрайта, ширина, высота, x, y) - отрисовка части спрайта
--) draw_sprite_stretched(название спрайта в обозревателе, номер изображения, x, y, ширина, высота) - отрисовка спрайта с его "растяжением". Если у спрайта включить и настроить функцию Nine Slice (девять "срезов"), то можно создавать масштабируемые (с одинаковым разрешением, пропорциями и качеством по итогу) элементы интерфейса: окошки, кнопки, etc.

-) Отрисовка фигур:
--) draw_circle(x, y, радиус, заполнение (True/False)) - рисуем круг. Аналогичное есть для прямоугольника (rectangle), стрелки (arrow), эллипса (ellipse), линии (line).
--) draw_button(x, y, x2, y2, up) - Отрисовка кнопки. Где up - True или False, нажата кнопка или нет.

-) Настройки отрисовки.
--) ВАЖНО. Любые настройки отрисовки нужно проводить ПЕРЕД отрисовкой. Можно делать в любом событии, но для удобства лучше тут же.
--) draw_set_color(col) - устанавливаем цвет отрисовки. Базовые значения цветов начинаются с приписки "c_". Пример: c_black, c_red.
---) Для продвинутых - можно использовать HEX, как в CSS: #11CCFF как пример. Используется стандартная RGB система. Если заменить решётку на $, то система сменится на BBGGRR, т.е. наоборот.
--) draw_set_alpha(alpha) - прозрачность отрисовки
--) draw_set_font(font) - устанавливаем шрифт
--) draw_set_halign(halign) - Расположение текста по горизонтальной оси (fa_ left/center/right)
--) draw_set_valign(valign) - Расположение текста по горизонтальной оси (fa_ top/middle/bottom)

Это та часть, что касается кода и с которой мы с вами будем непосредственно работать. Можете держать как памятку у себя перед глазами, когда начнём писать код.

Коллизия


Коллизия - иначе говоря, столкновение объектов, происходит благодаря "маске" спрайта. Если спрайт - это картинка объекта, то маска - это "твёрдое тело", отвечающее за считывание столкновений.
Чтобы её посмотреть, откройте настройки спрайта и разверните пункт "Collision Mask"

GameMaker Studio 2. Урок 2. События отрисовки. Коллизия. Как работают скрипты. Как подключить русский шрифт. Переходы между комнатами Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок

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

Mode - режим маски. Варианты:
- Автоматическая
- На всё изображение
- Ручная

Type - тип маски. Варианты:
- Прямоугольник
- Прямоугольник с поворотом
- Эллипс (работает медленнее)
- "Алмаз" (ромб) (работает медленнее)
- Предрасчет / Точный (медленнее) - ГМС постарается сам подстроить маску под форму объекта.
- Предрасчет / Точный по кадрам.

Мой совет:
Если игра небольшая, ставьте ручной режим и настраивайте сами те объекты, для которых это важно. Особенно те объекты, столкновение с которыми должно происходить только в определённой позиции (скажем, как в Стардью, сталкиваешься с основанием дерева, но не с его кроной).

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

Как работают скрипты.

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

Создать скрипт просто. Для этого нажмите ПКМ по папке "Scripts" и выберете там "Script". Назовите его scrMove
У вас откроется следующее окно:

GameMaker Studio 2. Урок 2. События отрисовки. Коллизия. Как работают скрипты. Как подключить русский шрифт. Переходы между комнатами Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок

scrMove - название нашей функции. В круглых скобках мы в неё можем передавать аргументы. Аргумент - это переменная, которая используется только в функции, передаётся в неё при вызове и может иметь любое название. В скобках напишите spd - это будет как раз наша переменная скорости.
Вырежьте код движения из Step-события у игрока и вставьте в скрипт, а в Step-событии игрока напишите следующий код:
scrMove(player_speed);

Получится следующая картина:

GameMaker Studio 2. Урок 2. События отрисовки. Коллизия. Как работают скрипты. Как подключить русский шрифт. Переходы между комнатами Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок
GameMaker Studio 2. Урок 2. События отрисовки. Коллизия. Как работают скрипты. Как подключить русский шрифт. Переходы между комнатами Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок

Как вы заметили, "x" и "y" у нас по прежнему работают. Это потому, что мы выполняем скрипт в объекте oPlayer: данные переменные подтягиваются из объекта автоматически.

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

Для того, чтобы скрипт вернул какое-то значение, нужно написать:

return название_переменной/значение.
Переменную можно написать только одну. Команда return прерывает выполнение скрипта. Переменную / возвращаемое значение можно не писать, тогда скрипт просто прервётся.
Конструкцию с прерыванием следует добавлять везде, где это может повлиять на игру, особенно её работоспособность.

Раньше скрипты выполнялись медленнее, чем код, написанный просто в объекте. Сейчас это не так (или, по крайней мере, не так критично).

Также стоит понимать, что создав объект "Скрипт", вы, по сути, создали просто файл для хранения кода. В одном скрипте может быть собрано сразу множество разных функций, отвечающих за выполнение одной задачи. При вызове обращаться нужно именно к имени функции.

Скрипт для отрисовки текста с обводкой.

Создаём новый скрипт и называем его scrOutlinedText
Нам понадобятся следующие аргументы:
-) xPos - позиция текста по X
-) yPos - позиция текста по y
-) col - цвет текста
-) outlineCol - цвет обводки
-) text - текст
-) curdepth - текущая "глубина" объекта.
-) Шрифт, которым будем рисовать.
-) Позиционирование по X
-) Позиционирование по Y

Общая схема работы скрипта:
1) Назначаем максимальную "глубину".
2) Рисуем текст цветом обводки.
3) Поверх рисуем текст нужным нам цветом, но с небольшим смещением.
4) Возвращаем глубину к первоначальным показателям.

Код:
https://pastebin.com/TahYZKc8
P.S.
Изначально код не мой, а честно стырен подсмотрен с интернета, но я в него добавил больше переменных для лучшей настройки и отрисовки. Фактически, значение глубины тоже можно передавать как аргумент, чтобы разный текст выводился по разному.

GameMaker Studio 2. Урок 2. События отрисовки. Коллизия. Как работают скрипты. Как подключить русский шрифт. Переходы между комнатами Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок

Создание русского шрифта

Проверим, как работает скрипт.
Сначала мы создадим русский шрифт. Для этого найдём папку "Fonts", нажмём ПКМ и создадим "Font".
Выбирайте любой шрифт и размер, который хотите. Нам с вами нужна кнопка "Add", чтобы добавить диапазон с русским шрифтом.

GameMaker Studio 2. Урок 2. События отрисовки. Коллизия. Как работают скрипты. Как подключить русский шрифт. Переходы между комнатами Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок
GameMaker Studio 2. Урок 2. События отрисовки. Коллизия. Как работают скрипты. Как подключить русский шрифт. Переходы между комнатами Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок

Здесь напишем 1040 to 1105, чтобы захватить весь русский алфавит, затем нажмём "Add Range".
Шрифт называйте на Ваше усмотрение. Я назову его: fontArialRusSmall

Теперь перейдём к объекту игрока. Создадим событие Draw GUI и напишем следующий код:
scrOutlinedText(10, 10, c_yellow, c_red, "Скорость: " + string(player_speed), depth, fontArialRusSmall, fa_left, fa_top)

Результат:

GameMaker Studio 2. Урок 2. События отрисовки. Коллизия. Как работают скрипты. Как подключить русский шрифт. Переходы между комнатами Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок

Можете заменить c_yellow и c_red на любые цвета. Помните, что можно передавать цвета в HEX с помощью # и $ (в обратном порядке).

Скрипт для поиска значения в массиве:

Так как эту тему мы не проходили, заострять внимание не буду, но вдруг кому пригодится:
https://pastebin.com/1VTNg4Bp

function scrFindInArray(array, value)
{
if is_array(array)
{
for (var i = 0; i < array_length(array); i++)
{
if array[i] == value
{
return i
}
}
return noone
}
else
{
return noone
}
}
Работает просто, но встроенной функции подобного толка в GMS нет:
- Проверяет, что аргумент является массивом.
- Обходим массив и, если какое-то значение является его элементом, возвращаем индекс. Если же совпадений не было или аргумент не массив - возвращаем "noone" - в GML это значение равняется -4.
По сути, код можно и упростить, убрав проверку на то, что переданное значение является массивом, но с ней меньше шансов накосячить. :)

Переходы между комнатами. Создаём меню.

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

GameMaker Studio 2. Урок 2. События отрисовки. Коллизия. Как работают скрипты. Как подключить русский шрифт. Переходы между комнатами Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок

Сделайте так, чтобы домик был напротив созданной комнаты.
Заметьте, что при создании новой комнаты у неё изменяются все настройки! Это важно.

Создадим в "Objects" папку "Меню", где создадим следующие объекты: "oMStart", "oMSettings", "oMExit", "oMMenu".
OMStart - кнопка для старта нашей игры.
OMSettings - кнопка для перехода в настройки игры.
oMExit - кнопка для выхода из игры
oMLoad - кнопка для загрузки игры
oMMenu - управляющий объект, который будет располагать наши кнопки.

Зайдём в комнату MainMenu и поставим там объект oMMenu.
Затем перейдём в код oMMenu и выберем событие создания (Create)

Там напишем следующий код:

buttons = [oMStart, oMLoad, oMSettings, oMExit]
Здесь мы поместили все наши кнопки в массив. Таким образом, мы можем обратиться к объекту-кнопке, используя название массива и квадратные скобки: 
buttons[0] - будет обращаться к первому элементу массива (oMStart)
Теперь нужно каждую из этих кнопок разместить. Но, чтобы правильно их разместить, сделаем на все кнопки один спрайт. Размер подбирайте под себя.

У каждого объекта создадим два события: Draw, которое оставим пустым, и Draw GUI, где напишем draw_self();
Там же напишем:
scrOutlinedText(x, y, c_white, c_black, "Здесь текст кнопки", depth, fontArialRusSmall, fa_center, fa_middle)
Где "Здесь текст кнопки" - заменить на нужное: "Начать игру", "Загрузить игру", "Настройки", "Выход"

Вернёмся в OMMenu.
Нажмите правой кнопкой мыши. Наведитесь на пункт "Code Snippets" и выберите пункт 5.
В данном меню расположены готовые шаблоны кода, а мы создали шаблон цикла.
Напишите следующий код:

for (var i = 0; i < array_length(buttons); ++i)

{

instance_create_layer(room_width / 2, room_height / 2 - 50 + sprite_get_height(sButton) / 2 + 100 * i, "Instances", buttons[i])

}

Этот код создаёт инстансы объектов, которые перечислены в нашем массиве, по центру комнаты, плавно опускаясь вниз. Не забудьте поставить Origin у спрайта кнопки по центру.
Координаты настраивайте под себя. Для этого изменяйте первые два аргумента (идут до "Instances".)
Скриншот:
GameMaker Studio 2. Урок 2. События отрисовки. Коллизия. Как работают скрипты. Как подключить русский шрифт. Переходы между комнатами Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок

Теперь нужно сделать отслеживание нашей мышки. Кнопку "Настройки" мы сделаем чуть позже. Пока перейдём к кнопке для выхода из игры.

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

dmxg = device_mouse_x_to_gui(0)
dmyg = device_mouse_y_to_gui(0)

ОК. Теперь мы отслеживаем нашу мышку относительно GUI. Нужно проверить, наведены ли мы на конкретный объект и если да - нарисовать поверх него полупрозрачный белый прямоугольник. Для этого есть команда:
instance_position(x, y, obj)

В x и y мы передадим переменные выше, а в качестве объекта напишем "self", чтобы проверять координаты нашего объекта.

Код:

if instance_position(dmxg, dmyg, self)
{
draw_set_color(c_white)
draw_set_alpha(0.15)
draw_rectangle(bbox_left, bbox_top, bbox_right, bbox_bottom, false)
draw_set_alpha(1)
}
Уже после этого пишем наш текст.
Теперь при наведении на кнопку "выхода" мы увидим, что она "активна".

Вырежем наши две переменные, добавим их в событие create. Теперь мы можем использовать эти переменные в любом коде этого объекта.

Теперь перейдём в событие step. Сюда также добавим наши две переменные. Теперь они будут постоянно обновляться.
Начнём настраивать логику работы.

Можете скопировать тот код, что мы писали выше, но удалить всё изнутри.
Нам нужно сделать проверку:
Если мы наведены на объект И нажата левая кнопка мыши - выйти из игры. То есть, немного её дополнить.

Для проверки нажатия кнопки левой мыши будем использовать команду:
mouse_check_button_pressed(mb_left)
Для выхода из игры:
game_end()

В итоге, в коде step будет следующий код:

dmxg = device_mouse_x_to_gui(0)
dmyg = device_mouse_y_to_gui(0)
if instance_position(dmxg, dmyg, self) and mouse_check_button_pressed(mb_left)
{
game_end()
}
Итого, если мы наведёмся на кнопку выхода - она подсветится. Нажмём - игра закроется.

Аналогичный код с подсветкой и проверкой просто скопируем в oMStart. В других кнопках логику работы в step пока не настраиваем на них, работаем с кнопкой начала игры.
Нужно внести одно изменение.

Вместо game_end() нам нужно написать код для смены уровня.
Для этого напишем:
room_goto(Room1)
Где Room1 - название комнаты, в которую вы хотите переместиться. Оно должно совпадать с названием комнаты в браузере ассетов.

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

Для этого нам придётся создать новый объект и назвать его oGameManager.
Разместим его в самой первой комнате и отметим у него чекбокс "Persistent"
Затем напишем у него в Step код:

Если индекс текущей комнаты != 0 и нажата ESC - деактивируем все инстансы, кроме текущего и переходим в меню.

if room != 0 and keyboard_check_pressed(vk_escape)
{
instance_deactivate_all(true)
room_goto(rmMainMenu)
}
Также отметим комнату с игроком как "Persistent".
Чтобы объекты в комнате, при возвращении, были активированы, немного дополним наш oGameManager. Для этого создадим у него событие "Room Start". 
GameMaker Studio 2. Урок 2. События отрисовки. Коллизия. Как работают скрипты. Как подключить русский шрифт. Переходы между комнатами Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок

При старте комнаты, если эта комната не является меню, мы будем запускать все инстансы.

if room != 0
{
instance_activate_all()
}
Всё. Теперь мы можем спокойно выходить в меню из игры и возвращаться обратно при необходимости.

Реализация сложнее. О том, как работает Nine Slice.

Ниже будет приведена реализация меню с использованием функции Nine Slice, которая позволит нам делать кнопки других размеров.
Используя Nine Slice мы не сможем проверять, наведены ли мы сейчас на нужный нам объект. Также эта функция годится только для отрисовки типовых форм с типовым шрифтом. Если у вас спрайт кнопки идёт с текстом, то данная функция не подойдёт.
Чтобы отслеживать нажатия и наведение, нам необходимо проверять, находятся ли координаты мыши в нужной области. Для этого существует команда

point_in_rectangle(px, py, x1, y1, x2, y2)

Итак. Первый шаг - это убрать спрайт у каждой нашей кнопки.
Для этого откроем объект каждой кнопки, нажмём на выбор спрайта и выберем "None".

Шаг второй.
У каждой кнопки в событии Create мы пропишем:

width = 0;
height = 0;
При создании кнопок мы и так будем знать их x и y координаты, а ширину и высоту будем передавать.

Шаг третий
Нарисуем спрайт в виде одноцветного квадрата размером 9х9 пикселей.
Затем добавим ему две обводки разных цветов, каждая шириной в 1 пиксель.
Получим следующую картину:
GameMaker Studio 2. Урок 2. События отрисовки. Коллизия. Как работают скрипты. Как подключить русский шрифт. Переходы между комнатами Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок

Теперь перейдём в настройки спрайта.
Назовём его sButtonSlice.
Слева выберем и откроем пункт Nine Slice.

После чего поставим галочку "Activate Nine Slice".
Далее - смотрим на наш спрайт и настраиваем его, как показано на скриншоте ниже:

GameMaker Studio 2. Урок 2. События отрисовки. Коллизия. Как работают скрипты. Как подключить русский шрифт. Переходы между комнатами Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост, Урок

Чтобы двигать фиолетовые полоски - достаточно на них навестись и зажать ЛКМ.
В правом окошке вы можете порастягивать данный спрайт, как хотите и посмотреть на итоговый результат.
Растягивающаяся рамка для кнопок готова!

Шаг четвертый.

Пропишем в Draw GUI отрисовку кнопки вместо draw_self():

draw_sprite_stretched(sButtonSlice, 0, x - width / 2, y - height / 2, width, height)

Таким образом, мы отрисовываем кнопку.

Шаг пятый.

Вернёмся в oMMenu, в Create, где немного допишем наш код.
Мы создадим переменную, которой будем присваивать значение ID только что созданного объекта. Затем, обращаясь к этой переменной, мы будем менять у данного объекта параметры: ширину и высоту.

buttons = [oMStart, oMLoad, oMSettings, oMExit]
var bwidth = 400
var bheight = 96
for (var i = 0; i < array_length(buttons); ++i)
{
inst = instance_create_layer(room_width / 2, room_height / 2 - 50 + 100 * i, "Instances", buttons[i])
inst.width = bwidth
inst.height = bheight
}

Меняя значения bwidth и bheight вы сможете сами настроить нужные размеры кнопок.

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

width = 0;
height = 0;
При создании наших объектов, а сам код для создания инстанса преобразится следующим образом:

for (var i = 0; i < array_length(buttons); ++i)
{
instance_create_layer(room_width / 2, room_height / 2 - 50 + 100 * i, "Instances", buttons[i], {width : bwidth, height : bheight})
}
Какой из вариантов удобнее - решать вам.
Вариант с переменными, которые объявляются при создании объекта, я лично считаю более надёжным, а функциональных отличий между данными командами не так много. С другой стороны, такая возможность появилось недавно и её нужно тестировать.


Шаг шестой.

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

Для этого мы берём координаты x и y, после чего проверяем, находится ли наша мышь в области:

x - width / 2, y - height / 2, x + width / 2, y + height / 2.

Остальное - копируем из прошлого кода. Получается следующий блок:

if point_in_rectangle(dmxg, dmyg, x - width / 2, y - height / 2, x + width / 2, y + height / 2)
{
draw_set_color(c_white)
draw_set_alpha(0.15)
draw_rectangle( x - width / 2, y - height / 2, x + width / 2, y + height / 2, false)
draw_set_alpha(1)
}


Шаг седьмой.

Проделать тоже самое в событии Step. (По сути, просто заменяем instance_position)

if point_in_rectangle(dmxg, dmyg, x - width / 2, y - height / 2, x + width / 2, y + height / 2)
and mouse_check_button_pressed(mb_left)
{
//Тут код нужной кнопки
}


Шаг восьмой.

Донастраиваем все кнопки.

Какой из вариантов использовать - решать Вам.

Можно вообще использовать следующий вариант:

Создать спрайт нужных размеров и залить его белым цветом с "прозрачностью" в 1 единицу. Совершенно незаметно. Используя bbox_left / top / right / bottom нарисовать поверх прозрачной основы кнопку и проверять уже не через "point_in_rectangle", а как в первом способе.

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

Что нужно подготовить к следующему гайду:
- Минимум два спрайта "породы" - на фон (темнее) и для объекта (светлее). Размер одного спрайта - 32х32.
- Желательно спрайты нескольких объектов, вроде стен, дверей. Исходите из того, что размер одной клетки будет равен 32х32 пикселей.
- Спрайт для неба/космоса/прочего на фон на ваше усмотрение.

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

И небольшой спойлер на будущее.
Взаимодействовать с миром можно будет двумя способами: через ЛКМ и наведением "тела" игрока на тот или иной объект.

Загрузить файл проекта и всё пощупать самому можно по ссылке:
Яндекс диск

Оставшиеся темы:

- Камера и её настройка. Разные способы реализации: от простого к сложному.

- Иерархия объектов. «Объекты-родители» и их «дети». Решение часто встречающихся проблем и немного про то, как удобно выстраивать взаимодействие с объектами. Глобальные переменные.

- Массивы и с чем их едят, а также grid (сетка комнаты), размещение объектов по сетке. Включая объяснение, в каких случаях лучше использовать встроенные функции, в каких – писать свои с нуля.

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

- Иные способы хранения информации в GMS2, когда их стоит или не стоит использовать.

- Сохранение. Встроенное VS самописное.

- Звуки.

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

Показать полностью 14

Только каждый третий пикабушник доходит до конца

А сможете ли вы уложить теплый пол, как супермонтажник?

Проверить

46

GameMaker Studio 2. Урок 1. Знакомство

Вступление

Привет-привет, мои хорошие! На связи Grimm Ironwill, в реальности – Вячеслав Брянцев.

Вкратце о себе. Студент третьего курса; программист. Занимался уже много чем, в том числе и преподаванием.

Это – первый пост из, я надеюсь, серии о том, как разрабатывать игры на Gamemaker Studio 2. На данном движке я работаю с декабря 21-го года и уже заимел опыт, который позволяет мне делать на нём полноценные игры, чем мы и будем здесь заниматься.

Так как пост первый, он же является вступительным. Здесь будет основная информация о движке; где его достать; что в нём есть и как в нём работать; как сделать передвижение персонажа; какую игру мы будем делать по данным гайдам.

Гайд нацелен на ту часть людей, что хотя бы немного понимает о том, что такое программирование, но не имеет опыта работы с игровыми движками, в создании игр. Первый гайд будет достаточно подробно объяснять, что есть что. В дальнейшем подобных описаний станет меньше.

О движке

С относительно недавних пор Gamemaker Studio 2 распространяется по подписочной системе, при этом сам движок – бесплатный. Подписки делятся на уровни и, в зависимости от уровня, дают Вам разные приколюхи, вроде бесплатных ассетов раз в месяц. Самое важное, что даёт подписка – возможность экспортировать свою игру на ту или иную платформу. Однако, разрабатывать игры под разные платформы вы можете сразу на бесплатной версии.

В рамках данных гайдов Вы будете работать на бесплатной версии движка (хотя можете приобрести любую). Я же имею лицензию GMS2 Desktop в стиме, поэтому работать буду на ней. Разницы в функционале между данными версиями нет.

Скачать бесплатную версию с официального сайта:

https://gamemaker.io/ru/get

После установки

Первое, что стоит сделать после установки движка – это в нём разобраться.

После запуска Вас будет ждать следующее меню:

GameMaker Studio 2. Урок 1. Знакомство Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост

Я замазал свои недавние проекты, оно у вас будет голое 😊

Так вот. Разберёмся со стартовым меню и почему не стоит переводить Гейммейкер, хоть такая возможность и присутствует.

Три самые важные кнопки – New, Open и Import позволяют создать новый проект или открыть существующий. Разница между Open и Import исключительно в формате файлов, которые они открывают.

Tutorial – понятное дело, ведёт на сайт с туториалами. Для закрепления материалов и изучения возможностей движка, помимо данной данных гайдов, настоятельно рекомендую ознакомиться.

В верхней панели также есть несколько важных разделов.

File – меню управления нашим окном. Здесь же, открыв Preferences или нажав CTRL+SHIFT+P, можно настроить нашу среду разработки, в том числе сменить её язык. Но делать этого не стоит, о чём ниже. Тут же можно поменять цветовую схему, шрифты и многое другое.

Перевод Gamemaker Studio 2 оставляет желать лучшего, хотя переведена не только среда, но и её мануал. Я надеюсь, что я ошибаюсь, но очень часто перевод выглядит так, словно его просто запихнули в гугл транслейт. В мануале к тому же переводят названия встроенных функций, что просто нонсенс. Поэтому только английский – только хардкор. В любом случае, непонятные фрагменты текста можно перевести самостоятельно, будет понятнее.

Build – запуск нашей игры или её дебаг. Очисткой не пользовался, не скажу. Разница между простым запуском и дебагом в количестве вылезающих окошек. Дебаг не только подсветит ошибку, если в ходе игры она произойдёт, но также выведет информацию о производительности. Сокращения: F5 для запуска и F6 для дебага.

Marketplace – местный «рынок», в котором очень много ВСЕГО. Готовые спрайты, звуки, полноценные ассеты, различные специалисты и даже функции. Мой совет: не покупайте здесь готовый код (функции). Огромный минус местного рынка – свои «работы» могут выкладывать простые пользователи и ставить им ценник, при этом их никто не проверяет (как мне кажется). Поэтому за пару баксов вам могут подкинуть кусок кода из пяти строк, в котором своего нет ничего – используются только встроенные в ГМ функции в определённом порядке. Знаю о чём говорю, так как сталкивался с таким. С другой стороны, спрайты, музыку и прочие ассеты можете брать на своё усмотрение.

Знакомство с движком

Цель данных гайдов – обучиться взаимодействию с движком, постепенно создавая свою игру. Это будет стратегия, похожая на «Oxygen not included» и «Fallout shelter», но со своими приколами. На мой взгляд, жанр стратегий подойдёт лучше для ознакомления с функционалом движка. 😊

Нажимаем кнопку «New», выбираем «New Blank», после чего в «Project Name» называем нашу игру (на Ваше усмотрение). Если у Вас вдруг выдало выбор между GML и DnD, выбираем GML – язык нашего движка. DnD – режим создания игр с помощью визуальных блоков.

Итак, у нас открыто окно проекта. Что у нас есть?

GameMaker Studio 2. Урок 1. Знакомство Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост

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

Откроем папку «Rooms», после чего два раза кликнем по «Room1». Это – наша первая игровая комната.

GameMaker Studio 2. Урок 1. Знакомство Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост

Не пугайтесь внешнего вида нашего движка. Пройдём последовательно по тому, что у нас есть.

Окошко «Layers – Room1»

Как понятно из названия, здесь расположены «слои» нашей комнаты. На разных слоях мы можем располагать разные объекты. В их число входят: слой заднего фона, слой инстансов, слой тайлов, слой путей, слой ассетов. Также имеются фильтры и папки для слоёв.

Инстанс и объект.

Рассматривать данные явления нужно в паре, так как инстанс – это копия объекта в игровом мире. Простой пример для понимания ситуации. Мы создали объект: мечник. В комнате у нас есть десять таких мечников (скажем, мы заспавнили их через код) – это десять инстансов.

Размещать инстансы можно только на слое инстансов.


Слой тайлов.

Позволяет размещать на себе «тайлы» - клетки. В гейммейкере есть встроенный инструмент под названием «Tile set» - это набор клеток (спрайтов), которые могут быть использованы в игре. С его помощью можно будет из готовых клеток «рисовать» свои собственные карты. В том числе, для этого есть инструмент «Auto Tile», который размещает нужные клетки в автоматическом порядке. Мы всё это рассмотрим в будущих гайдах.

Слой путей.

В игровой движок встроены функции, которые позволяют создавать пути между двумя точками. Используется алгоритм А* (А стар, а звёздочка) для квадратных сеток двух типов: с диагональным движением и без.

Минус встроенного алгоритма – он не учитывает стоимость движения и работает немножко топорно, но для простой игры он сойдёт.

Слой путей делать при этом необязательно. Слой путей позволяет расставлять пути самостоятельно.

Окошко «Properties – Room1»

Как ни странно, это настройки нашей текущей комнаты.

Чекбокс «Persistent» отвечает за то, является наша комната постоянной или нет. Иными словами, будет ли продолжать выполняться код в данной комнате, если мы из неё куда-то свалим или нет.

Чекбокс «Clear Display Buffer» советую держать включённым, так как по идее повышает производительность.

«Width» и «Height» - ширина и высота нашей комнаты. Размер комнаты != размер экрана и может сильно от него отличаться.

Ниже имеются вкладки Viewports and Cameras, а также Room Physics. Первая нужна для создания камеры и её настройки, вторая – для физики комнаты. Вторая вкладка нам не нужна, а с первой мы с вами ещё поработаем.

Скажу сразу. В гейммейкере некоторые моменты нужно писать самостоятельно. Самое простое, что приходит на ум: чтобы не перегружать компьютер, те объекты, которые «мы не видим», мы должны отключать (или не рисовать) самостоятельно.

Начало начал

Начнём с того, что создадим нашего игрока. Для этого выделите папку «Objects» и нажмите «ALT + O» или нажмите правой кнопкой мыши, выберите «Create», «Object».

В поле «Name» переименуем нашего игрока в «oPlayer».

GameMaker Studio 2. Урок 1. Знакомство Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост

Разберём, что у нас здесь есть.

1. Название нашего объекта.

2. Спрайт объекта (картинка, которая будет обозначать наш объект, двигаться, etc.)

3. Ряд настроек объекта:

a. Visible – видим наш объект или нет.

b. Persistent – по аналогии с комнатой, будет ли код данного объекта исполняться, пока мы переключены на другую комнату, или нет.

c. Solid – «твёрдость» объекта. Может ли он проходить сквозь другие объекты, которые имеют те же свойства. Нам не нужно.

d. Uses Physics – используется ли встроенная в движок физика. Тоже нам не нужно.

4. Ещё одно поле настроек.

a. Events – события, во время которых происходят какие-либо действия. Открыты по умолчанию (см поле под цифрой 5)

b. Parent – Есть ли у нашего объекта «родители» и «дети». Используется для того, чтобы объединить объекты в группы, с которыми можно взаимодействовать.

i. Пример: объединим объекты: «мечник», «лучник», «маг» в группу, назначив каждому из них в качестве родителя объект «юнит». Так мы можем обращаться не к каждому объекту по отдельности, а к объекту-родителю, чтобы, например, проверить, есть ли какой-либо из них в нашей комнате.

c. Physics – настройка встроенной в движок физики.

d. Variable Definitions – здесь мы можем объявить переменные и настроить их, но также это можно делать непосредственно в коде.

5. Окно событий. Сюда будут добавляться все события, которые мы выберем.

6. Кнопка для добавления событий.

Сначала нажмём на квадрат с плюсиком, под цифрой 2 на скриншоте. У нас открылось меню для рисования спрайта и он сразу же выбрался как спрайт объекта «oPlayer».

GameMaker Studio 2. Урок 1. Знакомство Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост

Изменим имя спрайта с «Sprite1» на «sPlayer». Справа сверху в выпадающем меню выберем «Middle Centre», чтобы выбрать в качестве центровой точки центр нашего спрайта. Относительно этой точки и будут происходит все вычисления. Затем нажмём «Edit image».

Лирическое отступление для внимательных. Мы назвали объект «oPlayer», а спрайт «sPlayer» для удобства. Первая буква перед основным названием нужна исключительно для того, чтобы определять, о чём у нас идёт речь, так как в своём коде мы можем взаимодействовать со всеми объектами, что у нас есть в обозревателе. Перейдём к «Edit image».

Здесь Вы можете самостоятельно разобраться в том, что за что отвечает. Наша с вами цель – нарисовать любой объект (можно хоть всё одним цветом залить).

Когда закончите, сверху выберите вкладку «Room1». Справа выберите «oPlayer», нажав по нему один раз ЛКМ, затем, с зажатым ALT, кликните по любому месту в комнате. Поздравляю, вы разместили игрока! Осталось его настроить. Для этого нажмите два раза по «oPlayer».

Нажмите «Add Event» и выберите «Create». Откроется окно для написания кода. В данном случае, написанный код будет исполняться при создании объекта в комнате.

Давайте назначим нашему игроку базовую характеристику: скорость:

player_speed = 5;

GameMaker Studio 2. Урок 1. Знакомство Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост

Точку с запятой писать необязательно, но у меня это привычка. 😊

В событии создания (Create) обычно прописывают константы – те переменные, которые не будут меняться в ходе работы программы. Впрочем, никто не запрещает для удобства перечислить здесь все переменные.

Итак, наша задача сейчас – заставить нашего игрока двигаться. Для этого, добавим новое событие:

«Add Event» -> «Step» -> «Step»

Таким образом мы создали событие, которое повторяется каждый «шаг», иными словами – кадр.

Немного теории.

Для изменения координат объекта в гейммейкере существуют «встроенные» переменные «x» и «y». Просто добавлять к ним скорость некорректно, так как пользователь может двигаться по диагонали и в случае простого добавления скорости, движение по диагонали выйдет слишком быстрым. Чтобы это исправить, мы будем:

- Считывать, какие клавиши нажаты.

- Использовать встроенные функции для расчёта передвижения.

Практика:

Чтобы сохранить клавишу, которую нажал игрок, удобнее всего будет запихнуть её в переменную.

В Step’е пишем:

key_up = keyboard_check(ord("W")) or keyboard_check(vk_up);

Что происходит: если пользователь нажмёт кнопку «W» или стрелочку вверх – переменная key_up станет равна 1, если же кнопки не нажаты – будет равна 0. «ord» нужно, чтобы указать клавишу на клавиатуре, однако для стрелочек существуют команды vk_up, vk_left, vk_right, vk_down. Такие же обозначения есть для других клавиш, вроде пробела, шифта и т.д. По аналогии, напишем код для других кнопок.

GameMaker Studio 2. Урок 1. Знакомство Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост

Теперь нам нужно понять, куда двигается наш игрок и двигается ли вообще.

Для того, чтобы узнать направление, нам нужно воспользоваться встроенной функцией point_direction(). Чтобы подробно узнать о том, как она работает, напишите данную функцию, наведитесь на неё и нажмите на колёсико мыши. У вас автоматически откроется справка с подробным описанием.

Открыть подобным образом можно любую встроенную функцию. Мануал также доступен на русском языке.

Функция point_direction возвращает вектор, образованный указанными парами координат. Соответственно, в скобках указываются: x1, y1, x2, y2. Так как мы знаем, что если кнопка не нажата, то она равна нулю, а если нажата – единице, то вычитая противоположные по направлению переменные, можем получить нужное нам направление движения.

movement_dir = point_direction(0, 0, key_right - key_left, key_down - key_up);

С-но, чтобы отследить нажатие кнопок, нам нужно создать вторую переменную. Работает она по такой логике: если была нажата одна из кнопок – переменная будет равна единице (true – истина в языке GML).

movement_input = (key_right - key_left != 0) or (key_down - key_up != 0);

У опытных или внимательных может возникнуть вопрос, зачем нам это, если мы можем отслеживать «movement_dir != 0»? Дело в том, что подобный вариант заставит наш объект «запинаться» во время резких поворотов, что нас не устраивает.

Теперь настало время знакомиться с логическими операторами. В GML’е их три и они имеют следующий синтаксис:

if событие {}

else if событие {}

else {}

Нам нужно отловить момент, когда нажата кнопка. Для этого напишем:

if (movement_input)

{

}

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

var h_speed = lengthdir_x(player_speed, movement_dir);

var v_speed = lengthdir_y(player_speed, movement_dir);

x += h_speed;

y += v_speed;

GameMaker Studio 2. Урок 1. Знакомство Разработка, Gamedev, Программирование, Инди, Инди игра, Gamemaker Studio 2, Образование, Длиннопост

Код целиком.

Для чего мы писали «var» перед объявлением переменной? Таким образом мы обозначили, что данные переменные являются локальными. Их нельзя будет использовать из других частей программы. Более того, их также нельзя будет использовать в других событиях этого же объекта. На самом деле, это необязательно, но бытует мнение, что ускоряет работу кода. Если вы точно знаете, что какая-то переменная больше нигде использоваться не будет и менять её нежелательно – смело делайте её локальной.

Итак, сохраним проект (CTRL + S) и нажмём F5, чтобы проверить.

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

В следующем гайде мы подробно разберём события отрисовки, как они работают. Напишем скрипт для вывода текста с обводкой, c нужным нам шрифтом и размером.

Примерный план:

- Общий обзор движка, передвижение. (Текущий гайд)

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

- Камера и её настройка. Разные способы реализации: от простого к сложному.

- Иерархия объектов. «Объекты-родители» и их «дети». Решение часто встречающихся проблем и немного про то, как удобно выстраивать взаимодействие с объектами. Глобальные переменные.

- Массивы и с чем их едят, а также grid (сетка комнаты), размещение объектов по сетке. Включая объяснение, в каких случаях лучше использовать встроенные функции, в каких – писать свои с нуля.

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

- Иные способы хранения информации в GMS2, когда их стоит или не стоит использовать.

- Сохранение. Встроенное VS самописное.

- Звуки.

П.С. Чукча не писатель и ориентируется на свои собственные ощущения от того, чего ему не хватало, когда он сам только изучал это всё дело, перебирая десятки форумов, сайтов и англоязычных видео.
Есть вопрос? Задавай! Постараюсь ответить.

Показать полностью 8
Отличная работа, все прочитано!