Pull to refresh

Создание игры на ваших глазах — часть 8: Визуальное скриптование кат-сцен в Unity (uScript)

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


И мы задумались о визуальном подходе. В этой статье я расскажу о нашем знакомстве с средством визуального скриптинга для 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. Он больше ориентирован именно на событийную модель.

Все статьи серии:
  1. Идея, вижен, выбор сеттинга, платформы, модели распространения и т.п
  2. Шейдеры для стилизации картинки под ЭЛТ/LCD
  3. Прикручиваем скриптовый язык к Unity (UniLua)
  4. Шейдер для fade in по палитре (а-ля NES)
  5. Промежуточный итог (прототип)
  6. Поговорим о пиаре инди игр
  7. 2D-анимации в Unity («как во флэше»)
  8. Визуальное скриптование кат-сцен в Unity (uScript)
Tags:
Hubs:
+22
Comments 16
Comments Comments 16

Articles