Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support 'prepare' phase before the scene load #456

Merged
merged 16 commits into from
Aug 15, 2023
Merged

Conversation

yu-ogi
Copy link
Contributor

@yu-ogi yu-ogi commented Jul 26, 2023

このpull requestが解決する内容

  • g.Scene のアセット読み込み後、任意の非同期処理を行うための prepare をサポートします。
  • g.Scene#vars を追加します。この値はゲームエンジンのロジックからは使用されず、ゲーム開発者が任意の値を代入することができます。

コードイメージは以下のようになります。

const scene = new g.Scene({ ... });
scene.onLoad.addOnce(() => {
  console.log(scene.vars.data);
});

g.game.pushScene(scene, {
  prepare: done => {
    someAsyncFunc()
      .then((data)=> {
        scene.vars.data = data;
        done();
       })
      .catch(_error => done());
  }
});

破壊的な変更を含んでいるか?

  • なし

sequence.push("scene1 prepared");
done();
},
100 // この値にとくに根據は無い
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

全くどうでもいいんですが、旧字体の「據」よりは常用漢字の「拠」にしときませんか。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a824ca7 ミスでした。修正済みです。

src/Game.ts Outdated
Comment on lines 230 to 231
preserveCurrent?: boolean;
prepare?: (done: () => void) => void;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このあたりのプロパティにもコメントつけておきたいです。(ここにないと API リファレンスに prepare の説明が出るところがなさそうです)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fca39e8 こちらコメント追加しました。

src/Game.ts Outdated
this._doPushScene(req.scene, false);
break;
case PostTickTaskType.ReplaceScene:
// NOTE: replaceSceneの場合、pop時点では_sceneChangedをfireしない。_doPushScene() で一度だけfireする。
this._doPopScene(req.preserveCurrent, false, false);
if (req.prepare) {
this._interruptSceneLoading(req.prepare);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this.loadingScene をここで代入する形になっていますが、よく見ると _doPushScene() が引数で loadingScene 指定できるので、そちらで済まないでしょうか?

Game#loadingScene を書き換えると正しく戻せるかどうかかなり見極めが必要そうなので (特に pushScene() が同期的に複数回呼ばれた場合) 、書き換えなしで済むならその方がすっきりするかと思います。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a84007a おっしゃるとおり_doPushScene() を使うパターンでもテスト上問題なさそうだったため修正しました。

this._doPushScene(req.scene, false);
break;
case PostTickTaskType.ReplaceScene:
// NOTE: replaceSceneの場合、pop時点では_sceneChangedをfireしない。_doPushScene() で一度だけfireする。
this._doPopScene(req.preserveCurrent, false, false);
if (req.prepare) {
this._interruptSceneLoading(req.prepare);
}
this._doPushScene(req.scene, false);
break;
case PostTickTaskType.PopScene:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_doPopScene() の中にもローディングシーンを積み直すパスがありますが、この時 prepare が呼ばれるか確認したいです。(難儀そうな気がしています)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

popScene() の際にも prepare を呼ぶ (待つ) べきでしょうか。念のため確認しておきたいです。

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

はい。当初考えていませんでしたが、状況によって待つ必要がありそうに思えています。(念のため: これは popScene() に引数を追加して popScene(scene, { prepare }) にするという話ではありません)

具体的には次のような状況を気にしています。

const scene1 = new g.Scene({ ..., assetIds: ["foo"] }); // なんらかのアセット読み込みが必要なシーン
const scene2 = new g.Scene(...); // 適当なシーン
g.game.pushScene(scene1, { prepare: (done) => { someAsyncOp().then(done); } });
g.game.pushScene(scene2);
g.game.popScene();

ロード中のシーン (scene1) のロード完了を待たずに次のシーン (scene2) が push された状況では、pop して scene1 に戻る時にローディングシーンを積み直してロード完了を改めて待つ必要があります。(過去、この「pop 時に発生するロード待ち」ができておらず不具合となったことがあります) このとき、prepare が指定されているなら呼ばないと不味そうです。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

なるほど、確かにシーンロード中に新たに別のシーンをロードするケースは想定できていませんでした。対応を考えます。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4e782df にて対応しました。
g.Scene#_currentPrepare を新設し、g.Game#popScene() 時に g.Game がその中身を参照するように修正しています。

src/Scene.ts Outdated
return (
this._sceneAssetHolder.waitingAssetsCount > 0 ||
(!!this._storageLoader && !this._storageLoader._loaded) ||
!!this._currentPrepare
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

読み込むアセットの無い scene に対して prepare を設定していた場合に prepare を待たず onLoad が発火してしまっていたため条件を変更しました。

src/Game.ts Outdated
});
// ローディングシーンを保持するためクロージャを許容
loadingScene.onTargetReady.addOnce(() => {
const done = loadingScene.end.bind(loadingScene);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

これ、 prepare が外部の非同期処理を想定する関係上、 done が呼ばれた時に「 SceneGame が既に破棄されていた状況」を想定しなければならないことに気づきました。

loadingScene#end そのまま渡すのではなく、「" Game が死んでいたら何もせず抜ける" などチェックをした上で end を呼ぶ関数」を使う必要がありそうです。

(類例として、非同期処理後に逐一破棄チェックしている GameDriver があります。あちらは正確には 例外で強引に処理を潰してる のでいい状態でもありませんが)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

49bd624 こちら対応しました。

src/Game.ts Outdated
explicitEnd: true,
name
});
// ローディングシーンを保持するためクロージャを許容
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

今更ですがこれコメント正しいでしょうか。クロージャで保持してるものは prepare のように見えます。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

89d0fe9 こちら実態に沿うようにコメント修正しました。

src/Scene.ts Outdated
@@ -961,5 +980,6 @@ export class Scene implements StorageLoaderHandler {
if (this._loadingState === "loaded-fired") return;
this.onLoad.fire(this);
this._loadingState = "loaded-fired";
this._currentPrepare = undefined; // TODO: 本来は _currentPrepare に値を代入する側 (e.g. g.Game) でクリアすべき
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO コメントになっていますが、これ _createPreparingLoadingScene()prepare の代わりに Scene を引数にとり、 onTargetReady のハンドラで prepare を呼ぶ直前にクリアすればよいのではないでしょうか。

これには二つの意味があるように思えています:

  • a) _currentPrepare を触る箇所を (Game に) 限定できて嬉しい (TODO コメントどおり)
  • b) prepare() を呼ぶ前に _currentPrepare をクリアしないと、二重に呼ぶ懸念が残りそう

後者が問題になるケースとして考えているのは「 prepare() の呼び出し後 done() で返るまでの間にまた pushScene()popScene() が走った時」です。(アセットロード中に pushScene() されるケースを織り込む一方で、 prepare 中のそれを想定しないのは片手落ちなので)

ただこのケースは、真面目に考慮しはじめると大掛かりになりすぎる気もします。どちらかというと (もともとエッジケースを十分に考慮できていたとは言えない) explicitEnd の問題修正を始めることになってしまうので……。この PR では一旦「 prepare 中にシーンスタックを操作してはいけない」という制限事項を加えて済ませるのも手かと思います。

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

備忘: 危うそうなところ

prepare() の呼び出し後 done() までの間にまた pushScene() され (a)、さらにその後 popScene() された (b) 時、

  • i) (b) 時点で、 Scene#_currentPrepare が残っているともう一回 prepare を呼びそう
  • ii) (a) 時点では「 Game も対象の Scene も生きているが、 自分 (LoadingScene) がスタックトップにいない」ので、ここで done() されるとまずそう
    • done()LoadingScene#end() を呼び、そこで popSceneRaw() が走ってスタックトップから自身 (ローディングシーン) を取り除こうとする=おそらくスタックが壊れる
    • しかし逆に LoadingScene#end() しないと、「 prepare が走って done() も呼ばれた」対象のシーンの onLoad を呼ぶ契機がなくなる

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • _createPreparingLoadingScene()sceneprepare を指定するように修正しました。
    • 呼び出し箇所が複数あり scene._currentPrepare への代入処理をまとめるため引数を分けました
  • prepare 中にシーンスタックを操作してはいけない という旨のコメントし、一旦は上記を制限事項とする方針にしました。

src/Game.ts Outdated
prepare(done);
} else {
// NOTE: 異常系ではあるが prepare が存在しない場合は loadingScene.end() を直接呼ぶ
this._postTickTasks.unshift({
Copy link
Member

@xnv xnv Aug 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

これ unshift が必要でしょうか? _pushPostTickTask() でよさそうですが。どっちみち相当な異常系なのでこだわりませんが、事情があるならコメントしておきたいように思います。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

04ff1b3 こちら修正しました。

Copy link
Member

@xnv xnv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

コメント加えていますが approved.

@yu-ogi yu-ogi merged commit ae7c647 into main Aug 15, 2023
12 checks passed
@yu-ogi yu-ogi deleted the support-scene-prepare branch August 15, 2023 07:16
@yu-ogi yu-ogi mentioned this pull request Aug 15, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants