diff --git a/CHANGELOG.md b/CHANGELOG.md index 78ec2d2bc..e37631353 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # ChangeLog +# 3.14.1 +* `g.Scene#vars` を追加 +* `g.Scene` のアセット読み込み後に任意の非同期処理を行うための `prepare` をサポート + * `g.Game#pushScene()` に第2引数 `PushSceneOption` を追加 + * `g.Game#replaceScene()` の第2引数を `boolean | ReplaceSceneOption` に変更 + ## 3.14.0 * @akashic/pdi-types@1.10.0 に追従 * `"binary"` アセットに対応 diff --git a/package-lock.json b/package-lock.json index 76062171a..6339c5aa1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@akashic/akashic-engine", - "version": "3.14.0", + "version": "3.14.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@akashic/akashic-engine", - "version": "3.14.0", + "version": "3.14.1", "license": "MIT", "dependencies": { "@akashic/game-configuration": "~1.12.0", diff --git a/package.json b/package.json index c104414cd..d4a8af730 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@akashic/akashic-engine", - "version": "3.14.0", + "version": "3.14.1", "description": "The core library of Akashic Engine", "main": "index.js", "dependencies": { diff --git a/src/Game.ts b/src/Game.ts index cce444463..76a7ec668 100644 --- a/src/Game.ts +++ b/src/Game.ts @@ -94,6 +94,12 @@ interface PostTickPushSceneTask { * 遷移先になるシーン。 */ scene: Scene; + + /** + * 現在のシーンのアセット読み込み後、任意の非同期処理を行うためのハンドラ。 + * prepare 中にシーンスタックを操作してはいけない点に注意。 + */ + prepare?: (done: () => void) => void; } /** @@ -114,6 +120,12 @@ interface PostTickReplaceSceneTask { * 現在のシーンを破棄するか否か。 */ preserveCurrent: boolean; + + /** + * 現在のシーンのアセット読み込み後、任意の非同期処理を行うためのハンドラ。 + * prepare 中にシーンスタックを操作してはいけない点に注意。 + */ + prepare?: (done: () => void) => void; } /** @@ -206,6 +218,32 @@ export interface EventTriggerMap { operation: Trigger; } +/** + * Game#pushScene() のオプション + */ +export interface PushSceneOption { + /** + * 現在のシーンのアセット読み込み後、任意の非同期処理を行うためのハンドラ。 + * prepare 中にシーンスタックを操作してはいけない点に注意。 + */ + prepare?: (done: () => void) => void; +} + +/** + * Game#replaceScene() のオプション + */ +export interface ReplaceSceneOption { + /** + * 現在のシーンを破棄するか否か。 + */ + preserveCurrent?: boolean; + /** + * 現在のシーンのアセット読み込み後、任意の非同期処理を行うためのハンドラ。 + * prepare 中にシーンスタックを操作してはいけない点に注意。 + */ + prepare?: (done: () => void) => void; +} + export type GameMainFunction = (g: any, args: GameMainParameterObject) => void; /** @@ -969,11 +1007,13 @@ export class Game { * このメソッドの呼び出しにより、現在のシーンの `stateChanged` が引数 `"deactive"` でfireされる。 * その後 `scene.stateChanged` が引数 `"active"` でfireされる。 * @param scene 遷移後のシーン + * @param option 遷移時のオプション */ - pushScene(scene: Scene): void { + pushScene(scene: Scene, option?: PushSceneOption): void { this._postTickTasks.push({ type: PostTickTaskType.PushScene, - scene: scene + scene, + prepare: option?.prepare }); } @@ -990,11 +1030,28 @@ export class Game { * @param scene 遷移後のシーン * @param preserveCurrent 真の場合、現在のシーンを破棄しない(ゲーム開発者が明示的に破棄せねばならない)。省略された場合、偽 */ - replaceScene(scene: Scene, preserveCurrent?: boolean): void { + replaceScene(scene: Scene, preserveCurrent?: boolean): void; + /** + * 現在のシーンの置き換えを要求する。 + * + * @param scene 遷移後のシーン + * @param option 遷移時のオプション + */ + replaceScene(scene: Scene, option?: ReplaceSceneOption): void; + replaceScene(scene: Scene, preserveCurrentOrOption?: boolean | ReplaceSceneOption): void { + let preserveCurrent: boolean; + let prepare: ((done: () => void) => void) | undefined; + if (typeof preserveCurrentOrOption === "object") { + preserveCurrent = !!preserveCurrentOrOption.preserveCurrent; + prepare = preserveCurrentOrOption.prepare; + } else { + preserveCurrent = !!preserveCurrentOrOption; + } this._postTickTasks.push({ type: PostTickTaskType.ReplaceScene, scene: scene, - preserveCurrent: !!preserveCurrent + preserveCurrent, + prepare }); } @@ -1792,12 +1849,24 @@ export class Game { if (oldScene) { oldScene._deactivate(); } - this._doPushScene(req.scene, false); + this._doPushScene( + req.scene, + false, + req.prepare + ? this._createPreparingLoadingScene(req.scene, req.prepare, `akashic:preparing-${req.scene.name}`) + : undefined + ); break; case PostTickTaskType.ReplaceScene: // NOTE: replaceSceneの場合、pop時点では_sceneChangedをfireしない。_doPushScene() で一度だけfireする。 this._doPopScene(req.preserveCurrent, false, false); - this._doPushScene(req.scene, false); + this._doPushScene( + req.scene, + false, + req.prepare + ? this._createPreparingLoadingScene(req.scene, req.prepare, `akashic:preparing-${req.scene.name}`) + : undefined + ); break; case PostTickTaskType.PopScene: this._doPopScene(req.preserveCurrent, false, true); @@ -1885,7 +1954,9 @@ export class Game { // 取り除いた結果スタックトップがロード中のシーンになった場合はローディングシーンを積み直す const nextScene = this.scene(); if (nextScene && nextScene._needsLoading() && nextScene._loadingState !== "loaded-fired") { - const loadingScene = this.loadingScene ?? this._defaultLoadingScene; + const loadingScene = nextScene._waitingPrepare + ? this._createPreparingLoadingScene(nextScene, nextScene._waitingPrepare, `akashic:preparing-${nextScene.name}`) + : this.loadingScene ?? this._defaultLoadingScene; this._doPushScene(loadingScene, true, this._defaultLoadingScene); loadingScene.reset(nextScene); } @@ -1972,6 +2043,34 @@ export class Game { this._modified = true; } + /** + * 引数に指定したハンドラが完了するまで待機する空のローディングシーンを作成する。 + */ + private _createPreparingLoadingScene(scene: Scene, prepare: (done: () => void) => void, name?: string): LoadingScene { + scene._waitingPrepare = prepare; + const loadingScene = new LoadingScene({ + game: this, + explicitEnd: true, + name + }); + // prepare 対象シーンを保持するためクロージャを許容 + loadingScene.onTargetReady.addOnce(() => { + const done = (): void => { + if (this._isTerminated) return; + loadingScene.end(); + }; + const prepare = scene._waitingPrepare; + scene._waitingPrepare = undefined; + if (prepare) { + prepare(done); + } else { + // NOTE: 異常系ではあるが prepare が存在しない場合は loadingScene.end() を直接呼ぶ + this._pushPostTickTask(loadingScene.end, loadingScene); + } + }); + return loadingScene; + } + private _cleanDB(): void { this.db.clean(); this._localDb.clean(); diff --git a/src/Scene.ts b/src/Scene.ts index 8974bafba..792e1bda5 100644 --- a/src/Scene.ts +++ b/src/Scene.ts @@ -360,6 +360,13 @@ export class Scene implements StorageLoaderHandler { */ operation: Trigger; + /** + * ゲーム開発者向けのコンテナ。 + * + * この値はゲームエンジンのロジックからは使用されず、ゲーム開発者は任意の目的に使用してよい。 + */ + vars: any; + /** * @private */ @@ -429,6 +436,11 @@ export class Scene implements StorageLoaderHandler { */ _assetHolders: AssetHolder[]; + /** + * @private + */ + _waitingPrepare: ((done: () => void) => void) | undefined; + /** * 各種パラメータを指定して `Scene` のインスタンスを生成する。 * @param param 初期化に用いるパラメータのオブジェクト @@ -464,6 +476,7 @@ export class Scene implements StorageLoaderHandler { this._ready = this._onReady; this.assets = {}; this.asset = new AssetAccessor(game._assetManager); + this.vars = {}; this._loaded = false; this._prefetchRequested = false; @@ -558,6 +571,7 @@ export class Scene implements StorageLoaderHandler { this.onAssetLoadFailure.destroy(); this.onAssetLoadComplete.destroy(); this.assets = {}; + this.vars = {}; // アセットを参照しているEより先に解放しないよう最後に解放する for (let i = 0; i < this._assetHolders.length; ++i) this._assetHolders[i].destroy(); @@ -566,6 +580,7 @@ export class Scene implements StorageLoaderHandler { this._storageLoader = undefined; this.game = undefined!; + this._waitingPrepare = undefined; this.state = "destroyed"; this.onStateChange.fire(this.state); @@ -858,7 +873,11 @@ export class Scene implements StorageLoaderHandler { * @private */ _needsLoading(): boolean { - return this._sceneAssetHolder.waitingAssetsCount > 0 || (!!this._storageLoader && !this._storageLoader._loaded); + return ( + this._sceneAssetHolder.waitingAssetsCount > 0 || + (!!this._storageLoader && !this._storageLoader._loaded) || + !!this._waitingPrepare + ); } /** diff --git a/src/__tests__/GameSpec.ts b/src/__tests__/GameSpec.ts index 766bfe4d7..aa2701b05 100644 --- a/src/__tests__/GameSpec.ts +++ b/src/__tests__/GameSpec.ts @@ -394,6 +394,100 @@ describe("test Game", () => { game._startLoadingGlobalAssets(); }); + it("popScene - waiting prepare", done => { + const game = new Game({ + width: 320, + height: 320, + main: "", + assets: { + img1: { + type: "image", + path: "/path1.png", + virtualPath: "path1.png", + width: 1, + height: 1 + }, + img2: { + type: "image", + path: "/path2.png", + virtualPath: "path2.png", + width: 1, + height: 1 + } + } + }); + + game._onLoad.add(() => { + // game.scenes テストのため _loaded を待つ必要がある + const scene1 = new Scene({ game: game, name: "SCENE1", assetIds: ["img1"] }); + const scene2 = new Scene({ game: game, name: "SCENE2" }); + const scene3 = new Scene({ game: game, name: "SCENE3", assetIds: ["img2"] }); + const sequence: string[] = []; + + game.pushScene(scene1, { + prepare: done => { + setTimeout( + () => { + sequence.push("scene1 prepared"); + done(); + }, + 100 // この値にとくに根拠は無い + ); + } + }); + game.pushScene(scene2, { + prepare: done => { + setTimeout( + () => { + sequence.push("scene2 prepared"); + done(); + }, + 100 // この値にとくに根拠は無い + ); + } + }); + game.pushScene(scene3, { + prepare: done => { + setTimeout( + () => { + sequence.push("scene3 prepared"); + done(); + }, + 100 // この値にとくに根拠は無い + ); + } + }); + + scene1.onLoad.addOnce(() => { + sequence.push("scene1 loaded"); + }); + scene2.onLoad.addOnce(() => { + sequence.push("scene2 loaded"); + game.popScene(); + game._flushPostTickTasks(); + }); + scene3.onLoad.addOnce(() => { + sequence.push("scene3 loaded"); + game.popScene(); + game._flushPostTickTasks(); + }); + + scene1.onLoad.addOnce(() => { + expect(sequence).toEqual([ + "scene3 prepared", + "scene3 loaded", + "scene2 prepared", + "scene2 loaded", + "scene1 prepared", + "scene1 loaded" + ]); + done(); + }); + game._flushPostTickTasks(); + }); + game._startLoadingGlobalAssets(); + }); + it("replaceScene", done => { const game = new Game({ width: 320, @@ -513,6 +607,136 @@ describe("test Game", () => { game._startLoadingGlobalAssets(); }); + it("pushScene - prepare", done => { + const game = new Game({ + width: 320, + height: 320, + main: "", + assets: { + foo: { + type: "image", + path: "/path1.png", + virtualPath: "path1.png", + width: 1, + height: 1 + } + } + }); + + // game.scenes テストのため _loaded を待つ必要がある + game._onLoad.add(() => { + const sequence: string[] = []; + const scene1 = new Scene({ game, name: "scene1" }); + const scene2 = new Scene({ game, assetIds: ["foo"], name: "scene2" }); + scene1.onLoad.addOnce(() => { + sequence.push("scene1 loaded"); + }); + scene2.onLoad.addOnce(() => { + sequence.push("scene2 loaded"); + }); + + game.pushScene(scene1, { + prepare: done => { + setTimeout( + () => { + sequence.push("scene1 prepared"); + done(); + }, + 100 // この値にとくに根拠は無い + ); + } + }); + + scene1.onLoad.addOnce(() => { + game.pushScene(scene2, { + prepare: done => { + setTimeout( + () => { + sequence.push("scene2 prepared"); + done(); + }, + 100 // この値にとくに根拠は無い + ); + } + }); + }); + scene2.onLoad.addOnce(() => { + // Scene#onLoad の前に prepare が完了していることを確認 + expect(sequence).toEqual(["scene1 prepared", "scene1 loaded", "scene2 prepared", "scene2 loaded"]); + done(); + }); + }); + game._startLoadingGlobalAssets(); + }); + + it("replaceScene - prepare", done => { + const game = new Game({ + width: 320, + height: 320, + main: "", + assets: { + foo: { + type: "image", + path: "/path1.png", + virtualPath: "path1.png", + width: 1, + height: 1 + } + } + }); + + // game.scenes テストのため _loaded を待つ必要がある + game._onLoad.add(() => { + // 初期シーンを replace することはできたいため一旦ダミーのシーンを push しておく + const scene = new Scene({ game }); + game.pushScene(scene); + scene.onLoad.addOnce(() => { + const sequence: string[] = []; + const scene1 = new Scene({ game, assetIds: ["foo"], name: "scene1" }); + const scene2 = new Scene({ game, assetIds: ["foo"], name: "scene2" }); + + scene1.onLoad.addOnce(() => { + sequence.push("scene1 loaded"); + }); + scene2.onLoad.addOnce(() => { + sequence.push("scene2 loaded"); + }); + + game.replaceScene(scene1, { + prepare: done => { + setTimeout( + () => { + sequence.push("scene1 prepared"); + done(); + }, + 100 // この値にとくに根拠は無い + ); + } + }); + + scene1.onLoad.addOnce(() => { + game.replaceScene(scene2, { + prepare: done => { + setTimeout( + () => { + sequence.push("scene2 prepared"); + done(); + }, + 100 // この値にとくに根拠は無い + ); + } + }); + }); + scene2.onLoad.addOnce(() => { + // Scene#onLoad の前に prepare が完了していることを確認 + expect(sequence).toEqual(["scene1 prepared", "scene1 loaded", "scene2 prepared", "scene2 loaded"]); + done(); + }); + }); + }); + game._startLoadingGlobalAssets(); + }); + it("tick", done => { const assets: { [id: string]: AssetConfiguration } = { mainScene: { diff --git a/src/__tests__/SceneSpec.ts b/src/__tests__/SceneSpec.ts index f43c51ec5..f3a978f38 100644 --- a/src/__tests__/SceneSpec.ts +++ b/src/__tests__/SceneSpec.ts @@ -49,6 +49,7 @@ describe("test Scene", () => { expect(scene.local).toBe("interpolate-local"); expect(scene.tickGenerationMode).toBe("manual"); expect(scene.name).toEqual("myScene"); + expect(scene.vars).toEqual({}); expect(scene.onUpdate instanceof Trigger).toBe(true); expect(scene.onLoad instanceof Trigger).toBe(true);