ブロック崩しゲーム
こんにちは、kazutoです。今回は、JSでブロック崩しゲームを作っていきます。
事前準備
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* { padding: 0; margin: 0; }
body{
background-color: #111111;
}
canvas {
background:#222; display: block; margin: 0 auto;
}
</style>
<title>Document</title>
<script type="module">
import Ball from "./ball.js";
import Block from './block.js'
import Paddle from './paddle.js';
Window.prototype.Ball = Ball
Window.prototype.Paddle = Paddle
Window.prototype.Block = Block
</script>
<script src="game.js"defer> </script>
</head>
<body>
<canvas id="myCanvas" width="480" height="320"></canvas>
</body>
</html>
class Game {
}
export default Game;
class Ball {
}
export default Ball;
class Paddle{
}
export default Paddle;
class Block{
}
export default Block;
※必ずコピペをしてください。正常に動かないおそれがあります。
<script type="module">
import Ball from "./ball.js";
import Block from './block.js'
import Paddle from './paddle.js';
Window.prototype.Ball = Ball
Window.prototype.Paddle = Paddle
Window.prototype.Block = Block
</script>
今回は、一つのファイルで一つのクラスを管理をしていきます。なので、ファイルを読み込む処理を上記のscriptタグの中で行っています。最終的に、ゲームの進行状況を管理するGameクラスで
- Ball
- Paddle
- Block
を使える様にしていきます。
Window.prototype.Ball = Ball
Window.prototype.Paddle = Paddle
Window.prototype.Block = Block
//インスタンス生成
// this.ball = new window.Ball("プロパティ")
こちらは、Windowオブジェクトの中にimportして、読み込んだクラスを格納をしています(prototype)。この様に記述する事で、game.jsファイルでも問題なくクラスを呼び出す事ができます。
事前準備が終了しましたので、次にクラスごとのプロパティや動作、役割を確認をしていきましょう。
Gameクラス
Gameクラスはゲームの進行を管理するクラスです。プロパティには各クラスのインスタンスの他、canvasタグの中で2Dグラフィックを描画するためにcanvasのノードを取得をして設定を行っていきます。
なお、属性については下記の表を参照してください。
プロパティ | 概要 |
---|---|
intervalID | インターバルをキャンセルするための識別子 |
node | canvasタグのノード |
ctx | 描画コンテキスト、画面に2dグラフィックスを描画 |
ball | Ballクラスのインスタンス |
paddle | Paddleクラスのインスタンス |
block | Blockクラスのインスタンス |
動作については下記をご覧ください。
- 関数game_start
→ゲームをスタートする関数 - 関数game_clear
→ゲームをクリアしたか判定する関数 - 関数game_over
→ゲームオーバをした判定する関数 - 関数BallCollisionDetection
→ボールの衝突判定をする関数 - 関数BlockCollisionDetection
→ブロックの衝突判定をする判定
Ball クラス
Ballクラスは、その名の通りボールを管理するクラスです。プロパティには、ボールを描画するための座標や半径などの情報を持ちます。
プロパティ | 概要 |
---|---|
x | x軸の座標 |
y | y軸の座標 |
dx | x軸にボールが進む速さ |
dy | y軸にボールが進む速さ |
rabius
|
円の半径 |
上記のプロパティの他に関数drawという動作を持ちます。関数drawは、画面上にボールを描画をするための関数です。
Paddleクラス
Paddleクラスは、その名の通りパドルを管理するクラスです。プロパティには、パドルを描画するための座標や長さなどの情報を持ちます。
プロパティ | 概要 |
---|---|
width | パドルの長さ |
height | パドルの高さ |
top | パドルが上に進むか判定 |
left | パドルが右に進むか判定 |
right | パドルが左に進むか判定 |
down | パドルが下に進むか判定 |
x | x軸の座標 |
y | y軸の座標 |
動作については下記をご覧ください。
- 関数draw
→パドルを画面に描画する関数 - 関数move
→パドル操作を管理する関数 - 関数keyDownHandler
→keydownイベント - 関数keyupHandler
→keyupイベント - BlockCollisionDetection
→ブロックの衝突判定をする判定
Blockクラス
Blockクラスは、その名の通りブロックを管理するクラスです。プロパティには、ブロックを描画するための座標や長さなどの情報を持ちます。
プロパティ | 概要 |
---|---|
row | ブロックの行 |
col | ブロックの列 |
width | ブロックの長さ |
height | ブロックの高さ |
padding | ブロック間の余白 |
setTop | ブロック描画を始めるy軸の位置 |
setLeft | ブロック描画を始めるx軸の位置 |
blocks | ブロック情報 |
動作については下記をご覧ください。
- 関数draw
→ブロックを画面に描画する関数 - 関数BlockSet
→描画するブロック情報をセットする関数 - 関数BlockStatusCheck
→ボールのステータスをチェックする関数
ボール
このトピックでは、ボールが上下左右の壁にぶつかっても、跳ね返ってくる所まで、実装していきます。
- ボールを描写をしよう
- ボールを動かそう
- 衝突判定
ボールを描写をしよう
まず初めは、画面にボールを描写をしていきます。
class Game {
constructor(node){
this.clearInterbal=null
this.node =document.querySelector(node)
this.ctx = this.node.getContext('2d')
this.ball =new window.Ball(this.node.width/2+10,this.node.height-30, -2,-2,10 )
}
game_start(){
this.ball.draw(this.ctx))
}
}
const game = new Game("#myCanvas");
game.game_start()
class Ball {
constructor(x,y,dx,dy,rabius){
this.x = x
this.y = y
this.dx = dx
this.dy = dy
this.rabius =rabius
}
draw(ctx){
ctx.beginPath();
ctx.arc(this.x, this.y, this.rabius, 0, Math.PI*2);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
}
export default Ball;
それでは、解説をしていきます。
class Game {
constructor(node){
this.clearInterbal=null
this.node =document.querySelector(node)
this.ctx = this.node.getContext('2d')
this.ball =new window.Ball(this.node.width/2+10,this.node.height-30, -2,-2,10 )
}
}
const game = new Game("#myCanvas");
まず、Gameクラスのインスタンスを生成をします。プロパティに関しては、「Gameクラス」で確認をしてください。インスタンスを生成するとJ Sでは、自動的にconstructor関数が呼び出されます。したがって、プロパティの初期値の設定は、constructor関数で行います。
class Ball {
constructor(x,y,dx,dy,rabius){
this.x = x
this.y = y
this.dx = dx
this.dy = dy
this.rabius =rabius
}
}
this.ball =new window.Ball(this.node.width/2+10,this.node.height-30, -2,-2,10 )
Gameクラスのconstructor内でBallクラスのインスタンスを生成をします。この様に記述をする事で、Gameクラス内でBallクラスのインスタンスをプロパティとして管理する事ができます
。
class Game {
game_start(){
this.ball.draw(this.ctx))
}
}
const game = new Game("#myCanvas");
game.game_start()
Gameクラスのインスタンスを生成をして、プロパティの初期化ができたら、次にGameクラスの関数game_startを呼び出します。
関数game_startの中身を見ていきましょう。
game_start(){
this.ball.draw(this.ctx)
}
thisは、Gameクラスのインスタンスを指します。したがって、「this.プロパティ名」とする事で、Gameクラスのプロパティを参照する事ができます。
上記の場合は、Ballクラスのインスタンスを格納してあるballプロパティに参照をして、Ballクラスの関数drawを呼び出しています。
draw(ctx){
ctx.beginPath();
ctx.arc(this.x, this.y, this.rabius, 0, Math.PI*2);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
次に関数drawの中身を見ていきましょう。
ctx.beginPath();
描画サブパスを初期化をします。Canvas 2D APIでは座標の情報のパスを記憶をして描画をしています。なので、初期化をしないと前回、描画をした座標の位置から処理がスタートしてしまうので、beginPathを用いて、初期化をしておきます。
ctx.arc(this.x, this.y, this.rabius, 0, Math.PI*2);
//Math.PI 円周率(3.14....)
//*2== 2π
//円周の長さ = 円周率*2π
//円周の長さ=3.14*2π
パスに円弧を加えます。引数には順に
- 円弧の中心のx座標値
- 円弧の中心のy座標値
- 円弧の半径
- 円弧の始まりの角度
- 円弧の終わりの角度
を渡して、円を描いていきます。
つまり、座標(x,y)を中心に、rabiusピクセルの円を描いています。円周は2πとして、円周率と乗算する事で円周の長さ(1周分の長さ)を求めています。
ctx.fillStyle = "#0095DD";
こちらは、塗りつぶす色を指定をしています。色については16進数で表現されています。
ctx.fill();
こちらでサブパスを塗りつぶしていきます。
ctx.closePath();
closePathを呼び出す事で、最終座標と開始座標を結んでパスを閉じます。つまり、円が完成をします。
図形を描く際は、beginPath〜closePathの間に処理を書いていきます。ボールを描画ができたのが確認できたら、次にボールを動かしていきましょう。
ボールを動かそう
次にボールを動かしていきましょう。
class Game{
game_start(){
this.ctx.clearRect(0,0,this.node.width,this.node.height)
this.ball.draw(this.ctx)
//追加
this.ball.x += this.ball.dx;
this.ball.y += this.ball.dy;
}
}
const game = new Game("#myCanvas");
//削除
// game.game_start()
//追加
game.clearInterbal=setInterval(function (params) {
game.game_start()
},10)
では、解説をしていきます。
game.clearInterbal=setInterval(function (params) {
game.game_start()
},10)
ボールを動かすという事は、パラパラ漫画の様にボールの座標を動かし、絵を更新をしていく必要があります。したがって、setIntervalを呼び出して、10ミリ秒ごとに関数game_startを呼び出していきます
。
game.intervalID=setInterval
- ゲームクリア
- ゲームオーバー
というイベントが起きた際には、繰り返し処理を止めたいので、intervalIDプロパティにsetIntervalの返り値(インターバルをキャンセルする識別子)を格納します。
次にgame_startの中身を見ていきましょう。
game_start(){
this.ctx.clearRect(0,0,this.node.width,this.node.height)
}
clearRectは指定した座標の領域内に描画されていたすべてのコンテンツは消去します。引数には
- 矩形領域の始点のX座標
- 矩形領域の始点のY座標
- 矩形領域の幅
- 矩形領域高さ
を順に渡します。
こちらの設定を行わないと下記の画像の通りボールの軌跡が残ってしまい、ボールの原型が失われてしまいます。
イメージは、10msごとに画面を更新して再描画をする感じです。
class Game {
...
this.ball.x += this.ball.dx;
this.ball.y += this.ball.dy;
}
上記の2行で、ボールを動かしています。1行目は、現在のX軸の座標に対して、1フレームで動かしたいX軸方向の距離分を足してあげます。2行目は、現在のY軸の座標に対して、1フレームで動かしたいY軸方向の距離分を足してあげます。この様に記述をして、setIntervalを用いる事で、ボールを動かす事ができる様になります。
以上で、「ボールを動かそう」の解説を終了します。次にボールが壁にぶつかったら、跳ね返ってくる様に衝突判定のロジックを組んでいきましょう。
衝突判定
では、衝突判定のロジックを組んでいきましょう。
class Game {
game_start(){
//削除
this.ball.x += this.ball.dx;
this.ball.y += this.ball.dy;
//追加
this.BallCollisionDetection()
}
BallCollisionDetection(){
if (this.ball.y+this.ball.dy <this.ball.rabius) {
this.ball.dy = -this.ball.dy
}
if (this.ball.y+this.ball.dy >this.node.height-this.ball.rabius) {
this.ball.dy = -this.ball.dy
}
if(this.ball.x+this.ball.dx > this.node.width - this.ball.rabius || this.ball.x+this.ball.dx < this.ball.rabius){
this.ball.dx = -this.ball.dx
}
this.ball.x += this.ball.dx;
this.ball.y += this.ball.dy;
}
}
では解説をしていきます。
game_start(){
//追加
this.BallCollisionDetection()
}
関数game_start内で関数BallCollisionDetectionを呼び出しましょう。
BallCollisionDetection(){
if (this.ball.y+this.ball.dy <this.ball.rabius) {
this.ball.dy = -this.ball.dy
}
if (this.ball.y+this.ball.dy >this.node.height-this.ball.rabius) {
this.ball.dy = -this.ball.dy
}
if(this.ball.x+this.ball.dx > this.node.width - this.ball.rabius || this.ball.x+this.ball.dx < this.ball.rabius){
this.ball.dx = -this.ball.dx
}
this.ball.x += this.ball.dx;
this.ball.y += this.ball.dy;
}
次に、関数BallCollisionDetectionの中身を見ていきましょう。
if (this.ball.y+this.ball.dy < this.ball.rabius) {
this.ball.dy = -this.ball.dy
}
こちらは、ボールの現在のY軸の座標に1フレームで動く、Y軸方向の距離分を足した和が、ボールの半径分より小さくなった場合と条件分岐をしています。つまり、上の壁にボールがぶつかった場合です。上の壁のY軸の座標は0なので、「ボールの半径を足す理由はあるの?」と疑問にもたれる方がいると思いますが、円の中心の衝突地点ではなく円周の衝突地点
を求める必要があるので、ボールの中心と辺の距離がボールの半径とちょうど等しくなったときに動く方向を変える必要があるので、上の壁の場合は、rabius(半径)を上の壁の座標(0)に足し合わせる必要があります。
if (this.ball.y+this.ball.dy >this.node.height-this.ball.rabius) {
this.ball.dy = -this.ball.dy
}
こちらは、ボールの現在のY軸の座標に1フレームで動く、Y軸方向の距離分を足した和が、画面の高さからボールの半径を引いた差より大きくなった場合と条件分岐をしています。つまり、下の壁にボールがぶつかった場合です。上の壁と同様、円の中心の衝突地点ではなく円周の衝突地点
を求める必要があるので、下の壁の場合は、下の壁のY軸座標(画面の高さ)からraibus(半径)を引く必要があります。
if(this.ball.x+this.ball.dx > this.node.width - this.ball.rabius || this.ball.x+this.ball.dx < this.ball.rabius){
this.ball.dx = -this.ball.dx
}
こちらも、同様です。
this.ball.x+this.ball.dx < this.ball.rabius
左の壁にぶつかった場合は、ボールの現在のX軸の座標に1フレームで動く、X軸方向の距離分を足した和がボールの半径より小さくなった場合と条件分岐を行えば、OKです。(左の壁のX軸座標は0のため)
this.ball.x+this.ball.dx > this.node.width - this.ball.rabius
右の壁にぶつかった場合は、ボールの現在のX軸の座標に1フレームで動く、X軸方向の距離分を足した和が、右の壁のX軸座標をボールの半径で引いた差より大きくなった場合と条件分岐を行えばOKです。
this.ball.x += this.ball.dx;
this.ball.y += this.ball.dy;
関数BallCollisionDetectionの末尾で座標を更新します。
この一連の流れを実装する事で、ボールを上下左右の壁にぶつかっても、跳ね返ってくるという動作を実現ができます。
以上で、「衝突判定」の解説を終了します。
パドル
このトピックでは、パドルでボールを跳ね返えす所まで実装していきます。
- パドルを描画をしよう
- パドルを動かそう
- ボールを跳ね返そう
パドルを描画しよう
まずは、パドルを描画をしていきましょう。
class Game{
constructor(node){
//追加
this.paddle = new window.Paddle(10,75,false,parseInt( (this.node.width-75)/2),parseInt(this.node.height-10))
}
game_start(){
//追加
this.paddle.draw(this.ctx)
}
}
class Paddle{
constructor(height,width,pressed,x,y){
this.width = width
this.height = height
this.top = pressed
this.left = pressed
this.right = pressed
this.down = pressed
this.x = x
this.y = y
}
draw(ctx){
ctx.beginPath();
ctx.rect(this.x,this.y ,this.width,this.height);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
}
それでは、解説をしていきます。
class Paddle{
constructor(height,width,pressed,x,y){
this.width = width
this.height = height
this.top = pressed
this.left = pressed
this.right = pressed
this.down = pressed
this.x = x
this.y = y
}
}
this.paddle = new window.Paddle(10,75,false,parseInt( (this.node.width-75)/2),parseInt(this.node.height-10))
Ballクラスのインスタンスと同様、Gameクラスのconstructor内でPaddleクラスのインスタンスを生成をします。この様に記述をする事で、Gameクラス内で、Paddleクラスのインスタンスをプロパティとして管理する事ができます
。
※Paddleクラスのプロパティに関しては、「Paddleクラス」でご確認ください。
class Game{
game_start(){
//追加
this.paddle.draw(this.ctx)
}
}
Gameクラスの関数game_start内でPaddleクラスの関数drawを呼び出します。関数drawの中身を確認をしていきましょう。
draw(ctx){
ctx.beginPath();
ctx.rect(this.x,this.y ,this.width,this.height);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
Ballクラスとほぼ同じ内容ですので、一部、解説を省略をします。
ctx.rect(this.x,this.y ,this.width,this.height);
rectは、四角形を作成する際に使用します。引数は
- 四角形の左上のx座標
- 四角形の左上のy座標
- 四角形の幅
- 四角形の高さ
を順に渡します。
以上で、「パドルを描画しよう」の解説を終了します。この段階でパドルが描画できているのを確認をした上で次のトピックに進むんでください。
パドルを動かそう
このトピックでは、パドルを動かす所まで、実装していきます。
class Game {
constructor(node){
//追加
document.addEventListener("keydown", this.paddle.keyDownHandler.bind(this.paddle), false);
document.addEventListener("keyup", this.paddle.keyupHandler.bind(this.paddle), false);
}
game_start(){
//追加
this.paddle.move(this.node)
}
}
class Paddle{
keyDownHandler(e){
switch (e.key) {
case "Top":
case "ArrowUp":
this.top = true
return
case "Down":
case "ArrowDown":
this.down = true
return
case "Right":
case "ArrowRight":
this.left = true
return
case "Left":
case "ArrowLeft":
this.right = true
return
}
}
keyupHandler(e){
switch (e.key) {
case "Top":
case "ArrowUp":
this.top = false
case "Down":
case "ArrowDown":
this.down = false
case "Right":
case "ArrowRight":
this.left = false
case "Left":
case "ArrowLeft":
this.right = false
break;
}
}
move(canvas){
if (this.top && this.y > 0) {
this.y -= 7
}else if (this.down && this.y < canvas.height - this.height) {
this.y += 7
}else if (this.left && this.x < canvas.width-this.width) {
console.log("test");
this.x += 7
}else if (this.right && this.x > 0) {
this.x -= 7
}
}
}
では、解説をしていきます。
class Game {
constructor(node){
//追加
document.addEventListener("keydown", this.paddle.keyDownHandler.bind(this.paddle), false);
document.addEventListener("keyup", this.paddle.keyupHandler.bind(this.paddle), false);
}
}
Gameクラスのconstructor関数が呼ばれるタイミングで、
- keydown
- keyup
上記の2つのイベントを登録をします。今回、パドル操作については、矢印キーで管理をしていきます。Gameクラスのインスタンスが生成されたタイミングでイベント登録を行う事で、他の関数に影響を与えないで、実装ができます。
イベントが発火後の処理内容も、簡単に確認をしておきましょう。
keyDownHandler(e){
switch (e.key) {
case "Top":
case "ArrowUp":
this.top = true
return
case "Down":
case "ArrowDown":
this.down = true
return
case "Right":
case "ArrowRight":
this.left = true
return
case "Left":
case "ArrowLeft":
this.right = true
return
}
}
該当するキーが押された場合は、対応するキーを制御するプロパティにtrueを格納します。
keyupHandler(e){
switch (e.key) {
case "Top":
case "ArrowUp":
this.top = false
case "Down":
case "ArrowDown":
this.down = false
case "Right":
case "ArrowRight":
this.left = false
case "Left":
case "ArrowLeft":
this.right = false
break;
}
}
該当するキーが押されて、キーを押し終えた場合には、対応するキーを制御するプロパティにfalseを格納します。
game_start(){
//追加
this.paddle.move(this.node)
}
関数game_startに中にPaddleの関数moveを呼び出します。関数moveの中身を見ていきましょう。
move(canvas){
if (this.top && this.y > 0) {
this.y -= 7
}else if (this.down && this.y < canvas.height - this.height) {
this.y += 7
}else if (this.left && this.x < canvas.width-this.width) {
console.log("test");
this.x += 7
}else if (this.right && this.x > 0) {
this.x -= 7
}
}
関数moveは、パドル操作を管理する関数です。
if (this.top && this.y > 0) {
this.y -= 7
}
paddleのプロパティのtopがtrueな場合で且つパドルのY軸座標が0より大きい場合と条件分岐を行っております。つまり、↑キーが押された場合です。
パドルが画面内のみで操作できる様に制御をしたいので、画面の上端のY軸座標(0)を基準にして、パドルのY軸座標が画面の上端のY軸座標より大きくなる様に条件を組んでいます。条件に該当する場合は、パドルのY軸座標から7を引いて、パドルが上に進みます。
else if (this.down && this.y < canvas.height - this.height) {
this.y += 7
}
paddleのプロパティのdownがtrueな場合で且つ画面の高さがパドルのY軸座標より大きい場合と条件分岐を行っております。つまり、↓キーが押された場合です。パドルが画面内のみで操作できる様に制御をしたいので、画面の下端のY軸座標(画面の高さ)を基準にして、パドルのY軸座標が画面の下端のY軸座標以下になる様に条件を組んでいます。条件に該当する場合は、パドルのY軸座標から7を足して、パドルが下に進みます。
}else if (this.left && this.x < canvas.width-this.width) {
this.x += 7
}
paddleのプロパティのleftがtureな限りで且つ、画面の幅がパドルのX軸座標より大きい場合と条件分岐を行っております。つまり、→キーが押された場合です。パドルが画面内のみで操作できる様に制御をしたいので、画面の右端のX軸座標(画面の幅)を基準にして、パドルのX軸座標が画面の右端のX軸座標以下になる様に条件を組んでいます。条件に該当する場合は、パドルのX軸座標から7を足して、パドルが右に進みます。
}else if (this.right && this.x > 0) {
this.x -= 7
}
paddleのプロパティのrightがtureな限りで且つ、パドルのX軸座標が0より大きい場合と条件分岐を行っております。つまり、←キーが押された場合です。パドルが画面内のみで操作できる様に制御をしたいので、画面の左端のX軸座標(0)を基準にして、パドルのX軸座標が画面の左端のX軸座標より大きくなる様に条件を組んでいます。条件に該当する場合は、パドルのX軸座標から7を引いて、パドルが左に進みます。
この一連のロジックを組む事でパドル操作を実現をしています。
以上で、「パドルを動かそう」の解説を終了します。この段階で、パドルが上下左右に移動できるのかを確認をしてください。
ボールを跳ね返そう
次にパドルを操作してボールを跳ね返す所まで実装していきます。
BallCollisionDetection(){
//変更
if (this.ball.y+this.ball.dy <this.ball.rabius) {
this.ball.dy = -this.ball.dy
this.ball.y += this.ball.rabius
}
if(this.ball.x+this.ball.dx > this.node.width - this.ball.rabius || this.ball.x+this.ball.dx < this.ball.rabius){
this.ball.dx = -this.ball.dx
}
if (Math.abs(this.paddle.y+(this.paddle.height/2)- this.ball.y) <= this.ball.rabius ) {
if (this.ball.x >= this.paddle.x && this.ball.x <= this.paddle.x +1) {
this.ball.dy = -this.ball.dy
this.ball.dx = -this.ball.dx
this.ball.x -= 2
}
else if(this.ball.x >= this.paddle.x + this.paddle.width -1 && this.ball.x <= this.paddle.x + this.paddle.width){
this.ball.dy = -this.ball.dy
this.ball.dx = -this.ball.dx
this.ball.x += 2
}else
if (this.ball.x >= this.paddle.x && this.ball.x <= this.paddle.x + this.paddle.width) {
this.ball.dy = -this.ball.dy
this.ball.y += this.ball.rabius
}
}
else if (Math.abs(this.paddle.y-(this.paddle.height/2)- this.ball.y) <= this.ball.rabius ) {
if (this.ball.x >= this.paddle.x && this.ball.x <= this.paddle.x +1) {
this.ball.dy = -this.ball.dy
this.ball.dx = -this.ball.dx
this.ball.x -= 2
}else if(this.ball.x >= this.paddle.x + this.paddle.width -1 && this.ball.x <= this.paddle.x + this.paddle.width){
this.ball.dy = -this.ball.dy
this.ball.dx = -this.ball.dx
this.ball.y = Math.abs(this.paddle.y-(this.paddle.height/2)) - this.ball.rabius
this.ball.x += 2
}else if (this.ball.x >= this.paddle.x && this.ball.x <= this.paddle.x + this.paddle.width) {
this.ball.dy = -this.ball.dy
this.ball.y = Math.abs(this.paddle.y-(this.paddle.height/2)) - this.ball.rabius
}
}
if ( this.ball.y+this.ball.dy >this.node.height) {
//this.game_over()
// this.ball.dy = -this.ball.dy
// this.ball.y -= this.ball.rabius
}
this.ball.x += this.ball.dx;
this.ball.y += this.ball.dy;
}
それでは、解説をしていきます。(変更点のみ解説をしていきます。)
if (this.ball.y+this.ball.dy <this.ball.rabius) {
this.ball.dy = -this.ball.dy
}
if(this.ball.x+this.ball.dx > this.node.width - this.ball.rabius || this.ball.x+this.ball.dx < this.ball.rabius){
this.ball.dx = -this.ball.dx
}
こちらは、「衝突判定」のトピックで解説をしました。
- 上の壁
- 左右の壁
にボールがぶつかった場合にif文内の処理が読み込まれ、ボールの向かう方向を反転します。
※詳しくは、上記のリンクをたどって、ご確認ください。
if (Math.abs(this.paddle.y+(this.paddle.height/2)- this.ball.y) <= this.ball.rabius ) {
if (this.ball.x >= this.paddle.x && this.ball.x <= this.paddle.x +1) {
this.ball.dy = -this.ball.dy
this.ball.dx = -this.ball.dx
this.ball.x -= 2
}
else if(this.ball.x >= this.paddle.x + this.paddle.width -1 && this.ball.x <= this.paddle.x + this.paddle.width){
this.ball.dy = -this.ball.dy
this.ball.dx = -this.ball.dx
this.ball.x += 2
}else
if (this.ball.x >= this.paddle.x && this.ball.x <= this.paddle.x + this.paddle.width) {
this.ball.dy = -this.ball.dy
this.ball.y += this.ball.rabius
}
}else if (Math.abs(this.paddle.y-(this.paddle.height/2)- this.ball.y) <= this.ball.rabius ) {
if (this.ball.x >= this.paddle.x && this.ball.x <= this.paddle.x +1) {
this.ball.dy = -this.ball.dy
this.ball.dx = -this.ball.dx
this.ball.x -= 2
}else if(this.ball.x >= this.paddle.x + this.paddle.width -1 && this.ball.x <= this.paddle.x + this.paddle.width){
this.ball.dy = -this.ball.dy
this.ball.dx = -this.ball.dx
this.ball.y = Math.abs(this.paddle.y-(this.paddle.height/2)) - this.ball.rabius
this.ball.x += 2
}else if (this.ball.x >= this.paddle.x && this.ball.x <= this.paddle.x + this.paddle.width) {
this.ball.dy = -this.ball.dy
this.ball.y = Math.abs(this.paddle.y-(this.paddle.height/2)) - this.ball.rabius
}
}
次にパドルとボールの衝突判定ロジックを確認をしていきましょう。今回は、パドルを上下左右に移動できる様な仕様にしたので、
- パドルの表側
- パドルの裏側
事に分岐処理を実装していきます。
まずは、パドルが裏側にボールがぶつかった場合の衝突判定を確認をしていきましょう。
if (Math.abs(this.paddle.y+(this.paddle.height/2)- this.ball.y) <= this.ball.rabius ) {
}
こちらは、パドルのY軸座標にパドルの高さ(半分)を足した和をボールのY軸座標で引いた差がボールの半径未満の場合と条件分岐を行っています。つまり、パドルの裏側にボールの円周が衝突した場合です。Y軸座標の値が増えるほど、画面の下方向に向かっていきます
。したがって、パドルのY軸座標にパドルの高さ(半分)を足している事からも、パドルの裏側を表している事が分かります。
if (this.ball.x >= this.paddle.x && this.ball.x <= this.paddle.x +1) {
this.ball.dy = -this.ball.dy
this.ball.dx = -this.ball.dx
this.ball.x -= 2
} else if(this.ball.x >= this.paddle.x + this.paddle.width -1 && this.ball.x <= this.paddle.x + this.paddle.width){
this.ball.dy = -this.ball.dy
this.ball.dx = -this.ball.dx
this.ball.x += 2
}else if (this.ball.x >= this.paddle.x && this.ball.x <= this.paddle.x + this.paddle.width) {
this.ball.dy = -this.ball.dy
this.ball.y += this.ball.rabius
}
条件に該当する場合は、「ボールがパドルの裏側に衝突した」と見做し、上記の処理に繋がります。
if (this.ball.x >= this.paddle.x && this.ball.x <= this.paddle.x +1) {
this.ball.dy = -this.ball.dy
this.ball.dx = -this.ball.dx
this.ball.x -= 2
}
こちらは、ボールのX軸座標がパドルのX軸以上で且つボールのX軸座標がパドルのX軸座標に1を足した和未満の場合と条件分岐を行っています。つまり、「パドルの裏側の左角」にボールが衝突した場合です。今回は、パドルの角にぶつかった場合と、そうではない場合でボールが跳ね返る角度を変更したいので、上記の条件分岐を追加をしました。
this.ball.dy = -this.ball.dy
条件に該当する場合は、ボールの向かう方向を変更します。
this.ball.dx = -this.ball.dx
角にぶつかった場合は、その角が示す方向にボールを跳ね返したいので、dxの符合を反転します。
this.ball.x -= 2
ボールのX軸座標に対して2を引きます。つまりボールの方向を左にずらします。この記述を忘れると
- 表側の角
- 裏側の角
の衝突判定の競合が起きてしまい、バグが起きてしまうので、忘れずに記述をしてください。
else if(this.ball.x >= this.paddle.x + this.paddle.width -1 && this.ball.x <= this.paddle.x + this.paddle.width){
this.ball.dy = -this.ball.dy
this.ball.dx = -this.ball.dx
this.ball.x += 2
}
こちらは、ボールのX軸座標が、パドルのX軸座標にパドルの長さを足した和に−1引いだ差以上で且つボールのX軸座標がパドルのX軸座標にパドルの長さを足した和未満の場合と条件分岐を行っています。つまり、「パドルの裏側の右角」にボールが衝突した場合です
this.ball.dy = -this.ball.dy
this.ball.dx = -this.ball.dx
this.ball.x += 2
条件が該当する場合は、上記の処理が読み込まれます。解説は重複をしてしまうので省略をします。
else if (this.ball.x >= this.paddle.x && this.ball.x <= this.paddle.x + this.paddle.width) {
this.ball.dy = -this.ball.dy
this.ball.y += this.ball.rabius
}
こちらは、ボールのX軸座標がパドルのX軸座標以上で且つボールのX軸座標がパドルのX軸座標にパドルの長さを足した和未満な場合と条件分岐を行っています。つまり「パドルの裏側にボールが衝突した場合」です。こちらのelse if文の前に
- パドルの裏側の左角にボールが衝突した場合
- パドルの裏側の右角にボールが衝突した場合
と上記2つの条件が定義をされているので、必然的に「パドルの裏側の左角、パドルの裏側の右角を除いた、パドルの範囲」を表します。
this.ball.dy = -this.ball.dy
条件に該当する場合は、ボールの向かう方向を変更します。
this.ball.y += this.ball.rabius
ボールが衝突した際に各条件式が競合を起こさせない様にしたいので、ボールが衝突をしたタイミングで、ボールのY軸座標をボールの半径分、下の方向にずらします。
次にパドルの表側にボールが衝突した分岐処理を確認をしていきましょう。
else if (Math.abs(this.paddle.y-(this.paddle.height/2)- this.ball.y) <= this.ball.rabius ) {
}
こちらは、パドルのY軸座標にパドルの高さ(半分)を引いた差をボールのY軸座標で引いた差がボールの半径未満の場合と条件分岐を行っています。つまり、パドルの表側にボールの円周が衝突した場合です。Y軸座標の値が減るほど、画面の上方向に向かっていきます
。したがって、パドルのY軸座標にパドルの高さ(半分)を引いている事からも、パドルの表側を表している事が分かります。
if (this.ball.x >= this.paddle.x && this.ball.x <= this.paddle.x +1) {
this.ball.dy = -this.ball.dy
this.ball.dx = -this.ball.dx
this.ball.x -= 2
this.ball.y = Math.abs(this.paddle.y-(this.paddle.height/2)) - this.ball.rabius
}else if(this.ball.x >= this.paddle.x + this.paddle.width -1 && this.ball.x <= this.paddle.x + this.paddle.width){
this.ball.dy = -this.ball.dy
this.ball.dx = -this.ball.dx
this.ball.y = Math.abs(this.paddle.y-(this.paddle.height/2)) - this.ball.rabius
this.ball.x += 2
}else if (this.ball.x >= this.paddle.x && this.ball.x <= this.paddle.x + this.paddle.width) {
this.ball.dy = -this.ball.dy
this.ball.y = Math.abs(this.paddle.y-(this.paddle.height/2)) - this.ball.rabius
}
条件に該当する場合は、「ボールがパドルの表側に衝突した」と見做し、上記の処理に繋がります。なお、条件式や処理内容については、ボールがパドルの裏側に衝突した場合と
ほぼ同じ内容ですので、簡単な解説で流していきます。
if (this.ball.x >= this.paddle.x && this.ball.x <= this.paddle.x +1) {
this.ball.dy = -this.ball.dy
this.ball.dx = -this.ball.dx
this.ball.x -= 2
}
こちらは、ボールがパドルの表側の左角に衝突した場合と条件分岐を組んでいます。
else if(this.ball.x >= this.paddle.x + this.paddle.width -1 && this.ball.x <= this.paddle.x + this.paddle.width){
this.ball.dy = -this.ball.dy
this.ball.dx = -this.ball.dx
this.ball.y = Math.abs(this.paddle.y-(this.paddle.height/2)) - this.ball.rabius
this.ball.x += 2
}
こちらは、ボールがパドルの表側の右角に衝突した場合と条件分岐を組んでいます。
}else if (this.ball.x >= this.paddle.x && this.ball.x <= this.paddle.x + this.paddle.width) {
this.ball.dy = -this.ball.dy
this.ball.y = Math.abs(this.paddle.y-(this.paddle.height/2)) - this.ball.rabius
}
こちらは、
- ボールがパドルの表側の右角
- ボールがパドルの表側の左角
を除く範囲にボールがぶつかった場合と条件式を組んでいます。
以上で、「ボールを跳ね返そう」のトピックを終了します。
ブロック
次に画面にブロックを配置をして、ボールがブロックに衝突をしたら消える所まで実装していきます。
- ブロックを描写しよう
- 衝突判定
ブロックを描画しよう
まずは、ブロックを画面に描画していきましょう。
class Game {
constructor(node){
this.block=window.Block(5,5,70,20,5,20,55)
}
game_start(){
//追加
this.block.draw(this.ctx)
}
}
class Block{
constructor(row,col,width,height,padding,setTop,setLeft){
this.row = row
this.col = col
this.width = width
this.height = height
this.padding = padding
this.setTop = setTop
this.setLeft = setLeft
this.blocks = []
this.BlockSet()
}
BlockSet(){
for (let c = 0; c < this.col; c++) {
this.blocks[c] = []
for (let r = 0; r < this.row; r++) {
this.blocks[c][r] = {x:0,y:0,status:1};
}
}
}
draw(ctx){
for (let c = 0; c < this.col; c++) {
for (let r = 0; r < this.row; r++) {
if (this.blocks[c][r].status == 1) {
let blockX = (c*(this.width+this.padding))+this.setLeft
let blockY = (r*(this.height+this.padding))+this.setTop
this.blocks[c][r].x = blockX
this.blocks[c][r].y = blockY
ctx.beginPath()
ctx.rect(blockX,blockY,this.width,this.height,)
ctx.fillStyle="#0095DD"
ctx.fill()
ctx.closePath()
}
}
}
}
}
export default Block;
それでは、解説をしていきます。
class Game {
constructor(node){
this.block=window.Block(5,5,70,20,5,20,55)
}
}
Gameクラスのconstructor内でBlockクラスのインスタンスを生成をします。この様に記述をする事で、Gameクラス内でBlockクラスのインスタンスをプロパティとして管理する事ができます
。プロパティに関しては、「Blockクラス」でご確認ください。
constructor(row,col,width,height,padding,setTop,setLeft){
...
this.blocks = []
this.BlockSet()
}
BlockSet(){
for (let c = 0; c < this.col; c++) {
this.blocks[c] = []
for (let r = 0; r < this.row; r++) {
this.blocks[c][r] = {x:0,y:0,status:1};
}
}
Blockクラスのインスタンスが生成された場合、プロパティに値をセットする他に関数を呼び出す処理を加えたので確認をしていきましょう。
BlockSet(){
for (let c = 0; c < this.col; c++) {
this.blocks[c] = []
for (let r = 0; r < this.row; r++) {
this.blocks[c][r] = {x:0,y:0,status:1};
}
}
}
関数BlockSetは、プロパティblocksにブロック情報をセットするための関数です。
for (let c = 0; c < this.col; c++) {
this.blocks[c] = []
for (let r = 0; r < this.row; r++) {
this.blocks[c][r] = {x:0,y:0,status:1};
}
}
こちらの2重のfor文でブロック情報をセットしていきます。
for (let c = 0; c < this.col; c++) {
this.blocks[c] = []
}
//[[],[],[]]
まず外側のループで、プロパティcol(カラム)分の配列を格納します。要するに列を表します。
for (let r = 0; r < this.row; r++) {
this.blocks[c][r] = {x:0,y:0,status:1};
}
//[
// [{x:0,y:0,status:1},{x:0,y:0,status:1},{x:0,y:0,status:1}],
// [{x:0,y:0,status:1},{x:0,y:0,status:1},{x:0,y:0,status:1}],
// [{x:0,y:0,status:1},{x:0,y:0,status:1},{x:0,y:0,status:1}]
// ]
次に内側のループで、各列にアクセスしていきプロパティrow(行)分、
- x
- y
- status
というプロパティを挿入していきます。最終的にプロパティblocksの中身は下記の画像の通りになります。
プロパティblocksの中にブロック情報が確認ができたので、ブロックを描画をしていきましょう。
class Game {
game_start(){
//追加
this.block.draw(this.ctx)
}
}
Gameクラスの関数game_start内にBlockクラスの関数drawを呼び出しましょう。
draw(ctx){
for (let c = 0; c < this.col; c++) {
for (let r = 0; r < this.row; r++) {
if (this.blocks[c][r].status == 1) {
let blockX = (c*(this.width+this.padding))+this.setLeft
let blockY = (r*(this.height+this.padding))+this.setTop
this.blocks[c][r].x = blockX
this.blocks[c][r].y = blockY
ctx.beginPath()
ctx.rect(blockX,blockY,this.width,this.height,)
ctx.fillStyle="#0095DD"
ctx.fill()
ctx.closePath()
}
}
}
}
では、関数drawの中身を確認をしていきましょう。
for (let c = 0; c < this.col; c++) {
}
まずは、プロパティcol(列)分、ループ処理を繰り返します。
for (let r = 0; r < this.row; r++) {
if (this.blocks[c][r].status == 1) {
let blockX = (c*(this.width+this.padding))+this.setLeft
let blockY = (r*(this.height+this.padding))+this.setTop
this.blocks[c][r].x = blockX
this.blocks[c][r].y = blockY
ctx.beginPath()
ctx.rect(blockX,blockY,this.width,this.height,)
ctx.fillStyle="#0095DD"
ctx.fill()
ctx.closePath()
}
}
次に内側のループで、各列にアクセスしていきプロパティrow(行)分、ループを回します。
if (this.blocks[c][r].status == 1) {
let blockX = (c*(this.width+this.padding))+this.setLeft
let blockY = (r*(this.height+this.padding))+this.setTop
this.blocks[c][r].x = blockX
this.blocks[c][r].y = blockY
ctx.beginPath()
ctx.rect(blockX,blockY,this.width,this.height,)
ctx.fillStyle="#0095DD"
ctx.fill()
ctx.closePath()
}
こちらは、列に所属する各ブロックのプロパティstatusが1な場合と条件分岐を組んでいます。
今回は、ボールがぶつかった場合にぶつかったブロックのstatusを0に変更して、statusが0なブロックは再描画されないよう
にします。
let blockX = (c*(this.width+this.padding))+this.setLeft
// ブロックX軸座標 = (列*(ブロック幅+余白))+ブロックの描画を始めるX軸の位置
ブロックを描画する位置のX軸座標を求め、変数blockXを格納します。
let blockY = (r*(this.height+this.padding))+this.setTop
//ブロックY軸座標=(行*(ブロックの高さ+余白))+ブロックの描画を始めるY軸の位置
ブロックを描画する位置のY軸座標を求め、変数blockYを格納します。
this.blocks[c][r].x = blockX
this.blocks[c][r].y = blockY
各ブロックの
- X軸座標
- Y軸座標
を更新します。
上記の画像の通り、プロパティが更新されているはずです。
ctx.beginPath()
ctx.rect(blockX,blockY,this.width,this.height,)
ctx.fillStyle="#0095DD"
ctx.fill()
ctx.closePath()
こちらでブロックを描画をしています。
以上で、「ブロックを描画しよう」のトピックを終了します。この段階で、ブロックが描画ができているのを確認してから、次に進んでください。
衝突判定
こちらのトピックでは、ボールとブロックの衝突判定を実装していきます。
class Game {
game_start(){
//追加
this.BlockCollisionDetection()
}
BlockCollisionDetection(){
for (let c = 0; c <this.block.col; c++) {
for (let r = 0; r < this.block.row; r++) {
let b =this.block.blocks[c][r]
if (b.status == 1) {
if (this.ball.x >b.x && this.ball.x< b.x +this.block.width && this.ball.y > b.y && this.ball.y < b.y+this.block.height) {
this.ball.dy = -this.ball.dy;
b.status = 0
}
}
}
}
}
}
それでは、解説をしていきます。
game_start(){
//追加
this.BlockCollisionDetection()
}
まずは、Gameクラスの関数game_startに関数BlockCollisionDetectionを追加をしてください。
BlockCollisionDetection(){
for (let c = 0; c <this.block.col; c++) {
for (let r = 0; r < this.block.row; r++) {
let b =this.block.blocks[c][r]
if (b.status == 1) {
if (this.ball.x >b.x && this.ball.x< b.x +this.block.width && this.ball.y > b.y && this.ball.y < b.y+this.block.height) {
this.ball.dy = -this.ball.dy;
b.status = 0
}
}
}
}
}
関数BlockCollisionDetectionの中身を確認をしていきましょう。
for (let c = 0; c <this.block.col; c++) {
}
まずは、外側のfor文を定義をして、ブロックの列の数だけ、ループを回します。
for (let r = 0; r < this.block.row; r++) {
let b =this.block.blocks[c][r]
if (b.status == 1) {
if (this.ball.x >b.x && this.ball.x< b.x +this.block.width && this.ball.y > b.y && this.ball.y < b.y+this.block.height) {
this.ball.dy = -this.ball.dy;
b.status = 0
}
}
}
次に内側のfor文を定義をして、ブロックの行の数だけ、ループを回します。
let b =this.block.blocks[c][r]
各ブロックの情報を変数bに格納します。
if (b.status == 1) {
}
こちらは、各ブロックのstatusが1な場合と条件分岐を組んでいます。つまり、ボールが画面に描画されている場合です。
if (this.ball.x >b.x && this.ball.x< b.x +this.block.width && this.ball.y > b.y && this.ball.y < b.y+this.block.height) {
}
こちらは、2つの条件で条件分岐を組みました
- ボールのX軸座標がブロックのX軸座標より大きい且つボールのX軸座標がブロックのX軸座標にブロックの長さを足した和より小さい場合
- ボールのY軸座標がブロックのY軸座標より大きい且つボールのY軸座標がブロックのY軸座標にブロックの高さを足した和より小さい場合
という条件です。要するにブロックにぶつかった場合です。
this.ball.dy = -this.ball.dy
b.status = 0
条件に該当する場合は、ボールの進行方向を逆方向に変更をして、ぶつかったブロックのプロパティstatusの値を0に変更をします。statusの値を0に変更する事で画面に再描画されなくなるので、結果的に「ボールが消えた挙動」を実現する事ができます。
以上で、「衝突判定」のトピックを終了します。この段階で、ボールとブロックが衝突したらブロックが消えるか、確認をしてください。
ゲーム
こちらのトピックでは、ゲームクリア・ゲームオーバーを実装していきます。
- ゲームクリアを実装しよう
- ゲームオーバを実装しよう
ゲームクリアを実装しよう
class Game{
BlockCollisionDetection(){
//追加
this.game_clear()
}
game_clear(){
let blocks_status =[]
this.block.BlockStatusCheck(blocks_status)
blocks_status = blocks_status.filter((bl)=>bl == true)
if (blocks_status.length == []) {
alert("ゲームクリア")
clearInterval(this.intervalID)
}
}
}
class Block {
//追加
BlockStatusCheck(bls){
for (let c = 0; c < this.col; c++) {
// this.blocks[c] = []
for (let r = 0; r < this.row; r++) {
if (this.blocks[c][r].status==1) {
bls.push(true)
}else{
bls.push(false)
}
}
}
}
}
では、解説をしていきます。
BlockCollisionDetection(){
//追加
this.game_clear()
}
関数BlockCollisionDetectionで関数game_clearを呼び出しましょう。ゲームをクリアするという事は、画面に描画されているブロックがなくなった場合を意味します。したがって、ブロックとボールの衝突判定を監視する関数BCD(省略)に関数game_clearを追加します。
game_clear(){
let blocks_status =[]
this.block.BlockStatusCheck(blocks_status)
blocks_status = blocks_status.filter((bl)=>bl == true)
if (blocks_status.length == []) {
alert("ゲームクリア")
clearInterval(this.intervalID)
}
}
次に関数game_clearの中身を確認をしていきましょう。
let blocks_status =[]
変数blocks_statusを定義します
BlockStatusCheck(bls){
for (let c = 0; c < this.col; c++) {
for (let r = 0; r < this.row; r++) {
if (this.blocks[c][r].status==1) {
bls.push(true)
}else{
bls.push(false)
}
}
}
}
this.block.BlockStatusCheck(blocks_status)
関数BlockStatusCheckを呼び出して、ブロックのstatusが1な場合は、trueを配列の中に挿入をして、そうではない場合は、falseを挿入をします。
blocks_status = blocks_status.filter((bl)=>bl == true)
次にfilterメソッドを呼び出して、ブロックのstatusがtrueな要素のみ抽出します。
if (blocks_status.length == []) {
alert("ゲームクリア")
clearInterval(this.intervalID)
}
こちらは、変数blocks_statusが空の配列の場合と条件を組んでいます。
変数blocks_statusが空の配列という事は、ブロックのstatusが全て0になった状態ですので、画面に描画されているブロックがなくなったことを意味をします。なので、条件に該当する場合は、alertでゲームクリアと表示をして、インターバルを止めます。
以上で「ゲームクリア」のトピックの解説を終了します。
ゲームオーバを実装しよう
では、最後にゲームオーバーをアラートで表示をしていきましょう。
class Game {
BallCollisionDetection(){
//追加
if ( this.ball.y+this.ball.dy >this.node.height) this.game_over()
}
game_over(){
alert("ゲームオーバー")
clearInterval(this.intervalID)
}
}
では、解説をしていきます。
BallCollisionDetection(){
//追加
if ( this.ball.y+this.ball.dy >this.node.height) this.game_over()
}
今回のブロック崩しゲームでは、ボールが下端にぶつかった場合にゲームオーバー
とします。したがって、関数BallCollisionDetectionの中に、ボールのY軸座標に1フレームで進み距離を足した和が画面の高さより大きい場合という条件を組みます。
game_over(){
alert("ゲームオーバー")
clearInterval(this.intervalID)
}
this.game_over()
関数game_overは、alertを用いて、ゲームオーバーと表示をして、インターバルを止めます(intervalID)。
以上で、「ゲームオーバを実装しよう」のトピックの解説は終了します。このトピックで、実装は、終了しました。完成したソースコードは、貼り付けておくので、ご活用ください。
class Game {
constructor(node){
this.intervalID=null
this.node =document.querySelector(node)
this.ctx = this.node.getContext('2d')
this.ball =new window.Ball(this.node.width/2+10,this.node.height-30, -2,-2,10 )
this.paddle = new window.Paddle(10,75,false,parseInt( (this.node.width-75)/2),parseInt(this.node.height-10))
this.block = new window.Block(5,5,70,20,5,20,55)
document.addEventListener("keydown", this.paddle.keyDownHandler.bind(this.paddle), false);
document.addEventListener("keyup", this.paddle.keyupHandler.bind(this.paddle), false);
}
game_start(){
this.ctx.clearRect(0,0,this.node.width,this.node.height)
this.ball.draw(this.ctx)
this.paddle.draw(this.ctx)
this.paddle.move(this.node)
this.block.draw(this.ctx)
this.BallCollisionDetection()
this.BlockCollisionDetection()
}
BallCollisionDetection(){
if (this.ball.y+this.ball.dy <this.ball.rabius) {
this.ball.dy = -this.ball.dy
this.ball.y += this.ball.rabius
}
if(this.ball.x+this.ball.dx > this.node.width - this.ball.rabius || this.ball.x+this.ball.dx < this.ball.rabius){
this.ball.dx = -this.ball.dx
}
if (Math.abs(this.paddle.y+(this.paddle.height/2)- this.ball.y) <= this.ball.rabius ) {
if (this.ball.x >= this.paddle.x && this.ball.x <= this.paddle.x +1) {
this.ball.dy = -this.ball.dy
this.ball.dx = -this.ball.dx
this.ball.x -= 2
}
else if(this.ball.x >= this.paddle.x + this.paddle.width -1 && this.ball.x <= this.paddle.x + this.paddle.width){
this.ball.dy = -this.ball.dy
this.ball.dx = -this.ball.dx
this.ball.x += 2
}else if (this.ball.x >= this.paddle.x && this.ball.x <= this.paddle.x + this.paddle.width) {
this.ball.dy = -this.ball.dy
this.ball.y += this.ball.rabius
}
}
else if (Math.abs(this.paddle.y-(this.paddle.height/2)- this.ball.y) <= this.ball.rabius ) {
if (this.ball.x >= this.paddle.x && this.ball.x <= this.paddle.x +1) {
this.ball.dy = -this.ball.dy
this.ball.dx = -this.ball.dx
this.ball.y = Math.abs(this.paddle.y-(this.paddle.height/2)) - this.ball.rabius
this.ball.x -= 2
}else if(this.ball.x >= this.paddle.x + this.paddle.width -1 && this.ball.x <= this.paddle.x + this.paddle.width){
this.ball.dy = -this.ball.dy
this.ball.dx = -this.ball.dx
this.ball.y = Math.abs(this.paddle.y-(this.paddle.height/2)) - this.ball.rabius
this.ball.x += 2
}else if (this.ball.x >= this.paddle.x && this.ball.x <= this.paddle.x + this.paddle.width) {
this.ball.dy = -this.ball.dy
this.ball.y = Math.abs(this.paddle.y-(this.paddle.height/2)) - this.ball.rabius
}
}
if ( this.ball.y+this.ball.dy >this.node.height) this.game_over()
this.ball.x += this.ball.dx;
this.ball.y += this.ball.dy;
}
BlockCollisionDetection(){
this.game_clear()
for (let c = 0; c <this.block.col; c++) {
for (let r = 0; r < this.block.row; r++) {
let b =this.block.blocks[c][r]
if (b.status == 1) {
if (this.ball.x >b.x && this.ball.x< b.x +this.block.width && this.ball.y > b.y && this.ball.y < b.y+this.block.height) {
this.ball.dy = -this.ball.dy;
b.status = 0
}
}
}
}
}
game_clear(){
let blocks_status =[]
this.block.BlockStatusCheck(blocks_status)
blocks_status = blocks_status.filter((bl)=>bl == true)
if (blocks_status.length == []) {
alert("ゲームクリア")
clearInterval(this.intervalID)
}
}
game_over(){
alert("ゲームオーバー")
clearInterval(this.intervalID)
}
}
const game = new Game("#myCanvas");
game.intervalID=setInterval(function (params) {
game.game_start()
},10)
class Ball {
constructor(x,y,dx,dy,rabius){
this.x = x
this.y = y
this.dx = dx
this.dy = dy
this.rabius =rabius
}
draw(ctx){
ctx.beginPath();
ctx.arc(this.x, this.y, this.rabius, 0, Math.PI*2);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
}
export default Ball;
class Paddle{
constructor(height,width,pressed,x,y){
this.width = width
this.height = height
this.top = pressed
this.left = pressed
this.right = pressed
this.down = pressed
this.x = x
this.y = y
}
draw(ctx){
ctx.beginPath();
ctx.rect(this.x,this.y ,this.width,this.height);
ctx.fillStyle = "#0095DD";
ctx.fill();
ctx.closePath();
}
keyDownHandler(e){
switch (e.key) {
case "Top":
case "ArrowUp":
this.top = true
return
case "Down":
case "ArrowDown":
this.down = true
return
case "Right":
case "ArrowRight":
this.left = true
return
case "Left":
case "ArrowLeft":
this.right = true
return
}
}
keyupHandler(e){
switch (e.key) {
case "Top":
case "ArrowUp":
this.top = false
case "Down":
case "ArrowDown":
this.down = false
case "Right":
case "ArrowRight":
this.left = false
case "Left":
case "ArrowLeft":
this.right = false
break;
}
}
move(canvas){
if (this.top && this.y > 0) {
this.y -= 7
}else if (this.down && this.y < canvas.height - this.height) {
this.y += 7
}else if (this.left && this.x < canvas.width-this.width) {
console.log("test");
this.x += 7
}else if (this.right && this.x > 0) {
this.x -= 7
}
}
key=()=>{
console.log("d");
}
}
export default Paddle
class Block{
constructor(row,col,width,height,padding,setTop,setLeft){
this.row = row
this.col = col
this.width = width
this.height = height
this.padding = padding
this.setTop = setTop
this.setLeft = setLeft
this.blocks = []
this.BlockSet()
}
BlockSet(){
for (let c = 0; c < this.col; c++) {
this.blocks[c] = []
for (let r = 0; r < this.row; r++) {
this.blocks[c][r] = {x:0,y:0,status:1};
}
}
}
BlockStatusCheck(bls){
for (let c = 0; c < this.col; c++) {
for (let r = 0; r < this.row; r++) {
if (this.blocks[c][r].status==1) {
bls.push(true)
}else{
bls.push(false)
}
}
}
}
draw(ctx){
for (let c = 0; c < this.col; c++) {
for (let r = 0; r < this.row; r++) {
if (this.blocks[c][r].status == 1) {
let blockX = (c*(this.width+this.padding))+this.setLeft
let blockY = (r*(this.height+this.padding))+this.setTop
this.blocks[c][r].x = blockX
this.blocks[c][r].y = blockY
ctx.beginPath()
ctx.rect(blockX,blockY,this.width,this.height,)
ctx.fillStyle="#0095DD"
ctx.fill()
ctx.closePath()
}
}
}
}
}
export default Block;
まとめ:ブロック崩しゲーム
今回は、ブロック崩しゲームを作成していきました。複雑な条件式や座標の設定など、とても難しかったと思います。この記事を全て読破した方は、次にご自身でゲームを作成しても面白いかもしれません。なお、こちらの「ブロック崩しゲーム」は、MDNのチュートリアルを参考にして作成しました。MDNのチュートリアルを学習したい方は下記のリンクからご参照してください。
以上、kazutoでした。