Горячее
Лучшее
Свежее
Подписки
Сообщества
Блоги
Эксперты
Войти
Забыли пароль?
или продолжите с
Создать аккаунт
Я хочу получать рассылки с лучшими постами за неделю
или
Восстановление пароля
Восстановление пароля
Получить код в Telegram
Войти с Яндекс ID Войти через VK ID
Создавая аккаунт, я соглашаюсь с правилами Пикабу и даю согласие на обработку персональных данных.
ПромокодыРаботаКурсыРекламаИгрыПополнение Steam
Пикабу Игры +1000 бесплатных онлайн игр Погрузись в захватывающий шутер!

FRAGEN

Шутер, Экшены, Шутер от первого лица

Играть

Топ прошлой недели

  • AlexKud AlexKud 38 постов
  • Animalrescueed Animalrescueed 36 постов
  • Oskanov Oskanov 7 постов
Посмотреть весь топ

Лучшие посты недели

Рассылка Пикабу: отправляем самые рейтинговые материалы за 7 дней 🔥

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

Спасибо, что подписались!
Пожалуйста, проверьте почту 😊

Помощь Кодекс Пикабу Команда Пикабу Моб. приложение
Правила соцсети О рекомендациях О компании
Промокоды Биг Гик Промокоды Lamoda Промокоды МВидео Промокоды Яндекс Директ Промокоды Отелло Промокоды Aroma Butik Промокоды Яндекс Путешествия Постила Футбол сегодня
0 просмотренных постов скрыто
18
GrimmIronwill
GrimmIronwill
3 года назад
Лига Разработчиков Видеоигр
Серия Gamemaker Studio 2: Сборная солянка

Gamemaker Studio 2. Система для создания простых модификаций⁠⁠

Привет!

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

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

Описание системы

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

Реализация

Для удобства я сделал два объекта:
oWeaponsManager (со свойством persistent) и oWeapon.
Первый - управляющий. Он создаётся вместе с другими управляющими объектами при запуске и здесь происходит основная логика, о которой мы с вами будем сегодня говорить.
Второй объект - это объект-оружие, которому мы в последствии будем передавать всю нужную информацию, спрайты и прочее-прочее.

Ссылка на код:
https://pastebin.com/T6NaBMsb

Дубль с пастебина:
Функция-конструктор для объектов:

function scrWeaponsAdd(_sprite_index, _images, _name, _distance, _penetration, _damage) constructor{
sprite_index = _sprite_index;
images = _images;
name = _name;
distance = _distance;
penetration = _penetration;
damage = _damage;
}
oWeaponsManager:
// Если файл уже существует - заполняем по новой.
if file_exists("weapons.txt") {
global.weapons = [];
var _buffer = buffer_load("weapons.txt");
var _string = buffer_read(_buffer, buffer_string);
buffer_delete(_buffer);
var _loadData = json_parse(_string);
// Загружаем оружие.
for (var i = 0; i < array_length(_loadData); ++i) {
// Если информация является строкой или такого спрайта не существует - создаём его и сохраняем
if is_string(_loadData[i].sprite_index) or !sprite_exists(_loadData[i].sprite_index) {
var name = _loadData[i].sprite_index;
if file_exists(name) {
var sprite = sprite_add(name, _loadData[i].images, false, false, 0, 0);
// Устанавливаем точку отсчёта в центре спрайта
sprite_set_offset(sprite, sprite_get_width(sprite) / 2, sprite_get_height(sprite) / 2);
_loadData[i].sprite_index = sprite;
}
}
array_push(global.weapons, _loadData[i]);
}
}
// Создаём с нуля, если не существует
else {
global.weapons = [
new scrWeaponsAdd(sPistol, 1, "Пистолет", 10, 1, 10),
]
var _string = json_stringify(global.weapons);
var _buffer = buffer_create(string_byte_length(_string) + 1, buffer_fixed, 1);
buffer_write(_buffer, buffer_string, _string);
buffer_save(_buffer, "weapons.txt");
buffer_delete(_buffer);
}
#endregion
#region Удаляем те объекты, что не смогли загрузиться
var i = 0;
while i != array_length(global.weapons) {
if is_string(global.weapons[i].sprite_index) {
array_delete(global.weapons, i, 1);
i -= 1;
}
i += 1;
}
#endregion
#region Создаём прошедшие проверку объекты. Дебаг.
for (var i = 0; i < array_length(global.weapons); ++i) {
var inst = instance_create_layer(32 + i * 32, 32, "Instances", oWeapon);
inst.sprite_index = global.weapons[i].sprite_index;
}
#endregion

Спрайты можно загружать как анимированные, так и одиночные. Всё зависит от параметра images, который должен быть равен количеству кадров. Принимает на вход: .png, .jpeg/jgp, .json, .gif.

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

Сочетанием клавиш "Windows + R" (или просто открыв окно "выполнить", либо прокликав пусть самостоятельно), вписывем туда: %localappdata%
Откроется папка Диск:\Пользователи\Пользователь\AppData\Local
Здесь ищем папку, название которой совпадает с названием нашего проекта. У меня это Pikabu.

Именно в эту папку у нас сохраняются файлы, с которыми мы работали. То есть, и сохранения, и файл weapons.txt, у нас находится здесь.
Чтобы добавить новое оружие - скинем его картинку в эту папку, затем немного подредактируем файл weapons.txt

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

Скопируем первую часть строки, что в фигурных скобках. Добавим запятую после закрывающей фигурной и вставим скопированный текст.
Что нам нужно заменить:
images - с 1 меняем на нужное число кадров. Как видно по скрину выше, у меня это число будет равняться трём, так как я хочу импортировать "анимированную" картинку. Важно знать, что на это число будет разделена картинка по ширине. Для анимации картинка должна быть выполнена в виде линии, где меняется только ширина. Хотите спрайт 32х32 с тремя кадрами анимации - делайте картинку 96х32 и размещайте в каждой новой "клетке" то, что нужно отрисовать.
sprite_index - пишем в кавычках полное название нашего файла. Полное название - это с учётом его формата. В моём случае - "sPistol3.png"
name, distance, penetration, damage - меняем по своему усмотрению, это уже чисто игровые параметры.
Сохраняем файл и заходим в игру, чтобы посмотреть результат.

Эпилептикам советую быть осторожнее.

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

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

Теперь поговорим о минусах.

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

Если что-то пошло не по плану, посмотреть исходный код можно здесь:
https://disk.yandex.ru/d/bOSzQ5bj8IdN9w

P.S.
Комната была уменьшена в размерах с 1920х1080 до 128х128, чтобы показать оба объекта.
P.P.S.
Если не получается найти папку, то можете в коде oWeaponsManager, после создания файла weapons.txt, прописать следующий код:
show_debug_message(filename_path("weapons.txt"))
И запустить игру. В выводе выскочит сообщение с путём прямиком до нужной папки.

Если что-то не получилось - пишите, постараюсь помочь.

Показать полностью 2
[моё] Разработка Gamedev Программирование Инди Инди игра Gamemaker Studio 2 Длиннопост Урок Игры Обучение Гифка
0
Yarin2
Yarin2
3 года назад

Гадание на кофию и по поисковику⁠⁠

Игра с подвохом. Крутая получается развилочка. Я например сразу три слова дописал, а потом жамкал до пределах))) Но все ровно... Дополнительные части предложения без деепричастных оборотов))) Этого поисковики пока не могут! и в окончаниях они путаются...

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


1. Каждое захватывающее приключение должно быть, соответствовать, существовать исключительно в виде предложения в предложении.


Не удержался и ещё раз поколдовал.

2. Каждое захватывающее приключение должно быть внимательным к людям с особенностями поведения на уроках физкультуры и гимнастики.


#fun@author_today

Автор арта: Santiago Fuentes

Гадание на кофию и по поисковику Игры, Книги, Урок, Gamedev, Разработка, Обучение
Показать полностью 1
[моё] Игры Книги Урок Gamedev Разработка Обучение
0
2
Randomgames
3 года назад

Движение к мечте или "Таков путь"⁠⁠

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

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

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

Как сделать игру на ПК???

С этими словами я отправился к своему соседу по лестничной клетке. Тогда ему ещё не было даже 30, он был “сисадмином” и в принципе вёл себя дружелюбно. (Жаль, что потом он спился и превратился… Ну вы сами можете предположить как ведут себя алкоголики.)

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

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

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

Провозившись с “нот юзер френдли” интерфейсом несколько недель, руки мелкого пацана, то бишь меня, опустились. Кто же знал, что нужно знать математику, программирование и много-много-много всякого?!

Было решено играть в игры и жить обычной жизнью среднестатистического ребёнка до нужного момента…

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

Я стою на коленях в грязи. Часть моих боевых товарищей была взята в плен, либо погибла. Надо мной склоняется ОН и задаёт лишь один вопрос: Готов ли я перейти на их сторону?

Моим ответом было “Да”.

И вот, теперь я не играю в игру. Я формирую игру!

Если быть кратким, в ту ночь я перешёл в касту “игроков в мастерской позиции” или “помощник мастера”.

Всё стало ясно как день. Вот, что мне хочется делать! Вот, что было во мне все эти годы! Моя мечта!

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

Обретя путь, я обрёл и своего сенсея, чтобы впоследствии самому стать сенсеем.

Задача по созданию “Игр на ПК” всё ещё находилась в ящике, но она лишь ждала своего часа.

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

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

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

Так шли годы… Я написал сотни, тысячи игр и провёл их.

Потом я встретил её. Девушку, из-за которой произошло следующее ключевое событие в моей жизни.

Мы захотели сделать свою игру, но уже на ПК.

Для меня было откровением, что надо ставить точку с запятой при написании некоторых строк на C#. В те дни я впервые столкнулся с программированием и Unity.

Скажу честно. Уметь что-то делать и хотеть это делать две разные вещи. Но что поделать? Денег на программиста нет, а желание делать игры никуда не делось.

Пришлось учиться снова…

“Hello world” в консоли и документация мои друзья + видео с курсов и ютьюба.

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

Что ж, не бустанулись. Первый проект ушёл в стол, второй тоже, третий туда же.

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

Unity оказалось интересным местом, столько возможностей и всяких инструментов, особенно меня зацепил VFX, есть в этом что-то такое непередаваемое…

Интересным этапом практики было комбинировать стандартные игры живого действия с приложениями созданными на Unity. Для меня стало нормой внедрять миниигры, АР камеру, и какие-то другие игромеханические примочки в сеттинги Sci-Fi.

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

Что же по итогу?

Играбельная глава была выложена на плей маркет и на этом закончился процесс…

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

Снова.

Показать полностью
[моё] Истории из жизни Unity Gamedev Инди Инди игра Разработка Мечта Воспоминания Разочарование Урок Длиннопост Текст
2
32
GrimmIronwill
GrimmIronwill
3 года назад
Лига Разработчиков Видеоигр
Серия Gamemaker Studio 2: серия гайдов

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
[моё] Разработка Gamedev Программирование Инди Инди игра Gamemaker Studio 2 Образование Длиннопост Урок Игры Стратегия Обучение Видео
4
29
GrimmIronwill
GrimmIronwill
3 года назад
Лига Разработчиков Видеоигр
Серия Gamemaker Studio 2: серия гайдов

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
[моё] Разработка Gamedev Программирование Инди Инди игра Gamemaker Studio 2 Образование Длиннопост Урок Игры Стратегия Обучение Видео Без звука
2
22
GrimmIronwill
GrimmIronwill
3 года назад
Лига Разработчиков Видеоигр
Серия Gamemaker Studio 2: серия гайдов

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
[моё] Разработка Gamedev Программирование Инди Инди игра Gamemaker Studio 2 Образование Длиннопост Урок Игры Стратегия
2
28
GrimmIronwill
GrimmIronwill
3 года назад
Лига Разработчиков Видеоигр
Серия Gamemaker Studio 2: серия гайдов

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
[моё] Разработка Gamedev Программирование Инди Инди игра Gamemaker Studio 2 Образование Длиннопост Урок
7
15
GrimmIronwill
GrimmIronwill
3 года назад
Лига Разработчиков Видеоигр
Серия Gamemaker Studio 2: серия гайдов

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
[моё] Разработка Gamedev Программирование Инди игра Gamemaker Studio 2 Образование Длиннопост Урок Обучение
5
Посты не найдены
О нас
О Пикабу Контакты Реклама Сообщить об ошибке Сообщить о нарушении законодательства Отзывы и предложения Новости Пикабу Мобильное приложение RSS
Информация
Помощь Кодекс Пикабу Команда Пикабу Конфиденциальность Правила соцсети О рекомендациях О компании
Наши проекты
Блоги Работа Промокоды Игры Курсы
Партнёры
Промокоды Биг Гик Промокоды Lamoda Промокоды Мвидео Промокоды Яндекс Директ Промокоды Отелло Промокоды Aroma Butik Промокоды Яндекс Путешествия Постила Футбол сегодня
На информационном ресурсе Pikabu.ru применяются рекомендательные технологии