LoginSignup
18
16

【Unity用】ノベルゲーム・アドベンチャーゲーム制作ライブラリYarn Spinnerを使ってみた その1

Last updated at Posted at 2022-12-16

Yarn Spinnerの紹介

Yarn Spinner はアドベンチャーゲームや、ノベルゲームの制作に特化したオープンソースライブラリです!
テキストベースで物語の分岐やコマンドを記述することができ、
また、その分岐をノードベースビュアーで確認することができます。
保存したテキストはそのままUnityで読み込んで再生可能なため、シナリオを書いてすぐに動作チェックできます!

▼公式URL

実例

実際にYarn Spinnerで実装した簡単なデモを用意しました!

公式のアドベンチャーゲームデモを翻訳したもの

この例のように、アドベンチャーゲームの会話パートだけをYarn Spinnerで管理することも可能です。
もちろん、条件によるテキスト分岐も可能です!

今回の記事内容を組み合わせたデモ

こちらのデモでは、変数とランダム値の計算を含んだ分岐を実装しています。
ノード図がこちらです。ノードを見ながらテキストベースでゲームを組むことができます。
image.png

テキストを書き換えながら即実行することができるので、
速いイテレーションでアドベンチャーパートのブラッシュアップができます。
Demo3.gif

Yarn Spinnerの独自性

  • 編集はテキストがメインです。ノードビューはリアルタイムにテキストを反映します!
  • ちょっとした分岐を扱うための簡単な記法があり、これが非常に便利です!
  • 新規コマンドを簡単に追加できる仕組みがあります!
  • unity上のViewクラスがロジッククラスと分離されており、簡単にViewの独自実装が可能です!
  • テキストを書く延長で画面上の演出をつけられるため、シナリオ担当さんでの試行錯誤が可能です!
  • 音声再生・ローカライズ機能も提供されています!

この記事の動作環境

Unity.2021.3.13f
Yarn Spinner 2.2.1
VSCode 1.74.1
Yarn Spinner VSCode Extension v2.2.77

導入方法

Yarn Spinner自体の紹介はこれくらいにして、さっそく導入方法についてみていきましょう!

テキストエディタの導入

  1. テキストエディタはVSCodeを使います。
    Extensionsの欄から Yarn Spinner の拡張機能を検索してインストールしてください!
    image.png

Unityへの導入

Uniryへは、OpenUPM 経由で導入することができます。

  1. Unityの編集>プロジェクトセッティングから、PackageManagerを開いてください。
    項目を追加して、以下の情報を入力してSaveしましょう!
     
     Name  OpenUPM
     URL  https://package.openupm.com
     Scopes dev.yarnspinner
    image.png

  2. Window>PackageManagerを開き、MyRegistoriesを選びます。
    image.png

  3. SecretLab の YarnSpinnerが選択肢に表示されます。
    現時点の最新版は2.2.3ですが、MAC環境でエラーが出てしまうため、2.2.1のインストールをおすすめします。
    インストールボタンを押してインストールしましょう。これでUnityへのインストールも完了です!
    image.png

クイックスタート

早速簡単なシナリオを書いて、Unity上で表示してみましょう!

  1. UnityのProjectの好きな場所に、Create>YarnSpinner>YarnScriptでYarnScriptを作ってください。
    createyarnscript.png
    ファイル名はHelloWorldに変更しておきます。
    image.png

  2. ダブルクリックするなどして、VSCodeでファイルを開きましょう。
    下図左側のノード画面だけが表示されている場合は、ノードをダブルクリックしてスクリプトエディタを開いてください。
    image.png

  3. 一番上の title のところが ”HelloWorld” になっていることを確認してください。
    違っていたら title: HelloWorld に変更しておきましょう。
    "---"と"==="の間に、Hello World!!と入力して保存してください。
    image.png

  4. Unity上の作業に戻ります。作ったyarn Scriptを選択しましょう。インスペクターから、CreateNewYarnProjectを選んで、Projectファイルを生成しておきます。
    createYarnProject.png
    ↓スクリプトファイルのすぐ下にプロジェクトファイルが生成されます。
    image.png

  5. Packagesの下の方にYarnSpinnerというフォルダがあります。
    このフォルダの中のPrefabフォルダから、DialogueSystemというPrefabをシーンにドロップしましょう。
    dropDialogueSystem.png
    ※TextMeshProが導入されていない場合は、インポートを促すメッセージがでてきます。インポートしましょう。
    image.png

  6. ヒエラルキーからDialogueSystemを選択しましょう。インスペクターのYarnProjectの欄に4で作ったProjectをアタッチします。
    続いてStart Nodeの欄にHelloWorldと入力します。
    projectattach.png

  7. 再生ボタンを押してみましょう。Hello World!!が表示されれば成功です!!
    image.png

日本語対応

  1. プレファブをアンパック
    いろいろ手を加えることになるので、今のうちにDialogueRunnerのプレファブをアンパックしておいてください。
    unpack.png

  2. 通常テキストの日本語対応
    Hierarchyの検索欄をTypeに変更して、TextMeshProUGUIに変更すると、TextMeshProが使われているコンポーネントが表示されます。
    これらのフォントを日本語対応フォントに変更しておいてください。
    ※日本語フォントの作成などについては、良い記事がたくさんありますのでここでは割愛します。
    image.png
    これでシーン上の設定は完了しました!

  3. 選択肢テキストの日本語対応
    yarnは、選択肢をプレファブから作成します。このとき使われるプレファブも、日本語対応しておきましょう!
    Packages>YarnSpinner>Prefabsの中のOptionViewをAssetの好きな場所にコピーし、中身のテキストを日本語対応フォントに変更しましょう。
    image.png
    ヒエラルキーのDialogueSystem>Canvas>OptionListViewを選択し、インスペクターのOptionViewPrefabに新しく作ったOptionViewをアタッチします。(わかりやすいように名前も変えておきましょう)
    prefabatach.png
    これで選択肢も日本語表示できるようになりました。

サポートされている機能

さっそく、yarnでサポートされている機能を使って、ノベルゲームを作ってみましょう!

選択肢を作る

  1. 書式
    ごく簡単な選択肢分岐は、->記号を使って書くことができます。
    選択肢の直後にインデントで下げた文章を書くと、その選択肢を選んだときの分岐を書くことができます。
    以下の例のように、二階層以上下げて書くことも可能です。

    目の前に犬が現れました。
    きびだんごをあげますか?
    ->あげない
        きびだんごをあげなかった。
        犬は悲しんでいる!!
        ->やっぱりあげる
            きびだんごをあげた
        ->それでもあげない
            やっぱりきびだんごはあげなかった
    桃太郎はまたあるき出した。
    
  2. 実例
    クイックスタートと同じ手順で、yarn scriptを用意して本文を書き換えてみましょう!

    image.png

    実行してみると...
    Animation.gif
    無事、想定どおり動作しました!

ノードを組み合わせる

単純な選択肢は、上の例で実装することができました。
ただ、全ての選択肢をこの方法で書いていくと、どんどんインデントが深くなってしまいます。
複数のノードをつかって、適切に整理しましょう!

  1. ノードそのものの記法
    yarnは文章をノードという単位で管理しています。
    ノードはタイトルと本文で構成されており、記号によって区切られています。最小構成は以下のようになります。

    title: ここにノード名
    ---
    本文を書くエリア
    
    ===
    

ノード名には、日本語を含む文字、数字、アンダースコアが使用可能です。
また、最初の一文字は数字やアンダースコアではなく、文字から始める必要があります。

○ first_Node
○ ノード01
× 1st_Node

  1. ノードのジャンプ
    <<jump ノード名>>と記載すると、別のノードに移動することができます。
    また、ノードは、一つのyarn scriptファイル内に続けて複数個書くことができます。
    つまり、2つのノードを使った最小の構成は、以下のようになります。

    title: ノードA
    ---
    
    <<jump ノードB>>
    
    ===
    title: ノードB
    ---
    
    ===
    
  2. 実例
    これらの記法を組み合わせるだけで、複雑な分岐を表現することができます。
    例として、以下のような分岐を書いてみました。
    image.png

    以下のサンプルテキストを使うと、同じノードを作ることができます。

    サンプルテキスト
    title: Momotaro
    ---
    //今後使うために事前準備ノードを残しておく。今はただジャンプするだけ
    
    <<jump 桃太郎犬と出会う>>
    
    ===
    title: 桃太郎犬と出会う
    color:red
    ---
    しばらく歩くと目の前に犬が現れた。
    きびだんごをあげますか?
    ->あげる
        <<jump 犬にきびだんごをあげた>>
    ->あげない
        きびだんごをあげなかった。
        犬は悲しんでいる!!
        ->やっぱりあげる
            <<jump 犬にきびだんごをあげた>>
        ->それでもあげない
            <<jump 犬にきびだんごをあげなかった>>
    ===
    title: 犬にきびだんごをあげた
    color:red
    ---
    きびだんごをあげた
    犬はたいへん喜んで、鬼退治に同行したいと言い始めた。
    桃太郎は犬にリードをつけ、予防接種をうけさせてから連れていくことにした。
    <<jump 桃太郎猿と出会う>>
    ===
    title: 犬にきびだんごをあげなかった
    color:red
    ---
    きびだんごをあげなかった。
    鬼退治の道は長く険しい。少しでも節約しなければ。
    桃太郎はこころを鬼にして先へと進んだ。
    <<jump 桃太郎猿と出会う>>
    ===
    title: 桃太郎猿と出会う
    color:red
    ---
    しばらく行くと目の前に猿が現れた。
    きびだんごをあげますか?
    ->あげる
    ->あげない
    ===
    

    ShowGraphボタンを押すと、ビューを表示することができます。
    image.png
    サンプルテキストをyarn scriptにコピペして、表示を見てみてください。
    一箇所に固まったような表示になっているはずです。
    ノードをドラッグで動かして、見やすいようにしてみましょう。
    dragnode.png

    ノードのヘッダーに色をつけたい場合は、titleと---の間にcolor:色名の記法で色を指定してください。
    red/blue/green/orangeなどが利用できます。

キャラクター名と表示領域

テキストをキャラクター名:セリフの書式で書くと、キャラクター名が表示されます。
キャラクター名は専用の欄に表示されます。
例えば、yarn scriptに以下のように書くと、

犬:桃太郎さん、ここからいい匂いがします!

次のように表示されます。
image.png

ヒエラルキー上でのそれぞれのオブジェクトと表示領域の関係を図にまとめてみました。
対応するオブジェクトを変更することで、表示領域などを変更することができます。
DialogueSystemとゲームビュー上の関係2.jpg

キャラ名や入力内容に応じた独自処理を実装したい場合は、dialogue viewのカスタマイズをご覧ください。

変数を使う

いよいよ変数と条件分岐を扱います。
これができるようになれば、ゲーム性のある処理を書けるようになるはずです。

  1. 変数の宣言
    使いたい変数は事前に宣言をしておく必要があります。
    書式は<<declare $変数名 = 初期値 as 型名>>です。

    as 型名の部分は省略可能です。省略された場合は、yarn側で型を類推します。
    型は、number/string/boolが使えます。(numberは内部的にはfloatになります)

  2. 変数宣言の実例
    最初のMomotaroノードで変数を宣言した実例を用意しました。

    サンプルテキスト
    title: Momotaro
    position: 16,-313
    ---
    <<declare $NumberOfKibidango = 3>> //きびだんごの数
    <<declare $MomotaroHumanity = 3>> //ももたろうの人間性
    <<declare $MomotaroPower = 1>> //ももたろうのちから
    
    <<declare $IsDogBuddy = false>> //犬が仲間になっているか
    <<declare $DogLove = 0>> //犬の好感度
    <<declare $IsMonkeyBuddy = false>> //猿が仲間になっているか
    <<declare $MonkeyLove = 0>> //猿の好感度
    <<declare $IsPheasantBuddy = false>> //雉が仲間になっているか
    <<declare $PheasantLove = 0>> //雉の好感度
    
    
    <<jump 桃太郎犬と出会う>>
    ===
    
  3. 変数への代入
    変数は、<<set $変数名 = 値>>の書式で再代入することができます。
    また、+= -= *= /=の書式にも対応しています。

    =の代わりにtoを使っても代入可能です。

  4. 変数代入の実例
    先程の例に変数代入を足してみましょう。

    サンプルテキスト
    title: 桃太郎犬と出会う
    ---
    しばらく歩くと目の前に犬が現れた。
    きびだんごをあげますか?
    ->あげる
        <<set $IsDogBuddy = true>> //犬が仲間になった
        <<jump 犬にきびだんごをあげた>>
    ->あげない
        きびだんごをあげなかった。
        犬は悲しんでいる!!
        ->やっぱりあげる
            <<set $IsDogBuddy = true>> //犬が仲間になった
            <<jump 犬にきびだんごをあげた>>
        ->それでもあげない
            <<set $MomotaroHumanity -= 1>> //桃太郎の人間性が下がった
            <<jump 犬にきびだんごをあげなかった>>
    ===
    

条件分岐を使う

  1. 条件分岐
    条件分岐は、<<if 条件式>>ではじめ、<<endif>>で閉じることで有効化します。
    また、<<<elseif 条件式>>や、<<else>>をつかって複数条件を書くことも可能です。

    ==の代わりにisを使っても一致判定可能です。
    その他の書き方については公式ページをご覧ください。

  2. 選択肢の表示分け
    選択肢を示す->の後に続けて、<<if 条件文>> と記載すると、
    条件を満たしたときにのみ表示される選択肢を実装することができます。

  3. 条件分岐の実例
    これも実例を見て見ましょう。

    サンプルテキスト
    title: 柿の木の下
    ---
    
    桃太郎は峠の柿の木の下までやってきた。
    
    <<if $IsDogBuddy == true>> //犬が仲間であれば
        犬:桃太郎さん、ここからいい匂いがします!
        犬はおもむろに木の根本を掘り始めた。
        なんと木の根元にはエクスカリバーが埋まっていた。
        桃太郎はエクスカリバーを引き抜いた!
        <<set $MomotaroPower += 3>> //桃太郎の力を+3する
    <<elseif $IsMonkeyBuddy == true>> //猿が仲間であれば
        猿:うまそうな柿がなってるぜ
        猿はするすると木に登ると、熟れた柿を取って降りてきた。
        桃太郎は柿をたべた。全身に力がみなぎる!
        <<set $MomotaroPower += 1 >> //桃太郎の力を+1する
        柿が一つ残った
        //雉が仲間になっていて、桃太郎の人間性が残っている場合のみ出る選択肢
        ->雉にあげる<<if $IsPheasantBuddy and $MomotaroHumanity > 0>> 
            雉はとても喜んだ
            <<set $PheasantLove += 1>>
        //いつでも出る選択肢
        ->自分で食べる
    <<else>>
        桃太郎は遠く鬼ヶ島を眺めると、黙ってあるき出した
    <<endif>>
    
    <<if visited("ノードの名前")>>
    <<endif>>
    ===
    

    yarnの記法上、条件分岐にインデントは本来必要ありません。
    ここでは読みやすさのためにインデントをいれています。

プリセットされているコマンド

プリセットされているコマンドがいくつかあるので、見ていきましょう!

  1. wait :待機処理
    <<wait 秒数>>と書くことで、指定秒数待機する処理を書くことができます。

  2. stop :中断処理
    <<stop>>と書くと、そこまででyarnの処理を止めることができます。

  3. visited(string node_name) :通過済判定
    visited("ノードの名前")と書くと、特定のノードを通過済か、bool値で判定できます。

    サンプルテキスト
    <<if visited("桃太郎犬と出会う")>>
    猿: 犬にはもう出会ったんだろう?
    <<endif>>
    
  4. visited_count(string node_name) :通過回数判定
    visited_count("ノードの名前")で何回そのノードを訪問したかの判定が可能です。

    サンプルテキスト
    <<if visited_count("きびだんごを補充にもどる") > 2 >>
    おばあさん:いったい何回戻ってくる気だい!もう団子はないよ!
    <<else>>
    おばあさん:よく戻ったねえ。さあ団子をおあがり。
    <<endif>>
    
  5. dice(number sides) :ランダムな値を取得
    dice(最大値)と書くと、1~最大値までのランダムな整数を取ることができます。

    サンプルテキスト
    <<declare $damage = 0>>
    
    桃太郎の攻撃!
    <<set $damage = dice(10)>>
    鬼に{$damage}のダメージ!
    <<set $OniHP -= $damage>>
    

その他、四捨五入や、小数点以下を含むランダム値取得などもあります。
その他のコマンドについては公式ページをご覧ください。

自作コマンドの導入(簡単な方法)

登録すれば、yarn scriptの中から、自作のコマンドを呼び出すことも可能です!
こちらでは、比較的簡単なアトリビュートを使った方法を紹介します。
基本的には、自作のメソッドに[YarnCommand("コマンド名")]のアトリビュートをつけるだけです!

Staticメソッド

ピュアクラスのメソッドを呼び出したい場合は、Staticなメソッドにアトリビュートをつけます。
以下のように書くだけです。

public class FadeCamera {

    [YarnCommand("fade_camera")]
    public static void FadeCamera() 
    {
        Debug.Log("Fading the camera!");
    }
}

呼び出し(yarn script)側からは、<<fade_camera>>と書いて実行します。

Monobehaviour継承クラスのメソッド

Monobehaviour継承クラスのであれば、Staticで無いメソッドも呼び出し可能です。
以下のように書くことができます。

public class CharacterMovement : MonoBehaviour {

    [YarnCommand("leap")]
    public void Leap() 
    {
        Debug.Log($"{name} is leaping!");
    }
}

呼び出し(yarn script)側は先程と少し異なります。
<<コマンド名 GameObject名>>と書いて実行します。
例えば上のCharacterMovementが、MyCharacterといGameObjectにアタッチされている場合は、
<<leap MyCharacter>>と書きます。

どちらの場合も、すぐ後ろに続けて書くことで、引数を渡すことができます。
例えば上の例がpublic void Leap(string destination)というメソッドだったとすると、
<<leap MyCharacter "Home">>などと書くことで、引数を渡したメソッド呼び出しが可能です。

Courutine

待機を含むコマンドは、Courutineを使うことで実装できます。以下のように書いてください。

public class CustomWaitCommand : MonoBehaviour {    

    [YarnCommand("custom_wait")]
    static IEnumerator CustomWait() 
    {
        // Wait for 1 second
        yield return new WaitForSeconds(1.0);        
    }    
}

呼び出し(yarn script)側からは、<<custom_wait>>と書いて実行できます。

独自コマンドの入力補完

独自コマンドであっても、VSCodeの入力補完を使うことができます。
ただし、yarn scrpitファイルを単独で開いている場合は有効になりません。
補完が効かない場合はエクスプローラー(Ctrl+Shift+E)を開いて、フォルダを開くからAssetフォルダなど、
自作スクリプトを含む上位のフォルダを選択してください。
openfolder.png

拡張機能の設定の CsharpLookup と DeepCommandLookupはオンにしておきましょう。(おそらく最初からオンになってます)
拡張機能.png
image.png

Tips

  • エラー表示
    yarn scriptを保存してunityに戻るとコンパイルが走ります。
    書式などのエラーについてはこの時点でコンソールとyarn scriptのインスペクターにエラーメッセージが出ます。
    また、コマンドが見つからない場合などは、実行時にコンソールにエラーが現れます。
    image.png

  • 選択肢の書式エラー
    ->の後に何も書いてない、または半角スペースのみを置いている場合、エラーになります。
    現状このエラーが正確に検出されていないのか、メッセージが非常に分かりづらくなっています。
    何を直してもうまくいかない場合は、->の後を見直してみるのがおすすめです。
    (公式から修正するとのコメントがありました)

  • ノードが表示されない
    稀に読み込みがうまく行かず、ノードビューが見られないことがあります。
    一度ノードビューを閉じて再度開くとうまくいきます。

  • yarnScriptを開いたら最初からテキストビューして欲しい!
    行数が増えてくると、ノードビューの読み込みに時間がかかる場合があります。
    右クリックのOpen With...メニューを開いて、default editorをTextEditorにすれば、最初からテキスト表示で見られるようになります。
    image.png
    image.png
    image.png

  • ノードがどこに繋がっているかわからない……!
    <<jump ノード名>>のノード名のところで、Ctrl+クリックや、右クリックのGo to Difinitionを使うと、遷移先のノードに自動で移動してくれます!

前半のまとめ

お疲れ様でした!!!
ここまでで、Yarn Spinnerそのものの機能はほぼ使いこなせるようになったはずです。
以上の内容を組み合わせるだけでも多くの表現が可能です。
特に自作コマンドを実装すれば、画面上の様々な要素にアクセスできます。

ぜひ色々と試してみてください!

次の記事ではより発展的な内容として、
アトリビュート以外の方法でコマンドを追加する方法、
ビュー要素や変数ストレージの改造などに触れていきます!

編集履歴

※240127 Tipsを追加しました。

おまけ デモで使ったスクリプト

デモ用に作ったスクリプトを置いておきます。

yarn script
```
title: Battle
tags:
position: -553,-420
---
<<declare $MomotaroHP to 20>>
<<declare $MomotaroHumanity to 0 >>
<<declare $DogHP to 3>>
<<declare $MonkeyHP to 3>>
<<declare $KijiHP to 3>>
<<declare $Damage to 0>>

<<jump Opening>>
===
title: Opening
tags:
position: -552,-306
color:red
---
<<ChangeImage BackGround onigashima>>
闇の力に呑まれた桃太郎が現れた
<<Shake Momotaro 1>>
<<Show Momotaro>>
犬:ああ!桃太郎さん、正気を取り戻してください…!
<<jump 犬の行動>>
===
title: 桃太郎の攻撃
tags:
position: -767,-174
color:red
---
桃太郎の攻撃
<<set $Damage to dice(2)>>
犬に{$Damage}のダメージ
<<jump 犬の行動>>
===
title: 犬の行動
tags:
position: -551,-176
color:red
---

犬の行動
->噛みつく
    犬は桃太郎に噛みついた
    <<Shake Momotaro 0.5>>
    <<set $MomotaroHP -= 1>>
->きびだんごを投げる
    犬:桃太郎さん!おばあさんのきびだんごですよ!
    きびだんごを投げた。
    <<Shake Momotaro 0.5>>
    桃太郎の体力が上がった。
    <<set $MomotaroHP += 1>>
    桃太郎は人間性を少し取り戻した
    <<set $MomotaroHumanity += 1 >>
->説得する
    犬:ももたろうさん!僕たちのことを思い出してください!
    <<set $MomotaroHumanity += 1 >>
<<jump StatusCheck>>

===
title: StatusCheck
tags:
position: -553,-25
color:red
---
<<if $MomotaroHumanity > 2>>
    <<jump MomotaroWakeup>>
<<endif>>

<<jump StatusShow>>
===
title: StatusShow
tags:
position: -553,94
color:red
---
桃太郎の残HP:{$MomotaroHP} <br> 桃太郎の人間性:{$MomotaroHumanity}

<<jump 桃太郎の攻撃>>
===
title: MomotaroWakeup
tags:
position: -338,-24
color:red
---

<<ChangeImage BackGround sougen>>
ももたろうは正気を取り戻した
===
```
Image操作用クラス(サンプルではMomotaroとBackGroundというGameObjectにアタッチ)
```
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using Yarn.Unity;

namespace Script
{
    [RequireComponent(typeof(Image))]
    public class SampleImageChanger : MonoBehaviour
    {
        private Image _image;
        private Image ThisImage => _image ? _image : _image = GetComponent<Image>();

        private float time;
        
        
        [YarnCommand("ChangeImage")]
        public void ChangeImage(string imageName)
        {
            ThisImage.sprite = Resources.Load<Sprite>(imageName);
        }
        
        [YarnCommand("Show")]
        public void ShowImage()
        {
            ThisImage.enabled = true;
            ThisImage.color = new Color(ThisImage.color.r, ThisImage.color.g, ThisImage.color.b, 1);
        }

        [YarnCommand("Shake")]
        public IEnumerator Shake( float  shakeTime)
        {
            var rectTran = this.transform as RectTransform;
            time = 0;
            
            while ( time < shakeTime )
            {
                time += Time.deltaTime;
                rectTran.anchoredPosition = new Vector2(Random.Range(-10, 10), Random.Range(-10, 10));
                yield return null;
            }
        }
        

    }
}
```
18
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
16