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

И мы задумались о визуальном подходе. В этой статье я расскажу о нашем знакомстве с средством визуального скриптинга для Unity — "uScript", о его возможностях и расскажу о нашем опыте.
Да, на скрине выше — реальные скрипт и схема.
Итак, давайте для начала посмотрим на то, что было. Ниже приведен реальный скрипт, создающий двух персонажей на экране, рисующий простенький диалог, дающий юзеру выбор из 2х вариантов и ветвящийся в этом месте.
В игре это выглядит так:

В принципе, в скрипте выше нет ничего страшного. Но представьте, что у вас не 1 ветвение, а два. Представьте, что вам нужно проверять какие-то игровые параметры и ветвить скрипт исходя из них. Это очень быстро может стать ненаглядным.
Именно в такой момент нам остро захотелось визуализации.
Посмотрев несколько плагинов для юнити, мы остановились на uScript. Он очень мощный, гибкий, и при этом просто расширяемый. Кроме того, он создает минимальный impact по быстродействию, т.к. на этапе сохранения схем сразу же компилит их в C#, т.е. для Unity скрипт собранный в таком редакторе не очень отличается от скрипта, написанного руками на шарпах.
Давайте сразу приведу скрин того, во что превратился вышеприведенный LUA-скрипт. (картинка кликабельна)

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

А на схеме — так:

И сразу видно, что произойдет при выборе ответа №1 и ответа №2. А если таких ветвлений будет больше — то тем более схема не потеряет наглядности.
Давайте быстро пробежимся по тому, из чего состоит схема. Собственно, основные модули (в терминологии uScript они называются «nodes») — это событие (с него обычно начинается скрипт или цепочка), action и переменные.

У action'он есть вход (обычно 1) и выход(ы). Например, у самого простого действия 1 вход и 1 выход. А у какого-нить блока условия — уже будет два выхода, например.
Снизу блока подключаются переменные. Треугольник означает, что в переменную будет произведена запись (output).
Например, в этом примере мы создаем персонажа (с помощью блока «Create char»), а потом выставляем ему же зеркальность в «true» (с помощью блока «Mirror»):

Кстати, все переменные могут иметь названия (в нашем случае «с1»). И все переменные одного типа с одинаковым названием будут синхронизированы в пределах одного скрипта (схемы). Т.е. пример выше совершенно идентичен такому:

Сделано это чтобы избавить вас от необходимости тянуть связи через два экрана.
Кроме того, если поставить галочку «expose to Unity», выбранная переменная станет public и будет видна другим скриптам (как визуальным, так и вашим рукописным). Массивы так же поддерживаются.
Все модули, которые вы видите на схеме — самописные. И были написаны за 1 вечер. Давайте посмотрим на их код.
Рассмотрим сначала что-нибудь очень простое. Например, action, который называется «Start fight». Он начинает бой (по сути, вызывает метод игровой логики) и принимает два параметра — айдишник боя и айдишник соперника.

Код для него:
Просто? Очень.
А теперь давайте усложним. Допустим, мы хотим проиграть какую-либо анимацию. И хотим иметь два выхода. Один — сразу, а второй, который запустится только когда анимация проиграется до конца.

Справа вы можете видеть блок с конфигурацией блока, куда вы вбиваете значения. У блока 3 входных параметра — CharacterGfx (непосредственно персонаж, которому мы проигрываем анимацию), Animation (название анимации) и Mirror (необходимость зеркаленья). И у блока есть два выхода: Out (выход сразу же) и Finished (только когда анимация закончится).
При этом переменная «Mirror» является энумератором с параметрами «да», «нет» и «не менять», которая представляется в виде dropdown-списка в окне свойств.
Код особо сложнее не стал:
Еще момент. Во всех блоках выше выход (Out) вызывался сразу же после выполнения кода блока.
А что если мы хотим сделать асинхронный action? Например, загрузку сцены. И чтобы выполнение нашего визуального скрипта приостановилось до того момента, пока асинхронно не прогрузится сцена.
Делается это так же просто. Вместо строчки
А далее в коде в тот момент, когда вы будете готовы продолжить выполнение скрипта, вам нужно вызвать этот хэндлер ровно так же, как было с Finished в предыдущем примере:
Все, что я привел выше — было написано нами, чтобы собрать все, что нужно в одно удобное место. Но это зачастую не обязательно. В uScript есть такая вещь, которая называется «reflection». На деле это означает, что uScript автоматически сканирует вашу сцену и вытягивает из нее все объекты, а так же их публичные методы и параметры, до которых может дотянуться. И предоставляет к ним доступ.
Например, вот так выглядит блок-reflection на метод GetComponent() камеры на сцене:

(внизу вы можете видеть блок «properties», где задаются все параметры метода)
Тулза нам однозначно понравилась и мы будем юзать ее дальше. Вообще, некоторые люди умудряются с помощью нее писать целые игры, но это уже чересчур.
Насколько глубоко мы сможем ее заюзать пока не знаем. Например, еще не решили, переписывать ли логику триггеров квестов с нашей lua-ориентированной на визуальную.
Но вот для скриптования кат-сцен и диалогов будем юзать однозначно.
Из минусов могу выделить только один (который является следствием плюса) — как я писал выше, uScript преобразует визуальные схемы в C# код. А следовательно каждая модификация схемы потребует перекомпиляции проекта.
В остальном — очень советую присмотреться к этому инструменту, если вы хотите скриптовать подобную логику. Так же, насколько я знаю, этот инструмент активно используют для написания AI.
Кстати, если вам нужна именно для скриптования поведения и взаимодействия объектов на сцене (например, триггеры на столкновения и т.п.), то присмотритесь к PlayMaker. Он больше ориентирован именно на событийную модель.
Все статьи серии:

И мы задумались о визуальном подходе. В этой статье я расскажу о нашем знакомстве с средством визуального скриптинга для Unity — "uScript", о его возможностях и расскажу о нашем опыте.
Да, на скрине выше — реальные скрипт и схема.
Введение.
Итак, давайте для начала посмотрим на то, что было. Ниже приведен реальный скрипт, создающий двух персонажей на экране, рисующий простенький диалог, дающий юзеру выбор из 2х вариантов и ветвящийся в этом месте.
Исходник LUA-скрипта
vhs.HUD(0)
vhs.SwitchZone("street")
local c1 = CharacterGfx()
c1.create("c1", "char_big")
c1.mirror(0)
c1.setpos("n_2")
c1.animate("f_idle")
local c2 = CharacterGfx()
c2.create("c2", "char_black")
c2.mirror(1)
c2.setpos("n_3")
c2.animate("f_idle")
c2.preset("opp_lmb")
char.animate("idle")
char.mirror(1)
char.setpos("n_1")
c1.say("I need your clothes, your boots and your motocycle")
c1.wait_bubble()
c2.say("Yep!")
c2.wait_bubble()
char.animate("f_idle")
char.mirror(0)
vhs.ShowMultiAnswer("Try to catch me! (run away)", "No way! (start fight)", "")
switch_answer {
case 1:
vhs.BlackScreen("You are not fast enough to run away. So Have to fight!")
vhs.StartFight(77,7)
end,
case 2:
vhs.StartFight(77,7)
end,
}
В игре это выглядит так:

В принципе, в скрипте выше нет ничего страшного. Но представьте, что у вас не 1 ветвение, а два. Представьте, что вам нужно проверять какие-то игровые параметры и ветвить скрипт исходя из них. Это очень быстро может стать ненаглядным.
Именно в такой момент нам остро захотелось визуализации.
Посмотрев несколько плагинов для юнити, мы остановились на uScript. Он очень мощный, гибкий, и при этом просто расширяемый. Кроме того, он создает минимальный impact по быстродействию, т.к. на этапе сохранения схем сразу же компилит их в C#, т.е. для Unity скрипт собранный в таком редакторе не очень отличается от скрипта, написанного руками на шарпах.
Давайте сразу приведу скрин того, во что превратился вышеприведенный LUA-скрипт. (картинка кликабельна)

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

А на схеме — так:

И сразу видно, что произойдет при выборе ответа №1 и ответа №2. А если таких ветвлений будет больше — то тем более схема не потеряет наглядности.
Принципы uScript.
Давайте быстро пробежимся по тому, из чего состоит схема. Собственно, основные модули (в терминологии uScript они называются «nodes») — это событие (с него обычно начинается скрипт или цепочка), action и переменные.

У action'он есть вход (обычно 1) и выход(ы). Например, у самого простого действия 1 вход и 1 выход. А у какого-нить блока условия — уже будет два выхода, например.
Снизу блока подключаются переменные. Треугольник означает, что в переменную будет произведена запись (output).
Например, в этом примере мы создаем персонажа (с помощью блока «Create char»), а потом выставляем ему же зеркальность в «true» (с помощью блока «Mirror»):

Кстати, все переменные могут иметь названия (в нашем случае «с1»). И все переменные одного типа с одинаковым названием будут синхронизированы в пределах одного скрипта (схемы). Т.е. пример выше совершенно идентичен такому:

Сделано это чтобы избавить вас от необходимости тянуть связи через два экрана.
Кроме того, если поставить галочку «expose to Unity», выбранная переменная станет public и будет видна другим скриптам (как визуальным, так и вашим рукописным). Массивы так же поддерживаются.
Немного практики.
Все модули, которые вы видите на схеме — самописные. И были написаны за 1 вечер. Давайте посмотрим на их код.
Рассмотрим сначала что-нибудь очень простое. Например, action, который называется «Start fight». Он начинает бой (по сути, вызывает метод игровой логики) и принимает два параметра — айдишник боя и айдишник соперника.

Код для него:
[NodePath("Actions/VHS Story/Fight")]
[NodeCopyright("Copyright 2014 by GameJam")]
[NodeAuthor("GameJam", "http://www.gamejam.ru")]
[FriendlyName("Start Fight", "")]
public class uScriptAct_StartFight : uScriptLogic
{
public bool Out { get { return true; } }
public void In (
[FriendlyName("Opp. id", "")] int opponent_id,
[FriendlyName("FightData id", "")] int fightdata_id
)
{
MainGame.me.StartSimpleFight(opponent_id, fightdata_id);
}
}
Просто? Очень.
А теперь давайте усложним. Допустим, мы хотим проиграть какую-либо анимацию. И хотим иметь два выхода. Один — сразу, а второй, который запустится только когда анимация проиграется до конца.

Справа вы можете видеть блок с конфигурацией блока, куда вы вбиваете значения. У блока 3 входных параметра — CharacterGfx (непосредственно персонаж, которому мы проигрываем анимацию), Animation (название анимации) и Mirror (необходимость зеркаленья). И у блока есть два выхода: Out (выход сразу же) и Finished (только когда анимация закончится).
При этом переменная «Mirror» является энумератором с параметрами «да», «нет» и «не менять», которая представляется в виде dropdown-списка в окне свойств.
Код особо сложнее не стал:
using uScriptEventHandler = uScript_GameObject.uScriptEventHandler;
[NodePath("Actions/VHS Story/Character")]
[NodeCopyright("Copyright 2015 by GameJam")]
[NodeAuthor("GameJam", "http://www.gamejam.ru")]
[FriendlyName("Char: Play anim", "")]
public class uScriptAct_CharacterPlayAnimation : uScriptLogic
{
public bool Out { get { return true; } }
[FriendlyName("Finished")]
public event uScriptEventHandler Finished;
public enum BooleanSet
{
NoChange = 0, True, False
}
public void In (
[FriendlyName("CharGfx", "The CharacterGfx.")] CharacterGfx ch,
[FriendlyName("Animation", "")] string anim_name,
[FriendlyName("Mirror", "")] [SocketState(false, false)] [DefaultValue(BooleanSet.NoChange)] BooleanSet mirror
)
{
ch.PlayAnimation(anim_name);
if (mirror != BooleanSet.NoChange) ch.SetMirror(mirror == BooleanSet.True);
ch.OnAnimationEndedCallback += () =>
{
if (null != Finished) Finished(this, new System.EventArgs());
};
}
}
Еще момент. Во всех блоках выше выход (Out) вызывался сразу же после выполнения кода блока.
А что если мы хотим сделать асинхронный action? Например, загрузку сцены. И чтобы выполнение нашего визуального скрипта приостановилось до того момента, пока асинхронно не прогрузится сцена.
Делается это так же просто. Вместо строчки
public bool Out { get { return true; } }
которая являлась флагом «скрипт всегда готов к выходу», мы пишем:public event uScriptEventHandler Out;
тем самым говоря — «Out теперь является хэндлером, а не вечно-истинным boolean'ном».А далее в коде в тот момент, когда вы будете готовы продолжить выполнение скрипта, вам нужно вызвать этот хэндлер ровно так же, как было с Finished в предыдущем примере:
if (Out != null) Out(this, new System.EventArgs());
Не обязательно писать код самому.
Все, что я привел выше — было написано нами, чтобы собрать все, что нужно в одно удобное место. Но это зачастую не обязательно. В uScript есть такая вещь, которая называется «reflection». На деле это означает, что uScript автоматически сканирует вашу сцену и вытягивает из нее все объекты, а так же их публичные методы и параметры, до которых может дотянуться. И предоставляет к ним доступ.
Например, вот так выглядит блок-reflection на метод GetComponent() камеры на сцене:

(внизу вы можете видеть блок «properties», где задаются все параметры метода)
Выводы.
Тулза нам однозначно понравилась и мы будем юзать ее дальше. Вообще, некоторые люди умудряются с помощью нее писать целые игры, но это уже чересчур.
Насколько глубоко мы сможем ее заюзать пока не знаем. Например, еще не решили, переписывать ли логику триггеров квестов с нашей lua-ориентированной на визуальную.
Но вот для скриптования кат-сцен и диалогов будем юзать однозначно.
Из минусов могу выделить только один (который является следствием плюса) — как я писал выше, uScript преобразует визуальные схемы в C# код. А следовательно каждая модификация схемы потребует перекомпиляции проекта.
В остальном — очень советую присмотреться к этому инструменту, если вы хотите скриптовать подобную логику. Так же, насколько я знаю, этот инструмент активно используют для написания AI.
Кстати, если вам нужна именно для скриптования поведения и взаимодействия объектов на сцене (например, триггеры на столкновения и т.п.), то присмотритесь к PlayMaker. Он больше ориентирован именно на событийную модель.
Все статьи серии:
- Идея, вижен, выбор сеттинга, платформы, модели распространения и т.п
- Шейдеры для стилизации картинки под ЭЛТ/LCD
- Прикручиваем скриптовый язык к Unity (UniLua)
- Шейдер для fade in по палитре (а-ля NES)
- Промежуточный итог (прототип)
- Поговорим о пиаре инди игр
- 2D-анимации в Unity («как во флэше»)
- Визуальное скриптование кат-сцен в Unity (uScript)