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 assetBundle #510

Merged
merged 9 commits into from
Nov 21, 2024
Merged
81 changes: 68 additions & 13 deletions src/AssetManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
VideoAssetConfigurationBase,
VectorImageAssetConfigurationBase,
BinaryAssetConfigurationBase,
ModuleMainPathsMap
ModuleMainPathsMap,
AssetBundleConfiguration,

Check failure on line 16 in src/AssetManager.ts

View workflow job for this annotation

GitHub Actions / Node 20.x / ubuntu-latest

'"@akashic/game-configuration"' has no exported member named 'AssetBundleConfiguration'. Did you mean 'AssetConfiguration'?

Check failure on line 16 in src/AssetManager.ts

View workflow job for this annotation

GitHub Actions / Node 18.x / ubuntu-latest

'"@akashic/game-configuration"' has no exported member named 'AssetBundleConfiguration'. Did you mean 'AssetConfiguration'?

Check failure on line 16 in src/AssetManager.ts

View workflow job for this annotation

GitHub Actions / Node 20.x / ubuntu-latest

'"@akashic/game-configuration"' has no exported member named 'AssetBundleConfiguration'. Did you mean 'AssetConfiguration'?

Check failure on line 16 in src/AssetManager.ts

View workflow job for this annotation

GitHub Actions / Node 18.x / ubuntu-latest

'"@akashic/game-configuration"' has no exported member named 'AssetBundleConfiguration'. Did you mean 'AssetConfiguration'?
BundledAssetConfiguration

Check failure on line 17 in src/AssetManager.ts

View workflow job for this annotation

GitHub Actions / Node 20.x / ubuntu-latest

'"@akashic/game-configuration"' has no exported member named 'BundledAssetConfiguration'. Did you mean 'AssetConfiguration'?

Check failure on line 17 in src/AssetManager.ts

View workflow job for this annotation

GitHub Actions / Node 18.x / ubuntu-latest

'"@akashic/game-configuration"' has no exported member named 'BundledAssetConfiguration'. Did you mean 'AssetConfiguration'?

Check failure on line 17 in src/AssetManager.ts

View workflow job for this annotation

GitHub Actions / Node 20.x / ubuntu-latest

'"@akashic/game-configuration"' has no exported member named 'BundledAssetConfiguration'. Did you mean 'AssetConfiguration'?

Check failure on line 17 in src/AssetManager.ts

View workflow job for this annotation

GitHub Actions / Node 18.x / ubuntu-latest

'"@akashic/game-configuration"' has no exported member named 'BundledAssetConfiguration'. Did you mean 'AssetConfiguration'?
} from "@akashic/game-configuration";
import type {
Asset,
Expand All @@ -32,6 +34,7 @@
import type { AssetManagerLoadHandler } from "./AssetManagerLoadHandler";
import type { AudioSystem } from "./AudioSystem";
import type { AudioSystemManager } from "./AudioSystemManager";
import { BundledScriptAsset } from "./auxiliary/BundledScriptAsset";
import { EmptyBinaryAsset } from "./auxiliary/EmptyBinaryAsset";
import { EmptyGeneratedVectorImageAsset } from "./auxiliary/EmptyGeneratedVectorImageAsset";
import { EmptyVectorImageAsset } from "./auxiliary/EmptyVectorImageAsset";
Expand Down Expand Up @@ -261,6 +264,11 @@
*/
private _generatedAssetCount: number;

/**
* アセットバンドル。
*/
private _assetBundle: AssetBundleConfiguration | null;

/**
* `AssetManager` のインスタンスを生成する。
*
Expand Down Expand Up @@ -290,6 +298,7 @@
this._refCounts = {};
this._loadings = {};
this._generatedAssetCount = 0;
this._assetBundle = null;

const assetIds = Object.keys(this.configuration);
for (let i = 0; i < assetIds.length; ++i) {
Expand All @@ -313,6 +322,7 @@
this._liveAssetPathTable = undefined!;
this._refCounts = undefined!;
this._loadings = undefined!;
this._assetBundle = undefined!;
}

/**
Expand Down Expand Up @@ -362,9 +372,23 @@
* プリロードすべきスクリプトアセットのIDを全て返す。
*/
preloadScriptAssetIds(): string[] {
return Object.entries(this.configuration)
.filter(([, conf]) => conf.type === "script" && conf.global && conf.preload)
.map(([assetId]) => assetId);
const assetIds: string[] = [];

if (this._assetBundle) {
assetIds.push(
...Object.entries(this._assetBundle.assets)
Copy link
Member

Choose a reason for hiding this comment

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

assetId の方使わなくなったので Object.values() でよさそうです。

Copy link
Contributor Author

Choose a reason for hiding this comment

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

69f3a1e にて修正しました。

.filter(([, conf]) => conf.type === "script" && conf.preload)

Check failure on line 380 in src/AssetManager.ts

View workflow job for this annotation

GitHub Actions / Node 20.x / ubuntu-latest

'conf' is of type 'unknown'.

Check failure on line 380 in src/AssetManager.ts

View workflow job for this annotation

GitHub Actions / Node 20.x / ubuntu-latest

'conf' is of type 'unknown'.

Check failure on line 380 in src/AssetManager.ts

View workflow job for this annotation

GitHub Actions / Node 18.x / ubuntu-latest

'conf' is of type 'unknown'.

Check failure on line 380 in src/AssetManager.ts

View workflow job for this annotation

GitHub Actions / Node 18.x / ubuntu-latest

'conf' is of type 'unknown'.

Check failure on line 380 in src/AssetManager.ts

View workflow job for this annotation

GitHub Actions / Node 20.x / ubuntu-latest

'conf' is of type 'unknown'.

Check failure on line 380 in src/AssetManager.ts

View workflow job for this annotation

GitHub Actions / Node 20.x / ubuntu-latest

'conf' is of type 'unknown'.

Check failure on line 380 in src/AssetManager.ts

View workflow job for this annotation

GitHub Actions / Node 18.x / ubuntu-latest

'conf' is of type 'unknown'.

Check failure on line 380 in src/AssetManager.ts

View workflow job for this annotation

GitHub Actions / Node 18.x / ubuntu-latest

'conf' is of type 'unknown'.
.map(([assetId]) => assetId)
);
}

assetIds.push(
...Object.entries(this.configuration)
.filter(([, conf]) => conf.type === "script" && conf.global && conf.preload)
.map(([assetId]) => assetId)
);

return assetIds;
}

/**
Expand Down Expand Up @@ -563,6 +587,15 @@
return "/" + virtualPath;
}

/**
* アセットバンドルを設定する。
*
* @param assetBundle アセットバンドル
*/
setAssetBundle(assetBundle: AssetBundleConfiguration | null): void {
this._assetBundle = assetBundle;
}

/**
* @ignore
*/
Expand Down Expand Up @@ -648,7 +681,23 @@
let id: string;
let uri: string;
let conf: AssetConfiguration | DynamicAssetConfiguration;
if (typeof idOrConf === "string") {
if (this._assetBundle && typeof idOrConf === "string") {
const id = idOrConf;
const conf = this._assetBundle.assets[id] as BundledAssetConfiguration;
const type = conf.type;
switch (type) {
case "script":
const asset = new BundledScriptAsset({
id,
...conf
});
return asset;
default:
throw ExceptionFactory.createAssertionError(
`AssertionError#_createAssetFor: unknown asset type ${type} for asset ID: ${id}`
);
}
} else if (typeof idOrConf === "string") {
id = idOrConf;
conf = this.configuration[id];
uri = this.configuration[id].path;
Expand Down Expand Up @@ -849,17 +898,23 @@
*/
_addAssetToTables(asset: OneOfAsset): void {
this._assets[asset.id] = asset;
let path: string | undefined;

// DynamicAsset の場合は configuration に書かれていないので以下の判定が偽になる
if (this.configuration[asset.id]) {
const virtualPath = this.configuration[asset.id].virtualPath!; // virtualPath の存在は _normalize() で確認済みのため 非 null アサーションとする
if (!this._liveAssetVirtualPathTable.hasOwnProperty(virtualPath)) {
this._liveAssetVirtualPathTable[virtualPath] = asset;
} else {
if (this._liveAssetVirtualPathTable[virtualPath].path !== asset.path)
throw ExceptionFactory.createAssertionError("AssetManager#_onAssetLoad(): duplicated asset path");
}
if (!this._liveAssetPathTable.hasOwnProperty(asset.path)) this._liveAssetPathTable[asset.path] = virtualPath;
path = this.configuration[asset.id].virtualPath!; // virtualPath の存在は _normalize() で確認済みのため 非 null アサーションとする
} else if (this._assetBundle && this._assetBundle.assets[asset.id]) {
path = this._assetBundle.assets[asset.id].path;
}

if (!path) return;

if (!this._liveAssetVirtualPathTable.hasOwnProperty(path)) {
this._liveAssetVirtualPathTable[path] = asset;
} else {
if (this._liveAssetVirtualPathTable[path].path !== asset.path)
throw ExceptionFactory.createAssertionError("AssetManager#_onAssetLoad(): duplicated asset path");
}
if (!this._liveAssetPathTable.hasOwnProperty(asset.path)) this._liveAssetPathTable[asset.path] = path;
}
}
9 changes: 5 additions & 4 deletions src/Game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type { EventFilter } from "./EventFilter";
import { ExceptionFactory } from "./ExceptionFactory";
import type { GameHandlerSet } from "./GameHandlerSet";
import type { GameMainParameterObject } from "./GameMainParameterObject";
import { InitialScene } from "./InitialScene";
import { LoadingScene } from "./LoadingScene";
import type { LocalTickModeString } from "./LocalTickModeString";
import { ModuleManager } from "./ModuleManager";
Expand All @@ -33,7 +34,7 @@ import { OperationPluginManager } from "./OperationPluginManager";
import type { InternalOperationPluginOperation } from "./OperationPluginOperation";
import { PointEventResolver } from "./PointEventResolver";
import type { RandomGenerator } from "./RandomGenerator";
import { Scene } from "./Scene";
import type { Scene } from "./Scene";
import type { SnapshotSaveRequest } from "./SnapshotSaveRequest";
import { SurfaceAtlasSet } from "./SurfaceAtlasSet";
import type { TickGenerationModeString } from "./TickGenerationModeString";
Expand Down Expand Up @@ -640,7 +641,7 @@ export class Game {
* グローバルアセットを読み込むための初期シーン。必ずシーンスタックの一番下に存在する。これをpopScene()することはできない。
* @private
*/
_initialScene: Scene;
_initialScene: InitialScene;

/**
* デフォルトローディングシーン。
Expand Down Expand Up @@ -1009,13 +1010,13 @@ export class Game {

this.onUpdate = new Trigger<void>();

this._initialScene = new Scene({
this._initialScene = new InitialScene({
game: this,
assetIds: this._assetManager.globalAssetIds(),
local: true,
name: "akashic:initial-scene"
});
this._initialScene.onLoad.add(this._handleInitialSceneLoad, this);
this._initialScene.onAllAssetsLoad.add(this._handleInitialSceneLoad, this);

this._reset({ age: 0 });
}
Expand Down
45 changes: 45 additions & 0 deletions src/InitialScene.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { AssetBundleConfiguration } from "@akashic/game-configuration";

Check failure on line 1 in src/InitialScene.ts

View workflow job for this annotation

GitHub Actions / Node 20.x / ubuntu-latest

'"@akashic/game-configuration"' has no exported member named 'AssetBundleConfiguration'. Did you mean 'AssetConfiguration'?

Check failure on line 1 in src/InitialScene.ts

View workflow job for this annotation

GitHub Actions / Node 18.x / ubuntu-latest

'"@akashic/game-configuration"' has no exported member named 'AssetBundleConfiguration'. Did you mean 'AssetConfiguration'?

Check failure on line 1 in src/InitialScene.ts

View workflow job for this annotation

GitHub Actions / Node 20.x / ubuntu-latest

'"@akashic/game-configuration"' has no exported member named 'AssetBundleConfiguration'. Did you mean 'AssetConfiguration'?

Check failure on line 1 in src/InitialScene.ts

View workflow job for this annotation

GitHub Actions / Node 18.x / ubuntu-latest

'"@akashic/game-configuration"' has no exported member named 'AssetBundleConfiguration'. Did you mean 'AssetConfiguration'?
import { Trigger } from "@akashic/trigger";
import type { SceneParameterObject } from "./Scene";
import { Scene } from "./Scene";

/**
* グローバルアセットを読み込むための初期シーン。
*/
export class InitialScene extends Scene {
/**
* ゲームの実行に必要なグローバルアセットがすべて読み込まれた際に発火される Trigger。
* `gameConfiguration` に `assetBundle` が指定されている場合は、そのアセットもすべて読み込み完了後に発火される。
* 一方、`this.onLoad` は `gameConfiguration` の `assetBundle` 指定を無視して発火する点に注意が必要。
*/
onAllAssetsLoad: Trigger<void>;

constructor(param: SceneParameterObject) {
super(param);
this.onAllAssetsLoad = new Trigger();
this.onLoad.add(this._handleLoad, this);
}

override destroy(): void {
super.destroy();
if (!this.onAllAssetsLoad.destroyed()) {
this.onAllAssetsLoad.destroy();
}
this.onAllAssetsLoad = undefined!;
}

_handleLoad(): void {
if (this.game._configuration.assetBundle) {

Check failure on line 32 in src/InitialScene.ts

View workflow job for this annotation

GitHub Actions / Node 20.x / ubuntu-latest

Property 'assetBundle' does not exist on type 'GameConfiguration'.

Check failure on line 32 in src/InitialScene.ts

View workflow job for this annotation

GitHub Actions / Node 18.x / ubuntu-latest

Property 'assetBundle' does not exist on type 'GameConfiguration'.

Check failure on line 32 in src/InitialScene.ts

View workflow job for this annotation

GitHub Actions / Node 20.x / ubuntu-latest

Property 'assetBundle' does not exist on type 'GameConfiguration'.

Check failure on line 32 in src/InitialScene.ts

View workflow job for this annotation

GitHub Actions / Node 18.x / ubuntu-latest

Property 'assetBundle' does not exist on type 'GameConfiguration'.
const assetBundle: AssetBundleConfiguration = this.game._moduleManager._internalRequire(this.game._configuration.assetBundle);

Check failure on line 33 in src/InitialScene.ts

View workflow job for this annotation

GitHub Actions / Node 20.x / ubuntu-latest

Property 'assetBundle' does not exist on type 'GameConfiguration'.

Check failure on line 33 in src/InitialScene.ts

View workflow job for this annotation

GitHub Actions / Node 18.x / ubuntu-latest

Property 'assetBundle' does not exist on type 'GameConfiguration'.

Check failure on line 33 in src/InitialScene.ts

View workflow job for this annotation

GitHub Actions / Node 20.x / ubuntu-latest

Property 'assetBundle' does not exist on type 'GameConfiguration'.

Check failure on line 33 in src/InitialScene.ts

View workflow job for this annotation

GitHub Actions / Node 18.x / ubuntu-latest

Property 'assetBundle' does not exist on type 'GameConfiguration'.
this.game._assetManager.setAssetBundle(assetBundle);
const assetIds = Object.keys(assetBundle.assets);
this.requestAssets(assetIds, this._handleRequestAssets.bind(this));
} else {
this.onAllAssetsLoad.fire();
}
}

_handleRequestAssets(): void {
this.onAllAssetsLoad.fire();
}
}
10 changes: 4 additions & 6 deletions src/ModuleManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,10 @@ export class ModuleManager {
const liveAssetVirtualPathTable = this._assetManager._liveAssetVirtualPathTable;
const moduleMainScripts = this._assetManager._moduleMainScripts;

// 0. アセットIDらしい場合はまず当該アセットを探す
if (path.indexOf("/") === -1) {
if (this._assetManager._assets.hasOwnProperty(path)) {
targetScriptAsset = this._assetManager._assets[path];
resolvedPath = this._assetManager._liveAssetPathTable[targetScriptAsset.path];
}
// 0. アセットIDと一致した場合は当該アセットを返す
if (this._assetManager._assets.hasOwnProperty(path)) {
targetScriptAsset = this._assetManager._assets[path];
resolvedPath = this._assetManager._liveAssetPathTable[targetScriptAsset.path];
yu-ogi marked this conversation as resolved.
Show resolved Hide resolved
}

if (!resolvedPath) {
Expand Down
6 changes: 3 additions & 3 deletions src/__tests__/GameSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1398,12 +1398,12 @@ describe("test Game", () => {
});

const loadScene = game._defaultLoadingScene;
expect(game._initialScene.onLoad.contains(game._handleInitialSceneLoad, game)).toBe(true);
expect(game._initialScene.onAllAssetsLoad.contains(game._handleInitialSceneLoad, game)).toBe(true);
expect(loadScene.onLoad.contains(loadScene._doReset, loadScene)).toBe(false);

game._loadAndStart();
expect(game.isLoaded).toBe(false); // _loadAndStartしたがまだ読み込みは終わっていない
expect(game._initialScene.onLoad.contains(game._handleInitialSceneLoad, game)).toBe(true);
expect(game._initialScene.onAllAssetsLoad.contains(game._handleInitialSceneLoad, game)).toBe(true);
expect(game.scenes.length).toBe(2);
expect(game.scenes[0]).toBe(game._initialScene);
expect(game.scenes[1]).toBe(loadScene);
Expand All @@ -1413,7 +1413,7 @@ describe("test Game", () => {
expect(loadScene2).not.toBe(loadScene);
expect(loadScene.destroyed()).toBe(true);
expect(game.isLoaded).toBe(false);
expect(game._initialScene.onLoad.contains(game._handleInitialSceneLoad, game)).toBe(true);
expect(game._initialScene.onAllAssetsLoad.contains(game._handleInitialSceneLoad, game)).toBe(true);
expect(loadScene2.onLoad.contains(loadScene2._doReset, loadScene2)).toBe(false);
expect(game.scenes.length).toBe(0);

Expand Down
79 changes: 79 additions & 0 deletions src/__tests__/InitialSceneSpec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { GameConfiguration } from "..";
import { Game } from "./helpers";

describe("test InitialScene", () => {
const configuration: GameConfiguration = {
width: 320,
height: 320,
main: "./main.js",
assetBundle: "./asset.bundle.js",
assets: {
main: {
type: "script",
path: "./main.js",
virtualPath: "main.js",
global: true
},
"asset.bundle": {
type: "script",
path: "./asset.bundle.js",
virtualPath: "asset.bundle.js",
global: true
}
}
};
const assetBundle = `{
assets: {
"/script/preload.js": {
type: "script",
path: "script/preload.js",
preload: true,
execute: (runtimeValue) => {
const { module } = runtimeValue;
const exports = module.exports;

module.exports = () => {
g.game.vars.preloaded = true;
}

return module.exports;
}
},
"/script/module.js": {
type: "script",
path: "script/module.js",
execute: (runtimeValue) => {
const { module } = runtimeValue;
const exports = module.exports;

exports.multiply = (a, b) => {
return a * b;
}

return module.exports;
}
},
},
}`;

it("should load from asset.bundle.js instead of the defined asset", done => {
const game = new Game(configuration);
game.resourceFactory.scriptContents["./main.js"] = "module.exports = () => g.game.__entry_point__();";
game.resourceFactory.scriptContents["./asset.bundle.js"] = `module.exports = ${assetBundle}`;
(game as any).__entry_point__ = () => {
expect(game.vars.preloaded).toBe(true); // エントリポイントに先行して /script/preload.js が実行されていることを確認

const assetBundle = game._moduleManager._internalRequire(configuration.assetBundle!);
expect(assetBundle.assets["/script/module.js"]).toBeDefined();
expect(assetBundle.assets["/script/module.js"].type).toBe("script");
expect(assetBundle.assets["/script/module.js"].path).toBe("script/module.js");
expect(assetBundle.assets["/script/module.js"].execute).toBeInstanceOf(Function);

const module = game._moduleManager._internalRequire("./script/module.js");
expect(module.multiply).toBeInstanceOf(Function);
expect(module.multiply(2, 10)).toBe(20);
done();
};
game._loadAndStart();
});
});
Loading
Loading