Surface
画面——2種類の Surface、ナビゲーションスタック、進入と退出のライフサイクル、そしてゲームステージがそれらからどう組み立てられるか
Surface とは1つの画面のことです——ルート要素、デザインサイズ、そしてその下の要素ツリーからなります。ゲーム内のあらゆる画面は Surface です——タイトルメニュー、設定パネル、ストーリーが話しかけてくるダイアログボックス。プロジェクトはそれらすべてを1つの UI ドキュメントにまとめて保持し、ランタイムは2通りの方法でそれらを表示します——ナビゲーションスタックに積み重ねるか、実行中のゲームステージのスロットに挿し込むかです。
2つの種類
| 種類 | host | どこに表示されるか | こういうときに使う |
|---|---|---|---|
appSurface | app | ナビゲーションスタック上のレイヤーとして | プレイヤーが開いたり閉じたりする画面が必要なとき——タイトル、設定、セーブ、ギャラリーなど |
stageSurface | player | 実行中のゲームステージの1つのスロットに | 実行中のゲーム自体のインターフェースの一部を置き換えたいとき——ダイアログボックス、選択肢メニュー、通知スタックなど |
3つ目の種類はありません。どちらのレコードも id、name、designSize、rootElementId、そして任意の settings(背景色とページアニメーション)を持ちます。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 がたまたま持っていたデザインサイズを、ステージはそのまま受け取ります。
ナビゲーションスタック
ランタイムはレイヤーの1つのスタックを保持します。レイヤーは Surface そのものではなく、Surface のインスタンスです——自分自身の props、自分自身のランタイムスコープ、自分自身の表示方法を持ちます。同じ Surface を2回開くと、何も共有しない2つの独立したレイヤーができます。
- 起動時、スタックは単一のレイヤーにリセットされます——ホストのエントリー Surface か、ドキュメント内で最初の
appSurfaceです。 - 開くと積まれます。
Go Pageは選択した Page を、そのprops入力に接続した値とともにプッシュします。Show Layerも同様にプッシュし、その表示を指し示すハンドルを返します——Hide Layer、Wait For Layer、Is Layer Mountedが扱う値がこれです。それ以外にレイヤーを指し示す方法はありません——重なる順序はマウント順そのものであり、特定の深さに位置することに依存するような画面は書けません。 - 閉じ方は4通り。
Go backはちょうど1つのレイヤーをポップします。Page セレクタをNoneにしたGo Pageはスタックを空にします。Clear Pageは実行中のゲームが積んだオーバーレイページだけを取り除き、ゲームの外では何もしません。Hide Layerはそのハンドルが指すレイヤーを閉じ、Close This Layerは実行中のブループリントがその内側にいるレイヤーを閉じます——それを待っている相手への結果を添えることもできます。 - 底は取り外せません。 残り1枚のレイヤーになると閉じる操作は拒否されるため、エントリー Surface は常に何かの下に居続けます。スタックの底で
Go backを呼んでもエラーにはならず、何もしないだけです。シグネチャは App ノードにあります。
表示方式
すべてのレイヤーは appPage か gameOverlay のどちらかです。これは作者ではなく、ランタイムが選びます。
- ゲームが実行されていない間、
Go PageはappPageレイヤーを開きます。下から透けて見えるのは直前の Page です。 Start GameとLoad Saveは、現在スタックにあるすべてのレイヤーを隠し、ゲームステージを表に出します。スタックはそれらを保持し続けますが、単に描画されなくなるだけです。- スタックが隠れている間、
Go Pageはステージの上に描かれるgameOverlayレイヤーを開きます。これらの下にあるのはゲームであり、あなたが来た元の Page ではありません——これがポーズメニューやゲーム内セーブ画面、シーン途中の設定画面の正体です。 Quit Gameはセッションを解体し、戻り先の Page を再びappPageとして開きます——他の Game ノードと一緒に置かれています。Quit Applicationはランタイム自体を終了させます——Dev Mode では Studio を閉じるのではなく Dev Mode セッションを停止し、意図的にウィンドウクローズイベントを発火させません。このイベントはプレイヤー自身がウィンドウを閉じたときのためだけのものです。
Is Game Overlay はこのフラグを読み取ります。この値はレイヤーインスタンスに固定されるため、退出中のポーズメニューはセッションがすでに片付けられていても、退出アニメーションの間ずっと overlay という答えを保ち続けます。すべてのステージスロット Surface は true を返し、overlay の中に埋め込まれた Page はそれを保持するレイヤーの答えを継承します。
下にあるレイヤーが隠されたものの1つであるときに gameOverlay を閉じると、退出アニメーションが再生されてステージが表に出ます。何も入場しません——下で何も描画されていないからです。
ライフサイクル
1つのレイヤーについて、順を追うと:
- 隠れた状態でマウントされ、一度描画されます。ウィジェットの
Initはこの隠れた描画の間に実行されるため、最初のフレームの変更は何かが見える前に落ち着きます。 - プリペイントが終わる →
Surface Init。これはランタイムスコープごとに一度発火します。同じ Surface を再び開くと新しいスコープになるため、再び発火します。 - 進入アニメーションが再生されます。
After Surface Enter——アニメーションが完了した後。アニメーションがない場合や reduced motion の場合は、プリペイントの直後に続きます。内部のどの経路が完了を報告したかにかかわらず、1回の進入につき正確に1度です。Before Surface Exit——退出アニメーションが始まる前。- 退出アニメーションが再生され、レイヤーがアンマウントされる →
Surface Unmount。
3つの純粋ノードは、同じことをエッジではなく状態として報告します。
| ノード | true になる期間 |
|---|---|
Is Surface Entering | マウントから進入アニメーションが完了するまで |
Is Surface Exiting | Before 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 ノードに一覧されています。
遷移中、ポインタ入力とキーボード入力は同じようには抑えられません。クリックは進入アニメーションが完了するまでブロックされ、Before Surface Exit で再びブロックされますが、キー入力はプリペイントの終了から両方のアニメーションを通してずっと届き続けます。遷移中にキーを無視しなければならないグラフは、自分で Is Surface Transitioning によって分岐する必要があります。
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を発火します。
params は、値が Page に届く経路です。
- Frame の
paramsプロパティは、埋め込まれた Page の props そのものです。その Page の中のGet Page Propsが読むのはこれです。 - ナビゲーションスタック上のレイヤーの場合、props は
Go Pageが渡したものです。同じノード、同じ読み取り——だからこそ Page はどちらの位置でも、自分がどこにいるかを知ることなく機能します。 - オブジェクトでない値は
{}に正規化され、存在しないフィールドは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 の子としてステージの上にマウントされます。ステージが表示されている間だけ描画され、その外殻は意図的にクリックスルーになっています——空白部分もストーリーを進められなければならないからです。全画面の対話的コンテナをここに置くと、そうしたクリックを飲み込んでしまいます。
各スロット 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 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 のデザイン空間の座標を出力します。一方、ウィジェット版は自分自身の要素に対してのみ発火します。Frame に埋め込まれた Page は、親 Surface がクリックを見るより先に、自分自身の Surface クリックを受け取ります。
ウィジェットレベルのブループリントとの対比:
widgetMainブループリントは1つのウィジェットインスタンスに属し、それとともにマウント・アンマウントされます。InitとUnmountがそのライフサイクルであり、Surface の場合はSurface InitとSurface Unmountです。- キーボード系ヘッドは、グローバルブループリント、アクティブな Surface ブループリント、そしてそれを宣言しているすべてのマウント済みウィジェットへディスパッチされます。同じヘッドを3か所すべてに置けば、3か所すべてが実行されます。
- Element ノードは、Surface ブループリントが自分の所有しないウィジェットに作用する手段です——これこそが Self / Element の区別が存在する理由のすべてです。