読者です 読者をやめる 読者になる 読者になる

フニゲの開発日記

Electronとか...

リプレイ機能を作る

Cocos2d-JS

 リプレイ機能そのものはまあ、あんまり需要ないかもしれないが、例えばチュートリアルを作るときとか、デモプレイを作るときに、操作の履歴をJSONデータに書き出して簡単に再生できるようにしておくと便利だ。

 チュートリアルは開発の最後に短期間で作らざるを得ないことが多いのだが、このへん最初に作っておかないと、あとで苦労することになると思う。

...
var GameLayer = cc.Layer.extend({
    ctor:function () {
        this._super();
        this.scheduleUpdate();

        // 操作を記録
        this.gameLogger = new GameLogger(this);
        this.addChild(this.gameLogger);

        this.gameLogger.doTouchMoved = function (pos) {
            this.player.setPosition(pos); // 自機を移動したとき
        }.bind(this);

        this.gameLogger.doStop = function () {
            cc.log("* replay end *"); // リプレイが終わったとき
            this.gameLogger.init();
        }.bind(this);

        // 自機を表示
        this.player = new cc.Sprite("res/ship.png");
        this.player.setPosition(200, 200);
        this.addChild(this.player);
        
        // ボタンなど表示
        var replayButton = new cc.MenuItemLabel(
            new cc.LabelTTF("[REPLAY]", 24),
            function () { 
                this.gameLogger.replay(); // リプレイボタンを押したらリプレイ開始
                cc.log("* replay *");
            }.bind(this));

        var dumpButton = new cc.MenuItemLabel(
            new cc.LabelTTF("[DUMP]", 24),
            function () { 
                cc.log(this.gameLogger.getLog()); // 履歴をJSONで出力
            }.bind(this));

        dumpButton.y -= 40;
        var menu = new cc.Menu(replayButton, dumpButton);
        this.addChild(menu);
    },

    update:function (dt) {
        this.gameLogger.step();
    },
});

 イベントリスナはGameLoggerの内部で生成して、addEventListenerしている。
 キャッチしたonTouchMovedとかを直接使うのではなく、1/60秒ごとにイベントをバッファに保存して、その履歴を見て自機を動かす処理をGameLoggerのdoTouchMovedなどに書くことにする。

var GameLogger = cc.Node.extend({
    ctor:function (target) {
        this._super();
        this.target = target;
        this.init();

        cc.eventManager.addListener(this.getListener(), target);
    },

    init:function () {
        this.log = {};
        this.mode = "record";
        this.count = 0;
    },

    replay:function () {
        this.pushRecord(this.count, {type:"stop"});
        this.mode = "play";
        this.count = 0;
    },

    step:function () {
        var count = this.count++;
        var record = this.getRecord(count);

        for (var i = 0; i < record.length; i++) {
            var item = record[i];
            switch (item.type) {
            case "touchBegan":
                this.doTouchBegan(item.pos);
                break;
            case "touchMoved":
                this.doTouchMoved(item.pos);
                break;
            case "touchEnded":
                this.doTouchEnded(item.pos);
                break;
            case "touchCancelled":
                this.doTouchCancelled(item.pos);
                break;
            case "stop":
                this.doStop();
                break;
            }
        }
    },

    doTouchBegan:function (pos) {},
    doTouchMoved:function (pos) {},
    doTouchEnded:function (pos) {},
    doTouchCancelled:function (pos) {},
    doStop:function () {},

    getLog:function () {
        return JSON.stringify(this.log);
    },

    getRecord:function (count) {
        if (this.log && this.log[count]) {
            return this.log[count];
        }
        return {};
    },

    pushRecord:function (count, record) {
        if (!this.log[count]) this.log[count] = [];
        this.log[count].push(record);
    },

    onTouchBegan:function (pos) {
        if (this.mode == "record") {
            this.pushRecord(this.count, {type:"touchBegan", pos:pos});
        }
        return true;
    },

    onTouchMoved:function (pos) {
        if (this.mode == "record") {
            this.pushRecord(this.count, {type:"touchMoved", pos:pos});
        }
    },

    onTouchEnded:function (pos) {
        if (this.mode == "record") {
            this.pushRecord(this.count, {type:"touchEnded", pos:pos});
        }
    },

    onTouchCancelled:function (pos) {
        if (this.mode == "record") {
            this.pushRecord(this.count, {type:"touchCancelled", pos:pos});
        }
    },

    getListener:function () {
        this.listener = new cc.EventListener.create({
            event:cc.EventListener.TOUCH_ONE_BY_ONE,
            swallowTouches:true,

            onTouchBegan:function (touch, event) {
                this.onTouchBegan(touch.getLocation());
                return true;
            }.bind(this),

            onTouchMoved:function (touch, event) {
                this.onTouchMoved(touch.getLocation());
            }.bind(this),

            onTouchEnded:function (touch, event) {
                this.onTouchEnded(touch.getLocation());
            }.bind(this),

            onTouchCancelled:function (touch, event) {
                this.onTouchCancelled(touch.getLocation());
            }.bind(this),
        });
        return this.listener;
    }
});

 bindが多くて気持ち悪いのだが、javaScriptだからしょうがないのかな。
 普通cocos2d-jsでゲームを作ってるとbindを使う機会がほとんど無いので、自信が無い。もうちょっときれいに書けると思う。

 この例では1/60秒ごとにupdateが呼ばれることを前提にしているけど、頻繁に処理落ちが起こるような思考型のゲームでは別の仕組みが必要になるだろう。

サンプル

広告を非表示にする