tmlib.js で今流行のフラットデザインを使ったタッチゲームを作ろう – Step03 UI を実装しよう

phiary に引っ越しました. 毎日プログラミングやWebに関する情報を発信しています! RSS 登録してたまに覗いたり, tweet やハテブして拡散してもらえると幸いです.

step03

Pocket

ここからは後半の Step となります.

ゲームに UI を追加してより遊びやすくしてきます.

Table of contents

チュートリアル目次

前半

後半

up

サンプル

今回作るサンプルです.

上に秒数のカウントが表示され, RESTART ボタンを押すとゲームが再スタートするのがわかるかと思います.

またいきなりゲームが始まるのではなくゲーム開始前にカウントダウンするようになっています.

up

ソースコード

main.js

画面上部に tm.app.Label を使って秒数を表示しています. 下部分には tm.app.FlatButton を使ってボタンを設置しています.

また, いきなりゲームが始まるのではなくゲーム開始前に CountdownScene を push することでカウントダウンするようにしています.

詳しい解説は下へ.

/*
 * main.js
 */

/*
 * contant
 */
var SCREEN_WIDTH    = 680;              // スクリーン幅
var SCREEN_HEIGHT   = 960;              // スクリーン高さ
var SCREEN_CENTER_X = SCREEN_WIDTH/2;   // スクリーン幅の半分
var SCREEN_CENTER_Y = SCREEN_HEIGHT/2;  // スクリーン高さの半分

var PIECE_NUM_X     = 5;                // ピースの列数
var PIECE_NUM_Y     = 5;                // ピースの行数
var PIECE_NUM       = PIECE_NUM_X*PIECE_NUM_Y;  // ピース数
var PIECE_OFFSET_X  = 90;               // ピースオフセットX 
var PIECE_OFFSET_Y  = 240;              // ピースオフセットY
var PIECE_WIDTH     = 120;              // ピースの幅
var PIECE_HEIGHT    = 120;              // ピースの高さ

var FONT_FAMILY_FLAT= "'Helvetica-Light' 'Meiryo' sans-serif";  // フラットデザイン用フォント

/*
 * main
 */
tm.main(function() {
    // アプリケーションセットアップ
    var app = tm.app.CanvasApp("#world");       // 生成
    app.resize(SCREEN_WIDTH, SCREEN_HEIGHT);    // サイズ(解像度)設定
    app.fitWindow();                            // 自動フィッティング有効
    app.background = "rgba(250, 250, 250, 1.0)";// 背景色

    app.replaceScene( GameScene() );    // シーン切り替え

    // 実行
    app.run();
});

/*
 * ゲームシーン
 */
tm.define("GameScene", {
    superClass: "tm.app.Scene",

    init: function() {
        this.superInit();

        var self = this;

        // カレント数
        self.currentNumber = 1;

        // ピースグループ
        this.pieceGroup = tm.app.CanvasElement();
        this.addChild(this.pieceGroup);

        // 数字配列
        var nums = [].range(1, PIECE_NUM+1);  // 1~25
        nums.shuffle(); // シャッフル

        // ピースを作成
        for (var i=0; i<PIECE_NUM_Y; ++i) {
            for (var j=0; j<PIECE_NUM_X; ++j) {
                // 数値
                var number = nums[ i*PIECE_NUM_X+j ];
                // ピースを生成してピースグループに追加
                var piece = Piece(number).addChildTo(this.pieceGroup);
                // 座標を設定
                piece.x = j * 125 + PIECE_OFFSET_X;
                piece.y = i * 125 + PIECE_OFFSET_Y;
                // タッチ時のイベントリスナーを登録
                piece.onpointingstart = function() {
                    // 正解かどうかの判定
                    if (this.number === self.currentNumber) {
                        // クリアかどうかの判定
                        if (self.currentNumber === PIECE_NUM) {
                            // 結果表示
                            var time = (self.app.frame/self.app.fps)|0;
                            alert("GameClear: {0}".format(time));
                        }
                        self.currentNumber += 1;// インクリメント
                        this.disable();         // ボタン無効
                    }
                };
            }
        }

        // タイマーラベル
        this.timerLabel = tm.app.Label("").addChildTo(this);
        this.timerLabel
            .setPosition(650, 160)
            .setFillStyle("#444")
            .setAlign("right")
            .setBaseline("bottom")
            .setFontFamily(FONT_FAMILY_FLAT)
            .setFontSize(128);

        // タイトルボタン
        var titleBtn = tm.app.FlatButton({
            width: 300,
            height: 100,
            text: "TITLE",
            bgColor: "#888",
        }).addChildTo(this);
        titleBtn.position.set(180, 880);
        titleBtn.onpointingend = function() {
            // TODO: replace title
        };
        // リスタートボタン
        var restartBtn = tm.app.FlatButton({
            width: 300,
            height: 100,
            text: "RESTART",
            bgColor: "#888",
        }).addChildTo(this);
        restartBtn.position.set(500, 880);
        restartBtn.onpointingend = function() {
            self.app.replaceScene(GameScene());
        };
    },

    onenter: function(e) {
        e.app.pushScene(CountdownScene());
        this.onenter = null;
    },

    update: function(app) {
        // タイマー更新
        var time = ((app.frame/app.fps)*1000)|0;
        var timeStr = time + "";
        this.timerLabel.text = timeStr.replace(/(\d)(?=(\d\d\d)+$)/g, "$1.");
    }
});


/*
 * ピースクラス
 */
tm.define("Piece", {
    superClass: "tm.app.Shape",

    init: function(number) {
        this.superInit(PIECE_WIDTH, PIECE_HEIGHT);
        // 数値をセット
        this.number = number;

        this.setInteractive(true);
        this.setBoundingType("rect");

        var angle = tm.util.Random.randint(0, 360);
        this.canvas.clearColor("hsl({0}, 80%, 70%)".format(angle));

        this.label = tm.app.Label(number).addChildTo(this);
        this.label
            .setFontSize(70)
            .setFontFamily(FONT_FAMILY_FLAT)
            .setAlign("center")
            .setBaseline("middle");
    },

    disable: function() {
        this.setInteractive(false);

        var self = this;
        this.tweener
            .clear()
            .to({scaleX:0}, 100)
            .call(function() {
                self.canvas.clearColor("rgb(100, 100, 100)");
            }.bind(this))
            .to({scaleX:1, alpha:0.5}, 100)
    }
});

tm.define("CountdownScene", {
    superClass: "tm.app.Scene",

    init: function() {
        this.superInit();
        var self = this;

        var filter = tm.app.Shape(SCREEN_WIDTH, SCREEN_HEIGHT).addChildTo(this);
        filter.origin.set(0, 0);
        filter.canvas.clearColor("rgba(250, 250, 250, 1.0)");

        var label = tm.app.Label(3).addChildTo(this);
        label
            .setPosition(SCREEN_CENTER_X, SCREEN_CENTER_Y)
            .setFillStyle("#888")
            .setFontFamily(FONT_FAMILY_FLAT)
            .setFontSize(512)
            .setAlign("center")
            .setBaseline("middle");

        label.tweener
            .set({
                scaleX: 0.5,
                scaleY: 0.5,
                text: 3
            })
            .scale(1)
            .set({
                scaleX: 0.5,
                scaleY: 0.5,
                text: 2
            })
            .scale(1)
            .set({
                scaleX: 0.5,
                scaleY: 0.5,
                text: 1
            })
            .scale(1)
            .call(function() {
                self.app.frame = 0;
                self.app.popScene();
            });
    },
});

up

解説

画面上にテキストを表示しよう

ここでも Step01 でピースの時に使った tm.app.Label を使います.

// タイマーラベル
this.timerLabel = tm.app.Label("").addChildTo(this);
this.timerLabel
    .setPosition(650, 160)
    .setFillStyle("#444")
    .setAlign("right")
    .setBaseline("bottom")
    .setFontFamily(FONT_FAMILY_FLAT)
    .setFontSize(128);

ピースのときよりも fontSize を大きめに設定しているので 画面上にボンッと表示されます.

また生成時には text に文字列を設定していないためまだ何も表示されません.

  update: function(app) {
      // タイマー更新
      var time = ((app.frame/app.fps)*1000)|0;
      var timeStr = time + "";
      this.timerLabel.text = timeStr.replace(/(\d)(?=(\d\d\d)+$)/g, "$1.");
  }

update は毎フレーム自動で呼ばれるメソッドです. ここで time を計算して, 先ほど生成した timerLabeltext に設定します.

これで時間が画面に表示され更新されるようになります.

replace の部分は正規表現を使って 3 文字間に “.” を追加しているだけです.

this.timerLabel.text = timeStr.replace(/(\d)(?=(\d\d\d)+$)/g, "$1.");

ボタンを設置しよう

tmlib.js には tm.app.FlatButton という クラスを定義しています.

これをシーンに追加するだけで画面上にボタンを 表示することができます.

// タイトルボタン
var titleBtn = tm.app.FlatButton({
    width: 300,
    height: 100,
    text: "TITLE",
    bgColor: "#888",
}).addChildTo(this);
titleBtn.position.set(180, 880);
titleBtn.onpointingend = function() {
    // TODO: replace title
};
// リスタートボタン
var restartBtn = tm.app.FlatButton({
    width: 300,
    height: 100,
    text: "RESTART",
    bgColor: "#888",
}).addChildTo(this);
restartBtn.position.set(500, 880);
restartBtn.onpointingend = function() {
    self.app.replaceScene(GameScene());
};

ボタンは tm.app.Object2d を継承しているので, 位置や幅, 高さを設定することができます.

restartBtn.position.set(500, 880);

またタッチした際に実行するメソッドは, onpointingend プロパティに設定することで登録できます.

restartBtn.onpointingend = function() {
    self.app.replaceScene(GameScene());
};

下記のようにすれば複数のメソッドを登録することも可能です.

addEventListener('pointingend', ...);

カウントダウンしてからゲームを開始するようにしよう

GameScene の上に CountdownScene を重ねてカウントダウンさせています.

tm.define("CountdownScene", {
    superClass: "tm.app.Scene",
 
    init: function() {
        this.superInit();
        var self = this;
 
        var filter = tm.app.Shape(SCREEN_WIDTH, SCREEN_HEIGHT).addChildTo(this);
        filter.origin.set(0, 0);
        filter.canvas.clearColor("rgba(250, 250, 250, 1.0)");
 
        var label = tm.app.Label(3).addChildTo(this);
        label
            .setPosition(SCREEN_CENTER_X, SCREEN_CENTER_Y)
            .setFillStyle("#888")
            .setFontFamily(FONT_FAMILY_FLAT)
            .setFontSize(512)
            .setAlign("center")
            .setBaseline("middle");
 
        label.tweener
            .set({
                scaleX: 0.5,
                scaleY: 0.5,
                text: 3
            })
            .scale(1)
            .set({
                scaleX: 0.5,
                scaleY: 0.5,
                text: 2
            })
            .scale(1)
            .set({
                scaleX: 0.5,
                scaleY: 0.5,
                text: 1
            })
            .scale(1)
            .call(function() {
                self.app.frame = 0;
                self.app.popScene();
            });
    },
});

CountDownScene の実装はすごくシンプルです.

まず下の GameScene が見えないように画面いっぱいに 白い四角形を表示します.

origin(0,0) にして基準位置を左上にするのを忘れないようにしましょう.

var filter = tm.app.Shape(SCREEN_WIDTH, SCREEN_HEIGHT).addChildTo(this);
filter.origin.set(0, 0);
filter.canvas.clearColor("rgba(250, 250, 250, 1.0)");

次に, カウントダウンで数字を表示するためのラベルを表示します. これは今まで同様 tm.app.Label をシーンに追加するだけです.

var label = tm.app.Label(3).addChildTo(this);

あとは tweener を使って良い感じでカウントダウンさせていきます.

まず, サイズを半分にして text プロパティを 3 に設定します. つまり 3 からカウントダウンを開始します.

.set({
    scaleX: 0.5,
    scaleY: 0.5,
    text: 3
})

そしてスケールアニメーションさせていきます.

.scale(1)

これを 3, 2, 1 と繰り返します.

.set({
    scaleX: 0.5,
    scaleY: 0.5,
    text: 2
})
.scale(1)
.set({
    scaleX: 0.5,
    scaleY: 0.5,
    text: 1
})
.scale(1)

そして最後に, call メソッドを実行し, popScene を実行して自分自身を削除します. つまり GameScene を有効にします.

.call(function() {
    self.app.frame = 0;
    self.app.popScene();
});

こうやってシーンを切り分けて作っておくことで依存関係が薄くなり, 分割して開発, メンテを行うことができます.

up

まとめ

おさえておいて欲しいポイント

  • tm.app.FlatButton でサクッとボタンを設置できる
  • ボタンを押した時に実行するメソッドは onpointingend プロパティで登録できる
  • pushScene, popScene でシーンを重ねたり, 取ったりできる

up

だいぶ画面がゲームっぽくなってきましたね.

ここまでで詰まったりうまく動かないなどありましたら 気軽に @phi_jpまでご連絡ください.

次回はタイトルシーン, リザルトシーンを作って 一連の流れ, シーケンスを作っていきます.

TRACK BACK URL

POST COMMENT

メールアドレスが公開されることはありません。

COMMENT