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

Логику уровней в моей игре собирает ИИ-агент. Поэтому связи между нодами чинятся сами

Признаюсь сразу, ради чего всё затевалось. Сигнально-нодовую систему в Hexardia я строил не как удобство для дизайнера-человека. Я строил её так, чтобы игровую логику - квесты, диалоги, гейты, ловушки, волны спавна - мог собирать ИИ-агент. Через структурированный JSON, не открывая код и не трогая C#.

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

Как это устроено, в двух абзацах

Каждая нода - компонент на GameObject-е. Видов три. Триггеры - точки входа: старт сцены, нажатие клавиши, вход в зону, смена хода фракции. Условия - развилки: проверяют набор условий и пускают сигнал либо в «Passed», либо в «Failed». Действия - собственно логика: заспавнить юнита, открыть дверь, показать реплику, выдать награду, сохранить прогресс.

Соединяются они сигналами. У триггера есть выходной сигнал (например «Enter»), ты подписываешь на него вход действия - связь готова. Выход любой ноды заводится на вход любой другой, поэтому из них собираются цепочки. Граф в редакторе раскладывается по иерархии: папка на глубине 1 становится визуальной рамкой-группой, ноды внутри - её содержимым. Порядок в сцене и есть раскладка графа.

Зачем она - на примерах цепочек

Проще показать. Вот реальные связки.

Диалог NPC. Триггер жизненного цикла (Started) -> OpenDialogueAction -> ShowSpeechAction с репликой -> по сигналу «Next» -> CloseDialogueAction.

Квест-гейт с развилкой. Триггер -> ConditionsNode (есть ли ключ) -> «Passed» открывает дверь, «Failed» кидает уведомление «нужен ключ». Одна нода-условие разветвляет весь сценарий.

Ловушка. AreaContactTrigger (Enter) -> TrapLogic -> DamageUnitAction. Наступил на плитку - получил урон. Кода ловушки нет, только три ноды и провод.

Волна врагов. Триггер старта -> TimerNode в режиме LoopInfinite -> на каждый «Tick» -> SpawnUnitAction. Интервал - поле в инспекторе.

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

А теперь то, ради чего всё строилось: это собирает агент

Человек кликает ноды мышкой. Агент делает то же самое, но пачкой операций в JSON, через BatchRunner. Вот целый диалоговый сценарий, собранный одной командой - создать группу, создать ноды, соединить их сигналами, проставить поля, провалидировать, сохранить:

{"operations": [
{"op":"create_group","name":"Dialogue_Greeting","parent":"[SCRIPTING]"},
{"op":"create_node","type":"LifecycleTrigger","name":"OnStart","parent":"[SCRIPTING]/Dialogue_Greeting"},
{"op":"create_node","type":"ShowSpeechAction","name":"Greeting","parent":"[SCRIPTING]/Dialogue_Greeting"},
{"op":"wire","listener":".../Greeting","source":".../OnStart","signal":"Started"},
{"op":"set_field","path":".../Greeting","field":"TextKey","value":"npc_greeting"},
{"op":"validate","path":"[SCRIPTING]/Dialogue_Greeting"},
{"op":"save"}
]}

Операции простые и полные: create_group, create_node, wire/unwire, set_field, validate, save. Отдельно важен шаг validate - он проверяет граф на осиротевшие ноды и битые связи. То есть агент не просто накидал логику, а тут же может убедиться, что она согласована. Никакого доступа к коду, никакого разбора C#: чтобы выложить диалог, квест-гейт или таймер-спавн, машине хватает этого словаря операций.

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

продолжение в следующем посте...

3
1