phina.jsのGameAppクラスを使ってアセットのロードを行うとき、よく以下のようなコードを見かけると思います。
phina.globalize();
const ASSETS = {
image: {
player: "./assets/player.png",
enemy: "./assets/enemy.png",
},
sound: {
bgm: "./assets/bgm.mp3",
},
font: {
aldrich: "./asset/aldrich.woff"
}
}
phina.main(function() {
var app = GameApp({
startLabel: 'main',
assets: ASSETS,
});
app.run();
});
ASSETSのところでimageとかsoundとかfontと適切に指定することで、それぞれ対応したファイルをロードしてくれます。
このロード機能を担っているのがphina.asset.AssetLoaderクラスなんですが、実はAssetLoaderを拡張することで好きなタイプのデータをロードしたりパースしたりすることもできます。
環境
phina.js v0.2.x
基本
非常にシンプルな例です。
phina.asset.AssetLoader.register('hoge', function(key, value) {
return phina.util.Flow(function(resolve) {
resolve(value);
});
});
以上のコードを記述しておくことで、「hoge」というタイプのファイルをロードできるようになります。(単なるオブジェクトですが)
/* main処理 */
phina.globalize();
phina.main(function() {
var app = GameApp({
startLabel: 'main',
assets: {
hoge: {
player: {
hp: 10,
atk: 2,
},
}
},
});
app.run();
});
phina.define('MainScene', {
superClass: 'DisplayScene',
init: function(options) {
this.superInit(options);
console.log(AssetManager.get('hoge', 'player')); // {hp: 10, atk:2}
},
});
見ての通り、登録にはphina.asset.AssetLoader.registerを使います。
2つの引数を取り、第一引数には文字列を渡します。この文字列がロード後、AssetManagerからgetをする際のデータタイプのキー名になります。
第二引数にはコールバック関数を渡しますが、必ずFlow(promise的なオブジェクト)を返す関数を指定します。
Flowの生成時、さらにコールバックを指定しますが、こちらではロードなりが終わって処理を進める準備ができたら、resolveでロードしたものを渡すよう仕込みます。
上記の例では特に何もせず、ただ右から左へ流してるだけとなります。
応用1:非同期処理パターン
基本例は実用性が欠片もありませんでしたが、真価を発揮するのはローディングなどの非同期処理を挟むときです。
例えばサウンド用ライブラリとしてhowler.jsを使いたいとき、これに対応するものは以下のような感じになります。
phina.namespace(function() {
var ASSET_TYPE = 'howl';
phina.asset.AssetLoader.register(ASSET_TYPE, function(key, path) {
// howler.jsを読み込んだ状態(window.Howlが存在する)であること
if (!phina.global.Howl) {
throw new Error("howler.js is not loaded");
}
return phina.util.Flow(function(resolve) {
var sound = new Howl({
src: [path]
});
sound.once('load', function() {
resolve(sound);
});
sound.once('loaderror', function(error) {
// rejectは無いのでエラーをresolveで返す
console.error(error);
resolve({error:error});
});
});
});
});
音源のロードが終わった地点でresolveします。
さらに今回はロードエラー時の処理も書いています。
resolveで返すものはとりあえずオブジェクトであれば一応処理上は問題ありません。(ホントは後述するAssetクラスのダミーを返したほうがいいかもしれません)
とにかくresolveはしておかないと処理が止まってしまうので、そのようにしておきます。
ちなみに使うときはこんな感じです。
/* main */
phina.globalize();
phina.main(function() {
var app = GameApp({
startLabel: 'main',
assets: {
howl: {
tamborine: './assets/tamborine.mp3',
}
},
});
app.run();
});
phina.define('MainScene', {
superClass: 'DisplayScene',
init: function(options) {
this.superInit(options);
},
update: function(app) {
var p = app.pointer;
// タップで音を鳴らす
if (p.getPointingStart()) {
AssetManager.get('howl', 'tamborine').play();
}
}
});
応用2:Assetクラス拡張パターン
TextureとかSoundといったほかのアセットクラスのソースを見るとphina.asset.Assetクラスなるものを継承していることがわかります。
コードが複雑になるため、あえて継承するパターンは避けてきましたが、継承させたほうが足並みが揃って行儀?は良いかと思います。
以下はAssetクラスを継承してyamlファイルのロードをするためのパターンです。(元からphinaにあるphina.asset.Fileクラスを参考にしています。)
phina.namespace(function() {
var ASSET_TYPE = 'yaml';
phina.define('phina.asset.Yaml', {
superClass: "phina.asset.Asset",
/**
* @constructor
*/
init: function() {
// yaml.jsを読み込んだ状態(window.YAMLが存在する)であること
if (!phina.global.YAML) {
throw new Error("yaml.js is not loaded");
}
this.superInit();
},
_load: function(resolve) {
// this.srcはインスタンス生成時に指定されるファイルへのパス
YAML.load(this.src, function(result) {
if (result != null) {
this.dataType = 'yaml';
this.data = result; // パース結果を代入
this.loaded = true;
resolve(this);
} else {
// ロード失敗時の処理
console.error("[phina.js] not found `{0}`!".format(this.src));
this.loadError = true;
resolve(this);
}
}.bind(this));
},
});
phina.asset.AssetLoader.register(ASSET_TYPE, function(key, path) {
return phina.asset.Yaml().load(path);
});
});
privateっぽい(仮想)関数_load
が先程までで言うところのFlowのコールバック関数に当たります。resolveは基本的には自分自身を返します。
registerする際にload()を実行するのを忘れずに。
まとめ
手順をまとめると
- registerでファイルタイプを登録
- 登録時の関数ではFlowを返すこと
- Flowのコールバックではロード完了等、適当なタイミングでresolveすること
- 使うときはAssetManagerを介して引っ張り出す
他にもajaxで何かを引っ張ってくる(ランキングデータとか?)ときなどにも使えるのではないかと思います。
基本的なデータタイプは大体用意されているので、わざわざ拡張を行う機会はそんなにないですが、ちょっと頭の片隅においておくといいかもしれません。