Inkシナリオ作成
※本ブログでは、トライ&エラーの過程そのものを重視しています。最終的に辿り着いた正解手順だけ抜粋して載せることもできますが、いろいろ試行錯誤することそのものが、結果的に読者にとって、色々な事態に対処するための手助けになると考えています。要するに、さっさと実行手順だけ知りたいという人には “まどろっこしい” or “やる必要がない” 手順が多々含まれていますので、一度最後まで読んでから、必要な手順だけをピックアップして実行することをお勧めします。
前回の記事で、Godot-Inkプラグインをインストールして有効化し、Godotエンジンで、「.ink」ファイルを読み込めるようになりました。このInkファイルは海外では有名なシナリオファイルです。ここでは、まずChatGPTの助けを借りて、サンプルのInkファイルを自動生成してみましょう。
Inkのスタイルで書かれた、5分程度で終わるシナリオを書けますか?
ひとまずこれだけで聞いてみました。これだけで通じるのでしょうかね・・?
ChapGPT:
もちろんです!以下は、インタラクティブなストーリーを作成するためのシナリオをInkのスタイルで書いた例です。プレイヤーが選択肢を選んで進めていくシンプルな短編シナリオです。5分程度で終わる内容を意識しました。
すごいですね。で、出てきたInkシナリオが以下です。
TITLE: "森の中での選択"
= 起点
あなたは深い森の中を歩いています。昼間だった空が、徐々に暗くなってきました。辺りは不気味な静寂に包まれています。遠くからかすかに川の音が聞こえてきますが、それ以外はすべてが静かです。
突然、道が二手に分かれています。右側の道は広く整備されており、木々が少なくなっています。左側は木々が鬱蒼としていて、足元がほとんど見えません。
どちらの道を進みますか?
* 右側の道を進む -> goto 右道
* 左側の道を進む -> goto 左道
=== 右道 ===
あなたは広く安全そうな右側の道を選びました。歩いていくと、だんだんと足取りが軽くなっていきます。鳥のさえずりも聞こえ、少し安心した気持ちになります。
しかし、前方に奇妙な人影が見えます。その人物はフードを被っていて顔が見えませんが、こちらに向かって手を振っています。
どうしますか?
* 人物に近づいて話しかける -> goto 話しかける
* 無視して先に進む -> goto 無視する
=== 話しかける ===
あなたは人物に近づき、声をかけます。「こんにちは、どうしましたか?」
フードの中から低い声が返ってきます。「旅人よ、私はこの森を守る者だ。君がこの道を選んだことには意味がある。この先に待ち受ける運命を、どう進むかは君次第だ。」
フードの人物は道の先を指し示します。
* 人物の言葉を信じて道を進む -> goto 結末_右道
* 怪しいので引き返す -> goto 戻る
=== 無視する ===
あなたは人物を無視して先に進みます。しかし、道は突然途切れ、巨大な崖が目の前に現れました。これ以上進むことはできません。
仕方なく引き返します。 -> goto 起点
=== 左道 ===
あなたは暗く危険そうな左側の道を選びました。道は狭く、枝や茂みが行く手を阻みます。しかし、遠くで何かが輝いているのが見えます。近づくと、それは美しい青い光を放つ湖です。
湖のほとりには、古びた小屋があります。中から煙が立ち上り、誰かが住んでいるようです。
どうしますか?
* 小屋に近づいて中を覗く -> goto 小屋の中
* 湖のほとりで休む -> goto 休憩
=== 小屋の中 ===
あなたは小屋の扉をそっと開け、中を覗きます。中には年老いた女性が座っており、あなたに微笑みかけます。
「おや、珍しい訪問者だね。この森で迷ってしまったのかい?」と彼女は言います。
* 老婆に助けを求める -> goto 助け
* 何も言わずに立ち去る -> goto 戻る
=== 休憩 ===
あなたは湖のほとりに座り、しばし休むことにしました。水面に映る自分の姿を見つめながら、しばらくの間静けさを楽しみます。
しかし、湖の奥から何かが近づいてきている気配を感じます…。
* 湖に潜る -> goto 結末_左道
* 急いで逃げる -> goto 戻る
=== 助け ===
老婆はあなたを優しく見つめ、手を差し出します。「この森から抜け出すには、心の目を開かねばならない。私についてきなさい。」
あなたは老婆に従い、小屋の裏手にある隠された道を通って森から抜け出しました。
-> END
=== 戻る ===
あなたは引き返し、再び分かれ道に戻ります。どちらに進むか、再び考えることができます。
* 右側の道を選ぶ -> goto 右道
* 左側の道を選ぶ -> goto 左道
=== 結末_右道 ===
あなたはフードの人物の指示に従い、道を進みます。すると、目の前に広大な平原が広がり、遠くにあなたが探していた村が見えてきます。無事に目的地にたどり着きました。
-> END
=== 結末_左道 ===
湖に潜ると、そこには別の世界が広がっていました。美しい光と静寂の中、あなたは新たな冒険の始まりを感じます。
-> END
このシナリオで正しく動くのかはいったん放置して、ここからは Godot-Ink の Wiki に従って進めていきましょう。
「.ink」ファイルを選択することが必要なようなので、上記のコードの右上にあるCopy用ボタンをクリックして、コードをまるっとクリップボードにコピーしてください。その後、「.csproj」のあるフォルダと同じ場所に、「FirstStory.ink」のテキストファイルを作成し、クリップボードの内容をペーストします。
次にGodotエンジンの[Ink Preview]ボタンを押すと、中央の表示部分の下側に新たなWindow枠が出てきます。そこに[読み込む]というボタンがありますのでそこをクリックします。
先ほど生成した「FirstStory.ink」ファイルが候補にあがっていますね。これを選択して、[開く]を押します。
何も起きたように見えませんが先に進みましょう。次は「go to the Import dock」とありますね。Import Dock はどこやねん・・・・。
あ、たぶんここですね。このタブをクリックして表示させることが、「go to the Import dock」の操作に該当するみたいです。
Wikiの「You’ll see it reads “Import As: Ink Story“」とある部分が、「プリセット」という文字の左に「Ink story」と表示されているのに該当するはずです。
次に「Check the Is Main File checkbox and click Reimport. 」とありますので、「Is Main File」というチェックボックスをONにして、「再インポート」を押しましょう。まだ適用されていない作業中の変更とは、「FirstChat.csproj」に加えたImportの一文のことかもしれないですね。
さて、実行してみましたが、特に画面には何も出なかったので成功している・・・のか??いや、違いますね。何やら左下の「FirstStory.ink」のところに、不穏な×印がついています。
これが先ほどWikiの最後の文章にあった、「Any error or warning coming from the compiler will be reported in the Output bottom panel.」とある部分ですね。何かエラーが起きている場合は「Output(出力)」のところにレポートがあがるようです。早速この「出力」ボタンを押して確認してみます。
いろいろ出ていますね。
1つ目のメッセージはこれです。
res://FirstStory.ink is not a valid ink story. Please make sure it was imported with is_main_file set to true.
[Is Main File]は確かに「オン」にして「再インポート」したはずなので、正しいはずですが・・いったんこれはおいておいて次のエラーを見てみましょう。
res://addons/GodotInk/Src/InkStoryImporter.cs:117 - ERROR: 'res://FirstStory.ink' line 10: Expected end of line but saw '右道'
Error importing 'res://FirstStory.ink'.
改行コードがあると思っていたのに、’右道’というキーワードが出てきたのでエラーとなったようです。問題になった行はここですね。
* 右側の道を進む -> goto 右道
これはInkシナリオの正しい書き方を教えてくれているサイトに飛ぶのが正解でしょう。Inkの本家サイトへ飛んで確認したところ、goto なんて使われていませんでした。
ChatGPTを問い詰めたところしれっと代案を提示してきやがったので、きっちり間違いを認めさせ、他にも同様の間違いがないか再チェックさせたのちにでてきたInkシナリオがこちらです。
TITLE: "森の中での選択"
= 起点
あなたは深い森の中を歩いています。昼間だった空が、徐々に暗くなってきました。辺りは不気味な静寂に包まれています。遠くからかすかに川の音が聞こえてきますが、それ以外はすべてが静かです。
突然、道が二手に分かれています。右側の道は広く整備されており、木々が少なくなっています。左側は木々が鬱蒼としていて、足元がほとんど見えません。
どちらの道を進みますか?
* 右側の道を進む
-> 右道
* 左側の道を進む
-> 左道
= 右道
あなたは広く安全そうな右側の道を選びました。歩いていくと、だんだんと足取りが軽くなっていきます。鳥のさえずりも聞こえ、少し安心した気持ちになります。
しかし、前方に奇妙な人影が見えます。その人物はフードを被っていて顔が見えませんが、こちらに向かって手を振っています。
どうしますか?
* 人物に近づいて話しかける
-> 話しかける
* 無視して先に進む
-> 無視する
= 話しかける
あなたは人物に近づき、声をかけます。「こんにちは、どうしましたか?」
フードの中から低い声が返ってきます。「旅人よ、私はこの森を守る者だ。君がこの道を選んだことには意味がある。この先に待ち受ける運命を、どう進むかは君次第だ。」
フードの人物は道の先を指し示します。
* 人物の言葉を信じて道を進む
-> 結末_右道
* 怪しいので引き返す
-> 起点
= 無視する
あなたは人物を無視して先に進みます。しかし、道は突然途切れ、巨大な崖が目の前に現れました。これ以上進むことはできません。
仕方なく引き返します。
-> 起点
= 左道
あなたは暗く危険そうな左側の道を選びました。道は狭く、枝や茂みが行く手を阻みます。しかし、遠くで何かが輝いているのが見えます。近づくと、それは美しい青い光を放つ湖です。
湖のほとりには、古びた小屋があります。中から煙が立ち上り、誰かが住んでいるようです。
どうしますか?
* 小屋に近づいて中を覗く
-> 小屋の中
* 湖のほとりで休む
-> 休憩
= 小屋の中
あなたは小屋の扉をそっと開け、中を覗きます。中には年老いた女性が座っており、あなたに微笑みかけます。
「おや、珍しい訪問者だね。この森で迷ってしまったのかい?」と彼女は言います。
* 老婆に助けを求める
-> 助け
* 何も言わずに立ち去る
-> 起点
= 休憩
あなたは湖のほとりに座り、しばし休むことにしました。水面に映る自分の姿を見つめながら、しばらくの間静けさを楽しみます。
しかし、湖の奥から何かが近づいてきている気配を感じます…。
* 湖に潜る
-> 結末_左道
* 急いで逃げる
-> 起点
= 助け
老婆はあなたを優しく見つめ、手を差し出します。「この森から抜け出すには、心の目を開かねばならない。私についてきなさい。」
あなたは老婆に従い、小屋の裏手にある隠された道を通って森から抜け出しました。
-> END
= 結末_右道
あなたはフードの人物の指示に従い、道を進みます。すると、目の前に広大な平原が広がり、遠くにあなたが探していた村が見えてきます。無事に目的地にたどり着きました。
-> END
= 結末_左道
湖に潜ると、そこには別の世界が広がっていました。美しい光と静寂の中、あなたは新たな冒険の始まりを感じます。
-> END
さっそくこれに置換して再度同じ手順で再インポートしてみます。が、やはり「右道」のところで例外のエラーが発生しますね。これはやっぱりアレですね。日本語の問題なんじゃないでしょうか。薄々気づいてましたが面倒なので気づかないフリをしていましたがもう限界です。
いま問題になっている -> の右側には、飛び先の番地を文字列で設定するところです。対応する番地は、「= 起点」とかなっている部分の文字列ですね。この2か所を、ChatGPTに頼んで英語にしてもらいました。また他にもいくつか修正させて、最終的に以下の形になりました。
TITLE: "森の中での選択"
-> Begin
=== Begin ===
= Start
あなたは深い森の中を歩いています。昼間だった空が、徐々に暗くなってきました。辺りは不気味な静寂に包まれています。遠くからかすかに川の音が聞こえてきますが、それ以外はすべてが静かです。
突然、道が二手に分かれています。右側の道は広く整備されており、木々が少なくなっています。左側は木々が鬱蒼としていて、足元がほとんど見えません。
どちらの道を進みますか?
* 右側の道を進む
-> RightPath
* 左側の道を進む
-> LeftPath
= RightPath
あなたは広く安全そうな右側の道を選びました。歩いていくと、だんだんと足取りが軽くなっていきます。鳥のさえずりも聞こえ、少し安心した気持ちになります。
しかし、前方に奇妙な人影が見えます。その人物はフードを被っていて顔が見えませんが、こちらに向かって手を振っています。
どうしますか?
* 人物に近づいて話しかける
-> TalkToFigure
* 無視して先に進む
-> IgnoreFigure
= TalkToFigure
あなたは人物に近づき、声をかけます。「こんにちは、どうしましたか?」
フードの中から低い声が返ってきます。「旅人よ、私はこの森を守る者だ。君がこの道を選んだことには意味がある。この先に待ち受ける運命を、どう進むかは君次第だ。」
フードの人物は道の先を指し示します。
* 人物の言葉を信じて道を進む
-> Ending_RightPath
* 怪しいので引き返す
-> Start
= IgnoreFigure
あなたは人物を無視して先に進みます。しかし、道は突然途切れ、巨大な崖が目の前に現れました。これ以上進むことはできません。
仕方なく引き返します。
-> Start
= LeftPath
あなたは暗く危険そうな左側の道を選びました。道は狭く、枝や茂みが行く手を阻みます。しかし、遠くで何かが輝いているのが見えます。近づくと、それは美しい青い光を放つ湖です。
湖のほとりには、古びた小屋があります。中から煙が立ち上り、誰かが住んでいるようです。
どうしますか?
* 小屋に近づいて中を覗く
-> Cottage
* 湖のほとりで休む
-> RestByLake
= Cottage
あなたは小屋の扉をそっと開け、中を覗きます。中には年老いた女性が座っており、あなたに微笑みかけます。
「おや、珍しい訪問者だね。この森で迷ってしまったのかい?」と彼女は言います。
* 老婆に助けを求める
-> AskForHelp
* 何も言わずに立ち去る
-> Start
= RestByLake
あなたは湖のほとりに座り、しばし休むことにしました。水面に映る自分の姿を見つめながら、しばらくの間静けさを楽しみます。
しかし、湖の奥から何かが近づいてきている気配を感じます…。
* 湖に潜る
-> Ending_LeftPath
* 急いで逃げる
-> Start
= AskForHelp
老婆はあなたを優しく見つめ、手を差し出します。「この森から抜け出すには、心の目を開かねばならない。私についてきなさい。」
あなたは老婆に従い、小屋の裏手にある隠された道を通って森から抜け出しました。
-> END
= Ending_RightPath
あなたはフードの人物の指示に従い、道を進みます。すると、目の前に広大な平原が広がり、遠くにあなたが探していた村が見えてきます。無事に目的地にたどり着きました。
-> END
= Ending_LeftPath
湖に潜ると、そこには別の世界が広がっていました。美しい光と静寂の中、あなたは新たな冒険の始まりを感じます。
-> END
そして「再インポート」を実行してみたところ・・・・成功しました。すんなり認識してくれましたね。やはり日本語か・・。この番地にあたる部分(Inkでは”divert”といいます)は英語でないとダメというのはInkの制限ぽいです。まあテキスト本文や選択肢の文字列は日本語なので、そこはシナリオ作成時に注意すればいいかなと思います。次の手順はこちらです。
画面下の[Ink preview]ボタン→[読み込む]で、英語化した FirstChat.ink を読み込みます。すると、下記のようになります。
左下の*.inkファイルにエラーはなく、「読み込む」の右に読み込まれたファイルが表示されています。そして、「開始」ボタンが表示されています。
ここからさらにWikiの手順どおりに進めていきます。次は下記ですね。
まずはシーンの作成からです。シーン作成の基本についてはこちらの記事を参照ください。ここでは、ルートノードに「その他のノード」を選択します。
すると、ノードの新規作成画面が出てくるので、”VBoxContainer”で検索し、それをルートノードとして「作成」します。
シーンのルートノードとして、VBoxContainerを作成しました。ここでいったん、メニューの[シーン]→[シーンを保存]で、「v_box_container.tscn」ファイルを保存しておきましょう。
次にこのルートノードに、スクリプトをアタッチします。ルートノードを右クリックしてアタッチを選択します。
下記の画面が出ます。言語を「C#」にして、パスのスクリプト名は任意ですが、「res://VBoxContainer_FirstChat」としましょう。親クラスのVBoxContainerを継承しているFirstChatのメインスクリプトなので、そのままファイル名に反映させています。InkのWikiでは、この名前は「QuickStartGuide」になっています。下記の設定で、「作成」ボタンを押します。
作成後、画面の中央に作成したスクリプトの内容が表示されます。
(画面の中央だけ抜粋しています)
これに対して、Wikiで記載されていた下記の記述を追加していきます。
using Godot;
using GodotInk;
public partial class QuickStartGuide : VBoxContainer
{
[Export]
private InkStory story;
}
QuickStartGuideの部分は”VBoxContainer_FirstChat”に変更して進めていますので脳内で読み替えてください。あとはこのクラスの中にprivate変数を追加する必要があるので下記のように追加します。また、ファイルの先頭の方に、「using GodotInk;」も追加します。
ここで、Godotエンジン画面のビルドボタンを使って一度ビルドします。
ビルドに成功すると、画面右側の「インスペクター」のところで、「Story」プロパティが有効になり、設定ができるようになります。
この内容が現在<空>になっていますので、右のプルダウンから、「読み込む」を選択し、「FirstChat.Ink」を読み込みます。
読み込むと、空だったところに「FirstStory」が設定されました。先ほどprivate変数のstoryをクラスに新規追加する箱を用意しましたが、その箱の中身に「FirstStory.ink」を入れた、ということですね。
Wikiの手順では、次の下記のコードを埋め込みなさいと言ってます。
public override void _Ready()
{
GD.Print(story.Continue());
}
InkStoryクラスはContinue()メソッドを持っていて、これがInkシナリオの次の行を読み込む処理なので、これをまず呼び出します。読み出すと読みだした行の文字列が取得できるので、それをGodotの基本クラスであるGDクラスを介して、Printメソッドで出力しなさいということです。では書き足してみましょう。
いったん実行(F5)ボタンを押して実行し、「出力」パネルで本当に文字列が出力されるか確認します。
プログラムのメインシーンの画面では当然何も表示されませんが、Godotエンジン画面の「出力」タブに、シナリオから読み取った文字が表示されていることがわかります。
(まだタイトルの1行だけですけれどね)
さて、これで作成したシナリオを読み込み、InkStoryクラスを使ってプログラム内で扱うための準備が整いました。次の記事では、プログラム内でシナリオを解釈して制御する部分をやります。
それでは今日はこのへんで。
コメント