Содержание

  1. Почему написана статья?

  2. Постановка проблемы

  3. Вариант решения проблемы

  4. Желаемый синтаксис будущего языка

  5. Разбор кода TextParser

  6. Разбор кода Quest

  7. Разбор класса DeliveryQuest

  8. Разбор кода ChatQuest

  9. Дополнительная команда Spawn

  10. Недостатки системы

  11. Преимущества системы

  12. Заключение


Почему написана статья?

Привет, 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. Они нужны для удобной работы со строками и их разбор не входит в тематику данной статьи.

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

GitHub

Жду пожеланий по улучшению системы :)

Спасибо за внимание!

Комментарии (27)


  1. Ka33yC
    09.07.2022 08:50
    +1

    Хмм, фактически, вы реализовали паттерны байткод и объект тип(в какой-то мере). Интересно, но для себя, возможно, изобрели велосипед


    1. Leo506 Автор
      09.07.2022 08:52
      +3

      Да, это действительно похоже на объект тип чем-то.

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


  1. Expurple
    09.07.2022 08:50
    +6

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

    Что бросилось в глаза:

    1. TextParser по факту не парсит язык) А просто определяет тип объекта и
      дальше дёргант класс квеста/спавна, чтобы он сам всё распарсил.
      Мне кажется, это неправильно, и квестам/спавнам совершенно не нужно
      отвечать за парсинг своей текстовой репрезентации. Не по SRP это, незачем
      им про неё знать вообще.

    2. А для чего вообще собственный язык и парсер, если можно взять условный
      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
          }
        ]
      }
      

    Интересно, почему решения именно такие.


    1. Leo506 Автор
      09.07.2022 08:53
      +1

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


      1. lgorSL
        09.07.2022 16:44

        есть ещё yaml - он без скобочек и может быть более удобным для ручного редактирования.


    1. Expurple
      09.07.2022 10:05

      UPD: невнимательно прокопипастил ChatQuest, должно быть так:

      {
        "name": "QuestName",
        "description": "QuestDesc",
        "type": "chat",
        "id": 0,
        "autoStart": false,
        "dialog": 2
      }
      


  1. Mingun
    09.07.2022 15:40

    Фактически вы изобрели систему, которая была в редакторе первого NWN. Спойлер — ей почти никто не пользовался :). Наверное потому, что в квестах лучше все же брать качеством, а не количеством.



    1. Leo506 Автор
      09.07.2022 17:48

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

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


      1. 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
                }
            ]
        }
        

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

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


        1. Mingun
          11.07.2022 12:45

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


          Единственный, как мне кажется, вариант — это мастер-генератор скрипта. Кстати, в NWN они тоже были и вот ими как раз и пользовались самые начинающие, пока не упирались в ограничения и не начали сами все писать с нуля (что, когда рука набита, быстрее).


  1. Sergey_Kovalenko
    09.07.2022 17:01
    +2

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


  1. arTk_ev
    09.07.2022 20:21

    Не очень удачная идея придумывать отдельный скриптовый язык, замучаетесь поддерживать и обучать дизайнеров. Если прям нужен отдельный язык - то удобнее использовать F# для его написания или писать на c# script.

    Но для квестов это излищне. Мы писали обычные иенумераторы для сценариев, чтобы была асинзронщина, а логику делали командами(and, or, not) и синхронизировали это с игровыми ивентами. Затем делался удобный нодовый редактор для геймдизов, который генерил уже чистый и читаемый с#, и наоборот.


    1. Leo506 Автор
      09.07.2022 20:51

      Ссылки на гитхаб нет? Я бы поизучал вашу систему, уж довольно интересно звучит


      1. 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-ивенты.

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


  1. GospodinKolhoznik
    10.07.2022 13:17

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

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

    Можно ли в вашем языке объединять квесты? Очевидно квесты это такие сущности, которые удобно было бы объединять. Причем различными способами: объединить два квеста в один, где неважен порядок исполнения квестов (комутативное сложение); где важен порядок (некомутативное сложение); объединить 2 квеста в один так, что игрок может для прохождения обойтись выполнением любого их них или выполнить оба (OR); выполнить только лишь один, а второй становится при этом недоступен (XOR). Возможно, что как то ещё. И самое главное, чтобы в результате выполнения таких объединенй получался новый квест, который тоже можно как-то объединять с любым другим квестом.

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


  1. 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) Желательный список проверок: любой обязательный квест должен быть выполним (начинаем и завершаем), уникальные предметы/объекты не дублируются и не пропадают, альтернативные квесты могут быть пропущены не ломая обязательные квесты, обнаружение циклических зависимостей квестов.


    1. Expurple
      11.07.2022 12:13

      Анализ зависимостей и выполнимости квестов звучит как задачка на графы. Видел недавно интересную статью как раз про квесты и графы.


      1. playermet
        11.07.2022 12:39

        Графы присутствуют, но это лишь часть решения. Существенная особенность в том, что в типичной РПГ игрок может брать/выполнять/фейлить/отменять много квестов сразу, причем в произвольном порядке. На небольших количествах квестов можно использовать полный перебор, но для универсального решения это не годится. Я предполагаю что нужно строить какое-то подобие таймлайна с диапазоном возможных инвариантов на точках или отрезках.


        1. Mingun
          11.07.2022 12:47

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


          1. playermet
            11.07.2022 13:20

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


            1. Expurple
              11.07.2022 13:33

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


              1. playermet
                11.07.2022 13:55

                А нужно ли проверять каждую перестановку?

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

                Потому что и игроку уследить за такими сложными и неочевидными взаимодействиями кучи квестов будет тяжело

                Игрок может брать ровно по одному квесту за раз. От этого 100! перестановок не пропадают.


                1. Expurple
                  11.07.2022 15:08
                  +1

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

                  Согласен. Но большинство этих параллельно доступных квестов обычно никаких особых последствий для персонажа/локации не имеют. Скажем, есть локация Х, есть 20 квестов, которым нужно просто побывать в ней в какой-то момент, и есть 1 важный сюжетный квест, после прохождения закрывающий к ней доступ. В этой модели порядок прохождения 20 "обычных" квестов для нас ни на что не влияет, поэтому значимых для нас вариантов прохождения не 21!, а всего лишь 1'048'576 (сумма сочетаний по k вовремя пройденных квестов из 20, для возможных k от 0 до 20).

                  Например ветки квестов гильгий в TES.

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

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


                  1. playermet
                    11.07.2022 18:18

                    Но большинство этих параллельно доступных квестов обычно никаких особых последствий для персонажа/локации не имеют.

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

                    Но они обычно слабо связаны с внешним миром.

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


            1. Mingun
              11.07.2022 13:57

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


              1. playermet
                11.07.2022 17:59

                все равно проверять 100!, никуда от этого не деться

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

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


                1. Mingun
                  11.07.2022 21:54

                  Все эти паттерны — это и есть зависимости. А у нас по условиям никаких зависимостей, ни явных, ни неявных.