enchant.jsで動く非同期処理プラグイン command.enchat.js

2013.04.02kickbaseenchant.js | JavaScript


enchant.js、便利ですね。スプライトシートを使ったアニメーションから、tl.enchant.jsを使ったイージングアニメーションまで、実に豊富な表現が可能です。デフォルトの機能では異なるオブジェクトに対しての直列、並列の非同期処理がサポートされていないようでしたので、それらを管理するライブラリを作成してみました。「複数のオブジェクトのモーションが全て完了したら、次のモーションへ移行」などのアニメーション管理などに便利かと思います。

こちらはきんくまデザインさんが公開されている非同期処理ライブラリのFork版となっています。元のソースは下記を参照してください。

参考サイト

[JavaScript] Javascript で非同期処理のライブラリ « きんくまデザイン

以下に追加した機能ところなどを書いていきます。

・追加した機能

  • SerialCommandのコンストラクタに可変長引数でCommandインスタンスを渡し、内部的にaddできるようにしました
  • ParallelCommandのコンストラクタに可変長引数でCommandインスタンスを渡し、内部的にaddできるようにしました
  • WaitCommandの待機時間を秒数、フレーム数に対応しました
  • TraceCommandを実装しました
  • TweenerCommandを実装しました
  • 追加した機能では、SerialCommandとParallelCommandのコンストラクタが地味に便利です。引数の数も気にしなくてよく、また引数がコマンドクラスのインスタンスかどうかの判定も入れているので、コマンドクラスを継承していない型が混ざっていても無視する仕様となっています。
    この安全設計は賛否が別れるところだと思いますので(期待通りの挙動をしない買ったときはエラーを返してほしいという設計もわかるので)、この機能が必要ない方は削除してください。

    型判定が不要な場合、下記コードを消すだけでOKです。

    if (command instanceof pkg.Command) 
    

    tl.enchant.jsはデフォルトでフレームベースのアニメーションをサポートしているので、WaitCommandにもフレーム数待機するモードを追加しました。これでアニメーションがイメージしやすいかと思います。

    TweenerCommandのexecuteのところは書き方をちょっと悩んだんですが。実行コンテキストによってthisの参照先が変わるので、bindメソッドを使って回避しています。もっとキレイに書けそうな気もするんですが現状これで。
    ※Function.protopype.bindが存在しない場合( ブラウザがECMAScript5に未対応 )の処理はenchant.js本体内に実装済みなのでこちらでは対応していません

    あとは実際のコードと動作例をご参照ください
    ※enchant.jsのバージョンは v0.6.1を使用しています

    sample1:直列実行

    sample2:複合

    command.enchat.js

    /**
     * ...
     * @author kickbase
     * forked from : http://www.kuma-de.com/blog/2011-12-01/2882
     */
    
    if (!window.command) {
        window.command = {};
    }
    
    (function (pkg) {
        /**
         * Command - コマンドの基底クラス
         */
        pkg.Command = function () {
            this.delegate;
        };
        pkg.Command.prototype = {
            execute:function () {
            },
            onComplete:function () {
                if (this.delegate) {
                    this.delegate();
                }
            }
        };
    
        /**
         * FuncCommand - 関数を実行するクラス
         * @param func {Function} - 実行したい関数
         */
        pkg.FuncCommand = function (func) {
            this.targetFunction = func;
        };
        pkg.FuncCommand.prototype = new pkg.Command();
        pkg.FuncCommand.prototype.execute = function () {
            this.targetFunction();
            this.onComplete();
        };
    
        /**
         * WaitCommand - 指定時間停止するコマンド
         * @param interval{int} - 秒数かフレーム数を指定する
         * @param fps{int} - game.fpsを渡す。省略した場合は第一引数を秒数とみなす
         */
        pkg.WaitCommand = function (interval, fps) {
            if (typeof fps === 'undefined') fps = false;
            this.interval = (fps) ? interval * 1000 / fps : interval * 1000;
            this.timerId;
        };
        pkg.WaitCommand.prototype = new pkg.Command();
        pkg.WaitCommand.prototype.execute = function () {
            var self = this;
            this.timerId = setTimeout(function () {
                self.onComplete();
            }, this.interval);
        };
    
        /**
         * TraceCommand - 出力コマンド
         * @param obj {Object} - 出力対象
         */
        pkg.TraceCommand = function (obj) {
            this.obj = obj;
        };
        pkg.TraceCommand.prototype = new pkg.Command();
        pkg.TraceCommand.prototype.execute = function () {
            console.log(this.obj);
            this.onComplete();
        };
    
        /**
         * ParallelCommand - 並列実行
         * 可変長引数でCommandインスタンスを渡せる
         */
        pkg.ParallelCommand = function () {
            this.commands = [];
            this.count;
            var l = arguments.length;
            for (var i = 0; i < l; i++) {
                var command = arguments[i];
                if (command instanceof Command) this.commands.push(command);
            }
        };
        pkg.ParallelCommand.prototype = new pkg.Command();
        pkg.ParallelCommand.prototype.execute = function () {
            if (this.commands.length == 0) {
                this.commands = [];
                this.onComplete();
            } else {
                this.count = 0;
                var i, len;
                len = this.commands.length;
                var command;
                var self = this;
                for (i = 0; i < len; i++) {
                    command = this.commands[i];
                    command.delegate = function () {
                        self.commandComplete()
                    };
                    command.execute();
                }
            }
        };
        pkg.ParallelCommand.prototype.add = function (command) {
            if (command instanceof pkg.Command) this.commands.push(command);
        };
        pkg.ParallelCommand.prototype.commandComplete = function () {
            this.count++;
            if (this.count == this.commands.length) {
                this.onComplete();
            }
        };
    
        /**
         * SerialCommand - 直列実行
         * 可変長引数でCommandインスタンスを渡せる
         */
        pkg.SerialCommand = function () {
            this.commands = [];
            this.currentCommand;
            var l = arguments.length;
            for (var i = 0; i < l; i++) {
                var command = arguments[i];
                if (command instanceof pkg.Command) this.commands.push(command);
            }
        };
        pkg.SerialCommand.prototype = new pkg.Command();
        pkg.SerialCommand.prototype.execute = function () {
            if (this.commands.length == 0) {
                this.onComplete();
            } else {
                this.currentCommand = this.commands.shift();
                var self = this;
                this.currentCommand.delegate = function () {
                    self.commandComplete();
                };
                this.currentCommand.execute();
            }
        };
        pkg.SerialCommand.prototype.add = function (command) {
            if (command instanceof pkg.Command) this.commands.push(command);
        };
        pkg.SerialCommand.prototype.commandComplete = function () {
            this.currentCommand = null;
            this.execute();
        };
    
        /**
         * TweenerCommand - tl.tweenを実行する
         * @param target{Entity} - tweenさせるオブジェクト
         * @param params{Object} - モーションのオブジェクト
         */
        pkg.TweenerCommand = function (target, params) {
            this.target = target;
            this.params = params;
        };
        pkg.TweenerCommand.prototype = new pkg.Command();
        pkg.TweenerCommand.prototype.execute = function () {
            var fn = this.onComplete.bind(this);
            this.target.tl.tween(this.params).then(function () {
                fn();
            })
        }
    })(command);
    

    test.js( 例1:直列実行 )

    enchant();
    
    window.onload = function () {
        var game = new Game(320, 320);
    
        game.onload = function () {
            var sp = new Sprite(20, 20);
            sp.backgroundColor = '#FF0000';
            game.rootScene.addChild(sp);
    
            var time = 30;
            // SerialCommandとParallelCommandはコンストラクタの引数に可変長引数としてコマンドを渡せます
            var s = new command.SerialCommand(
                new command.TweenerCommand(sp, {x:200, time:time, easing:enchant.Easing.SIN_EASEOUT}),
                new command.TweenerCommand(sp, {y:200, time:time, easing:enchant.Easing.SIN_EASEOUT}),
                new command.TweenerCommand(sp, {x:0, time:time, easing:enchant.Easing.SIN_EASEOUT}),
                new command.TweenerCommand(sp, {y:0, time:time, easing:enchant.Easing.SIN_EASEOUT})
            );
            s.delegate = function () {
                game.rootScene.removeChild(sp);
                console.log('end.');
            };
            s.execute();
        };
        game.start();
    };
    

    test.js( 例2:複合 )

    enchant();
    
    window.onload = function () {
        var game = new Game(320, 320);
    
        game.onload = function () {
            var max = 20;
            var arr = [];
    
            var p1 = new command.ParallelCommand();
            for (var i = 0; i < max; i++) {
                var sp = new Sprite(10, 10);
                sp.backgroundColor = "rgb(" + rand(255) + ',' + rand(255) + ',' + rand(255) + ')';
                game.rootScene.addChild(sp);
                var t = new command.TweenerCommand(sp, {x:rand(320), y:rand(320), time:2 * i, easing:enchant.Easing.SIN_EASEOUT});
                p1.add(t);
                arr.push(sp);
            }
    
            var p2 = new command.ParallelCommand();
            for (i = 0; i < max; i++) {
                sp = arr[i];
                t = new command.TweenerCommand(sp, {y:320 - 10, time:4 * i, easing:enchant.Easing.BOUNCE_EASEOUT});
                p2.add(t);
            }
    
            var s = new command.SerialCommand();
            for (i = 0; i < max; i++) {
                sp = arr[i];
                t = new command.TweenerCommand(sp, {x:0, y:0, time:4, easing:enchant.Easing.BOUNCE_EASEOUT});
                s.add(t);
            }
    
            var master = new command.SerialCommand(p1, p2);
            master.add(s);
            master.add(new command.TraceCommand('Please wait 60 frame.'));
            master.add(new command.WaitCommand(60, game.fps));
            master.add(new command.FuncCommand(
                function () {
                    for (i = 0; i < max; i++) {
                        sp = arr[i];
                        game.rootScene.removeChild(sp);
                    }
                    console.log('Remove child done.');
                }
            ));
    
            master.delegate = function () {
                console.log('All Commands end.');
            };
    
            master.execute();
        };
        game.start();
    };
    
    function rand(num) {
        return Math.floor(Math.random() * num);
    }
    

    index.html

    <!DOCTYPE html>
    <html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>enchant-command-demo</title>
        <meta name="viewport" content="width=device-width, user-scalable=no">
        <meta name="apple-mobile-web-app-capable" content="yes">
        <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
        <script src="enchant.js" type="text/javascript"></script>
        <script type="text/javascript" src="plugins/command.enchant.js"></script>
        <script src="test.js" type="text/javascript"></script>
        <style type="text/css">
            body {
                margin: 0;
            }
        </style>
    </head>
    <body>
    </body>
    </html>
    

    ページトップへ