Как не сломать свою игру при внесении изменений
Это вторая статья из серии посвященной упрощению разработке вашей игры
Видеоформат:
К концу первой статьи ваш геймдизайн стал аккуратным: враги живут в одной папке, персонаж превратился в игровой объект с полями, а движок читает .ima.json напрямую. А между видео мы добавили персонажам и врагам фотографии. Но при работе с эдокументом вы наверняка сталкивались с тем, что почувствовал каждый: меняешь одно, и что-то рядом перестаёт совпадать. Поле «Урон» сортируется как текст, у каждого врага заново набиваются одни и те же поля, имя персонажа набрано в десяти местах и переименование оставляет устаревшие следы, а в поле «Роль» кто-то пишет Защитник, а кто-то защитник.
Проблема — данные в дизайне ещё не получили ни типа, ни схемы, ни связей. Сегодня мы дадим каждому полю настоящий тип, соберём схему категории один раз в базовом объекте, заменим скопированные имена ссылками, зафиксируем словарь структурами и перечислениями и в конце увидим весь дизайн одним графом, по которому можнопермещаться.
Типизация значений
Поля ваших врагов хранят значения: Здоровье = 60, Скорость = 30, Урон = 8, Размер = 1. Смысл значения понятен из названия поля: Урон = 8 урон, Здоровье = 60 . А вот поведение, т.е что в поле можно записывать и что с ним можно делать — никакнеобозначено не задано. Без типа каждое поле это просто свободный текст, и ни на один базовый вопрос ответить нельзя.
Можно ли его сортировать или сравнивать? Текстовое поле сортирует 60, 30, 8 как слова, а не как числа — так 8, наименьшее значение, окажется после 60, и таблица баланса упорядочит значения неправильно.
Значение вообще валидно? В поле здоровья можно написать шестьдесят текстом, булевое значение: True, true, да
Поймёт ли движок введённое значение? В коде вещественные, целые и булевые типы ведут себя совершенно по-разному. Если тип неизвестен, движку приходится гадать, а это ровно то, с чего геймдизайн и код начинают расходиться.
Решение — задать каждому полю тип. Типизированное поле не меняет смысл значения: 10 остаётся 10. Тип добавляет поведение: значение можно проверять, сортировать, сравнивать и отдавать движку как ровно то, чего он ждёт.
Самые ходовые типы в IMS Creators: Строка — одна строка текста, как имя; Текст — более длинный многострочный текст, который можно форматировать; Число — значение с десятичной частью (float), например 8.5; Целое число — целое значение, например 60; Флажок — переключатель true/false.
Каждый тип меняет поведение поля. Строка и Текст хранят слова, а не числа — так в поле имени не окажется случайно вычислений. Число и Целое число хранят значения, с которыми можно работать математически: их можно сортировать, сравнивать и балансировать как числа. Число (float) сохраняет десятичные части, а Целое число ограничивает значения целыми — здоровье 60 . Флажок строго хранит true или false — никакой записи текстом и никакой двусмысленности.
Применим это к врагу из бестиария. В блоке Свойства у каждого поля есть своя настройка типа: откройте меню поля (нажав три вертикальные точки в ячейке поля), выберите Изменить настройки и укажите Тип. Задаём врагу:
Имя — поле Строка. Хранит имя врага одной строкой текста.
Описание — поле Текст. Более длинное многострочное описание, которое можно форматировать.
Здоровье — поле Целое число 60 у Посетителя-ходячего остаётся чистым целым.
Скорость — поле Число. Скорость движения может быть дробной, поэтому десятичное значение допустимо.
Урон — поле Число. Здесь допустимо десятичное значение, потому что урон может быть дробным.
Размер — поле Число. Враги могут быть меньше единицы, у бронированного врага Размер = 1.5, и такое значение помещается только в десятичное поле.
Благодаря типизации гейм-дизайнер мыслит глобальнее и может добавить новые объекты со сложной логикой — например, множитель урона по врагам. Такой эффект он пишет как x2, потому что именно так задумал его у себя в голове. Поле Число не примет x2. Редактор просто откажется: «это не число». Если внести эффект в одно нетипизированное текстовое поле, значение сохранится, и в момент ввода никакой ошибки не вылезет. Проблема всплывёт позже, когда способность с двойным уроном дойдёт до программиста, который в лучшем случае реализует лишь простое сложение. Наша задача — предусмотреть такую ошибку до финальной интеграции в систему, чтобы каждая переменная по типу (множителя, сложения или процента) вписывалась в баланс.
Решение — задать для операции тип поля: Множитель урона (Число). Рядом держим Бонус урона (Число) для аддитивных эффектов, у каждой операции своё типизированное поле, и движок получает однозначную логику. Теперь x2 выразимо: поле множителя хранит 2, а тип поля определяет, что это именно множитель.
Когда тип задан, данные начинают себя проверять: редактор проверяет значения по ходу ввода, таблица баланса сравнивает настоящие числа, а движок при загрузке JSON видит, float это, целое или булево.
Настроил один раз — используешь постоянно
Типы решают поведение отдельных полей. Но каждый новый враг всё ещё требует заново описывать одни и те же свойства. Пока врагов 3, это просто надоедает, когда бестиарий разрастётся до 50 позиций, рутинное копирование структуры съест всё время вместо геймдизайна. А попробуйте добавить новое свойство... Вообщем, те ещё приключения.
Решение — перестать определять схему для каждого элемента и определить её один раз, для всех. За это отвечает базовый объект: обычный игровой объект, который вы отметили как шаблон. Он хранит схему полей категории, и от него можно создать экземпляр, новый игровой объект, который наследует ту же схему полей и готов к заполнению значений.
Отметка шаблоном необязательна, наследование работает и без неё, но так шаблон не мешается в списках задач. Связь простая: базовый объект определяет что есть у каждого элемента, экземпляр — что именно представляет собой этот конкретный элемент.
Возьмём врагов. Определите новый элемент, который будет хранить общую схему, и поместите его в отдельную папку Типы, чтобы базовый объект не смешивался с бестиарием. В блоке Свойства добавьте поля, которые должны быть у каждого врага: Здоровье — Целое число, Скорость — Число, Урон — Число, Размер — Число. Значения оставьте пустыми — это список полей для всех элементов категории «Враг».
Отметьте элемент как шаблон: нажмите три вертикальные точки в правой части шапки элемента и отметьте Является шаблоном. Теперь щёлкните правой кнопкой по шаблону и выберите Создать экземпляр. IMS Creators создаст новый игровой объект с четырьмя уже определёнными полями. Назовите его Громила и заполните: Здоровье = 200, Скорость = 15, Урон = 15, Размер = 3. Тяжёлый медленный враг готов.
А с уже существующими врагами пересоздавать ничего не нужно: откройте настройки врага через три вертикальные точки в правой части шапки элемента, найдите поле Тип — оно указывает, от чего элемент наследует, и выберите базовый объект врага. Текущий объект сразу станет экземпляром: появятся общие поля, он будет обновляться вслед за категорией, а все введённые значения сохранятся.
Тот же паттерн для протагонистов. Создайте базовый объект Персонаж и задайте поля, которые есть у каждого персонажа: Здоровье — Целое число, Выносливость — Число, Роль — Строка (пока просто строка текста — фиксированный словарь появится позже на этой неделе). Создайте экземпляр и назовите его Дмитрий: Здоровье = 120, Выносливость = 30, Роль = Разведчик . А Хлоя, которая живёт в проекте с первой недели, может присоединиться к категории Персонажей без пересоздания, достаточно указать её Тип на базовый объект.
Колода — ещё один новый элемент структуры игры. На первой неделе предметы и способности были просто разрозненными идеями, теперь они становятся игровыми картами, которые персонажи разыгрывают и сбрасывают. Каждая карта, какого бы типа она ни была, содержит одни и те же базовые поля: Имя — Строка, Стоимость — Целое число (энергия на разыгрывание), Урон — Число, Легендарная — Флажок (редкость). Легендарная карта Огнетушитель: Стоимость = 6, Урон = 4, Легендарная = да. Банановая кожура: Стоимость = 1, Урон = 1, Легендарная = нет. Две карты, одна схема, ноль повторного ввода.
Если в будущем у каждой карты должно появиться поле Описание, вы добавляете его один раз в базовый объект. Каждый существующий и будущий экземпляр автоматически получает новое поле, изменение распространяется мгновенно. Добавление следующего врага, персонажа или карты превращается в заполнение готовой формы, а правка одной схемы обновляет все экземпляры сразу.
Ссылки на объекты вместо имён
Схемы определены, но карты и персонажи пока в отдельных списках. И наступает утро, когда вы решаете переименовать Дмитрия. Лёгкое изменение в дереве проекта, но что, если его имя вписано вручную в десяти разных местах? В описании его фирменной способности, в заметках о том, кому достаётся эта карта, в черновике списка колоды. Ни одно из этих мест не обновится при переименовании, каждое хранит имя как статичную строку. Одна маленькая правка, и у вас устаревший документ и рассогласованная колода.
Решение — хранить ссылки (связи), которые указывают на сам элемент, а не на его заголовок.
Чтобы понять, как это работает: ваши игровые карты неодинаковы. Игровая карта — её может использовать любой персонаж: Огнетушитель, Банановая кожура разыгрываются, кем угодно. Карта-способность принадлежит одному конкретному персонажу: только Дмитрий применяет свою фирменную способность, значит эта карта его. Сделаем отношения между объектами полноценной частью игры.
Удобно, что это ложится на многоуровневое наследование: базовый объект может сам строиться на другом базовом объекте. Отметьте новый базовый объект как экземпляр Игровой карты, и он наследует всю схему карты: Имя, Стоимость, Урон, Легендарная. Затем мы лишь добавляем одно поле — Владелец. Назовите этот базовый объект Карта способности. Теперь цепочка читается так: Игровой объект → Игровая карта → Карта способности — каждый уровень наследует всё, что выше, и добавляет своё. Предметы — экземпляры Игровой карты, способности — экземпляры Карты способности.
Поле Владелец является главным. Задайте его тип Выбор элемента — поле станет живой ссылкой на другой элемент, а не текстовым значением. И вот приятная деталь: выбор элемента можно ограничить в его настройках. Укажите в параметре поля ваш базовый объект Персонаж, и выборщик будет предлагать только персонажей.
Фирменная способность Дмитрия — Дымовая завеса (экземпляр Карты способности). Откройте её и задайте Владелец = Дмитрий. Всё: одна ссылка, хранящаяся ровно там, где ей место. У связи чёткое направление: карта → персонаж. Отсутствие владельца означает «может использовать любой» — разница между двумя видами карт становится данными, а не договорённостью из документа. Если вы решите что-то переименовать — это можно сделать в один клик.
Структуры и перечисления
Связи соединяют элементы. Но посмотрит на поле Роль у Хлои: оно хранит Защитник,но это верно ровно до следующей опечатки. Кто-то пишет защитник, другой помечает ЗащитниК, и для таблицы баланса и для движка это три разных значения, которые никогда не совпадут.
Та же неопределённость касается и структурированных данных. Таблица добычи каждого врага — это строка «что выпадает и с какой вероятностью». Опишите её свободным текстом — и каждый решит по-своему: один напишет Огнетушитель 30%, другой огнетушитель / часто, третий придумает новый формат. Одни и те же данные, три формы, сравнивать нечего.
Определим эти вещи один раз как настоящие типы: структуры, которые собирают несколько связанных полей в одну переиспользуемую форму, и перечисления, которые ограничивают поле фиксированным набором значений.
Дадим строке добычи настоящую форму. Нажмите Создать элемент, выберите Другое, тип Структура, назовите её Строка Таблицы добычи. Откроется выделенный редактор структуры. Нажмите Добавить поле: у каждого поля есть имя, тип и служебное имя. Задайте два поля, которые есть у любого выпадения: Карта — Тип: Выбор элемента (ссылка на Игровую карту) и Шанс выпадения — Тип: Число (насколько вероятно выпадение, 0.3 = 30%).
Перейдите к базовому объекту Враг, откройте блок Свойства и добавьте поле Таблица добычи — Тип: Структура, в параметрах выберите Строка Таблицы добычи. Одна строка — одно выпадение. Если с врага падает несколько карт — ему нужен список строк: прямо в настройках поля отметьте Множественный.
Теперь каждый экземпляр врага хранит список строк добычи: добавьте строку, выберите карту выбором элемента, впишите шанс.
Теперь словарь ролей. Нажмите Создать элемент, Другое, выберите тип Перечисление, назовите его Роль Персонажа. Откроется выделенный редактор перечисления с пустым списком значений. Нажмите Добавить элемент, чтобы добавить каждую допустимую роль: Защитник, Разведчик, Лидер. У каждой записи есть Имя (то, что вы видите) и служебное имя (стабильный ключ, сохраняемый в данных, — показан тегом). Редактор отклоняет повторяющиеся служебные имена, поэтому вы просто не можете завести Защитник и защитник как отдельные записи, и каждый элемент, использующий это перечисление, выбирает одно и то же значение.
Задаем полю тип Перечисление . На базовом объекте Персонаж найдите поле Роль, измените его тип на Перечисление и в параметрах выберите Роль Персонажа. Теперь в выпадающем списке: Защитник, Разведчик, Лидер. Выберите для Хлои Защитник, и в данных сохраняется ровно ключ Защитник. У перечисления есть режимы отображения — выпадающий список по умолчанию или радиокнопки для коротких словарей.
Граф, по которому можно задавать вопросы
Карты способностей указывают на своего Владельца-персонажа, враги содержат таблицы добычи, связанные с картами, и каждый элемент наследует от базового объекта. Всё связано, но вы не видите этого, пока на экране только один элемент.
Чтобы спросить «какие способности есть у Дмитрия?», пришлось бы открыть каждую карту способности и прочитать её Владельца. Ссылки знают — вы нет. Воспользуемся инструментом Граф чтобы исправить это.
Откройте любой элемент, нажмите три вертикальные точки в шапке элемента и выберите Показать связи. Откроется диалог Связи: ваш элемент в центре, а всё, что с ним связано, нарисовано вокруг одним графом. Базовый объект показан скруглённым прямоугольником, элементы, производные от него, располагаются вокруг. И главное: граф рисует связи в обе стороны — не только те элементы, на которые указывает ваша ссылка, но и все, кто указывает на вас.
Отвечаем на первый вопрос: «какие способности есть у Дмитрия?» Откройте Дмитрия, выберите Показать связи, и граф нарисует вокруг него карты способностей, чей Владелец указывает на него, включая фирменную Дымовую завесу. Одно чтение графа, и вся колода персонажа видна без дополнительного поля на самом персонаже.
Второй вопрос: «из кого выпадает игровая карта?». Откройте карту Огнетушитель, выберите Показать связи — граф встанет в центр на карту и нарисует каждого врага, чья таблица добычи ссылается на неё: Посетитель-ходячий с шансом 0.3, бронированный вариант с 0.5.
По графу можно перемещаться. Кликните по соседнему элементу — граф перецентрируется на нём, и его собственные связи расходятся вокруг. Кликайте дальше, чтобы проследить цепочку: Дмитрий → Дымовая завеса → базовая карта способности → базовая игровая карта → карта «Огнетушитель» → враг, из которого она выпадает... можно продолжать ещё долго. Кнопки Назад и Вперёд возвращают вас по следу, В центр мгновенно возвращает к фокусному элементу, а Предыдущие связи и Следующие связи листают соседей страницами (примерно по 25 на страницу). Подписи на рёбрах указывают тип связи — связь наследования между базовым объектом и его экземплярами подписана наслед.
Прежде чем менять Урон карты, выясните на какие элементы это повлияет? Кликните по карте в графе и вы увидите каждого персонажа, для которого это способность, и каждого врага, чья таблица добычи её указывает. Изменение, казавшееся числом в одном блоке свойств, на деле задевает шесть элементов. Работает и в обратную сторону: прежде чем править базовый объект, граф показывает каждый экземпляр, наследующий от него.
Итоги
На этой неделе разрозненные, слабо сформулированные идеи превратились в геймдизайн, которому можно задавать вопросы. Типы данных дали значениям реальное поведение: поле это число или строка, а не двусмысленный текст. Базовые объекты убрали повторения — враг, персонаж и карта получают схему из одного определения, а многоуровневое наследование помогает создавать новые элементы, основываясб на уже созданных. Ссылки заменили скопированные имена стабильными связями. Структуры и перечисления стандартизировали словарь: таблицы добычи стали списками типизированных строк, а роль персонажа — конкретным значением, а не произвольной строкой. И граф связей сделал каждую связь видимой: что умеет персонаж и кто роняет карту. От изменений, которые ломают проект, до дизайна, который упрощает дальнейшую разработку.
Дальше: баланс
Ваш геймдизайн структурирован, связан и стандартизирован. Далее игре нужны числа, а таблица баланса, которую вы держите отдельно,возможно уже разошлась с геймдизайном. На следующей неделе мы перенесём числа, чтобы баланс и дизайн больше не конфликтовали.
Попробуйте сами. Скачайте IMS Creators, откройте , и пройдите недельный путь на своём проекте: задайте типам поля, соберите схему в базовом объекте, свяжите карты с владельцами, зафиксируйте словарь структурой и перечислением и отслеживайте все изменения в графе связей.