ブロック崩しを動かそう
16フレーム目“play_set”のコードは以下の通りです。
// フレーム移動 this.gotoAndStop("play"); var _this = this; var _area = this.area; //------ プレイエリア var _racket = this.racket; //-- ラケット var _ball = this.ball; //------ ボール _ball.maxSpd = 8; //ボールの横向きの最大値 //-------------------- ブロック生成 var blockSet = new createjs.MovieClip(); //空のムービークリップ(blockSet)を生成 blockSet.x = 15; blockSet.y = 45; blockSet.name = "blockSet"; this.addChild(blockSet); //thisの子インスタンスとして配置 for (var ix = 0; ix < 9; ix++) { for (var iy = 0; iy < 4; iy++) { var mc_block = new lib.mc_block(); //ブロックのムービークリップ・インスタンスを生成 mc_block.x = ix * 35; //横のピッチ mc_block.y = iy * 15; //縦のピッチ mc_block.name = ("x" + ix + "_y" + iy); blockSet.addChild(mc_block); //blockSetの子インスタンス } } //ボールの初期値 _ball.spdX = (Math.random() * 2 - 1) * _ball.maxSpd; //-- 横のスピード(ランダム) _ball.spdY = -5; //-- 縦のスピード(固定値) // ボールの動き _ball.addEventListener("tick", onBallMove); //-- ボールの動き function onBallMove(e) { _ball.x += _ball.spdX; _ball.y += _ball.spdY; //---------------- 壁に当たる if (_ball.x <= 0) { // 左 _ball.x *= -1; _ball.spdX = Math.abs(_ball.spdX); } else if (_ball.x >= _area.width) { // みぎ _ball.x = _area.width * 2 - _ball.x; _ball.spdX = (-1) * Math.abs(_ball.spdX); } if (_ball.y <= 0) { //天井 _ball.y *= -1; _ball.spdY = Math.abs(_ball.spdY); } //------------------------------ ブロックに当たる var b_loc = _ball.localToLocal(0, 0, blockSet); //ボールとブロックセットの相対距離 if (blockSet.hitTest(b_loc.x, b_loc.y)) { //ボールがブロックセットに当たった //当たった向きの確認 var check = 0; if (blockSet.hitTest((b_loc.x - _ball.spdX), b_loc.y) == false) { _ball.spdX *= (-1); //よこ方向を反転 check++; } if (blockSet.hitTest(b_loc.x, (b_loc.y - _ball.spdY)) == false) { _ball.spdY *= (-1); //たて方向を反転 check++; } if (check == 0) { //どちらのチェックも引っかからないレアケース _ball.spdX *= (-1); //よこ方向を反転 _ball.spdY *= (-1); //たて方向を反転 } //当たったブロックを見つけて削除 for (var _block of blockSet.children) { var b_loc0 = _ball.localToLocal(0, 0, _block); //ボールとブロックの相対距離 if (_block.hitTest(b_loc0.x, b_loc0.y)) { //ブロックを消す blockSet.removeChild(_block); } } } //------------------------------ ラケットに当たる var b_racket = _ball.localToLocal(0, 0, _racket); //ボールとブロックの相対距離 if (_racket.hitTest(b_racket.x, b_racket.y)) { var hitValue = (_ball.x - _racket.x) / (_racket.width / 2); _ball.spdX = hitValue * _ball.maxSpd; _ball.spdY = Math.abs(_ball.spdY) * (-1); } //------------------------------ ゲームオーバー if (_ball.y > _area.height) { _ball.removeEventListener("tick", onBallMove); //-- ボールの動き _this.gotoAndPlay("miss"); } //------------------------------ クリア if (blockSet.numChildren == 0) { _ball.removeEventListener("tick", onBallMove); //-- ボールの動き _this.gotoAndPlay("clear"); } }
ブロックを作りましょう
ブロックのムービークリップ・シンボルを準備する
ブロックは見た目の大きさを幅30x高さ10にします。このサイズにすると横に9列、縦に4列でちょうどいいサイズです。
ここでちょっと一工夫。ブロックのシンボルを作る時に、見た目の大きさよりも天地左右に6pxずつ外側に、ほとんど透明(アルファ1%)のわくをつけて実際の大きさを幅42x高さ22にします。座標点は見える部分の左上にします。
こうすると、ボールが近づいてきて、透明わくの中にボールの座標点が入った瞬間が『ボールがブロックに当たった瞬間』のように見えるのです。
ブロックを消す方法
そもそも『ブロック崩し』はブロックを消すゲームですよね。この “消す” という状況を作るのに、実はいろいろな方法が考えられます。
例えばこんな感じです。
[見えないように隠す]というのは手っ取り早いかもしれませんね。消したいブロックの上に背景と同じ色の物体を置いてしまえば、消えたように見えます。
[見えない所に動かす]というのもいいかもしれませんね。例えば画面の外に出してしまうとか。
[透明にしてしまう]も可能ですね。インスタンスのタイムラインに何もない場所を作ってラベル貼って移動させれば消えてしまうでしょう。
しかし今回は本当に “消す” 方法で作ってみようと思います。
“消せるブロック” にするためには、あらかじめタイムラインにブロックを配置せずにプログラムで生み出すほうがいいでしょう。こういうのを『動的に生成する』なんて言い方をします。
さて動的にブロックを生成するために、先ほど準備したブロックのシンボルにプログラムで呼び出せるような名前を付けます。
シンボルにリンケージ名をつける
メニューの「ウィンドウ」から「ライブラリ」を選んで、ブロックのムービークリップ・シンボルを見つけます。
アイテム一覧の右側「リンケージ」の辺りをダブルクリックすると文字が入力できるようになっているので、ここに「mc_block」と入力します。
ブロックのインスタンスを動的に生成する
インスタンスの生成はこんなコードで行います。
var インスタンス変数 = new lib.シンボル(リンケージ名); インスタンス変数.x = x座標; インスタンス偏す.y = y座標; this.addChild(インスタンス変数);
今回は、はじめに何も無いムービークリップ・インスタンス(blockSet)を生成して、さらにblockSetの子インスタンスとしてmc_blockのシンボル・インスタンスを9x4=36個、動的に生成します。
//-------------------- ブロック生成 var blockSet = new createjs.MovieClip(); //空のムービークリップ(blockSet)を生成 blockSet.x = 15; blockSet.y = 45; blockSet.name = "blockSet"; this.addChild(blockSet); //thisの子インスタンスとして配置 for (var ix = 0; ix < 9; ix++) { for (var iy = 0; iy < 4; iy++) { var mc_block = new lib.mc_block(); //ブロックのムービークリップ・インスタンスを生成 mc_block.x = ix * 35; //横のピッチ mc_block.y = iy * 15; //縦のピッチ mc_block.name = ("x" + ix + "_y" + iy); blockSet.addChild(mc_block); //blockSetの子インスタンス } }
2行目、new createjs.MovieClip() と書くと何も入っていないムービークリップが生まれます。
これを座標を(x15, y45)に設定、プロパティ.name を “blockSet”と記入して
this(=exportRoot)に生み出し addChild します。
次に8-9行目、2つのfor文(繰り返し文)を使って9x4のブロックを生成して、先に作った “blockSet” の子インスタンスとして生み出し(addChild)します。
for文の入れ子はひとつめのforでx方向に9回の繰り返し、ふたつめのforでy方向に4回、それぞれの繰り返し変数を使って生成したインスタンスの表示座標も計算して.x,.yに代入します。
ここまでのコードだけでもプレビューすると、タイトル画面で[START]を押した直後に、動的に生成されたブロックが一気に表示されます。
ボールを動かす
ボールはゲームが始まると同時に動き出してゲームが終わるまで動き続けます。このような動きは “tick” イベントのリスナーを設置して行いましょう。
tickイベントは一定間隔で発生します。発生の間隔はAnimateではムービープロパティのフレームレートの設定に依ります。フレームレートが30.0ならば1秒間に30回のペースでtickイベントが発生します。
ActionScriptのときは「Event.ENTER_FRAME」イベントでしたがJavaScriptでは「tick」を使います。
//ボールの初期値 _ball.spdX = (Math.random() * 2 - 1) * _ball.maxSpd; //-- 横のスピード(ランダム) _ball.spdY = -5; //-- 縦のスピード(固定値) // ボールの動き _ball.addEventListener("tick", onBallMove); //-- ボールの動き function onBallMove(e) { _ball.x += _ball.spdX; _ball.y += _ball.spdY; }
ボールの動きは、インスタンスballに勝手プロパティ .spdX と .spdY (speed x, y)を作って tickイベント毎にボールの座標に.spdX と .spdY を足すように作ります。こうすると1/30秒毎にボールがどんどん進むプログラムになります。
ボールが壁に当たる
ボールが壁に当たる=ボールのx座標が可動エリアの外に出たら、はみ出た分を計算して戻し、横向きの速度 ball.spdX を逆転させます。
//---------------- 壁に当たる if (_ball.x <= 0) { // 左 _ball.x *= -1; _ball.spdX = Math.abs(_ball.spdX); } else if (_ball.x >= _area.width) { // みぎ _ball.x = _area.width * 2 - _ball.x; _ball.spdX = (-1) * Math.abs(_ball.spdX); } if (_ball.y <= 0) { //天井 _ball.y *= -1; _ball.spdY = Math.abs(_ball.spdY); }
左にはみ出すのはball.spdXがマイナスの時なので、絶対値 Math.abs(_ball.spdX) でプラス方向にします。右のときは絶対値に-1を掛けてマイナス方向にします。
天井に当たった時は Math.abs(_ball.spdX) で縦方向の速度をプラスに反転します。
ブロックに当たる
ゲーム作りでは「ボールがブロックに当たったら…」とか「弾が敵に当たったら…」とか、とかく物が当たっているかどうかをチェックすることが多いです。これを『当たり判定』と言ったりします。
HTML5 Canvas(CreateJS)には当たり判定に便利な関数があります。
あるインスタンスで当たり判定する場合、そのインスタンスの基準点から測った座標x,座標yがインスタンスに重なっているかを返す関数です。
図で説明しましょう。
blockSet というインスタンスでチェックするとして、ポイントAは blockSetの基準点から横に a.x、縦に a.y の位置にあります。
この場合、このように書くと true(当たり)が返ってきます。
で、つぎにポイントBはblockSetの基準点から(b.x, b.y)の位置です。この場所はblockSetの矩形の中にありますが要素が無い部分です。
この場合は false(はずれ)が返ってきます。
つまり、関数 hitTest() は単にインスタンスの矩形の中かどうかではなく、その点にインスタンスの要素が有るか無いかを返します。これば便利ですね。
ちなみにインスタンスに画像を貼っても、アルファ0%のピクセルでは false が返ります。
//------------------------------ ブロックに当たる var b_loc = _ball.localToLocal(0, 0, blockSet); //ボールとブロックセットの相対距離 if (blockSet.hitTest(b_loc.x, b_loc.y)) { //ボールがブロックセットに当たった /*実行アクション*/ }
というわけで、今回はこんなコードを書いて当たり判定しています。
ブロック一つ一つに当たり判定を実行するとなにかと面倒なこともありますが、今回はブロックをblockSetの子インスタンスにしたので、ボールがブロックに当たったかどうかの評価は親であるblockTestを対象に1度だけ hitTest()すれば判ります。おかげでコードがスッキリしました。
こちらはhitTest()と併せて覚えておきたい関数です。hitTest()を行うためには対象インスタンスからの相対的な座標が必要なのですが、このlocalToLocal()を使うと、インスタンスAについて、インスタンス0からの相対距離を出してくれます。
返ってくるのが {x: x座標, y: y座標} というオブジェクトになっているので、実際にはこんな感じで使います。
if (blockSet.hitTest(b_loc.x, b_loc.y)) {
/*実行アクション*/
}
blockSetの基準点からの_ballの相対座標を変数b_locに代入します。
そして、blockSetと(b_loc.x, b_loc.y)をhitTest()を使って当たり判定しています。
当たった時の方向を確認する
ブロック崩しでは、ブロックにボールが当たるとブロックが消えてボールは跳ね返ります。この跳ね返る方向をチェックします。
hitTest()で当たりが返ってきた時の一回前は、ボールはblockSetに当たっていなかったはずです。一回前と当たった時の状態を比べればどう当たったのか判るはず。
上または下から当たった時は、ボールはy方向の.spdYが反転します。横から当たった時は、x方向の.spdXが反転します。なので縦で当たったか横で当たったかが判る方法を考えました。
当たった時に、x, y両方の速度を引き算すると一回前の状態にもどり、ボールは当たってない位置になります。
ではx方向だけ、y方向だけを戻すとどうなるか。実はこれらをhitTest()でチェックすると当たった方向がわかります。
図を見てください。下(または上)から当たった場合、x方向を戻した位置はhitTest()でtrueが返りますが、y方向を戻した位置はfalseになります。
逆にx方向を戻すとfalseで、yを戻すとtrueになるのは横から当たった場合です。
これをよくよく考えると…
ball.y – ball.spdY が falseなら横に当たっているのでspdYを反転させる。
これなら、角に当たった時にはspdXもspdYも反転するので角対応は必要ありません。
ただし、くぼみに当たった場合は、x, yともtrueになるので、この場合だけチェックしてspdX, spdYともに反転させてあげます。
確認ですが、spdX, spdYともに反転するということは一回前の場所に戻る…というか来た道をそのまま戻ることになります。
当たったブロックを消す
ボールがブロックに当たったらブロックを消します。隠したり透明にするのではなくプログラム上から動的に削除します。
まず当たったブロックを特定します。一次的な当たり判定はblockSetで行いましたが、個々のブロックを特定するには一つずつhitTest()でチェックすればいいでしょう。
//当たったブロックを見つけて削除 for (var _block of blockSet.children) { var b_loc0 = _ball.localToLocal(0, 0, _block); //ボールとブロックの相対距離 if (_block.hitTest(b_loc0.x, b_loc0.y)) { //ブロックを消す blockSet.removeChild(_block); } }
ムービークリップ・インスタンスには .children というプロパティがあって、中には全ての子インスタンスを収めたArray型の配列が入っています。今回はこのchildrenの配列を使ってfor ofの繰り返し処理で対応します。
手順としてはfor ofの繰り返し処理で、ブロックの子インスタンスとボールの相対座標を localToLocal で取って、hitTest()で当たり判定をして、trueであればその子インスタンスを削除します。
インスタンスを削除するには…
ということで、blockSet.removeChild(_block);(_blockはfor of でchildren配列から取った子インスタンス)すれば簡単に削除できます。
ラケットに当たった時のアクション
続いてボールがラケットに当たった時のアクションです。
色々なタイプのブロック崩し系ゲームがありますが、私はボールがラケットに当たる位置が中央ならまっすぐに、端っこなら斜めにと、プレイヤーがボールの向きをコントロールできるタイプが好きです。
//------------------------------ ラケットに当たる var b_racket = _ball.localToLocal(0, 0, _racket); //ボールとブロックの相対距離 if (_racket.hitTest(b_racket.x, b_racket.y)) { var hitValue = (_ball.x - _racket.x) / (_racket.width / 2); _ball.spdX = hitValue * _ball.maxSpd; _ball.spdY = Math.abs(_ball.spdY) * (-1); }
なので今回は自分の好みで作りました。計算方法はコードの通り。ここは割と単純な計算にしておきました。
ゲームが終わったらブロックを全部消す
ボールをラケットで受けられずにボールを画面下に落とすか、すべてのブロックを崩すか、いずれかでゲームは終了です。それぞれの演出アニメーションを再生したらブロックを消してタイトルに戻ります。
クリア失敗、クリア成功のそれぞれの演出の最後のフレームには以下のコードがあります。
this.gotoAndPlay("title_set"); this.removeChild(this.getChildByName("blockSet"));
先ほどブロックを消すところでも使った removeChild()を使ってインスタンスblockSetを削除します。ブロックは全部この親インスタンスの中に入っているので、こいつを消してしまえば一発で全部消えます。
getChildByName(“blockSet”)となっているのは、動的に生成したインスタンスの場合、通常のタイムラインに配置したインスタンスと同じようにthis.hogehoe.hogeのような指定方法が使えないのです。そこでaddChild()で動的に生成したときに.nameプロパティに名前を付けておき、getChildByName()関数を使って指定します。
他にもグローバル変数に代入したり、thisに勝手プロパティで登録したり、インスタンス管理用の配列を作っておいてそこの入れておいたり、やり方は人それぞれ好みがあるようです。
次は「ブロック崩しのサンプル」
解説が長くなりましたが、次のページではワードプレスのページに貼り付けたサンプルをご覧ください。