#はじめに
最近、tmlibを使う機会が多いため
簡単なゲームでtmlibのチュートリアル的なものを書いてみようかと思います。
技術ブログ的なことは初めて書くので、見づらいかもです…
とりあえず、プログラミングでサンプルが多いブロック崩しを作成!
ちなみに、画像は一切使用せず、図形だけでつくります。
##1.どこに書こう
プログラムソースとかサーバーに置くの面倒なので、
とりあえず、runStantで書いていきます!
runStant
tmlib.js の公式エディタツール!
書いたコードがすぐに実行されるので、
tmlibやhtml5、javascript等をさくっと実行するのに便利。
さらに、書いた内容はURLで保存できるので、ブックマーク等を使用して
いつでも続きから書ける。
runStantはこちら
##2.tmlibの準備
最初にrunStantのページへアクセスすると、
下の画像のようになっているはずです。(2014年12月)
使用しているブラウザはchromeです。
ここからまずは、tmlibを使えるようにします。
1.ページ右上のhtmlタブをクリック
2.下記のコードを参考に変更
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<title>${title}</title>
<meta name="description" content="${description}" />
<style>${style}</style>
//変更
<script src="https://cdn.rawgit.com/phi-jp/tmlib.js/develop/build/tmlib.min.js"></script>
<script>${script}</script>
</head>
<body>
//変更
<canvas id="world"></canvas>
</body>
</html>
3.今度は、javascriptタブをクリック
tm.main(function(){
console.log("start tmlib");
});
左下のフレームにstart tmlib
と表示されていればtmlibを使用する準備ができています。
できない場合はこちら
##3.とりあえず適当に表示
手始めに適当な大きさの四角を描画してみました。
var app = tm.display.CanvasApp("#World")
tmlibで使用するアプリケーションクラスを作成します。
指定している**#worldはhtmlで記述したcanvasタグのid**です。
// キャンバスの大きさを変更
app.resize(SCREEN_WIDTH, SCREEN_HEIGHT);
// キャンバスの大きさを現在のウィンドウの大きさに合わせる
app.fitWindow();
キャンバスサイズを変更して、比率を変えずに現在のブラウザに合わせてくれます。
// 四角を作る
var rect = tm.display.RectangleShape(200 , 200);
//画面(app.currentScene)に追加
rect.addChildTo(app.currentScene);
// 位置をセット ※画面の中心においてみる
rect.setPosition(SCREEN_WIDTH * 0.5, SCREEN_HEIGHT * 0.5);
この3行で、四角形を描画する準備をしています。
tm.display.RectangleShapeで四角形を生成し、addChildToで画面(シーン)に追加。
setPositionで表示位置の設定をしています。
//実行
app.run();
描画や更新処理が始まるようになります。
ここまでのコードはこちら
##4.ボールを表示
ここからブロック崩し自体の実装になります。
いざ、ボールを描画...と思ったのですが、折角なので見やすいコードをめざして、
クラス化しながら実装してみます。
tmlibでは、クラス作成用のメソッドがあります。
tm.define();
とりあえず、ゲーム用のシーンをクラス化してみましょう。
//=============================================
// ゲーム用のシーンクラス
//=============================================
tm.define("GameScene" , {
//基本となるシーンクラスを継承
superClass:"tm.app.Scene",
//初期化処理
init:function(){
//継承元クラスの初期化処理
this.superInit();
}
});
tm.app.Sceneというクラスを継承したGameSceneクラスができました。
GameSceneを基本にしていくために、tm.mainも変更します。
tm.main(function(){
// キャンバスからアプリケーションクラスを作る
var app = tm.display.CanvasApp("#world");
// キャンバスの大きさを変更
app.resize(SCREEN_WIDTH, SCREEN_HEIGHT);
// キャンバスの大きさを現在のウィンドウの大きさに合わせる
app.fitWindow();
//ゲーム用にシーンを用意する
var gameScene = GameScene();
//シーンをゲーム用のシーンと差し替える
app.replaceScene(gameScene);
// 実行
app.run();
});
tm.mainでGameSceneを作成して、app.replaceSceneで使用するシーンを差し替えます。
これでゲームを作る準備ができたので、同じようにボールクラスから作成していきます。
//ボールのサイズ
var BALL_SIZE = 25;
//ボールの色
var BALL_COLOR = {fillStyle:"red" , strokeStyle:"transparent"};
//==============================================================
// ブロック崩し用のボール
//==============================================================
tm.define("Ball" ,{
//円を表示するクラスを継承
superClass:"tm.display.CircleShape",
//初期化処理
init:function(){
//継承元クラスの初期化 (幅、高さ、色)
this.superInit(BALL_SIZE , BALL_SIZE , BALL_COLOR);
},
});
ボールクラスは円を描画してくれるtm.display.CircleShapeを継承しています。
先ほど作成したGameSceneにボールを追加
//=============================================
// ゲーム用のシーンクラス
//=============================================
tm.define("GameScene" , {
//基本となるシーンクラスを継承
superClass:"tm.app.Scene",
//初期化処理
init:function(){
//継承元クラスの初期化処理
this.superInit();
//ボールを追加
var ball = Ball();
//ゲームシーン内に追加
ball.addChildTo(this);
//表示位置を設定
ball.setPosition(SCREEN_CENTER_X , SCREEN_CENTER_Y);
//ゲームシーンでボールを保持
this.ball = ball;
}
});
ただの円を表示しただけなので、なに作ってるかわからないですね。
ここまでのコードはこちら
##5.パドルを表示
ボールを跳ね返すパドルを作ります。
ボールと同じように、パドルクラスを下のように作成
//パドルの幅
var PADDLE_WIDTH = 120;
//パドルの高さ
var PADDLE_HEIGHT = 20;
//パドルの色
var PADDLE_COLOR = {fillStyle:"yellow" , strokeStyle:"transparent"};
//=============================================
// ブロック崩し用のパドル
//=============================================
tm.define("Paddle" , {
//四角を表示するクラスを継承
superClass:"tm.display.RectangleShape",
init:function(){
//継承元クラスの初期化 (幅、高さ、色)
this.superInit(PADDLE_WIDTH , PADDLE_HEIGHT , PADDLE_COLOR);
}
});
パドルは四角形なので、tm.display.RectangleShapeを継承しました。
パドルをGameSceneに追加します。
//=============================================
// ゲーム用のシーンクラス
//=============================================
tm.define("GameScene" , {
//基本となるシーンクラスを継承
superClass:"tm.app.Scene",
init:function(){
//継承元クラスの初期化処理
this.superInit();
//======== ボール ================
//ボールを追加
var ball = Ball();
//ゲームシーン内に追加
ball.addChildTo(this);
//表示位置を設定
ball.setPosition(SCREEN_CENTER_X , SCREEN_CENTER_Y);
//ゲームシーンでボールを保持
this.ball = ball;
//======== パドル ================
//パドルを追加
var paddle = Paddle();
//ゲームシーン内に追加
paddle.addChildTo(this);
//表示位置を設定
paddle.setPosition(SCREEN_CENTER_X , SCREEN_HEIGHT - 80);
//ゲームシーンでパドルを保持
this.paddle = paddle;
}
});
まだ肝心なものが描画されていませんね。
ここまでのコードはこちら
##6.ブロックを表示
次はブロックを表示してみます。
ひとまず、1つのブロックを表示していきます。
まずは、ブロッククラスを作ってみましょう。
//ブロックの幅
var BLOCK_WIDTH = 140;
//ブロックの高さ
var BLOCK_HEIGHT = 30;
//ブロックの色
var BLOCK_COLOR = {fillStyle:"blue" , strokeStyle:"white" , lineWidth:2};
//=============================================
// ブロック崩し用のブロック
//=============================================
tm.define("Block" , {
//四角を表示するクラスを継承
superClass:"tm.display.RectangleShape",
init:function(){
//継承元クラスの初期化 (幅、高さ、色)
this.superInit(BLOCK_WIDTH, BLOCK_HEIGHT , BLOCK_COLOR);
}
});
ブロックも四角形なので、tm.display.RectangleShapeを継承しました。
色とサイズ以外は、パドルと同じですね。
これを、GameSceneに追加します。
//=============================================
// ゲーム用のシーンクラス
//=============================================
tm.define("GameScene" , {
//基本となるシーンクラスを継承
superClass:"tm.app.Scene",
init:function(){
//継承元クラスの初期化処理
this.superInit();
//======== ボール ================
//ボールを追加
var ball = Ball();
//ゲームシーン内に追加
ball.addChildTo(this);
//表示位置を設定
ball.setPosition(SCREEN_CENTER_X , SCREEN_HEIGHT - 100);
//ゲームシーンでボールを保持
this.ball = ball;
//======== パドル ================
//パドルを追加
var paddle = Paddle();
//ゲームシーン内に追加
paddle.addChildTo(this);
//表示位置を設定
paddle.setPosition(SCREEN_CENTER_X , SCREEN_HEIGHT - 80);
//ゲームシーンでパドルを保持
this.paddle = paddle;
//======== ブロック ================
//ブロックを追加
var block = Block();
//ゲームシーン内に追加
block.addChildTo(this);
//表示位置を設定
block.setPosition(SCREEN_CENTER_X , 100);
}
});
表示されたのが下の画像です。
ボールの位置もパドルに近づけました。
やっとブロック崩しっぽくなってきました。
ここまでのコードはこちら
##7.ブロックを複数表示
先ほど作成したブロックを、複数表示してみましょう。
//ブロックの表示数(横)
var BLOCK_LINE_NUM_X = 4;
//ブロックの表示数(縦)
var BLOCK_LINE_NUM_Y = 4;
//全ブロック合わせた横幅
var BLOCK_TOTAL_WIDTH = BLOCK_LINE_NUM_X * BLOCK_WIDTH;
//ブロックの表示位置(X)
var BLOCK_OFFSET_X = (SCREEN_WIDTH - BLOCK_TOTAL_WIDTH) * 0.5 + BLOCK_WIDTH * 0.5;
//ブロックの表示位置(Y)
var BLOCK_OFFSET_Y = 50;
//=============================================
// ゲーム用のシーンクラス
//=============================================
tm.define("GameScene" , {
//~~~ 省略 ~~~
//======== パドル ================
//パドルを追加
var paddle = Paddle();
//ゲームシーン内に追加
paddle.addChildTo(this);
//表示位置を設定
paddle.setPosition(SCREEN_CENTER_X , SCREEN_HEIGHT - 80);
//ゲームシーンでパドルを保持
this.paddle = paddle;
//======== ブロック ================
var blockArray = [];
//縦
for(var y = 0 ; y < BLOCK_LINE_NUM_Y ; y++){
//横
for(var x = 0; x < BLOCK_LINE_NUM_X ; x++){
var block = Block();
//ゲームシーン内に追加
block.addChildTo(this);
//表示位置を設定
block.setPosition(
BLOCK_OFFSET_X + x * BLOCK_WIDTH ,
BLOCK_OFFSET_Y + y * BLOCK_HEIGHT
);
//作成したブロックは配列で保持
blockArray.push(block);
}
}
}
});
for文で縦と横の数だけブロックを表示しました。
結果が下の画像
見た目は完全にブロック崩しになりましたね。
ここまでのコードはこちら
##8.パドルを動かす
ここから遊べるようにするために動きをつけていきます。
画像では結果が説明しづらいため、
動いている状態はrunstantのリンクから確認してみて下さい。
まずはパドルをキーボードで動かせるようにしましょう。
作成したPaddleクラスとGameSceneクラスを変更していきます。
//パドルの移動量
var PADDLE_MOVE_VALUE = 6;
//=============================================
// ブロック崩し用のパドル
//=============================================
tm.define("Paddle" , {
//四角を表示するクラスを継承
superClass:"tm.display.RectangleShape",
init:function(){
//継承元クラスの初期化 (幅、高さ、色)
this.superInit(PADDLE_WIDTH , PADDLE_HEIGHT , PADDLE_COLOR);
},
//移動処理
move:function(app){
//左キーが押されている場合
if (app.keyboard.getKey("left")){
this.x -= PADDLE_MOVE_VALUE;
}
//右キーが押されている場合
else if (app.keyboard.getKey("right")){
this.x += PADDLE_MOVE_VALUE;
}
},
});
//=============================================
// ゲーム用のシーンクラス
//=============================================
tm.define("GameScene" , {
//基本となるシーンクラスを継承
superClass:"tm.app.Scene",
init:function(){
//~~~ 省略 ~~~
},
//定期更新処理
update:function(app){
//パドルの移動 appをパドルクラスの移動処理に渡す
this.paddle.move(app);
},
});
**update()**は、毎フレーム呼ばれるため、
定期的な更新処理を書くことができます。
updateの引数のappから、
キーボードの状態を知ることができます。
app.keyboard.getKey("left")
左キーが押されていればtrueが返ってきます。
app.keyboard.getKey("right")
右キーが押されていればtrueが返ってきます。
☆ここまでのコードはこちら
##9.ボールを動かす
パドルを動かしたので、今度はボールを動かしましょう。
BallクラスとGameSceneクラスを変更していきます。
//ボールの移動量(X)
var BALL_MOVE_VALUE_X = 6;
//ボールの移動量(Y)
var BALL_MOVE_VALUE_Y = -6;
//=============================================
// ブロック崩し用のボール
//=============================================
tm.define("Ball" ,{
//円を表示するクラスを継承
superClass:"tm.display.CircleShape",
init:function(){
//継承元クラスの初期化 (幅、高さ、色)
this.superInit(BALL_SIZE , BALL_SIZE , BALL_COLOR);
this.isMove = true;
this.addValueX = BALL_MOVE_VALUE_X;
this.addValueY = BALL_MOVE_VALUE_Y;
},
//ボールの移動処理
move:function(){
//移動可能な場合
if(this.isMove){
//座標を更新する
this.x += this.addValueX;
this.y += this.addValueY;
}
}
});
//=============================================
// ゲーム用のシーンクラス
//=============================================
tm.define("GameScene" , {
//基本となるシーンクラスを継承
superClass:"tm.app.Scene",
init:function(){
//~~~ 省略 ~~~
},
//定期更新処理
update:function(app){
//パドルの移動 appをパドルクラスの移動処理に渡す
this.paddle.move(app);
//ボールの移動
this.ball.move();
},
});
BallクラスのmoveをGameSceneのupdateで呼ぶことによって、
定期的にBallのxとy座標が更新されるため、
ボールが右上に向かって動くようになります。
ちなみに、Ballクラスでupdateを作成しても同じように動きますが、
定期更新する際に、処理の順番がよくわからなくなってしまうことがあるので、
GameSceneのupdateでまとめたほうが、後々複雑になったときに楽になります。
ここまでのコードはこちら
##10.ボールと画面の当たり判定
ボールが動くようにはなりましたが、
右上に向かって、そのまま画面外まで飛んでいってしまうため、
画面内でバウンドするようにしましょう。
BallクラスとGameSceneクラスを変更していきます。
//=============================================
// ブロック崩し用のボール
//=============================================
tm.define("Ball" ,{
//円を表示するクラスを継承
superClass:"tm.display.CircleShape",
init:function(){
//~~~ 省略 ~~~
},
//ボールの移動処理
move:function(){
//~~~ 省略 ~~~
},
//画面とボールのバウンド処理
boundScreen:function(){
//画面左側
if(this.left < 0){
//移動量を逆にする
this.addValueX *= -1;
//画面端にめり込み続けないように補正する
this.x = BALL_SIZE * 0.5;
}
//画面右側
else if(this.right > SCREEN_WIDTH){
//移動量を逆にする
this.addValueX *= -1;
//画面端にめり込み続けないように補正する
this.x = SCREEN_WIDTH - BALL_SIZE * 0.5;
}
//画面上側
if(this.top < 0){
//移動量を逆にする
this.addValueY *= -1;
//画面端にめり込み続けないように補正する
this.y = BALL_SIZE * 0.5;
}
//画面下側
else if(this.bottom > SCREEN_HEIGHT){
//移動量を逆にする
this.addValueY *= -1;
//画面端にめり込み続けないように補正する
this.y = SCREEN_HEIGHT - BALL_SIZE * 0.5;
}
},
});
//=============================================
// ゲーム用のシーンクラス
//=============================================
tm.define("GameScene" , {
//基本となるシーンクラスを継承
superClass:"tm.app.Scene",
init:function(){
//~~~ 省略 ~~~
},
//定期更新処理
update:function(app){
//パドルの移動 appをパドルクラスの移動処理に渡す
this.paddle.move(app);
//ボールの移動
this.ball.move();
//ボールと画面のバウンド処理
this.ball.boundScreen();
},
});
とりあえず、画面の右、上、左、下とボールの位置によってバウンドの処理を作りました。
判定の処理については、あまり詳しく書きませんが、
図形や画像の基準点が、中心となっているため、
右側の壁と判定する場合、そのまま処理を書こうとすると
if( (this.x + this.width * 0.5 ) > SCREEN_WIDTH)
のように、
ボールの右側を計算してあげないといけないのですが、
tmlibでは
if( this.right > SCREEN_WIDTH)
this.rightで、右側の座標が取得できます。
これ、すごく楽です。
- this.left
- 左の座標
- this.top
- 上の座標
- this.bottom
- 下の座標
- this.x
- 中心の座標
が取得できます。
これで、ボールが画面内をバウンドして動き続けるようになったかと思います。
ここまでのコードはこちら
##11.ボールとパドルの当たり判定
さて、もう少しです。
パドルでボールを打ち返せるようにしましょう。
BallクラスとGameSceneクラスを変更します。
//=============================================
// ブロック崩し用のボール
//=============================================
tm.define("Ball" ,{
//円を表示するクラスを継承
superClass:"tm.display.CircleShape",
init:function(){
//~~~ 省略 ~~~
},
//ボールの移動処理
move:function(){
//~~~ 省略 ~~~
},
//画面とボールのバウンド処理
boundScreen:function(){
//~~~ 省略 ~~~
},
//パドルとボールのバウンド処理
boundPaddle:function(paddle){
//パドルとの判定は移動が下方向だった場合のみ処理する
if(this.addValueY > 0){
if(this.isHitElement(paddle)){
this.addValueY *= -1;
this.y = paddle.top - BALL_SIZE * 0.5;
}
}
}
});
//=============================================
// ブロック崩し用のパドル
//=============================================
tm.define("Paddle" , {
//四角を表示するクラスを継承
superClass:"tm.display.RectangleShape",
init:function(){
//継承元クラスの初期化 (幅、高さ、色)
this.superInit(PADDLE_WIDTH , PADDLE_HEIGHT , PADDLE_COLOR);
//形の種類を四角に設定する
this.setBoundingType("rect");
},
move:function(app){
//~~~ 省略 ~~~
},
});
//=============================================
// ゲーム用のシーンクラス
//=============================================
tm.define("GameScene" , {
//基本となるシーンクラスを継承
superClass:"tm.app.Scene",
init:function(){
//~~~ 省略 ~~~
},
//定期更新処理
update:function(app){
//パドルの移動 appをパドルクラスの移動処理に渡す
this.paddle.move(app);
//ボールの移動
this.ball.move();
//ボールと画面のバウンド処理
this.ball.boundScreen();
//ボールとパドルのバウンド処理
this.ball.boundPaddle(this.paddle);
},
});
今回は、ボールの移動方向が下向きの場合のみ打ち返すようにしました。
**isHitElement()**でElement(ボールとかパドルとかブロックとか)同士の当たり判定ができます。
ここでワンポイント
Paddleクラスに追加したsetBoundingTypeで当たり判定の形を設定するのを忘れないようにしましょう。
- setBoundingType("rect")
- 判定の形を四角に設定する
- setBoundingType("circle")
- 判定の形を円に設定する
これでパドルでボールが打ち返せるようになりました。
ここまでのコードはこちら
##12.ボールとブロックの当たり判定
最後にボールとブロックの当たり判定を実装します。
この当たり判定については、
比較的簡単な判定方法で作成してみました。
BlockクラスとGameSceneクラスを変更していきます。
//=============================================
// ブロック崩し用のボール
//=============================================
tm.define("Ball" ,{
//円を表示するクラスを継承
superClass:"tm.display.CircleShape",
init:function(){
//継承元クラスの初期化 (幅、高さ、色)
this.superInit(BALL_SIZE , BALL_SIZE , BALL_COLOR);
//形の種類を円に設定する
this.setBoundingType("circle");
//移動可能フラグ
this.isMove = true;
//ボールの初期移動量と方向
this.addValueX = BALL_MOVE_VALUE_X;
this.addValueY = BALL_MOVE_VALUE_Y;
//ボールに攻撃力を設定
this.attack = 1;
},
//ボールの移動処理
move:function(){
//~~~ 省略 ~~~
},
//画面とボールのバウンド処理
boundScreen:function(){
//~~~ 省略 ~~~
},
//パドルとボールのバウンド処理
boundPaddle:function(paddle){
//パドルとの判定は移動が下方向だった場合のみ処理する
if(this.addValueY > 0){
if(this.isHitElement(paddle)){
this.addValueY *= -1;
this.y = paddle.top - BALL_SIZE * 0.5;
}
}
}
});
//=============================================
// ブロック崩し用のブロック
//=============================================
tm.define("Block" , {
//四角を表示するクラスを継承
superClass:"tm.display.RectangleShape",
init:function(){
//継承元クラスの初期化 (幅、高さ、色)
this.superInit(BLOCK_WIDTH, BLOCK_HEIGHT , BLOCK_COLOR);
//形の種類を四角に設定する
this.setBoundingType("rect");
//ブロックに耐久力を設定
this.hp = 1;
},
//ボールとブロックの判定
damage:function(ball){
//耐久力が残っている場合のみ判定
if(this.hp > 0){
//ボールとブロックがあたっているか判定
if(this.isHitElement(ball)){
//ブロックの耐久値をボールの攻撃力分減らす
this.hp -= ball.attack;
//ブロックの耐久値が0ならば非表示にする
if(this.hp <= 0){
this.setVisible(false);
}
//上方向に進んでいるとき
if(ball.addValueY < 0){
//右方向に進んでいるとき
if(ball.addValueX > 0){
if(ball.y < this.bottom){
ball.addValueX *= -1;
}else {
ball.addValueY *= -1;
}
}
//左方向に進んでいるとき
else{
if(ball.y < this.bottom){
ball.addValueX *= -1;
}else {
ball.addValueY *= -1;
}
}
}
//下方向に進んでいるとき
else{
//右方向に進んでいるとき
if(ball.addValueX > 0){
if(ball.y > this.top){
ball.addValueX *= -1;
}else {
ball.addValueY *= -1;
}
}
//左方向に進んでいるとき
else{
if(ball.y > this.top){
ball.addValueX *= -1;
}else {
ball.addValueY *= -1;
}
}
}
}
}
},
});
//=============================================
// ゲーム用のシーンクラス
//=============================================
tm.define("GameScene" , {
//基本となるシーンクラスを継承
superClass:"tm.app.Scene",
init:function(){
//~~~ 省略 ~~~
//ブロックのリストをGameSceneで保持する
//(複数のブロックを表示する際に忘れてました)
this.blockArray = blockArray
},
update:function(app){
//パドルの移動
this.paddle.move(app);
//ボールの移動
this.ball.move();
//ボールと画面のバウンド処理
this.ball.boundScreen();
//ボールrとパドルのバウンド処理
this.ball.boundPaddle(this.paddle);
//ボールとブロックの衝突判定
this.blockArray.each(function(block){
block.damage(this.ball);
}.bind(this));
},
});
判定の方法ですが、
ボールの移動方向と、ブロックから見たボールの位置で処理を分けました。
結構単純な処理ですが、いい感じに動いてくれます。
これで、ブロック崩しの最低限の処理は実装されました。
ここまでのコードはこちら
##さいごに
こんなに長い記事を読んで頂きありがとうございます。
ゲームつくってみよう系を全部説明すると一回じゃ無理ですね。
実際にはゲーム開始やゲームオーバー処理まで書きたかったのですが、
長すぎるため、次回続きを書こうかと思います。
初めて記事書いてみましたが、
javascriptの記事書く際にranstantって便利すぎじゃありません?
Advent Calendar 12/3担当のh_mjlifeでした。
遅くなってすいません。