ボイスオーバーを利用したダイアログは、現代のビデオゲームに欠かせない要素の1つです。プレイヤーが特定の音声にキャラクターを結び付けるだけでなく、全般的な声の抑揚からキャラクターの心情がとらえやすくなります。これを、プレイヤーの言動に応じてキャラクターの発言内容や言い回しを変化させる、複雑なダイアログシステムが強化します。
キャラクターは、プレイヤーが過去に頼み事を聞いてくれた場合、より友好的に行動するかもしれませんし、自分の倫理観に著しく反することをした場合、より攻撃的に行動するかもしれません。これがいわゆるダイナミックダイアログです。
今回のデモでは、ナレーションを中心としたダイアログに焦点を当て、WwiseとUnityを使ってダイアログを実行します。Wwiseにはビデオゲームにダイナミックダイアログを実装する機能がありますが、今回は別のWwise機能を使ってダイアログシステムを動かします。今回の記事では、主に3つの領域を見ていきます。
1. プレイヤーによる決定に左右されない、シーケンスコンテナを使った基本的なナレーション。
2. 複数言語に対応するゲームのデザインを行っている人向けの、Wwiseのローカリゼーションシステム。
3. プレイヤーの影響を受ける変数に応じて変化する、スイッチコンテナを使ったダイナミックなナレーション。
これらのデモでは、Wwiseのシーケンスコンテナやスイッチコンテナについて学べるだけでなく、Wwiseに関する追加的なUnity C#スクリプト(イベントのポスト、イベントコールバック、スイッチやステートの変更など)も紹介します。
Wwise - シーケンスコンテナを使ったナレーションとローカリゼーション
最初のデモでは、プレイヤーに指示を出す指揮官のボイスオーバーをデザインします。指示は5つの異なるボイスオーバーに分割し、次々と再生されるようにプログラミングします。
1. “Hello? Hello? Can you hear me?”(「ハロー、ハロー。聞こえますか、どうぞ。」)
2. “Oh good. Listen, I need you to do something very, very important for me.”(「よし。聞いてくれ、君に非常に重要な任務を与える。」)
3. “In front of you are these little... obstacles that I need you to overcome.”(「君の前に、何と言えばいいのか…小さな障害物がある。それらを乗り越えてほしい。」)
4. “I just need you to jump over the hurdle, go under the archway, and to the green path. Not the red one. That would be bad.”(「ハードルを飛び越えて、アーチをくぐり、緑の道へと進んでほしい。赤の道ではないぞ。それはまずい。」)
5. “If you could do that for me, that would greatly help us over here in corporate. Over and out.”(「そうしてもらえれば、本社の方では非常に助かる。以上だ。」)
このナレーションは、状況に関わらずゲーム開始時に全く同じように再生され、変更される可能性はありません。そこでシーケンスコンテナを使用して、各オーディオトラックが変更されることなく次々と再生されるようにします。また、プレイヤーの好みに応じて英語またはスペイン語でオーディオを再生するため、Wwiseのローカリゼーションシステムも見ていきます。
まず、Audioタブから始めます。Actor-Mixer Hierarchyの下に新しいシーケンスコンテナを作成(Ctrl + Shift + Alt + Q)し、MissionBriefingという名前にします。次に、オーディオファイルを、新しいシーケンスコンテナの下にアップロードします。この操作を行うには、シーケンスコンテナを右クリックして“Import Audio Files...”を選択するか(Shift + i)、オーディオファイルをシステムからシーケンスコンテナにドラッグします。
Audio File Importerで、「Import as: 」フィールドを「Sound SFX」から「Sound Voice」に切り替え、「Import」をクリックします。
サウンドボイスはサウンドSFXと同じですが、Wwiseプロジェクトで使用されている全言語に対応する、複数のオーディオファイルをインポートできます。言語設定を変更すると、すべてのサウンドボイスがその言語のオーディオファイルに切り替わります。
さて、英語のサウンドファイルをすべてインポートできたので、Wwiseプロジェクトに他の言語を追加できるようにしましょう。これを行うには、トップバーのProjectを開き、Languagesを選択します(または、Shift + Jを押します)。Language Managerでは、言語の追加、削除、名前変更のほか、すべてのサウンドボイスのゲインを言語ごとに変更できます。とりあえず、Addボタンを押して、表示されたボックスにSpanishと入力してみましょう。「OK」をクリックし、続いて表示される警告メッセージで変更内容を確認します。
新しく作成したサウンドボイスを1つ選択してみると、Contents Editorに英語とスペイン語用に2つのセクションが表示されます。英語のオーディオファイルはすでに用意されているので、スペイン語のオーディオファイルをContents EditorのSpanishフィールドにぞれぞれドラッグしてインポートしましょう。この操作を、5つのナレーション音すべてで行います。
これらのサウンドボイスを再生すると、デフォルトの言語で聞こえます。ここでWwiseの左上でデフォルトの言語からスペイン語に切り替え、もう一度サウンドを再生すると、Spanishフィールドに入れたサウンドが聞こえてきます。このように、開発者がスイッチ1つで簡単に、ある言語から別の言語に変更することができます。
それでは、シーケンスコンテナを確認してみましょう。コンテナを選択した後、Contents Editorと、その右にあるPlaylistセクションを見てみます。デフォルトでは、シーケンスコンテナは内部のすべてのサウンドオブジェクトを再生するわけではなく、プレイリストに含まれるサウンドオブジェクトのみを再生します。サウンドボイスを、再生したい順番通りにContents EditorからPlaylistにドラッグします。
サウンドを編集したい場合を除いて、ここで行う操作はこれで終わりです。Eventsタブを開き、Default Work Unitに2つの新しいイベントを作成します。
1. Play_MissionBriefingという名前の「Play」アクションのイベント。シーケンスコンテナを再生します。
2. Reset_MissionBriefingという名前の「Reset Playlist」アクションのイベント。シーケンスコンテナのプレイリストを最初のサウンドオブジェクトにリセットします。
注:今回の一連のデモでは、ダイナミックダイアログシステムの作業を行いますが、EventsタブのDynamic Dialogue部分は使用しません。
2つ目のイベントの目的は、Narration(ナレーション)を再生するときは常に、必ずシーケンスコンテナの最初のサウンドから再生されるようにすることです。セーフティーネットのようなものです。
あとは、サウンドバンクを作成して、2つの新しいイベントを入れるだけです。トップバーの「Layouts」でSoundBankレイアウトを選択するか、F7を押します。SoundBank ManagerでNewを選択し、新しいサウンドバンクを作成します。私はこれをMainと呼ぶことにします。
SoundBank Managerで新しいサウンドバンクを選択し、Play_MissionBriefingイベントとReset_MissionBriefingイベントの両方を、Event ViewerからSoundBank Editorにドラッグします。
SoundBank Managerで、新しいサウンドバンク、プラットフォーム(Windows、Macなど)、プロジェクトのためにつくった言語の、それぞれのチェックボックスにチェックを入れます。最後に「Generate Selected」ボタンを押します。
Wwise - ダイナミックダイアログ
プレイヤーは、指揮官からミッションを与えられた後、ミッションをクリアする時間を与えられます。プレイヤーが、どのようにミッションをクリアしたかによって、指揮官の返答も変わってきます。始める前に、プレイヤーに対する指揮官の発言を決定する変数をマッピングします。まず、プレイヤーに与えられた3つの目的を見てみましょう。
1. ハードルを越える。
2. アーチをくぐる。
3. 赤の道ではなく、緑の道を進む。
このミッションの終わり方は複数あります。正確には5通りです。
1. プレイヤーは何もしない。(失敗、失敗、失敗)
2. プレイヤーは「ハードルを越える」だけをクリアする。(成功、失敗、失敗)
3. プレイヤーは「ハードルを越える」と「アーチをくぐる」をクリアする。 (成功、成功、失敗)
4. プレイヤーは「ハードルを越える」と「アーチをくぐる」をクリアし、赤の道を進む(成功、成功、赤)
5. プレイヤーは「ハードルを越える」と「アーチをくぐる」をクリアし、緑の道を進む(成功、成功、緑)
つまりハードル、アーチ、道の3つの変数を追跡することになります。ここで登場するのが、スイッチやステートです。
まだSoundBankレイアウトを開いている場合は、「Layouts -> Designer」を選択するか、F5を押して、Designerレイアウトに戻ります。
Project ExplorerのGame Syncsタブを開き、SwitchesセクションのDefault Work Unitに、3つの新しいスイッチグループとして、Hurdle(ハードル)、Archway(アーチ)、Path(道)を作成します。HurdleとArchwayの両スイッチグループに、Fail(失敗)とPass(成功)の2つの新しいスイッチを作ります。Pathスイッチグループには、Fail、Red(赤)、Green(緑)の3つの新しいスイッチを作ります。
Audioタブに戻ります。Actor-Mixer Hierarchyで、Default Work Unitの下に新しいスイッチコンテナを作ります(Ctrl + Shift + Alt + W)。私はこれをMissionResultと名付けます。
新しく作成したスイッチコンテナを選択し、Property Editorの右側にある「Switch」の設定を見ます。このコンテナでは、まず初めに、プレイヤーがHurdleテストに成功したか失敗したかを確認します。失敗した場合は、落胆した指揮官の音声が再生されます。成功した場合は、Archwayテストに進みます。
そのために、まずSwitchのGroupをHurdleグループに設定し、Default Switch/StateをFailに設定します。これにより、下のAssigned Objectsに、PassとFailの2つのスイッチが表示されます。今度はこれらに対応する適切な“オブジェクト”が必要です。
まず、MissionResultスイッチコンテナに、“hurdlefailed(ハードルに失敗)”という条件の新しいオーディオファイルをインポートします。次に、MissionResultコンテナの中に新しいスイッチコンテナを作り、これをArchwayと名付けます。
あとは、この2つをContents Editorから、それぞれのAssigned Objectsに移すだけです。“hurdlefailed”を“Fail”に、“Archway”コンテナを“Pass”に移動します。
今度は、Archwayスイッチコンテナの中で、この手順を繰り返します。SwitchのGroupをArchwayスイッチグループに設定し、Default Switch/StateをFailに設定します。“archwayfailed(アーチに失敗)”のオーディオをArchwayコンテナにインポートし、Pathという名前のスイッチコンテナを新しく作成します。あとは、先ほどと同じようにAssigned Objectsに入れるだけです。“archwayfailed”を“Fail”に、“Path”コンテナを“Pass”に入れます。
最後にもう一度この手順を繰り返しますが、スイッチの数は2つから3つに増えています。Pathスイッチコンテナの中で、SwitchのGroupをPathスイッチグループに設定し、Default Switch/Stateは今回もFailに設定します。
あとは、最後の3つのオーディオファイルpathfailed(道に失敗)、pathred(赤の道)、pathgreen(緑の道)をインポートするだけです。3つとも、対応するAssigned Objectsに入れます。
それでは、今の内容を簡単にまとめます。Hurdleテストに失敗すると、他のスイッチコンテナは表示されず、「ハードルに失敗」のメッセージが再生されるだけです。成功すると、Archwayテストに進みます。Archwayテストに失敗すると、Pathスイッチコンテナは表示されず、「アーチに失敗」のメッセージが再生されます。Archwayテストに成功すると、最後のPathスイッチコンテナへと移動します。ここで、赤の道に進んだか、緑の道に進んだか、どちらの道にも進まなかったか、確認します。3つの条件のいずれに合致するかに応じて、対応するオーディオファイルが再生されます。
以上が完了したら、イベントの作成は簡単です。イベントを、Eventタブではなく、Audioタブで作成します。MissionResultスイッチコンテナを右クリックして、New EventからPlayを選択してください。これにより、Play_MissionResultという名前の新しいPlayイベントが自動的に作成されます。
F7を押してSoundBankレイアウトに戻り、新しいPlay_MissionResultイベントを、Event ViewerからSoundBank Editorにドラッグします。先ほどと同じようにサウンドバンクを生成したら、必ずWwiseプロジェクトを保存してください。
Unity - シーケンスのNarrationの統合
次にシーケンスコンテナで作ったナレーションを統合していきます。通常であれば、ナレーションのオーディオを5回連続して再生するだけの簡単な作業です。でもセグメントが終わるタイミングが分からなければ、次のセグメントの再生を開始できません。各セグメントの間に、わずかなディレイを入れたい場合もあります。この2つを実現する1つの方法として、イベントコールバックとコルーチンを活用する方法があります。
最初にバンクをロードする必要があります。初めに、HierarchyでWwiseGlobalオブジェクトを選択します。InspectorでAdd Componentを選択し、AkBankを検索します。これをWwiseGlobalに入れたら、「Name:」リストからサウンドバンクを選択します。
次に、ナレーションを再生させる新しいスクリプトが必要です。Hierarchy内で、右クリックして「Create Empty」を選択し、新しい空のオブジェクトを作成します。新しいオブジェクトには好きな名前をつけることができますが、私は「Narration」と名付けます。Inspectorの「Add Component」をもう一度クリックし、検索フィールドに「Narration」と入力します。「New Script」を選択し、最後に「Create and Add」を選択します。これで新しいスクリプトが作成され、メインのAssetsフォルダに追加されます。「Project」タブで、新しく作成したスクリプトを選択します。
このスクリプトの主な目的は、プレイヤーがシーンに入ってきたときに最初ののナレーションオーディオを再生し、それが終了したら、短いディレイの後に次の音声を再生させることです。シーケンスコンテナ内の全オブジェクトが再生されるまで、これが繰り返されます。
では、Start関数のすぐ上に配置されている3つのメンバー変数について説明します。
1. Wwiseで作ったPlay_MissionBriefingイベントを、Narrationイベントとします。publicにするのは、Inspectorからこのイベントを設定するためですが、これはスクリプトを完成させた後に行います。
2. Unityにリスナーの位置を知らせるため、Playerゲームオブジェクトを使用します。publicにするのは、InspectorからPlayerゲームオブジェクトを設定するためですが、これはスクリプトを完成させた後に行います。
3. スクリプトに、再生されたオーディオファイル数を知らせるため、NarrationのInteger(整数)を使用します。この数値が5に達すると、スクリプトはオーディオファイルの再生をやめます。
以上の設定が済んだら、スクリプトを少し整理し、新しい関数を作成しましょう。Update関数は必要ないので削除します。代わりに、新しくPlayNarration関数を作成します。
この関数でナレーションのオーディオを再生する前に、スクリプトがすでにシーケンスコンテナ内のすべてのオーディオファイルを再生したかどうかを確認する必要があります。シンプルな「if」文で、それを実行します。再生済みの回数が5回未満の場合にのみ、このオーディオを再生します。使用するコーディング用語は、NarrationのIntegerです。
ここでようやく、if文の中で、ナレーションオーディオを再生するためのイベントをポストします。出だしは次の通りです: narrationEvent.Post(player)
この時点では、イベントをプレイヤーにポストしている状態です。それだけであれば問題ありませんが、今回はこのイベントが終わった後に、別のイベントをポストしたいと考えています。そこでこれを、イベントコールバックを使って行います。このスクリプト行の続きを以下に示します。
AkCallbackTypeは、このイベントで使用するコールバックを表しています。コールバックには様々なタイプがあり、そのほとんどが音楽イベントに関するものですが、ここではイベントの終了をコールバックに追跡させます。
AkCallbackTypeの先頭にある(uint)は、AkCallbackTypeの結果を1つの値からuintに「キャスト」していることに注意してください。これは単に関数の要件を満たすためのもので、Callbackスクリプトを作成する際には必ず使用されます。
この関数の最後の部分(NarrationEnd)は、Unityに「このイベントが終わったら、この関数を実行してほしい」と伝えています。今はまだ、その関数が存在しないため、NarrationEndでエラーが返されます。さっそく作りましょう!
なお、NarrationEndの括弧内のオブジェクトはすべて必要です。これは、イベントコールバック関数が、ポストされたイベントの情報や使用されたコールバックのタイプなど、いくつかの情報をNarrationEndに送信しているためです。これらの情報はいずれも今回のデモでは使いませんが、必要に応じて利用できます。
NarrationEnd関数の説明に移る前に、PlayNarration関数に最後にもう1つ追加する必要があります。narrationEvent.Postの直後に次の行を追加します。
数字の変数の後に++をつけると、その変数を1ずつ「インクリメント」することになります。
これで、イベントをポストし、そのイベントの終了を待って何かをするという、1つのハードルを乗り越えました。次に、イベントが終了してから次のナレーションイベントをポストするまでの、ディレイを追加する必要があります。コルーチンでこれを実現します。NarrationEnd関数の下に、新しい関数を作ってみましょう。
- IEnumeratorの関数(コルーチン)は、関数を実行する前にバックグラウンドで時間を経過させます。特定の時間だけ関数を遅らせるのに最適です。
- yield return new WaitForSeconds(1f)は、yieldに続いてコードを実行するまでの待ち時間を、Unityに伝えるためのものです。ここでは、Unityに1秒待つように指示しています。
- 最後に、PlayNarration()関数にループバックします。
この新しいコルーチンを、NarrationEnd関数の中で呼び出します。これは、一般的な関数呼び出しとは異なる方法で実現されています。
StartCoroutine(Wait());
PlayNarration関数に戻ると、Unityは再生するナレーションが残っているかどうかをチェックします。残っている場合、次のナレーションを再生し、ループを最初から繰り返します。残っていない場合は、ループが止まり、ナレーションが正式に終了します。
これを完成させる前に、さらにいくつかのことをする必要があります。以下を、Start関数に追加します。
これで、PlayNarration()ループを開始する前に、Resetイベントがポストされます。
最後にUnityに戻り、Inspectorで、Narration (Script)の新しいNarration EventフィールドとPlayerフィールドに、以下の項目を追加します。
ここまでの進捗を保存したら、Playを押して作業の結果を確認してみましょう。期待通りの結果が得られなかった場合は、下記の私のスクリプトとあなたのスクリプトを比較してみてください。
Unity – Mission Briefingの統合
ミッションの説明を受けたプレイヤーは、いよいよミッションに赴きます。ゲームは、プレイヤーが実際にハードルを越え、アーチをくぐり、緑または赤の道に進んだかを把握する必要があります。そこでハードル、アーチ、および両方の道に、ボックスコライダーを設けます。
キューブオブジェクトを作成し、これを実現します。Hierarchy内で、右クリックして「3D Object -> Cube」を選択し、4つの新しいキューブオブジェクトを作ります。ハードルの上の、プレイヤーが絶対にぶつかるであろう場所にキューブを1つ置き、アーチの下にも同じように置きます。最後に、残りの2つのキューブを、緑と赤の道の真上に配置します。これらのキューブは、移動(w)、回転(e)、スケール(r)の各ツールを使って自由に調整してください。
各キューブで、必ずMesh Rendererコンポーネントの左側にあるチェックボックスのチェックを外し、メッシュレンダラーを無効にしてください。これにより、キューブが見えなくなります。次に、Box Colliderコンポーネントで、「Is Trigger」のチェックボックスにチェックを入れます。これにより、プレイヤーはキューブを通過し、イベントを「トリガー」できるようになります。
ハードルのキューブとアーチのキューブについては、InspectorのAdd Componentをクリックし、AkSwitchスクリプトを探して選択します。次に同じ手順で、AkTriggerEnter コンポーネントを、両方のキューブに追加します。
まず、それぞれのAkSwitchで、Trigger Onを「AkTriggerEnter」に変更します(そしてStartの選択を外します)。次に、「Use Other Object」のチェックボックスにチェックを入れます。これを有効にすると、スイッチ変更のトリガーを起動するオブジェクトを指定することができます。この場合、Ak Trigger Enterコンポーネントの「Trigger Object」が、トリガーを起動するオブジェクトとなります。あとは、それぞれのAkTriggerEnterで、プレイヤーのオブジェクトを、HierarchyからTrigger Objectフィールドにドラッグするだけです。
最後に、各オブジェクトに適切なスイッチ名を選択します。ハードルのオブジェクトは“Hurdle / Pass”、アーチのオブジェクトは“Archway / Pass”とします。
最終的には以下のようになります。
プレイヤーがこれらのキューブに入るたびに、適切なAkSwitchが「トリガー」され、FailからPassに切り替わります。切り替わりは、これらのトリガーオブジェクトを、プレイヤーが通過した場合にのみ発生します。これは、AkTriggerEnterのコンポーネントで、トリガーを起動する唯一のオブジェクトとして、プレイヤーを設定したためです。
次に、赤の道と緑の道のキューブに移ります。これらのコライダーでは、別の処理を行います。単純にそれぞれの道にスイッチを設定するのではなく、プレイヤーがどちらかの道に足を踏み入れたら、すぐにミッションが終了するようにします。それには新しいスクリプトを作成する必要があります。
どちらかのパスのInspectorに移動し、新しいコンポーネントを作成します。「MissionEnd」などのスクリプト名を入力し、New Scriptを選択し、Create and Addを選択します。このスクリプトは必ず、もう一方のパスにも追加してください。
この新しいスクリプトに、2つのメンバー変数を新たに追加します。
- boolは、あるものが真か偽かを単純に追跡する変数です。今回の場合は、ミッションが完了したかどうかを追跡しています。
- Wwise Switchは、プレイヤーが進む道に応じて、Path(道)スイッチグループをRedまたはGreenに設定します。Wwise Switchはスクリプト完成後にInspectorで設定します。
次に、Start関数とUpdate関数を削除します。代わりに、次の実行内容の関数を新しく追加します。
- OnTriggerEnterはUnity特有の関数です。(コライダー付きの)何かが、オブジェクトのトリガーゾーンに入ったかどうかをチェックします。入った場合は、この関数が、トリガーを起動したオブジェクトを“other”変数で追跡します。
- 次に、ミッションが「完了していない」かどうかを確認します。完了していない場合は、Mission Complete変数をtrueに設定します。これは、このスクリプトが1度しか再生されないようにするためです。
- 続いて、プレイヤーが選んだ道に応じて、Switchの値をRedまたはGreenに設定します。
- 最後に、“Play_MissionResult”イベントを“other.gameObject”にポストします。この場合“other.gameObject”はプレイヤーです。
Unityに戻り、Inspectorで赤と緑のパスのコライダーを確認します。必ず赤の道にはPath / Redスイッチ、緑の道にはPath / Greenスイッチを設定します。それでは、テストしてみましょう。ミッションの説明を聞いて、赤か緑の道を進んでミッションを完了します。
今のところ、これはゲームで実行可能です。しかし問題があります。今、利用できるエンディングは、プレイヤーが「赤い道」を進んだときと「緑の道」を進んだときの2つだけです。Wwiseでは、他に3つのエンディングを設定しました。
これを解決するために、最初のナレーションが終わった後に、5秒のタイマーを設定します。プレイヤーがどちらかの道にたどり着く前に5秒タイマーが終了すると、ミッションは自動的に失敗となり、プレイヤーの達成度に応じてMission Resultのナレーションが再生されます。これは、最初のUnityのセクションで作ったNarrationスクリプトで実現できます。
Narrationスクリプトを開き、新しいコルーチンを作ります。
- 最初のコルーチンと同じように、このスクリプトの関数を一定時間、今回は5秒間、一時停止させます。
- その後、MissionEndスクリプトでMissionComplete変数を参照します(これはMission Complete変数がpublicの静的メンバー変数の場合のみ可能です)。まず、プレイヤーがどちらかの道に進んでミッションが終了していないかを、確認します。
- プレイヤーがまだミッションを終了していない場合は、MissionComplete変数をtrueに設定して、ミッションが終了したことを示します。これによりプレイヤーは、5秒タイマーが終了した後に道に入り、もう一度ミッションを完了することができなくなります。
- 最後に、MissionResultをプレイヤーに再生します。
あとは、PlayNarration()関数の最後に、StartCoroutine(Wait2())を追加するだけです。それでは、保存してテストしてみてください。5種類のシナリオを1つずつ試して、それぞれが正常に動作するかどうかを確認してみましょう。問題があれば、何か見落としがなかったかどうか、自分の作品をもう一度確認してみてください。正常に動作したら、おめでとうございます!うまく機能するダイナミックなダイアログシステムの構築に成功しました。
来週は、WwiseとUnrealを使ったダイアログの実装について書きますので、お楽しみに。
コメント