Содержание
Почему написана статья?
Привет, habr! Это моя первая статья, поэтому будет хорошим тоном представиться. Я независимый разработчик мобильных видеоигр. Два года работаю на Unity и прогаю на C#. Выпустил одну инди-игрушку, которая хоть и не выстрелила, и не принесла денег, получила неплохие отзывы.
Но вот пришел тот день, когда я захотел попробовать себя на хабре и рассказать об интересной вещи, которую я сам смог придумать и реализовать.
Сегодня речь пойдет о квестовой системе в играх. Почему была выбрана эта тема? Потому что я не смог найти в сети достаточно подробной и исчерпывающей информации о ней, вследствие чего мне пришлось придумывать ее самостоятельно. Итак, давайте приступим.
Постановка проблемы
Давайте введем небольшую терминологию, чтобы мы разговаривали на одном языке.
Квест – некое задание, действие, которое игрок должен выполнить.
Почтовый квест – квест типа «подай(найди)-принеси», самый распространенный и скучный из всех видов квестов.
«Общительный квест» - квест типа «поговори с», представляет собой простое указание на персонажа, с которым игроку надо начать диалог.
Существует множество видов квестов. Из-за этого мы сталкиваемся с проблемой, что во время разработки игры те или иные виды квестов могут появляться в игре (особенно когда сюжет и нарратив не утверждены). Поэтому во время разработки нам нужна достаточно простая и расширяемая система квестов.
Проблема: если в игре должны быть квесты, то разработчикам нужна простая и расширяемая квестовая система
Вариант решения проблемы
Для решения поставленной проблемы я предлагаю создать собственный несложный язык – Язык Описания Квестов (далее ЯОК).
Данный язык будет состоять из инструкций по созданию квестов, а также из дополнительных инструкций для еще более удобной работы.
В данной статье мы создадим инструкции для создания почтового и общительного квеста, а также дополнительную инструкцию для спавна определенных объектов.
Ниже на картинке приведена будущая иерархия классов, которую мы создадим.
Горизонтальными линиями представлены отношения композиции. Вертикальными – наследования (в случае интерфейса – реализации)
Желаемый синтаксис будущего языка
Далее представлены инструкции языка, который мы будем создавать.
delivery from 0 to 1 dialogs 1 -1 name QuestName description QuestDesc
Здесь мы говорим, что хотим создать почтовый квест (инструкция delivery), что мы получаем квест от NPC с id 0 и сдаем квест NPC с id 1 (from 0 to 1), после чего указываем id соответствующих диалогов (dialogs 1 -1) и в конце название и описание квеста (name и description).
chat id 0 autoStart false dialog 2 name QuestName description QuestDesc
Здесь мы говорим, что создаем общительный квест (chat), что нужно поговорить с NPC с id 0 (id 0), указываем id диалога (dialog 2) и название с описанием. Параметр autoStart отвечает будет ли квест получен сразу после завершения предыдущего или будет получен после диалога с кем-либо.
spawn CutSceneTrigger pos 12,57 1 16,22 scene 0
Здесь мы говорим, что хотим создать на сцене некий объект (spawn), указываем что за объект (CutSceneTrigger), позицию объекта и специфические для этого объекта параметры. В данном случае – id кат-сцены.
Разбор кода TextParser
Итак, давайте приступим уже к реализации. Первое, что мы сделаем – создадим парсер нашего языка.
public class TextParser
{
string[] lines;
public void Parse(string content)
{
lines = content.Split(new char[] { '\n' }, System.StringSplitOptions.RemoveEmptyEntries);
}
public object CreateQuest()
{
object toReturn = null;
foreach (var line in lines)
{
List<string> words = line.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).ToList();
string type = words[0];
if (IsQuestCommand(words[0]))
{
type = "QuestLanguage." + type.ToTitleCase() + "Quest";
Type t = Type.GetType(type);
toReturn = Activator.CreateInstance(t, new object[] { line.Remove(0, words[0].Length) });
}
else
{
type = "QuestLanguage." + type.ToTitleCase();
Type t = Type.GetType(type);
Activator.CreateInstance(t, new object[] { line.Remove(0, words[0].Length) });
}
}
return toReturn;
}
private bool IsQuestCommand(string command)
{
return Type.GetType("QuestLanguage." + command.ToTitleCase() + "Quest") != null;
}
}
В методе Parse мы разбиваем исходную строку на массив строк по разделителю - \n
В методе CreateQuest мы пробегаемся по каждой строке в массиве lines, берем первое слово и проверяем является ли оно инструкцией по созданию квеста (проверка осуществляется в методе IsQuestCommand).
Если оно является такой инструкцией – то создаем экземпляр этого квеста и передаем в его конструктор остаток линии (строка 24). В конце метода мы вернем полученный экземпляр.
Если слово не является инструкцией по созданию квестов – это дополнительная команда. Просто создаем новый объект этой команды, конструктор которой сделает все остальное.
Разбор кода Quest
Идем дальше. На очереди базовый класс Qust
public class Quest
{
public static event System.Action<Quest> QuestPassedEvent;
public static event System.Action<Quest> QuestGotEvent;
public string QuestName { get; protected set; }
public string QuestDescription { get; protected set; }
public virtual void Pass() => QuestPassedEvent?.Invoke(this);
public virtual void Got() => QuestGotEvent?.Invoke(this);
public virtual void Start() { }
public virtual void Destroy() { }
public Quest(string parametrs)
{
List<string> parList = parametrs.GetWords();
var nameIndex = parList.FindIndex(s => s == "name");
var descIndex = parList.FindIndex(s => s == "description");
QuestName = "";
for (int i = nameIndex + 1; i < descIndex; i++)
QuestName += parList[i] + " ";
QuestDescription = "";
for (int i = descIndex + 1; i < parList.Count; i++)
QuestDescription += parList[i] + " ";
}
}
Данный класс состоит из двух статических событий, сообщающих о получении и сдачи квеста, методов Pass и Got (что они делают, думаю, понятно) и методов Start и Destroy. Start и Destroy нужны для того, чтобы квест мог проинициализировать какие-то свои переменные (Start) или прибрать за собой (Destroy).
Конструктор принимает строку и ищет название и описание квеста, после чего инициализирует соответствующие поля.
Разбор класса DeliveryQuest
Теперь посмотрим, как создать почтовый квест
public class DeliveryQuest : Quest
{
private int fromID;
private int toID;
private StartDialogComponent sender;
private StartDialogComponent target;
public DeliveryQuest(string parametrs) : base(parametrs)
{
ParsingUtility utility = new ParsingUtility(parametrs);
fromID = utility.GetValue<int>("from");
toID = utility.GetValue<int>("to");
var dialogIDs = utility.GetValues<string>("dialogs", 2);
sender = NPCManagement.NPCManager.GetNPC(fromID).gameObject.AddComponent<StartDialogComponent>();
sender.SetDialogID(dialogIDs[0]);
target = NPCManagement.NPCManager.GetNPC(toID).gameObject.AddComponent<StartDialogComponent>();
target.SetDialogID(dialogIDs[1]);
DialogSystem.DialogText.DialogActionEvent += GotQuest;
}
private void GotQuest(string id, string action)
{
if (action == "GotQuest")
Got();
}
private void PassQuest(string id, string action)
{
if (action != "PassQuest")
return;
Pass();
}
public override void Destroy()
{
DialogSystem.DialogText.DialogActionEvent -= GotQuest;
}
public override void Got()
{
base.Got();
GameObject.Destroy(sender);
DialogSystem.DialogText.DialogActionEvent -= GotQuest;
DialogSystem.DialogText.DialogActionEvent += PassQuest;
}
public override void Pass()
{
base.Pass();
GameObject.Destroy(target);
}
}
Данный класс в своем конструкторе принимает строку, находит id NPC, добавляет им компоненты StartDialogComponent (он просто может начать диалог с определенным id) и определяет условия получения и сдачи квеста.
Здесь как раз и начинают проявляться преимущества данной системы. Вы можете абсолютно самостоятельно определить, когда и как будет происходить получение и сдача квеста, а также другие дополнительные действия (как, например, добавление и удаление компонентов). В моем проекте таким условием является событие от диалоговой системы.
Разбор кода ChatQuest
Теперь очередь общительного квеста
public class ChatQuest : Quest
{
private int npcID;
private StartDialogComponent dialogComponent;
private string dialogID;
public ChatQuest(string parametr) : base(parametr)
{
ParsingUtility utility = new ParsingUtility(parametr);
npcID = utility.GetValue<int>("id");
bool autoStart = utility.GetValue<bool>("autoStart");
dialogID = utility.GetValue<string>("dialog");
if (autoStart)
Got();
else
DialogSystem.DialogText.DialogActionEvent += GotQuest;
}
private void PassQuest(string id, string action)
{
if (action == "PassQuest")
Pass();
}
private void GotQuest(string id, string action)
{
if (action != "GotQuest")
return;
Got();
}
public override void Destroy()
{
DialogSystem.DialogText.DialogActionEvent -= PassQuest;
}
public override void Got()
{
base.Got();
dialogComponent = NPCManagement.NPCManager.GetNPC(npcID).gameObject.AddComponent<StartDialogComponent>();
dialogComponent.SetDialogID(dialogID);
DialogSystem.DialogText.DialogActionEvent -= GotQuest;
DialogSystem.DialogText.DialogActionEvent += PassQuest;
}
public override void Pass()
{
GameObject.Destroy(dialogComponent);
base.Pass();
}
}
Данный класс похож на DeliveryQuest с тем отличием, что здесь используется только один компонент диалога и есть параметра автостарта, о котором я говорил выше. Все остальное такое же. Определяем, когда и как получается и сдается квест и выполняем соответствующие действия.
Дополнительная команда Spawn
Как уже говорилось ранее, команда spawn нужна чтобы спавнить на сцене определенные объекты. Например, в моем проекте при помощи этой команды я спавню триггер кат-сцены. Таким образом мы получаем большую игровую вариативность, за что геймдизайнеры нам только спасибо скажут.
public class Spawn
{
public Spawn(string parametrs)
{
List<string> parList = parametrs.GetWords();
string typeName = parList[0];
Type type = Type.GetType("QuestLanguage." + typeName + "Spawner");
var posIndex = parList.FindIndex(s => s == "pos");
Debug.Log("X: " + parList[posIndex + 1]);
float x = float.Parse(parList[posIndex + 1]);
float y = float.Parse(parList[posIndex + 2]);
float z = float.Parse(parList[posIndex + 3]);
Vector3 pos = new Vector3(x, y, z);
string str = "";
for (int i = posIndex + 4; i < parList.Count; i++)
str += parList[i] + " ";
ISpawner spawner = Activator.CreateInstance(type) as ISpawner;
spawner.Spawn(str, pos);
}
}
Конструктор команды принимает строку, находит название объекта, который надо создать, после чего создает специализированный спавнер для этого объекта, которому потом передает остаток строки с доп параметрами и позицию спавна.
Интерфейс ISpawner имеет следующий вид.
public interface ISpawner
{
void Spawn(string parametrs, Vector3 pos);
}
CutSceneTriggerSpawner реализует данный интерфейс. Он парсит строку с доп параметрами (id кат-сцены), после чего создает триггер в заданной позиции. Когда игрок затронет триггер – начнется необходимая кат-сцена.
Недостатки системы
Для ее поддержки и расширения необходим программист. Иными словами, геймдизайнер не сможет по своей прихоти создать новый вид квестов самостоятельно, ему обязательно нужна помощь программиста (если он сам не программист)
Довольно большое кол-во файлов и классов, что может потом запутать
Сложность синтаксиса. Поначалу будет сложно запомнить, как правильно писать каждую инструкцию. А если они будут потом еще добавляться (что и подразумевает данная система) выучить их все будет еще сложнее
Преимущества системы
Расширяемость. Для добавления новых инструкций не нужно изменять существующий код. Просто пишешь новый класс квеста
Изолированность. Квесты полностью изолированы от остальной части игры (в том плане, что никто не знает об квестах)
Большая игровая вариативность. Благодаря ЯОК мы можем создать квест, добавить после этого кат-сцену или переход на какую-либо другую сцену, что даст геймдизайнеру простор для творчества
Заключение
Вот такой вышла моя первая статья. Внимательный читатель мог обратить внимание на не рассмотренные, но используемые методы и классы ParsingUtility, GetWords и ToTitleCase. Они нужны для удобной работы со строками и их разбор не входит в тематику данной статьи.
Если читатель хочет побольше познакомиться с системой, то вот ссылка на проект, в котором она сейчас используется.
Жду пожеланий по улучшению системы :)
Спасибо за внимание!
Комментарии (27)
Expurple
09.07.2022 08:50+6Дисклеймер: я далёк от геймдева, просто увидел заголовок и заинтересовался
идеей какой-то обобщённой системы для квестов, которую можно втыкать в
различные игры как библиотечку.Что бросилось в глаза:
TextParser
по факту не парсит язык) А просто определяет тип объекта и
дальше дёргант класс квеста/спавна, чтобы он сам всё распарсил.
Мне кажется, это неправильно, и квестам/спавнам совершенно не нужно
отвечать за парсинг своей текстовой репрезентации. Не по SRP это, незачем
им про неё знать вообще.-
А для чего вообще собственный язык и парсер, если можно взять условный
JSON/YAML/XML и готовую библиотеку, которая его распарсит?
В языке из статьи например есть баг, что название квеста не может
содержать слово "description" (парсер, увидев его, подумает, что название
закончилось и надо переходить к описанию).
В JSON примеры из статьи выглядели бы так:{ "quests": [ { "name": "QuestName", "description": "QuestDesc", "type": "delivery", "from": 0, "to": 1, "dialogs": [1, -1] }, { "name": "QuestName", "description": "QuestDesc", "type": "chat", "id": 0, "to": 1, "dialogs": [1, -1] } ], "spawns": [ { "type": "CutSceneTrigger", "pos": [12.57, 1, 16.22], "scene": 0 } ] }
Интересно, почему решения именно такие.
Expurple
09.07.2022 10:05UPD: невнимательно прокопипастил ChatQuest, должно быть так:
{ "name": "QuestName", "description": "QuestDesc", "type": "chat", "id": 0, "autoStart": false, "dialog": 2 }
Mingun
09.07.2022 15:40Фактически вы изобрели систему, которая была в редакторе первого NWN. Спойлер — ей почти никто не пользовался :). Наверное потому, что в квестах лучше все же брать качеством, а не количеством.
Leo506 Автор
09.07.2022 17:48Я попытался сделать систему, где пришлось бы использовать минимум жёстко заскриптованных моментов, но при этом оставалось достаточное пространство для творчество.
Не очень приятно, когда ты несколько часов скриптовал какой-то квест, а потом оказывается, что его надо полностью переделать. С этой системой подобные ситуации легче пережить (я надеюсь)
Expurple
11.07.2022 12:05Не вижу, как тут решается проблема с переписыванием. Что код при изменении квеста надо было переписывать, что этот файлик со скриптами надо будет.
Но в целом согласен, что такой отдельный декларативный файлик удобнее, чем хардкод. Во-первых, быстрое редактирование без перекомпиляции проекта. Во-вторых, в теории им теперь действительно могут заниматься геймдизайнеры/модеры, не являющиеся программистами. В теории даже в блокноте. Знать им потребуется только синтаксис JSON (или другой формат, который вы выберете) и набор сущностей/полей, которые в нём можно объявлять. Его лучше задокументировать. В идеале - составить JSON Schema, тогда и валидация будет автоматизирована, и ИДЕшка при редактировании будет сама все поля подсказывать.
геймдизайнер не сможет по своей прихоти создать новый вид квестов самостоятельно
Эту проблему наверное можно решить, если предоставить скриптам более низкоуровневое API. Не готовые захардкоженные типы квестов, а набор всех доступных действий в игре: "разговор", "убийство" и т.п. Тогда квесты становятся просто контейнерами действий, плюс метаданные с name и description. Условный DeliveryQuest из статьи становится просто Quest, содержащим два Разговора:
{ "name": "QuestName", "description": "QuestDesc", "actions": [ { "type": "Conversation", "npc": 0, "dialog": 1 }, { "type": "Conversation", "npc": 1, "dialog": -1 } ] }
И геймдизайнер может комбинировать действия как угодно в нужные ему квесты.
Этот мой пример с массивом действий всё ещё не особо гибкий: он определяет строгую последовательность действий, без альтернативных или опциональных шагов. Также он никак не описывает зависимости между разными квестами. Чтобы сделать всё правильно, есть смысл присмотреться к этому комментарию.
Mingun
11.07.2022 12:45В итоге придем к скриптовому языку и необходимостью в программисте, чтобы на нем писать. Собственно, пример NWN и показывает, что что-либо годное там делалось только скриптованием, а не мастером квестов. Без скриптов геймдизайнер всегда будет загнан в рамки и простые инструменты быстро станут слишком простыми для его задач.
Единственный, как мне кажется, вариант — это мастер-генератор скрипта. Кстати, в NWN они тоже были и вот ими как раз и пользовались самые начинающие, пока не упирались в ограничения и не начали сами все писать с нуля (что, когда рука набита, быстрее).
Sergey_Kovalenko
09.07.2022 17:01+2Поздравляю с первой публикацией и с тем, что у вас получилось создать некую формальную вселенную. Мне часто приходится делать похожие вещи (формальные вселенные), но не как программисту, а как математику. В математике язык - это прежде всего круг обсуждаемых понятий и только потом - некая грамматика, способная эти понятия обозначать. В качестве совета порекомендую вам разделить описание вашего мира и его программную реализацию. Попробуйте в следующий раз сначала описать, какие объекты есть в вашем мире, какие между ними возможны отношения, а уже после изложите, каким образом вы моделируете эти объекты и отношения при помощи программных средств - возможно такой стиль повествование будет прозрачнее передавать его смысл.
Желаю успехов.
arTk_ev
09.07.2022 20:21Не очень удачная идея придумывать отдельный скриптовый язык, замучаетесь поддерживать и обучать дизайнеров. Если прям нужен отдельный язык - то удобнее использовать F# для его написания или писать на c# script.
Но для квестов это излищне. Мы писали обычные иенумераторы для сценариев, чтобы была асинзронщина, а логику делали командами(and, or, not) и синхронизировали это с игровыми ивентами. Затем делался удобный нодовый редактор для геймдизов, который генерил уже чистый и читаемый с#, и наоборот.
Leo506 Автор
09.07.2022 20:51Ссылки на гитхаб нет? Я бы поизучал вашу систему, уж довольно интересно звучит
arTk_ev
09.07.2022 23:21Код не могу дать, NDA . Прототип ранний могу:
https://github.com/svLimones/SOLIDex/blob/master/ScenarioService.cs - сервис
https://github.com/svLimones/SOLIDex/blob/master/OrScenarioStep.cs - команда логики
https://github.com/svLimones/SOLIDex/blob/master/LibraryOpenDialogScenario.cs - пример сценария
Для этого нужна SOLID-архитектура и что-то похожее на RX-ивенты.
Нодовый редактор можно найти готовый, тулза больше всего по времени требует, но ничего сложного и можно ее сделать потом.
GospodinKolhoznik
10.07.2022 13:17В видео лекциях SICP от 1986 года автор говорит примерно следующее, воспроизведу по памяти (простите, что взял на себя смелость префразировать, что то не могу быстро найти где он это говорить, а пересматривать многочасовые лекции нет времени):
"Если вы создаёте новый язык, подумайте о том, является ли ваш язык замкнутым в математическом смысле. Это очень важное свойство языка. А именно, если язык оперирует некоторыми сущностями, то можно ли эти сущности каким либо образом объединять (сумма, произведение, конкатенация, композиция, комбинация и т.п.) ? А если можно, то можно ли результатом такого объединение пользоваться как изначальной сущностью? Удобно, когда в языке можно сделать массив массивов, и очень неудобно когда нельзя, а ведь в некоторых языках нельзя."
Можно ли в вашем языке объединять квесты? Очевидно квесты это такие сущности, которые удобно было бы объединять. Причем различными способами: объединить два квеста в один, где неважен порядок исполнения квестов (комутативное сложение); где важен порядок (некомутативное сложение); объединить 2 квеста в один так, что игрок может для прохождения обойтись выполнением любого их них или выполнить оба (OR); выполнить только лишь один, а второй становится при этом недоступен (XOR). Возможно, что как то ещё. И самое главное, чтобы в результате выполнения таких объединенй получался новый квест, который тоже можно как-то объединять с любым другим квестом.
Ну и конечно нужен некоторый контракт - интерфейс обобщенного квеста, чтобы квестом можно было назвать всё, что удовлетворяет соглашению этого интерфейса, а функции оперирующие с квестами (в том числе и объединяющие несколько квестов в один) должны оперировать лишь с интерфейсом обобщенного квеста.
playermet
10.07.2022 13:56+1Хотелось бы увидеть дальнейшее развитие идеи. Причем интересует не столько способ хранения и редатирования (ноды, DSL, json, и т.д.), сколько формализация и возможность статический анализа. Например способность системы доказать, что квест сломать невозможно, или что наоборот его невозможно выполнить.
Когда-то я пытался размышлять на эту тему и вывел несколько соображений.
1) Зависимость между квестами должна задаваться обратно. Т.е. не квест1 после выполнения активирует квест2, а квест2 должен иметь в зависимостях квест1. Таким образом при добавлении новых квестов не нужно будет трогать старые, и не будет проблем если квест зависит от выполения любого из нескольких предыдущих. В принципе весь SOLID имеет смысл распространять на квесты. Проверка квестов при этом должна выполняться в прямом порядка, т.к. возможность завершения квеста не имеет значения если он не может быть даже начат.
2) Основные элементы квестовой системы для большинства РПГ это: Quest, Entity, Resource, и Location. В качестве Entity могут быть персонажи, сундуки, интерактивные объекты и т.д. В качестве Resource выступают валюты, предметы, навыки и т.д. Между Location есть переходы, которые могут быть открыты или закрыты. Entity могут перемещаться между Location, Resource могут перемещаться между Entity. У Entity могут быть разные состояния. Сами Quest можно сделать по разному, от простой пары предусловие-последствие до целого графа, где каждый узел может быть либо условием (включая всякие all/any), либо изменением состояния мира.
3) В зависимости от необходимости квесты могут не завязываться напрямую на другие квесты. Если квест требует доступности локации или предмета, имеет смысл делать предусловия напрямую на них. Однако иногда это может быть нежелательно, например если персонаж в ходе квеста лишь краем пробегает по какой-то локации, и после квеста она сразу недоступна.
4) Изменения состояния мира должны быть как можно более атомарными и естественными. Т.е. вместо "выключить объект персонажа в сцене 1, включить объект персонажа в сцене 2", должно быть "переместить персонажа в сцену 2". Таким образом ситуации когда персонажа и там и там показывают будут исключены.
5) Обобщенная проверка на валидность квестов звучит так: "Зависимости квеста не могут стать навсегда недоступными до его завершения.". Если второй квест ломает первый, то он либо не должен быть способен начат раньше чем будет закончен первый квест, либо после него должен быть выполним третий квест который вернет выполнимость первому.
6) Желательный список проверок: любой обязательный квест должен быть выполним (начинаем и завершаем), уникальные предметы/объекты не дублируются и не пропадают, альтернативные квесты могут быть пропущены не ломая обязательные квесты, обнаружение циклических зависимостей квестов.
Expurple
11.07.2022 12:13Анализ зависимостей и выполнимости квестов звучит как задачка на графы. Видел недавно интересную статью как раз про квесты и графы.
playermet
11.07.2022 12:39Графы присутствуют, но это лишь часть решения. Существенная особенность в том, что в типичной РПГ игрок может брать/выполнять/фейлить/отменять много квестов сразу, причем в произвольном порядке. На небольших количествах квестов можно использовать полный перебор, но для универсального решения это не годится. Я предполагаю что нужно строить какое-то подобие таймлайна с диапазоном возможных инвариантов на точках или отрезках.
Mingun
11.07.2022 12:47Думаю, это типичный анализ мертвого кода. Сомневаюсь, что у компилятора меньше переменных для определения того, нужно выкидывать ветку или нет
playermet
11.07.2022 13:20У компилятора есть лимиты глубины анализа. Если доказательство слишком сложное, он просто от него оказывается. При этом есть четкий порядок выполнения, пусть и содержащий ветления и циклы, позволяющий применить к нему логику Хоара. В ситуации когда есть 100 квестов, которые могут быть выполнены в любом порядке, есть 100! возможных перестановок, каждая из которых должна быть верна. Проверить их таким же образом не получится.
Expurple
11.07.2022 13:33А нужно ли проверять каждую перестановку? По сути нас волнуют только перестановки внутри наборов квестов, у которых есть общий стейт: использование одного и того же персонажа/предмета/локации, захардкоженное влияние на доступность другого квеста, и т.п. И то, только тогда, когда у хотя бы одного квеста есть сайд-эффекты и этот стейт мутируется: персонаж погибает, квест блокируется и т.п. Наверное, как-то можно отфильтровывать такие ситуации и сильно уменьшить пространство для перебора. А если даже так всё слишком запутанно, слишком много тестов влияют друг на друга и слишком долго всё просчитывается, то может это уже повод дизайнеру задуматься. Потому что и игроку уследить за такими сложными и неочевидными взаимодействиями кучи квестов будет тяжело, и самому дизайнеру поддерживать, чтобы дальше ничего не сломалось.
playermet
11.07.2022 13:55А нужно ли проверять каждую перестановку?
Нужно, но невозможно. Поэтому нужно придумать способ проверки, при котором не будет необходимости в полном переборе. Общие объекты в квестах встречаются регулярно - например одинаковые персонажи или локации. Например ветки квестов гильгий в TES.
Потому что и игроку уследить за такими сложными и неочевидными взаимодействиями кучи квестов будет тяжело
Игрок может брать ровно по одному квесту за раз. От этого 100! перестановок не пропадают.
Expurple
11.07.2022 15:08+1Общие объекты в квестах встречаются регулярно - например одинаковые персонажи или локации.
Согласен. Но большинство этих параллельно доступных квестов обычно никаких особых последствий для персонажа/локации не имеют. Скажем, есть локация Х, есть 20 квестов, которым нужно просто побывать в ней в какой-то момент, и есть 1 важный сюжетный квест, после прохождения закрывающий к ней доступ. В этой модели порядок прохождения 20 "обычных" квестов для нас ни на что не влияет, поэтому значимых для нас вариантов прохождения не 21!, а всего лишь 1'048'576 (сумма сочетаний по k вовремя пройденных квестов из 20, для возможных k от 0 до 20).
Например ветки квестов гильгий в TES.
Верно. Но они обычно слабо связаны с внешним миром. Например, в Обливионе ветки гильдий магов и воров не связаны, благодаря чему возможен мем, что архимаг выкрадывает посох сам у себя. Или в Скайриме тёмное братство, убивающее императора, никак не влияет на ход гражданской войны.
А сложность этой изолированной ветки внутри гильдии - см. первый абзац. Обычно на доступность контента влияет только прогресс по основному сюжету гильдии. А условные 10 одновременно доступных побочных квестов друг на друга не влияют.
playermet
11.07.2022 18:18Но большинство этих параллельно доступных квестов обычно никаких особых последствий для персонажа/локации не имеют.
Обычно не имеют. Но интересно разработать систему которая смогла бы распознавать потенциальные баги даже если имеют. Зависимости при этом могут быть неявными и опосредованными. Например необходимость передать персонажу предмет получаемый за квест со собором шкур падающих из монстров в локации доступ в которую может быть невозможен потому что игрок по сюжету другого квеста застрял на другом острове.
Но они обычно слабо связаны с внешним миром.
Это как раз примеры того, что могло бы быть обнаружено и исправлено. Разные ветки квестов скорее всего делали разные группы людей, а одной только внимательностью синхронизировать квесты в проекте такого размера нереально.
Mingun
11.07.2022 13:57Если у вас 100 независимых квестов (т.е. в них участвуют разные персонажи, на разных локациях, нужны разные предметы и зависят от разных параметров игрока), то хочешь-не хочешь, а все равно проверять
100!
, никуда от этого не деться. Но обычно, как и в программе, зависимости естьplayermet
11.07.2022 17:59все равно проверять
100!
, никуда от этого не детьсяВ том то и дело, что есть паттерны, которые при распознании позволяют сокращать число вариантов. Например нахождение неявных зависимостей между квестами, перекрытие инвариантов (один более строгая версия другого), схлопывание инвариантов (альтернативные ветви дающие аналогичный результат) и т.д. Нужно только выявить все эти паттерные и свести в единую систему.
Причем речь не только о алгоритме анализатора, но и об дизайне самой квестовой системы. Она не обязана быть Тьюринг-полной. В самом строгом случае можно даже принудить все цепочки квестов возвращать мир к изначальному состоянию, а видимые для игрока побочки подпереть хитрым костылем. Но намного интересней найти решение для как можно более свободной системы.
Mingun
11.07.2022 21:54Все эти паттерны — это и есть зависимости. А у нас по условиям никаких зависимостей, ни явных, ни неявных.
Ka33yC
Хмм, фактически, вы реализовали паттерны байткод и объект тип(в какой-то мере). Интересно, но для себя, возможно, изобрели велосипед
Leo506 Автор
Да, это действительно похоже на объект тип чем-то.
Мы все, иногда, пишем велосипеды. Это лучший способ в чем-то разобраться