ホッケーゲーム

Javascript

こんにちは、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-color: black;
         display: block;
          margin: 0 auto;
          }
      h1{
        text-align: center;
        color: #333333;
      }
      .filed{
        text-align: center;
        margin: auto;
        font-size: 30px;
        color: #333333;
      }
  </style>
  <title>Document</title>
  <script type="module">
    import  Ball  from "./ball.js";
    import  Paddle from './paddle.js';
    import  Score from './score.js';
    Window.prototype.Ball   = Ball
    Window.prototype.Paddle = Paddle
    Window.prototype.Score  = Score
  </script>
   <script  src="game.js"defer> </script>
</head>
<body>
  <main>
    <h1>Score</h1>
    <div class="filed">
    </div>
    <canvas id="myCanvas" width="480" height="320"></canvas>
  </main>
 </body>
</html>
class Ball{
}
export default Ball;
class Paddle{
}

export default Paddle
class Score{
}
export default Score

※必ずコピペをしてください。正常に動かないおそれがあります。

<script type="module">
    import  Ball  from "./ball.js";
    import  Paddle from './paddle.js';
    import  Score from './score.js';
    Window.prototype.Ball   = Ball
    Window.prototype.Paddle = Paddle
    Window.prototype.Score  = Score
  </script>

今回は、一つのファイルで一つのクラスを管理をしていきます。なので、ファイルを読み込む処理を上記のscriptタグの中で行っています。最終的に、ゲームの進行状況を管理するGameクラスで

  • Ball
  • Paddle
  • Score

を使える様にしていきます。

こちらは、Windowオブジェクトの中にimportして、読み込んだクラスを格納をしています(prototype)。この様に記述する事で、game.jsファイルでも問題なくクラスを呼び出す事ができます。

事前準備が終了しましたので、次にクラスごとのプロパティや動作、役割を確認をしていきましょう。

※こちらの記事は、「ブロック崩しゲーム」を読破をした方を対象に進めていきますので、時間がある方は、リンクに飛んで参照をしてから、読み進めてください。

Gameクラス

Gameクラスはゲームの進行を管理するクラスです。プロパティには各クラスのインスタンスの他、canvasタグの中で2Dグラフィックを描画するためにcanvasのノードを取得をして設定を行っていきます。

なお、属性については下記の表を参照してください。

プロパティ 概要
intervalID インターバルをキャンセルするための識別子
node canvasタグのノード
ctx 描画コンテキスト、画面に2dグラフィックスを描画
ball Ballクラスのインスタンス
player
プレイヤーのパドル
enemy
対戦相手のパドル
score
Scoreクラスのインスタンス

動作については下記をご覧ください。

  • 関数game_start
    →ゲームをスタートする関数
  • 関数game_clear
    →ゲームをクリアしたか判定する関数
  • 関数game_over
    →ゲームオーバをした判定する関数
  • 関数BallCollisionDetection
    →ボールの衝突判定をする関数
  • 関数keyupHandler
    →keyupイベント
  • 関数reassignment
    →各部品の位置を初期化する関数

Ball クラス

Ballクラスは、その名の通りボールを管理するクラスです。プロパティには、ボールを描画するための座標や半径などの情報を持ちます。

プロパティ 概要
x x軸の座標
y y軸の座標
dx x軸にボールが進む速さ
dy y軸にボールが進む速さ
rabius
円の半径
run ボールの動きを制御

上記のプロパティの他に関数drawという動作を持ちます。関数drawは、画面上にボールを描画をするための関数です。

Paddleクラス

Paddleクラスは、その名の通りパドルを管理するクラスです。プロパティには、パドルを描画するための座標や長さなどの情報を持ちます。

プロパティ 概要
width パドルの長さ
height パドルの高さ
top パドルが上に進むか判定
left パドルが右に進むか判定
right パドルが左に進むか判定
down パドルが下に進むか判定
x x軸の座標
y y軸の座標
dy 1フレームでY軸方向に進む距離
color パドルの色
run パドルの動きを制御

動作については下記をご覧ください。

  • 関数draw
    →パドルを画面に描画する関数
  • 関数move
    →パドル操作を管理する関数
  • 関数keyDownHandler
    →keydownイベント
  • 関数keyupHandler
    →keyupイベント
  • 関数moveAutomatically
    →自動でパドルを動かす関数

Scoreクラス

Scoreクラスは、その名の通り点数を管理するクラスです。プロパティには

  • プレイヤー
  • 対戦相手

のスコア情報を持ちます。

プロパティ 概要
playerScore
プレイヤーのスコア情報
enemyScore
対戦相手のスコア情報
filed
スコア情報を書き込むノード

動作については下記をご覧ください。

  • 関数draw
    →ブロックを画面に描画する関数
  • 関数playerScoreUp
    →プレイヤーのスコアを上げる関数
  • 関数enemyScoreUp
    →対戦相手のスコアを上げる関数
  • 関数update
    →スコア情報を書き込む関数

ボール

このトピックでは、ボールが上下左右の壁にぶつかっても、跳ね返ってくる所まで、実装していきます。

  • ボールを描写をしよう
  • ボールを動かそう
  • 衝突判定

ボールを描写をしよう

まず初めは、画面にボールを描写をしていきます。

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
   this.run = false
  }
  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を用いる事で、ボールを動かす事ができる様になります。

以上で、「ボールを動かそう」の解説を終了します。次にボールが壁にぶつかったら、跳ね返ってくる様に衝突判定のロジックを組んでいきましょう。

衝突判定

画像に alt 属性が指定されていません。ファイル名: 03cf4482621d7d2dd982c949e904fd71.gif

では、衝突判定のロジックを組んでいきましょう。

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.player = new window.Paddle(75,10,false,0,parseInt(this.node.height/2)-parseInt(75/2),"#0095DD")
      this.enemy  = new window.Paddle(75,10,true,this.node.width-10,parseInt(this.node.height/2)-parseInt(75/2),"red")
  }
  game_start(){
    //追加
    this.player.draw(this.ctx)
    this.enemy.draw(this.ctx)
  }
}
class Paddle{

   constructor(height,width,pressed,x,y,color,dy=4,){
      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.dy=dy
      this.color=color
      this.run = false
    }

   draw(ctx){
      ctx.beginPath();
      ctx.rect(this.x,this.y ,this.width,this.height);
      ctx.fillStyle = this.color;
      ctx.fill();
      ctx.closePath();
   }

}

それでは、解説をしていきます。

class Paddle{
  constructor(height,width,pressed,x,y,color,dy=4,){
      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.dy=dy
      this.color=color
      this.run = false
    }
} 
this.player = new window.Paddle(75,10,false,0,parseInt(this.node.height/2)-parseInt(75/2),"#0095DD")
this.enemy  = new window.Paddle(75,10,true,this.node.width-10,parseInt(this.node.height/2)-parseInt(75/2),"red") 

Ballクラスのインスタンスと同様、Gameクラスのconstructor内でPaddleクラスのインスタンス2つを生成をします。

  • プレイヤーのパドル
  • 対戦相手のパドル

です。

この様に記述をする事で、Gameクラス内で、Paddleクラスのインスタンスをプロパティとして管理する事ができます

※Paddleクラスのプロパティに関しては、「Paddleクラス」でご確認ください。

class Game{
  game_start(){
    //追加
    this.player.draw(this.ctx)
    this.enemy.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.player.keyDownHandler.bind(this.player), false);
      document.addEventListener("keyup"  , this.player.keyupHandler.bind(this.player), false);
    }
    game_start(){
    //追加
      this.player.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
    }
  }

  keyupHandler(e){
    switch (e.key) {
      case "Top":
      case "ArrowUp":
        this.top = false
      case "Down":
      case "ArrowDown":
        this.down = false
    }
  }

  move(canvas){
    if (this.run) {
      if (this.top && this.y > 0) {
        this.y -= 7
      }else if (this.down && this.y < canvas.height - this.height) {
        this.y += 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
      case "Down":
      case "ArrowDown":
        this.down = true
  }
}

該当するキーが押された場合は、対応するキーを制御するプロパティにtrueを格納します。

keyupHandler(e){
    switch (e.key) {
      case "Top":
      case "ArrowUp":
        this.top = false
      case "Down":
      case "ArrowDown":
        this.down = false
    }
  }

該当するキーが押されて、キーを押し終えた場合には、対応するキーを制御するプロパティにfalseを格納します。

game_start(){
  //追加
    this.player.move(this.node)
}

関数game_startに中にPaddleの関数moveを呼び出します。関数moveの中身を見ていきましょう。

move(canvas){
    if (this.run) {
      if (this.top && this.y > 0) {
        this.y -= 7
      }else if (this.down && this.y < canvas.height - this.height) {
        this.y += 7
      }
    }
  }

関数moveは、パドル操作を管理する関数です。

if (this.run) {
     
 }

こちらは、プロパティrunがtrueな場合と条件を組んでいます。ホッケーゲームは、スコアが更新されるごとに部品の位置を初期化して動きを止めたいです。なので、動きを制御するプロパティrunを定義をしてパドルを制御していきます。

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を足して、パドルが下に進みます。

この一連のロジックを組む事でパドル操作を実現をしています。

以上で、「プレイヤーのパドルを動かそう」の解説を終了します。この段階で、パドルが上下左右に移動できるのかを確認をしてください。

対戦相手のパドルを自動で動かそう

次に対戦相手のパドルを自動で上下に移動させていきましょう。

class Game{
  game_start(){
      //追加
      this.enemy.moveAutomatically(this.node,5)
    }
}
class Paddle{
   moveAutomatically(canvas){
    if (this.run) {
      if (this.y < 0) {
        this.dy = -this.dy
      }
      if (this.down && this.y > canvas.height - this.height) {
        this.dy = -this.dy
      }
      this.y += this.dy
    }
  }
}

では、解説をしていきます。

class Game{
  game_start(){
      //追加
      this.enemy.moveAutomatically(this.node,5)
    }
}

関数game_startに中にPaddleの関数moveAutomaticallyを呼び出します。関数moveの中身を見ていきましょう。

 moveAutomatically(canvas){
    if (this.run) {
      if (this.y < 0) {
        this.dy = -this.dy
      }
      if (this.down && this.y > canvas.height - this.height) {
        this.dy = -this.dy
      }
      this.y += this.dy
    }
  }
 if (this.run) {
 }

こちらは、プロパティrunがtrueな場合と条件を組んでいます。ホッケーゲームは、スコアが更新されるごとに部品の位置を初期化して動きを止めたいです。なので、動きを制御するプロパティrunを定義をしてパドルを制御していきます。

 if (this.y < 0) {
        this.dy = -this.dy
  }

こちらは、パドルのY軸座標が0より小さい場合と条件を組んでいます。つまり、パドルの先端が上端に衝突した場合です。条件に該当する場合は、パドルの進む方向を変更します。

if (this.y > canvas.height - this.height) {
  this.dy = -this.dy
}

こちらは、パドルのY軸座標が画面の高さからパドルの高さを引いた差より大きい場合と条件を組んでいます。つまり、パドルの先端が下端に衝突した場合です。条件に該当する場合は、パドルの進む方向を変更します。

this.y += this.dy

末尾でY軸座標を更新します。

この一連の流れを実装する事で対戦相手のパドルを自動で上下に移動を繰り返すという動作を実現ができます。

以上で、「対戦相手のパドルを自動で動かそう」の解説を終了します。

ボールを跳ね返そう

画像を編集

次にボールを跳ね返して、ラリーを続けられる様にしていきましょう。

//変更
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.player.width+this.ball.rabius ) {
        if (this.ball.y >=this.player.y && this.ball.y<= this.player.y + this.player.height  ) {
          this.ball.dx = -this.ball.dx
          this.ball.x += 4
        }
      }
     if (this.ball.x+this.ball.dx < 0) {
        this.ball.dx = -this.ball.dx
      }

      if(this.ball.x+this.ball.dx > this.node.width - this.enemy.width-this.ball.rabius){
        if (this.ball.y >=this.enemy.y && this.ball.y<= this.enemy.y + this.enemy.height) {
          this.ball.dx = -this.ball.dx
          this.ball.x += 4
        }
      }
      if (this.ball.x+this.ball.dx> this.node.width - this.ball.rabius) {
        this.ball.dx = -this.ball.dx
      }
      if (this.ball.run) {
        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
 }
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.player.width+this.ball.rabius ) {
 }

こちらは、ボールのX軸座標に1フレームで進む距離を足した和が、プレイヤーのパドルの幅にボールの半径分を足した和より、小さい場合と条件を組んでいます。つまり、パドルのX座標にボールの半径を足した位置にボールが到達した事を表します。

条件に該当する場合は、以下の処理につながります。

  if (this.ball.y >=this.player.y && this.ball.y<= this.player.y + this.player.height  ) {
     this.ball.dx = -this.ball.dx
     this.ball.x += 4
    }

こちらは、ボールのY軸座標がプレイヤーのパドルのY軸座標以上で且つボールのY軸座標がプレイヤーのパドルのY軸座標にパドルの高さを足した和未満な場合と条件を組んでいます。つまり、パドルの範囲を表します。

this.ball.dx = -this.ball.dx

条件に該当する場合は、ボールの進行方向を反転をします。

this.ball.x += 4

ボールのX軸座標を更新しています。この記述をする事で際どい座標な場合に条件式が競合を起こさない様になります。

 if (this.ball.x+this.ball.dx < 0) {
   this.ball.dx = -this.ball.dx
 }

こちらは、ボールのX軸座標に1フレームで進む距離を足した和が、0以下な場合と条件を組んでいます。つまり、左端にボールが衝突した場合です。

if(this.ball.x+this.ball.dx > this.node.width - this.enemy.width-this.ball.rabius){
 }

こちらは、ボールのX軸座標に1フレームで進む距離を足した和が画面の幅に対戦相手のパドルの幅とボールの半径を引いた差より小さい場合と条件を組んでいます。つまり、パドルのX座標にボールの半径を引いた位置にボールが到達した事を表します。

条件に該当する場合は、下の処理が読み込まれます。

if (this.ball.y >=this.enemy.y && this.ball.y<= this.enemy.y + this.enemy.height) {
  this.ball.dx = -this.ball.dx
  this.ball.x += 4
}

こちらは、ボールのY軸座標が対戦相手のパドルのY軸座標以上で且つボールのY軸座標が対戦相手のY軸座標にパドルの高さを足した和未満な場合と条件を組んでいます。

this.ball.dx = -this.ball.dx

条件に該当する場合は、ボールの進行方向を変更します。

this.ball.x += 4

ボールのX軸座標を更新しています。この記述をする事で際どい座標な場合に条件式が競合を起こさない様になります。

if(this.ball.x+this.ball.dx> this.node.width - this.ball.rabius){
  this.ball.dx = -this.ball.dx
}

こちらは、ボールのX軸座標に1フレームですすむ距離を足した和が、画面の幅にボール半径を引いた差より大きい場合と条件を組んでいます。つまり、左端にボールが衝突した場合です。条件に該当する場合は、ボールの進行方向を反転します。

if (this.ball.run) {
  this.ball.x += this.ball.dx;
  this.ball.y += this.ball.dy;
}

こちらは、Ballクラスのプロパティrunがtureな場合と条件分岐を行っています。つまり、ボールが動いて良い状態の時にif文内の条件が読み込まれます。

以上で、「ボールを跳ね返そう」の解説を終了します。

ボールが左端・右端に衝突したらゲームを中断しよう

次に

  • 左端
  • 右端

にボールが衝突したら、ゲームをリセットをして部品を初期の位置に戻していきます。

class Game{

  reassignment(){
      this.ball.run= false
      this.ball.x = this.node.width/2
      this.ball.y = this.node.height-this.ball.rabius
      this.enemy.run = false
      this.enemy.x  = this.node.width-10
      this.enemy.y  = parseInt(this.node.height/2)-parseInt(75/2)
      this.player.run = false
      this.player.x = 0
      this.player.y = parseInt(this.node.height/2)-parseInt(75/2)
   }
   BallCollisionDetection(){
    if (this.ball.x+this.ball.dx < 0) {
        this.ball.dx = -this.ball.dx
        //追加
        this.reassignment()
      }
     if (this.ball.x+this.ball.dx> this.node.width - this.ball.rabius) {
        //追加
        this.reassignment()
      }
   }
}

では、解説をしていきます。

 BallCollisionDetection(){
    if (this.ball.x+this.ball.dx < 0) {
        this.ball.dx = -this.ball.dx
        //追加
        this.reassignment()
      }
     if (this.ball.x+this.ball.dx> this.node.width - this.ball.rabius) {
        //追加
        this.reassignment()
      }
   }

ゲームをリセットするタイミングは、

  • 左端
  • 右端

にボールが衝突した場合です。したがって、上記のif文の中に各部品の位置を初期化する関数reassignmentを呼び出します。

関数reassignmentの中身をみていきましょう。

 reassignment(){
      this.ball.run= false
      this.ball.x = this.node.width/2
      this.ball.y = this.node.height-this.ball.rabius
      this.enemy.run = false
      this.enemy.x  = this.node.width-10
      this.enemy.y  = parseInt(this.node.height/2)-parseInt(75/2)
      this.player.run = false
      this.player.x = 0
      this.player.y = parseInt(this.node.height/2)-parseInt(75/2)
   }

関数reassignmentを呼び出す事で、各部品を位置を初期化する事ができます。

this.ball.run= false

Ballクラスのプロパティrunにfalseを格納します。この記述する事で、ボールの動きを止める事ができます。

this.ball.x = this.node.width/2
this.ball.y = this.node.height-this.ball.rabius

ボールの座標を初期化をします。

this.enemy.run = false

対戦相手のパドルのプロパティrunにfalseを格納します。この記述する事で、対戦相手のパドルの動きを止める事ができます。

this.enemy.x  = this.node.width-10
this.enemy.y  = parseInt(this.node.height/2)-parseInt(75/2)    

対戦相手のパドルの座標を初期化します。

this.player.run = false

プレイヤーのパドルのプロパティrunにfalseを格納します。この記述する事で、プレイヤーのパドルの動きを止める事ができます。

this.player.x = 0
this.player.y = parseInt(this.node.height/2)-parseInt(75/2)

プレイヤーのパドルの座標を更新します。この一連の流れを実装する事で、ゲームをリセットするという動作を実現ができます。

以上で、「ボールが左端・右端に衝突したらゲームを中断しよう」の解説を終了します。

Score

画像に alt 属性が指定されていません。ファイル名: 4d05a3e2e40e9b36639151614cbfc74a.png

では最後に画面にスコアを表示をしていきましょう。

  • スコアを描画しよう
  • どちらかの点数10になったら試合結果を表示しよう

スコアを描画しよう

画像に alt 属性が指定されていません。ファイル名: 4d05a3e2e40e9b36639151614cbfc74a.png

まずは、スコアを描画していきましょう。

class Game{
 constructor(node){
  this.score  = new window.Score(0,0)
 }
}
class Score{
  constructor(playerScore,enemyScore){
    this.playerScore =  playerScore
    this.enemyScore  =  enemyScore
    this.filed       =  document.querySelector(".filed")
    this.update()
  }
  update(){
    this.filed.textContent =  `${this.playerScore}:${this.enemyScore}`
  }
}
export default Score

では、解説をしていきます。

class Game{
 constructor(node){
  this.score  = new window.Score(0,0)
  }
}

Gameクラスのconstructor内でScoreクラスのインスタンスを生成をします。この様に記述をする事で、Gameクラス内でScoreクラスのインスタンスをプロパティとして管理する事ができます

class Score{ 
 constructor(playerScore,enemyScore){
    this.update()
 }
}

Scoreクラスのインスタンス生成した際に関数updateを呼び出します。

関数updateの中身をみていきましょう。

update(){
    this.filed.textContent =  `${this.playerScore}:${this.enemyScore}`
 }

関数updateは、シンプルにスコア情報を書き込むノードのfiledにtextContentを用いてスコア情報を挿入するだけです。

以上で「スコアを描画しよう」の解説を終了します。

どちらかの点数が10点になったら試合結果を表示しよう

最後にどちらかの点数が10点になったら試合結果を表示していきましょう。

class Game{

   BallCollisionDetection(){
      if (this.ball.x+this.ball.dx < 0) {
        this.ball.dx = -this.ball.dx
        //追加
        if (this.score.enemyScoreUp()) {
          this.game_over()
        }
        this.reassignment()
       }
      if (this.ball.x+this.ball.dx> this.node.width - this.ball.rabius) {
        this.ball.dx = -this.ball.dx
        //追加
        if (this.score.playerScoreUp()){
          this.game_clear()
        }
        this.reassignment()
      }
   }
   //追加
    game_clear(){
        alert("ゲームクリア")
        clearInterval(this.intervalID)
    }
  //追加
    game_over(){
       alert("ゲームオーバー")
       clearInterval(this.intervalID)
    }
}
class Score{
  playerScoreUp(){
    this.playerScore+=1
    this.update()
    if (this.playerScore==10) {
      return true
    }
    return false
  }
  enemyScoreUp(){
    this.enemyScore+=1
    this.update()
    if (this.enemyScore==10) {
      return true
    }
    return false
  }
}

では、解説をしていきます。

class Score{
  playerScoreUp(){
    this.playerScore+=1
    this.update()
    if (this.playerScore==10) {
      return true
    }
    return false
  }
  enemyScoreUp(){
    this.enemyScore+=1
    this.update()
    if (this.enemyScore==10) {
      return true
    }
    return false
  }
}

まずは、新たにScoreクラスに追加をした

  • 関数playerScoreUp
  • 関数enemyScoreUp

を確認をしていきましょう。

 playerScoreUp(){
    this.playerScore+=1
    this.update()
    if (this.playerScore==10) {
      return true
    }
    return false
  }

関数playerScoreUpはプレイヤーのスコアを更新する関数です。

this.playerScore+=1

ScoreクラスのプロパティplayerScoreの値を1を足して更新します。

this.update()

関数updateを呼び出して画面を更新をします。

if (this.playerScore==10) {
      return true
}

playerScoreの値が10になった場合は、tureを返り値として返します。

 return false

条件に該当しない場合は、返り値としてfalseを返します。

 enemyScoreUp(){
    this.enemyScore+=1
    this.update()
    if (this.enemyScore==10) {
      return true
    }
    return false
  }

関数enemyScoreUpもほぼ同じ内容なので解説は省略をします。

BallCollisionDetection(){
      if (this.ball.x+this.ball.dx < 0) {
        this.ball.dx = -this.ball.dx
        //追加
        if (this.score.enemyScoreUp()) {
          this.game_over()
        }
        this.reassignment()
       }
      if (this.ball.x+this.ball.dx> this.node.width - this.ball.rabius) {
        this.ball.dx = -this.ball.dx
        //追加
        if (this.score.playerScoreUp()){
          this.game_clear()
        }
        this.reassignment()
      }
   }

次に、関数BallCollisionDetectionに処理を追加をしていきましょう。

if (this.ball.x+this.ball.dx < 0) {
   this.ball.dx = -this.ball.dx
        //追加
 if (this.score.enemyScoreUp()) {
    this.game_over()
  }
   this.reassignment()
}

対戦相手のスコアが更新されるのは、「左端にボールが衝突した場合」を指すので、上記の条件式の中に処理を追加します。

if (this.score.enemyScoreUp()) {
    this.game_over()
}

scoreクラスの関数enemyScoreUpを呼び出しスコアを更新します。今回、if文を記述した意図は、関数enemyScoreUpの返り値によって挙動を変えたいためです。対戦相手が10点先取をしたら関数enemyScoreUpの返り値がtrueになり、処理が読み込まれます。

this.game_over()
game_over(){
       alert("ゲームオーバー")
       clearInterval(this.intervalID)
}

条件に該当する場合、Gameクラスの関数game_overを呼び出して、「ゲームオーバ」というアラートを表示をして、clearIntervalを呼び出してゲームを終了します。

if (this.ball.x+this.ball.dx> this.node.width - this.ball.rabius) {
  this.ball.dx = -this.ball.dx
        //追加
  if (this.score.playerScoreUp()){
    this.game_clear()
  }
  this.reassignment()
}

次に、プレイヤーのスコアが更新される様にしていきましょう。プレイヤーのスコアが更新されるのは、「右端にボールが衝突した場合」を指すので、上記の条件式の中に処理を追加します。

if (this.score.playerScoreUp()){
  this.game_clear()
}

scoreクラスの関数playerScoreUpを呼び出しスコアを更新します。今回、if文を記述した意図は、関数playerScoreUpの返り値によって挙動を変えたいためです。プレイヤーが10点先取をしたら関数playerScoreUpの返り値がtrueになり、処理が読み込まれます。

game_over(){
  alert("ゲームオーバー")
  clearInterval(this.intervalID)
}
this.game_clear()
画像に alt 属性が指定されていません。ファイル名: cb50b659eb85f146875d38b19215bfaf.png

条件に該当する場合、Gameクラスの関数game_clearを呼び出して、「ゲームオーバ」というアラートを表示をして、clearIntervalを呼び出して、ゲームを終了します。

以上で、「どちらかの点数が10点になったら試合結果を表示しよう」の解説を終了します。また、このトピックで実装も終わりなので、完成したソースコードを貼り付けておきますのでご活用ください。

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.player = new window.Paddle(75,10,false,0,parseInt(this.node.height/2)-parseInt(75/2),"#0095DD")
      this.enemy  = new window.Paddle(75,10,true,this.node.width-10,parseInt(this.node.height/2)-parseInt(75/2),"red")
      this.score  = new window.Score(0,0)
      document.addEventListener('keyup',this.keyupHandler.bind(this))
      document.addEventListener("keydown", this.player.keyDownHandler.bind(this.player), false);
      document.addEventListener("keyup"  , this.player.keyupHandler.bind(this.player), false);
    }

    keyupHandler(e){
      if (e.code=="Space") {
        if (!this.ball.run) {
          this.ball.run  = true
          this.enemy.run = true
          this.player.run = true
        }
      }
    }

    game_start(){
      this.ctx.clearRect(0,0,this.node.width,this.node.height)
      this.player.draw(this.ctx)
      this.player.move(this.node)
      this.enemy.draw(this.ctx)
      this.enemy.moveAutomatically(this.node,5)
      this.ball.draw(this.ctx)
      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.player.width+this.ball.rabius ) {
        if (this.ball.y >=this.player.y && this.ball.y<= this.player.y + this.player.height  ) {
          this.ball.dx = -this.ball.dx
          this.ball.x += 4
        }
      }
      if (this.ball.x+this.ball.dx < 0) {
        this.ball.dx = -this.ball.dx
        if (this.score.enemyScoreUp()) {
          this.game_over()
        }
        this.reassignment()
       }
      if(this.ball.x+this.ball.dx > this.node.width - this.enemy.width-this.ball.rabius){
        if (this.ball.y >=this.enemy.y && this.ball.y<= this.enemy.y + this.enemy.height) {
          this.ball.dx = -this.ball.dx
          this.ball.x += 4
        }
      }
      if (this.ball.x+this.ball.dx> this.node.width - this.ball.rabius) {
        this.ball.dx = -this.ball.dx
        if (this.score.playerScoreUp()){
          this.game_clear()
        }
        this.reassignment()
      }
      if (this.ball.run) {
        this.ball.x += this.ball.dx;
        this.ball.y += this.ball.dy;
      }
    }

    reassignment(){
      this.ball.run= false
      this.ball.x = this.node.width/2
      this.ball.y = this.node.height-this.ball.rabius
      this.enemy.run = false
      this.enemy.x  = this.node.width-10
      this.enemy.y  = parseInt(this.node.height/2)-parseInt(75/2)
      this.player.run = false
      this.player.x = 0
      this.player.y = parseInt(this.node.height/2)-parseInt(75/2)
    }

      game_clear(){
          alert("ゲームクリア")
          clearInterval(this.intervalID)
      }

      game_over(){
        alert("ゲームオーバー")
        clearInterval(this.intervalID)
      }
}

const game = new Game("#myCanvas");
game.intervalID= setInterval(() => {
  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
    this.run = false
  }

  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,color,dy=4,){
      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.dy=dy
      this.color=color
      this.run = false
    }
  draw(ctx){
      ctx.beginPath();
      ctx.rect(this.x,this.y ,this.width,this.height);
      ctx.fillStyle = this.color;
      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
    }
  }

  keyupHandler(e){
    switch (e.key) {
      case "Top":
      case "ArrowUp":
        this.top = false
      case "Down":
      case "ArrowDown":
        this.down = false
    }
  }

  move(canvas){
    if (this.run) {
      if (this.top && this.y > 0) {
        this.y -= 7
      }else if (this.down && this.y < canvas.height - this.height) {
        this.y += 7
      }
    }
  }

  moveAutomatically(canvas){
    if (this.run) {
        if (this.y < 0) {
        this.dy = -this.dy
      }
      if ( this.y > canvas.height - this.height) {
        this.dy = -this.dy
      }
      this.y += this.dy
    }
  }

}

export default Paddle
class Score{

  constructor(playerScore,enemyScore){
    this.playerScore =  playerScore
    this.enemyScore  =  enemyScore
    this.filed       =  document.querySelector(".filed")
    this.update()
  }

  playerScoreUp(){
    this.playerScore+=1
    this.update()
    if (this.playerScore==10) {
      return true
    }
    return false
  }

  enemyScoreUp(){
    this.enemyScore+=1
    this.update()
    if (this.enemyScore==10) {
      return true
    }
    return false
  }

  update(){
    this.filed.textContent =  `${this.playerScore}:${this.enemyScore}`
  }

}

export default Score

まとめ:ホッケーゲーム

今回は、ホッケーゲームを作成していきました。前回作成した、ブロック崩しゲームと比較するとブロックとの衝突判定が無い分、簡単に作成ができたと思います。次は、自分でゲームを作成してみましょう!!

以上、kazutoでした。