NarraLeaf

Surface

画面。2種類の Surface、ナビゲーションスタック、進入と退出のライフサイクル、そしてゲームステージがそれらからどう組み立てられるか

Surface とは1つの画面のことです——ルート要素、デザインサイズ、そしてその下の要素ツリーからなります。ゲーム内のあらゆる画面は Surface です——タイトルメニュー、設定パネル、ストーリーが話しかけてくるダイアログボックス。プロジェクトはそれらすべてを1つの UI ドキュメントにまとめて保持し、ランタイムは2通りの方法でそれらを表示します——ナビゲーションスタックに積み重ねるか、実行中のゲームステージのスロットに挿し込むかです。

画面上の重なる順序と、ゲームの開始、ページを開いたとき、ゲームの終了でそれがどう変わるかは、実行時のインターフェース で順を追って図示しています。このページはその仕組みのリファレンスです。

2つの種類

種類hostどこに表示されるかこういうときに使う
appSurfaceappページスタック上のページとして、またはページの上のレイヤーとしてプレイヤーが開いたり閉じたりする画面が必要なとき。タイトル、設定、セーブ、ギャラリーなど
stageSurfaceplayer実行中のゲームステージの1つのスロットに実行中のゲーム自体のインターフェースの一部を置き換えたいとき——ダイアログボックス、選択肢メニュー、通知スタックなど

3つ目の種類はありません。どちらのレコードも id、name、designSize、rootElementId、任意の settings(背景色、背景画像、ページアニメーション)、そして任意の actions(その Surface が応答するプロジェクトの入力アクション)を持ちます。stageSurface はさらに自分のスロットを指定する mount を持ち、ステージの上に重なるため背景が透明な状態で作成されます。

Studio の UI マネージャーはこれらを Page と Game UI と表記し、ノード名もその表記に従います——Go Page は appSurface を開きます。

デザインサイズ

designSize はプロジェクトではなく、各 Surface に属します。これはあなたが編集の際に使う座標系です——レイアウトはデザインピクセルで記述され、ランタイムはツリー全体をウィンドウに合わせて拡大縮小し、アスペクト比を保つためにレターボックスを入れます。

  • アクティブな Surface のデザインサイズがビューポートの枠を決めます。エントリー Surface のデザインサイズは、ゲームウィンドウの初期サイズでもあります。
  • Page を作成するとキャンバスサイズの入力欄が表示され、ドキュメント内の既存の Surface から初期値が入ります。Game UI を作成する場合はサイズ欄がありません——ステージと座標系を共有しなければならないため、プロジェクトのデザインサイズをそのまま使います。

NarraLeaf のステージは、ゲームセッションがマウントされた瞬間にアクティブだった Surface のデザインサイズで構築されます——プロジェクト設定からでも、スロットを埋める Game UI の surface からでもありません。プレイヤーが Start Game を押したその Page がたまたま持っていたデザインサイズを、ステージはそのまま受け取ります。

ナビゲーションスタック

ナビゲーションは 2 つのスタックからなります。Go Page が開くページと、Show Layer が表示するレイヤーです。レイヤーは常にすべてのページより上に描画されます。どちらのスタックの項目も Surface そのものではなく Surface のインスタンスであり、自分自身の props とランタイムスコープを持ちます。同じ Surface を2回開くと、何も共有しない2つの独立したインスタンスができます。

  • 起動時、ページスタックには入口ページが 1 つだけあります。
  • 開くと積まれます。 Go Page は選択した Page を、その入力が渡すパラメータと props とともにページスタックにプッシュし、新しいページが画面上のページに取って代わります。Replace Page は同じようにページを開き、置き換えたページをスタックから取り除くため、Go back も None にした Go Page もそのページには戻りません。Show Layer はレイヤースタックにプッシュし、その表示を指し示すハンドルを返します。Hide Layer、Wait For Layer、Is Layer Mounted が扱う値がこれです。それ以外にレイヤーを指し示す方法はありません。重なる順序はマウント順そのものであり、特定の深さに位置することに依存するような画面は書けません。レイヤーは、その後に開かれたページも含め、すべてのページの上に位置します。
  • モーダルレイヤー。 モーダルレイヤーは、その下にあるすべてのものから入力を取り上げ、キーボードを持ちます。モーダルでないレイヤーは、その下のページを操作可能なままにします。同じグループを指定したレイヤーは積み重ならずに順番待ちをし、レイヤーはそれを表示した Surface が閉じると自動的に閉じます。
  • 閉じ方は5通り。 Go back は、最上位のレイヤーが閉じられるものであればそれを閉じ、そうでなければちょうど1つのページをポップします。Page セレクタを None にした Go Page は、ページスタックを最初のページまで空にします。Clear Page は実行中のゲームの中で開いたページだけを閉じ、ゲームの外では何もしません。Hide Layer はそのハンドルが指すレイヤーを閉じ、Close This Layer は実行中のブループリントがその内側にいるレイヤーを閉じます。それを待っている相手への結果を添えることもできます。
  • 底は残ります。 Go back、None にした Go Page、Clear Page は、ページスタックの最初のページを閉じません。ゲームの起動時は入口ページ、Quit Game の後は Quit Game が開いたページです。このページが画面に出ている間に Replace Page を実行すると、そのノードが開いたページが代わりに最初のページになります。スプラッシュページはこの方法でタイトルページに引き継ぎます。スタックの底で Go back を呼んでもエラーにはならず、何もしないだけです。シグネチャは App ノードにあります。

表示方式

すべてのページは appPage か gameOverlay のどちらかです。これは作者ではなく、ランタイムが選びます。

  • ゲームが実行されていない間、Go Page は appPage ページを開きます。描画されるのは一番上のページだけで、その下のページは Go back のためにスタックに残ります。
  • Start Game と Load Save は、現在スタックにあるすべてのページを隠し、すべてのレイヤーを閉じ、ゲームステージを表に出します。スタックはページを保持し続けますが、単に描画されなくなるだけです。
  • スタックが隠れている間、Go Page はステージの上に描かれる gameOverlay ページを開きます。これらの下にあるのはゲームであり、あなたが来た元のページではありません。ポーズメニューやゲーム内セーブ画面、シーン途中の設定画面がこれにあたります。こうしたページが画面に表示されている間、dialog、nvl、choice、onStage スロットの Surface はマウントされたままフェードアウトし、そのページが閉じるとフェードインして戻ります。notification スロットはそのまま残ります。レイヤーはこれらを隠しません。
  • レイヤーも表示されたときに同じように区別されます。スタックが隠れている間に Show Layer で表示したレイヤーは gameOverlay、それ以外のときは appPage です。
  • gameOverlay のページやレイヤーは、自分の背景をわずかに透明にして描き、シーンが透けて見えます。その上のウィジェットの不透明度は変わりません。
  • Quit Game はセッションを解体し、戻り先の Page を再び appPage として開き、その下のページをすべて閉じます。他の Game ノードと一緒に置かれています。Quit Application はランタイム自体を終了させます。Dev Mode では Studio を閉じるのではなく Dev Mode セッションを停止し、意図的にウィンドウクローズイベントを発火させません。このイベントはプレイヤー自身がウィンドウを閉じたときのためだけのものです。

ページやレイヤー自身のブループリントでは、Is Game Overlay はこのフラグを読み取ります。この値はインスタンスに固定されるため、退出中のポーズメニューはセッションがすでに片付けられていても、退出アニメーションの間ずっと overlay という答えを保ち続けます。すべてのステージスロット Surface は true を返し、overlay の中に埋め込まれた Page はそれを保持するページの答えを継承します。App logic(アプリロジック)はどのページにも属さず、そこではこのノードは実行中のゲームがページかモーダルレイヤーに覆われているかどうかを返します。Is Game Overlay を参照してください。

下にあるページが隠されたものの1つであるときに gameOverlay を閉じると、退出アニメーションが再生されてステージが表に出ます。下で何も描画されていないため、何も入場しません。

ライフサイクル

1つのページまたはレイヤーについて、順を追うと:

  1. 隠れた状態でマウントされ、一度描画されます。ウィジェットの Init はこの隠れた描画の間に実行されるため、最初のフレームの変更は何かが見える前に落ち着きます。
  2. プリペイントが終わる → Surface Init。これはランタイムスコープごとに一度発火します。同じ Surface を再び開くと新しいスコープになるため、再び発火します。
  3. 進入アニメーションが再生されます。
  4. After Surface Enter——アニメーションが完了した後。アニメーションがない場合や reduced motion の場合は、プリペイントの直後に続きます。内部のどの経路が完了を報告したかにかかわらず、1回の進入につき正確に1度です。
  5. Before Surface Exit——退出アニメーションが始まる前。
  6. 退出アニメーションが再生され、アンマウントされる → Surface Unmount。

3つの純粋ノードは、同じことをエッジではなく状態として報告します。

ノードtrue になる期間
Is Surface Enteringマウントから進入アニメーションが完了するまで
Is Surface ExitingBefore Surface Exit からアンマウントされるまで
Is Surface Transitioning上記のいずれかが成立している間

この状態は、対応するイベントヘッドが実行される前に切り替わります。After Surface Enter の内部では Is Surface Entering はすでに false を返し、Before Surface Exit の内部では Is Surface Exiting はすでに true を返します。今何が起きているかを尋ねるにはこれらを読み、いつ起きたかにはイベントヘッドを使ってください。アニメーションレイヤーがまったくない環境では、3つとも false を返します。

Before Surface Exit と After Surface Enter は、マウント済みウィジェットの private ブループリントにも届きます——ただしその瞬間に生きているものだけです。Surface Init と Surface Unmount は届きません——これらは Surface に属するものだからです。ウィジェット自身の対は Init と Unmount です。すべてのヘッドは Events ノードに一覧されています。

ページとレイヤーは、表示された瞬間から進入アニメーションの間もポインタ入力とキーボード入力を受け取り、退出アニメーションが始まるとどちらも受け取らなくなります。まだ進入中の間に入力を無視しなければならないグラフは、自分で Is Surface Entering によって分岐する必要があります。退出アニメーションを再生している間、ほかのページやレイヤーが受け取らなかったクリックは、ゲームステージに届かずに破棄されます。イベントのバブリングを参照してください。

Surface、Page、Frame

nl.frame は、別の app Surface を現在の Surface の中に描画するウィジェットです。それが表示するものが Page です。

Page は役割であって、別のドキュメント種別ではありません。同じ appSurface が、ある場所ではページやレイヤーであり、別の場所では Frame の中の Page でもあり得ます。Frame は iframe でもありません。対象は同じレンダラーで、同じドキュメントの中で描画されます。

  • 対象は別の app Surface でなければなりません。Frame を自分自身、Game UI、存在しない Surface、または Frame を保持している Surface に巡り戻るものに向けることは拒否され、Frame は描画を壊す代わりにプレースホルダーを表示します。
  • 各 Frame インスタンスは、親スコープ、Frame 要素、対象から導かれる自分自身のランタイムスコープを持ちます。同じ Page を表示する2つの Frame は、surface state もウィジェットの locals もライフサイクルも共有しません。埋め込まれた Page は、Frame が最初にそれをマウントしたときに自分自身の Surface Init を発火し、対象が変わるか Frame が消えたときに Surface Unmount を発火します。

値は、Page がプロパティで宣言するパラメータを通じて Page に届きます。ページのパラメータを参照してください。

  • ページとレイヤーの場合、値は Go Page、Replace Page、Show Layer、Show Confirm が渡します。最初の3つには、選んだページのパラメータごとの入力が増えます。
  • Frame の params プロパティは、埋め込まれた Page の props そのものです。Frame の Properties には、対象の Page のパラメータごとのフィールドが表示されます。
  • Page の中では、テキストやボタンが Page param(ページのパラメータ)でテキストのパラメータを表示し、List がリストのパラメータを表示し、Get Page Param が任意のパラメータを読み取ります。渡されなかったパラメータは初期値を読みます。ページ、レイヤー、Frame の中のどこにあっても、同じページは同じ方法でパラメータを読みます。
  • Get Page Props はオブジェクト全体を読み取ります。宣言されたパラメータとその初期値、そして Page props 入力や Frame が渡したほかのフィールドです。オブジェクトでない値は {} に正規化され、存在しないフィールドは null として読まれます。
  • Set Frame Page は対象を切り替え、同じ呼び出しで params を書き込むこともできます。パラメータを渡さず props 入力も未接続のままなら、既存の params が保たれます。Frame ノードを参照してください。
  • 子は Emit Page Event で話しかけ返し、それは Frame 要素自身のブループリント上の Page Event ヘッドが受け取ります。単独で表示されたページやレイヤーには親 Frame がないため、そこで発行しても誰にも届きません。

ステージスロット

ストーリーの実行中、ステージは NarraLeaf が所有し、Studio はそこにあなたの Game UI surface を渡します。スロットは5つあり、それぞれに1つの有効な Game UI があります。

スロット引き継ぐものここに作るもの
dialogダイアログボックス名前タグと Sentence、加えて行を進めるための仕組み
choice選択肢メニュー{ text, index, disabled } を対象とした Choice List。Item Click を Select Choice に接続する
notification通知スタック{ id, message } を対象とした Notification List
nvl全画面ノベルモードのダイアログ{ nametag, isActive, index } を対象とした NVL List、加えて NVL Texts
onStage何も引き継がない——これは追加のレイヤーである実行中のステージの上に重ねる HUD

空のままにしておいたスロットは NarraLeaf 自身の組み込みコンポーネントを保持し続けるため、Dialog surface を削除してもゲームからダイアログボックスがなくなることはありません。スロット選択画面はすでに使われているスロットを無効化します。もしドキュメントが何らかの理由で1つのスロットに対して2つの Surface を持っていた場合、ランタイムはドキュメント順で最初のものを使い、警告を出します。

onStage はこの一覧の例外です。NarraLeaf にはこれのための注入点がないため、Player の子としてステージの上にマウントされます。ステージが表示されている間だけ描画され、その外殻は意図的にクリックスルーになっています——空白部分もストーリーを進められなければならないからです。全画面の対話的コンテナをここに置くと、そうしたクリックを飲み込んでしまいます。

notification スロットは表示専用です。その上のものはクリックを受け取らず、通知の上でのクリックはその下にあるものへ渡され、その Surface ブループリントはキーも入力アクションも受け取りません。

各スロット Surface は、ゲームセッション、スロット、Surface をキーとする自分自身のランタイムスコープで実行され、そのすべての内部で Is Game Overlay は true です。

Surface ブループリント

各 Surface は1つのブループリントを持ち、オーナーは surfaceMain です。その寿命は、それをマウントしたページ、レイヤー、または Frame インスタンスの寿命とちょうど一致します。Surface を2回開けば、そのブループリントも2回実行され、locals と surface state はそれぞれ独立します。

到達できるもの:

  • 自分自身の要素。 Element リテラルは { surfaceId, elementId, elementType } を保持し、構造上同一 Surface に限定されます。他の Surface のツリーに手を伸ばすものは何もありません。
  • 自分自身の surface state。インスタンスのランタイムスコープをキーとするため、同じ Surface の2つのインスタンスが衝突することはありません。スコープと寿命については変数を参照してください。
  • 自分自身のパラメータと props。Get Page Param と Get Page Props を通して。
  • そもそも Surface ごとに分かれていないものすべて——永続変数、グローバル状態、セーブ、実行中のゲーム。

そのイベントヘッド: Surface Init、Surface Unmount、Before Surface Exit、After Surface Enter、Mouse Click、Right Click、On Action、キーボード系ヘッド、ゲーム設定変更系ヘッド、フルスクリーンとウィンドウフォーカスの変更、ウィンドウクローズ要求、ブロードキャスト受信、そしてバインド要素に関する2つのヘッド。

Mouse Click と Right Click は、ウィジェットの場合とここでは挙動が異なります。Surface 上では、その Surface に届いたあらゆるクリックに対して発火し、その Surface のデザイン空間の座標を出力します。一方、ウィジェット版は自分自身の要素とその内側の要素に対して発火します。Page は自分の要素が描かれている場所でしかクリックを受け取らないため、Page の空いている部分でのクリックはその Page のものではありません。Frame に表示されている Page は、自分の要素の上での押下に対して自分自身の Surface の Mouse Click と Right Click を自分のデザイン空間で実行し、続いて Frame を保持している Surface も同じ押下を受け取ります。表示されている Page の空いている部分での押下は、Frame を保持している Surface だけが受け取ります。

ウィジェットレベルのブループリントとの対比:

  • widgetMain ブループリントは1つのウィジェットインスタンスに属し、それとともにマウント・アンマウントされます。Init と Unmount がそのライフサイクルであり、Surface の場合は Surface Init と Surface Unmount です。
  • キーボード系ヘッドは、まず App logic へ、次にキーボードを持っている Surface とその上でそれを宣言しているすべてのウィジェットへディスパッチされます。同じヘッドを3か所すべてに置けば、3か所すべてが実行されます。どの Surface がキーボードを持つかはキーボードを参照してください。
  • Element ノードは、Surface ブループリントが自分の所有しないウィジェットに作用する手段です——これこそが Self / Element の区別が存在する理由のすべてです。

このページの目次