コアダンプの数だけ強くなれるよ

見習いエンジニアの備忘log

canvasで少しずつ作るブロック崩し(4/5)


前回は右クリックでボールを出現、左クリックでバーを上下させボールを打ち返す機能を加えました。今回はいよいよブロックを作ってボールが当たると消える機能をつけてみます。


完成品

左クリック: バーを下げる
右クリック: ボール出現

ソースコード

<html>
<head>
<meta charset="UTF-8">
<script type="text/javascript">
(function() {

  var canvas;
  var ctx;
  var mouseX;
  var mouseY;
  
  // バーの情報
  var BAR = {
    'HEIGHT' : 10,
    'WIDTH'  : 50,
    'UNDER'  : 25,
    'X'      : 0,
    'Y'      : 0,
    'PUSH'   : 10,
    'Vx'     : 0,
    'Vx0'    : 0,
    'Vy'     : 0,
    'Vy0'    : 150,
    'M'      : 5,
    'E'      : 0.7,
    'E0'     : 10,
  };
  
  // フィールドの情報
  var FIELD = {
    'HEIGHT'  : 0,
    'WIDTH'   : 0,
    'GRAVITY' : 588, // 重力加速度(0.9 * FPS)
    'FPS'     : 60,  // frame per second
    'E'       : 0.7, 
    'E0'      : 10,
  };
  
  // ボールの情報
  var BALL = [];
  var Ball = function() {
    this.Alive  = 0;
    this.X      = 0;
    this.Y      = 0;
    this.Vy     = 0;
    this.Vx     = 100;
    this.Vx0    = 0;
    this.Vy0    = 200;
    this.RADIUS = 5;
    this.M      = 1;
    this.HUE    = 0.5;
  };
  
  // ブロックの情報
  var BLOCK = [];
  var Block = function() {
    this.Alive  = 0;
    this.WIDTH  = 30;
    this.HEIGHT = 10;
    this.X      = 0;
    this.Y      = 0;
  };
  
  // 初期化処理
  function initialize() {
    canvas = document.getElementById('canvas');
    if(!canvas && !canvas.getContext) {
      return false;
    }
    
    // キャンバス作成
    ctx = canvas.getContext('2d');
    FIELD.WIDTH = ctx.canvas.width ;
    FIELD.HEIGHT = ctx.canvas.height;
    
    // バーの設定
    mouseX = FIELD.WIDTH/2;  // バーの初期位置は中心
    BAR.X = mouseX;
    BAR.Y = FIELD.HEIGHT-BAR.UNDER;
    
    // ブロックの生成
    createBlocks();
    
    // 各種イベント設定
    canvas.addEventListener('mousemove', getMouseCoordinate, false);
    canvas.addEventListener('mousedown', pushBar, false);
    canvas.addEventListener('mouseup', popBar, false);
    canvas.addEventListener('contextmenu', putBall, false);
    
    setInterval(drawField, 1000/FIELD.FPS);
  };
  
  
  // マウス座標の更新
  function getMouseCoordinate(e) {
    var rect = e.target.getBoundingClientRect();
    mouseX = Math.floor(e.clientX - rect.left);
    mouseY = Math.floor(e.clientY - rect.top);
  };
  
  // ボールの生成
  function putBall(e) {
    e.preventDefault();
    
    // ボールの初期位置は中心
    var tail = BALL.length;
    BALL[tail] = new Ball();
    BALL[tail].Alive = 1;
    BALL[tail].X = BAR.X;
    BALL[tail].Y = FIELD.HEIGHT-BAR.UNDER;
    BALL[tail].Vy = BALL[tail].Vy0 * (-1);
  };
  
  // バーの収縮
  function pushBar(e) {
    if (!e.pageX) {
      e = event.touches[0];
    }
    
    if (e.button == 0) {
      setTimeout(pushBarEvent, 1000/FIELD.FPS);
    }
  };
  
  // バーの反発
  function popBar(e) {
    if (!e.pageX) {
      e = event.touches[0];
    }
    
    if (e.button == 0) {
      setTimeout(popBarEvent, 1000/FIELD.FPS);
    }
  };
  
  // バーの収縮処理
  function pushBarEvent() {
    if (BAR.Y < FIELD.HEIGHT - BAR.UNDER + BAR.PUSH) {
      BAR.Vy = BAR.Vy0;
      BAR.Y += BAR.Vy * (FIELD.FPS/1000);
      setTimeout(pushBarEvent, 1000/FIELD.FPS);
    } else {
      BAR.Vy = 0;
      BAR.Y = FIELD.HEIGHT - BAR.UNDER + BAR.PUSH;
    }
  };
  
  var BarTimer;
  // バーの反発処理
  function popBarEvent() {
    if (BAR.Y > FIELD.HEIGHT-BAR.UNDER) {
      BAR.Vy = BAR.Vy0 * (-1);
      BAR.Y  += BAR.Vy * (FIELD.FPS/1000);
      setTimeout(popBarEvent, 1000/FIELD.FPS);
    } else {
      BAR.Y = FIELD.HEIGHT-BAR.UNDER;
      setTimeout(resetBarSpeed, 100);
    }
  };
  
  function resetBarSpeed() {
    BAR.Vy = 0;
  };
  
  // ブロックの生成
  function createBlocks() {
    var bxmax = 13;
    var bymax = 6;
    var btop  = 20;
    var bleft = 20;
    var bidx = 0;
    var bint = 5;
    
    for (var x = 0; x < bxmax; x++) {
      for (var y = 0; y < bymax; y++) {
        BLOCK[bidx] = new Block();
        BLOCK[bidx].X = x * BLOCK[bidx].WIDTH + btop + (x * bint);
        BLOCK[bidx].Y = y * BLOCK[bidx].HEIGHT + bleft + (y * bint);
        BLOCK[bidx].Alive = 1;
        
        bidx++;
      }
    }
    
  };
  
  
  // 画面の描画
  function drawField() {
    
    calcBallP();
    drawBack();
    drawBall();
    drawBar();
    drawBlock();
  };
  
  // ボール位置計算
  function calcBallP() {
    
    for (var i = 0; i < BALL.length; i++) {
      
      if (BALL[i] == null) {
        continue;
      }
      
      // 生存しているボールのみ計算
      if (BALL[i].Alive == 1) {
        // 床・天井接触
        if (BALL[i].Y <= 0 || FIELD.HEIGHT <= BALL[i].Y) {
          if (BALL[i].Y <= 0) {
            
            // 天井
            BALL[i].Y = FIELD.E0;
            BALL[i].Vy = BALL[i].Vy * FIELD.E * (-1)
          } else {
            // 床
            // 床に接触したボールは死亡
            BALL[i].Alive = 0;
            BALL[i].Vx = 0;
            BALL[i].Vy = 0;
          }
        }
        
        // 壁接触
        if (BALL[i].X <= 0 || FIELD.WIDTH <= BALL[i].X) {
          if (BALL[i].X <= 0) {
            BALL[i].X = FIELD.E0;
          } else {
            BALL[i].X = FIELD.WIDTH - FIELD.E0;
          }
          
          BALL[i].Vx = BALL[i].Vx * (-1);
        }
        
        // バー接触
        if (BAR.X <= BALL[i].X && BALL[i].X <= BAR.X + BAR.WIDTH) {
          if (Math.abs(BALL[i].Y - BAR.Y) <= BAR.HEIGHT) {
            
            // バーとボールの境界でバタつきを防ぐための処置
            BALL[i].Y = BAR.Y - BAR.E0;
            
            // バー接触後の速度計算
            BALL[i].Vy = (BALL[i].M * BALL[i].Vy - BAR.M * BAR.Vy) / BALL[i].M;
            
            // Vyが初速度より減速した場合は、初速度に戻す
            BALL[i].Vy = Math.abs(BALL[i].Vy) > BALL[i].Vy0 ? (BALL[i].Vy * BAR.E * (-1)) : (BALL[i].Vy0 * (-1));
          }
        }
        
        // ブロック接触
        for (var bi = 0; bi < BLOCK.length; bi++) {
          var xtouch = 0;
          var ytouch = 0;
          
          if (BLOCK[bi] == null) {
            continue;
          }
          
          if (BLOCK[bi].Alive == 0) {
            continue;
          }
          
          if ((Math.abs(BALL[i].X - BLOCK[bi].X) < BALL[i].RADIUS/2 + BLOCK[bi].WIDTH/2) &&
              (Math.abs(BALL[i].Y - BLOCK[bi].Y) < BALL[i].RADIUS/2 + BLOCK[bi].HEIGHT/2)) {
            
            BLOCK[bi].Alive = 0;
            BALL[i].Vy *= (-1);
          }
        }
        
        // 縦計算
        BALL[i].Vy += FIELD.GRAVITY * (1/FIELD.FPS);
        BALL[i].Y  += BALL[i].Vy * (1/FIELD.FPS);
        
        // 横計算
        BALL[i].X += BALL[i].Vx * (1/FIELD.FPS);
      }
    }
    
    deleteAllDeadBall();
  };
  
  // 死亡したボールを削除
  function deleteAllDeadBall() {
    var isDeadBall = 1;
    while(isDeadBall != 0) {
      isDeadBall = 0;
      for (var i = 0; i < BALL.length; i++) {
        if (BALL[i] == null) {
          continue;
        }
        
        if(BALL[i].Alive == 0) {
          delete BALL[i];
          BALL.splice(i,1);
          isDeadBall = 1;
          break;
        }
      }
    }
  }
  
  
  function drawBack() {
    ctx.fillStyle = 'rgb(0, 0, 0)';
    ctx.fillRect(0, 0, FIELD.WIDTH, FIELD.HEIGHT);
  };
  
  
  // ボールの描画
  function drawBall() {
    
    ctx.save();
    
    // 生存しているボールの数だけ描画
    for(var i = 0; i < BALL.length; i++) {
      
      if (BALL[i] == null) {
        continue;
      }
      
      if (BALL[i].Alive) {
        // 円の描画設定
        ctx.beginPath();
        ctx.arc(BALL[i].X, BALL[i].Y, BALL[i].RADIUS, 0, 2*Math.PI, true);
        ctx.closePath();
        
        // 色設定
        BALL[i].HUE += 0.5;
        ctx.strokeStyle = 'hsl(' + BALL[i].HUE + ', 50%, 50%)';
        ctx.fillStyle = 'hsl(' + BALL[i].HUE + ', 50%, 50%)';
        ctx.shadowColor = 'hsl(' + BALL[i].HUE + ', 50%, 50%)';
      }
      
      // 描画実行
      ctx.stroke();
      ctx.fill();
    
    }
    
    ctx.restore();
  };
  
  // バーの描画
  function drawBar() {
    var delay = 1;
    
    BAR.X = (mouseX + delay * BAR.X) / (delay+1);
    
    // 色設定
    ctx.fillStyle = 'rgb(255,255,255)';
    
    // バーの描画設定
    ctx.fillRect(BAR.X, BAR.Y, BAR.WIDTH, BAR.HEIGHT);
    
  };
  
  // ブロックの描画
  function drawBlock() {
    // 色設定
    ctx.fillStyle = 'rgb(0,255,0)';
    
    for (var i = 0; i < BLOCK.length; i++) {
      if (BLOCK[i] == null) {
        continue;
      }
      
      if (BLOCK[i].Alive) {
        ctx.fillRect(BLOCK[i].X, BLOCK[i].Y, BLOCK[i].WIDTH, BLOCK[i].HEIGHT);
      }
    }
  }
  
  // 初期化イベント
  window.addEventListener('load', initialize, false);
  
} ) ();
</script>
</head>
<body>
<canvas id='canvas' width=500 height=300></canvas>
<p>
左クリック: バーを下げる <br />
右クリック: ボール出現
</p>
</body>
</html>



ボールとブロックの当たり判定がイマイチなので変な動きが多いです。
他にも色々ツッコミどころはありますが、改善点含めて次回でまとめて完成としたいです。

関連ページ

canvasで少しずつ作るブロック崩し(1/5) - Segmentation Fault
canvasで少しずつ作るブロック崩し(2/5) - Segmentation Fault
canvasで少しずつ作るブロック崩し(3/5) - Segmentation Fault
canvasで少しずつ作るブロック崩し(5/5) - Segmentation Fault