ここからは後半の Step となります.
ゲームに UI を追加してより遊びやすくしてきます.
Table of contents
チュートリアル目次
前半
後半
サンプル
今回作るサンプルです.
上に秒数のカウントが表示され, RESTART ボタンを押すとゲームが再スタートするのがわかるかと思います.
またいきなりゲームが始まるのではなくゲーム開始前にカウントダウンするようになっています.
ソースコード
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(); }); }, });
解説
画面上にテキストを表示しよう
ここでも 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
を計算して, 先ほど生成した timerLabel
の text
に設定します.
これで時間が画面に表示され更新されるようになります.
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(); });
こうやってシーンを切り分けて作っておくことで依存関係が薄くなり, 分割して開発, メンテを行うことができます.
まとめ
おさえておいて欲しいポイント
tm.app.FlatButton
でサクッとボタンを設置できる- ボタンを押した時に実行するメソッドは
onpointingend
プロパティで登録できる pushScene
,popScene
でシーンを重ねたり, 取ったりできる
だいぶ画面がゲームっぽくなってきましたね.
ここまでで詰まったりうまく動かないなどありましたら 気軽に @phi_jpまでご連絡ください.
次回はタイトルシーン, リザルトシーンを作って 一連の流れ, シーケンスを作っていきます.