Help us understand the problem. What is going on with this article?

[phina.js] FlowでAssetLoaderを便利に使う

More than 1 year has passed since last update.

はじめに

こんにちは。皆さんAssetLoader使ってますか?

最近AssetLoaderの記事が多くなってきた気がするので、今回はAssetLoaderとFlowを絡めて使う方法を書いていきます。

AssetLoaderやFlowの記事はこちらが詳しいです。後述しますが、読んでおくとこの記事がわかりやすくなると思います。

console.log(phina.VERSION); // => v0.2.x

FlowとPromiseについて

本題に入る前にphina.jsのビルトインクラスであるFlowについて若干説明します。

まずFlowとは、非同期処理を扱いやすくするためのものです。Flowについて知らなくてもES2015のPromisesについて知っていれば(失敗文脈がなかったり若干の仕様違いはありますが)大体同じものだと思って間違いありません。

より詳しく知りたい方にははじめに紹介したFlowの記事をおすすめします。
それでは見ていきましょう。

AssetLoaderとFlowの関係

まずは普通にAssetLoaderを使ってみる

とりあえずAssetLoaderを普通に使ってみます。大体こんな感じで使われていると思います。

var loader = AssetLoader();

loader.onload = function(){
    // ...
};

loader.load({
    image: { /* ... */ },
    sound: { /* ... */ },
    // ...
});

もしくはこんな感じかもしれません。

var loader = AssetLoader();

loader.on('load', function(){
    // ...
});

loader.load({
    image: { /* ... */ },
    sound: { /* ... */ },
    // ...
});

AssetLoaderを普通に使う限りではどちらを使ってもそれほど大差は無いと思います。
個人的に後者のほうが好きなので、記事中では後者のほうを使います。

AssetLoader#loadはFlowを返す

どの記事でも言及されていません(あったらすみません)が、実はAssetLoader#loadはFlowのインスタンスを返すようになっています。もともとAsset系クラスは非同期処理にFlowを使っていて、AssetLoaderはそれに乗っかる形でFlowを使っています。なのでAssetLoader内部ではFlow.allで全てのアセットの読み込みを検知し、その時点でloadイベントを発火させるようになっています。

// flowsにはAssetをloadした時のFlowが入っている
return phina.util.Flow.all(flows).then(function(args){
    self.flare('load');
});

ここでポイントですが、Flowはthenメソッドで処理をつなげていくことができました。thenはFlowのインスタンスを返すのでこのようにメソッドチェーンとして書くことができます。

// Flow.resolve()はすぐに終了するFlowのインスタンスを返す
// 書き方を統一するために先頭にくっつけられることがある
Flow.resolve()
    .then(function(){ /* ... */ })
    .then(function(){ /* ... */ })
    // ...

そのため、AssetLoaderはloadの戻り値としてFlowを返します。つまり、これを利用すると読み込み終了の検知は次のようなコードでも実現することができます。

AssetLoader()
    .load({
        image: { /* ... */ },
        sound: { /* ... */ },
        // ...
    })
    .then(function(){
        console.log('読み込み終了');
    });

メソッドチェーンでつながりました。

なにが嬉しいのか

もう一度普通に使った方のコードを見てみましょう。イベントを使ったやり方では、先にハンドラを登録しなくてはならないので、処理の順番が少しわかりにくいです。

var loader = AssetLoader();

loader.on('load', function(){
    // ...
});

loader.load({
    image: { /* ... */ },
    sound: { /* ... */ },
    // ...
});

どちらが見通しが良いかなど好みが別れるところではありますが、Flowのチェーンを使ったほうが流れ的にも若干すっきりして見えるので個人的にはこちらのほうが好きです。

AssetLoaderのFlowを応用してみる

ただの書き方の違いじゃないか、と思った方。確かにこのままではちょっと書き方が変わっただけであまり意味がありません。

でもAssetLoaderがFlowを返すことを利用すると、AssetLoaderをもう少し便利に扱うことができます。

たくさんのアセットを読み込みたい時

AssetLoaderは前述の通り、指定されたアセットをいっぺんに読み込んでしまいます。そうすると問題になってくるのが、アセットの数が多い場合サーバーに余計な負担がかかってしまうことです。

// いっぺんに読み込むと負担がかかる
AssetLoader().load({
    image: {
        // とてもたくさんある
    },
    sound: {
        // すごくたくさんある
    },
    // ...
});

ゲームではアセットをたくさん使うので割とよくある状況かもしれません。こんな時はアセットを分割して少しずつ読み込みたいですね。ちょっとやってみましょう。

var asset1 = {
    image: {
        // ...
    },
    sound: {
        // ...
    },
    // ...
}
var asset2 = {
    image: {
        // ...
    },
    sound: {
        // ...
    },
    // ...
}
var asset3 = {
    image: {
        // ...
    },
    sound: {
        // ...
    },
    // ...
}

var loader1 = AssetLoader();
var loader2 = AssetLoader();
var loader3 = AssetLoader();

// 1つ目を読み込み
loader1.on('load', function(){
    loader2.load(asset2);
});

// 2つ目を読み込み
loader2.on('load', function(){
    loader3.load(asset3);
});

// 3つ目を読み込み
loader3.on('load', function(){
    console.log('読み込み終了');
});

loader1.load(asset1);

できました。asset1の読み込みが終わればasset2の読み込みを始め、asset2の読み込みが終われば...となります。
しかしイベントを何回も書いているあたりがどうも見づらい気がします。数が多くなったら大変ですし、コピペなんかしたら数字も間違えそうです。何か解決する方法はないでしょうか?

あります。Flowです。

Flowを使って非同期処理を順次実行する

Flowを使って上記と同等の処理を書いてみましょう。

var asset1 = {
    image: {
        // ...
    },
    sound: {
        // ...
    },
    // ...
}
var asset2 = {
    image: {
        // ...
    },
    sound: {
        // ...
    },
    // ...
}
var asset3 = {
    image: {
        // ...
    },
    sound: {
        // ...
    },
    // ...
}

var loader = AssetLoader();

Flow.resolve()
    .then(function(){
        return loader.load(asset1); // 1つ目を読み込み
    })
    .then(function(){
        return loader.load(asset2); // 2つ目を読み込み
    })
    .then(function(){
        return loader.load(asset3); // 3つ目を読み込み
    })
    .then(function(){
        console.log('読み込み終了');
    });

runstantで確認

thenに渡す関数がFlowのインスタンスを返せば、次のthen(に渡された関数)の実行はそのFlowが解決するまで待たれます。つまり1つ目のthenでasset1のloadが実行されたあと、2つ目のthenでasset2のloadが実行されるのは1つ目の読み込みが完了したあとだということです。

どうでしょう、イベントをちまちま書くより処理の見通しが良くなっていませんか?しかもイベントを使っていないのでAssetLoaderを使い回すことができます。イベントを使った方では、使いまわすと逆に複雑になってしまいます。

もっとすっきり書きたい時

上のコードでも十分見やすいですが、眺めていると同じような記述が多いのでもう少し短く書けそうな気がしてきました。
少し蛇足ですがやってみます。

// 配列にしておく
var assets = [
    {
        image: {
            // ...
        },
        sound: {
            // ...
        },
        // ...
    },
    {
        image: {
            // ...
        },
        sound: {
            // ...
        },
        // ...
    },
    // ...
];

var loader = AssetLoader();

assets
    .reduce(function(flow, asset){
        return flow.then(function(){
            return loader.load(asset);
        });
    }, Flow.resolve())
    .then(function(){
        console.log('読み込み終了');
    });

reduceでthenをくっつけてやります。
この場合の処理内容も一つ前のコードと同じものです。

reduceでPromiseを順次実行するテクニックは調べるといくつか見つけることができます。

分かりやすいか分かりにくいかで言えば、慣れていないとかなり分かりづらいです。
しかしコードは短くすることができたので関数化すると使いやすいかもしれません。

// AssetLoaderの静的メソッドにしてみる
phina.asset.AssetLoader.sequence = function(assets){
    var loader = phina.asset.AssetLoader();
    return assets.reduce(function(flow, asset){
        return flow.then(function(){
            return loader.load(asset);
        });
    }, phina.util.Flow.resolve());
};


AssetLoader.sequence(assets)
    .then(function(){
        console.log('読み込み終了');
    });

関数化したおかげで随分すっきりしました。

おわりに

いかがだったでしょうか、AssetLoaderにはFlowを利用した便利な使い方があります。イベントやFlowを始めとした非同期処理は慣れないとなかなか難しいものがあると思いますが、使いこなせるととても便利です。

最後に紹介したアセットの分割読み込みですが、phina.js公式でもAssetLoaderに分割して読み込ませる機能を実装する予定があるようです。どのような形で実装されるかわかりませんが、非常に楽しみですね。

今回紹介したものはほんの一例ですが、何かの役に立てば幸いです。最後まで見ていただきありがとうございました。

おまけ

記事中に出てきたこんなコードですが、実はもう少し短く書けます。

Flow.resolve()
    .then(function(){
        return loader.load(asset);
    });

これがこうです。

Flow.resolve()
    .then(loader.load.bind(loader, asset));

functionが消えて2行減りました。すっきりはしますが、thisが絡むと説明が難しくなるので本文中では割愛しました。
実際こういうコードが出てくると一瞬悩むのでわかりやすさで言えばどうかな、といったところです。

ES2015を使える場合はアロー関数を使うととても短く書けますし分かりやすいです。

Flow.resolve()
    .then(() => loader.load(asset));

また、ES2017のasync/awaitを使える場合には、AssetLoaderをPromiseでくるんでしまうのが一番わかりやすいかもしれません。(FlowはthenableなオブジェクトなのでPromise.resolveでPromiseに変換することができます)

await Promise.resolve(loader.load(asset1));
await Promise.resolve(loader.load(asset2));
Negiwine_jp
趣味JSerです。 Twitter: https://twitter.com/Negiwine_jp Runstant: http://runstant.com/Negiwine
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした