こんな感じの鳥が羽ばたく壁避けゲームをつくります。
Contents
無理ゲーにならないようにする
このゲームは適当に壁に穴を開けていてはそのときのプレイヤーの位置によっては絶対に通り抜けることができない無理ゲーになってしまいます。そこでタイミングよく羽ばたくことができれば必ずクリアできるように壁の穴の位置を設定しなければなりません。
鳥の軌跡の描画
まずは羽ばたいた鳥の軌跡を描画してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>テスト</title> <meta name = "viewport" content = "width=device-width, initial-scale = 1.0"> </head> <body> <canvas id = "canvas"></canvas> <script src= "./test1.js"></script> </body> </html> |
test1.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
const CANVAS_WIDTH = 1000; const CANVAS_HEIGHT = 480; const AXIS_X = 80; const G_ACCELERATION = 0.02; // canvas要素 const $canvas = document.getElementById('canvas'); const ctx = $canvas.getContext('2d'); window.onload = () => { $canvas.width = CANVAS_WIDTH; $canvas.height = CANVAS_HEIGHT; clearCanvas(); draw(); } function clearCanvas(){ ctx.fillStyle = '#000'; ctx.fillRect(0, 0, $canvas.width, $canvas.height); } function draw(){ for(let i=0; i<$canvas.width; i++){ const x = i; const y = G_ACCELERATION / 2 * Math.pow(x - AXIS_X, 2) + 100; ctx.fillStyle = '#ff0'; ctx.fillRect(x, y, 2, 2); } ctx.font="14px Arial"; const arr = [0, AXIS_X, AXIS_X * 2]; for(let i=0; i<arr.length; i++){ const x = arr[i]; const y = G_ACCELERATION / 2 * Math.pow(x - AXIS_X, 2) + 100; ctx.fillStyle = '#f00'; ctx.fillRect(x-3, y-3, 6, 6); ctx.fillStyle = '#fff'; ctx.fillText(`${x}, ${y}, `, x + 10, y); } } |
鳥の起動の変更
羽ばたいたときはその座標を起点に放物線を描画します。放物線の開始点の座標が(0, 164)なので羽ばたいた地点の座標と比較してそのぶん全体を平行移動させればいいですね。
getPositionY関数を以下のように定義して、draw関数も書き直します。
1 2 3 4 5 6 7 |
function getPositionY(x, jumpX, jumpY){ if(jumpX == undefined) return G_ACCELERATION / 2 * Math.pow(x - AXIS_X, 2) + 100; else return G_ACCELERATION / 2 * Math.pow(x - AXIS_X - jumpX, 2) + 100 - (164 - jumpY); // 164 は放物線の開始点の座標が(0, 164)なので } |
以下はX座標が200の時に羽ばたいた場合の鳥の軌跡です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
function draw(){ ctx.font="14px Arial"; const nextStartX = 200; const nextStartY = getPositionY(nextStartX); ctx.fillStyle = '#f00'; ctx.fillRect(nextStartX-3, nextStartY-3, 6, 6); ctx.fillStyle = '#fff'; ctx.fillText(`${nextStartX}, ${nextStartY}, `, nextStartX + 10, nextStartY); for(let i=0; i<nextStartX; i++){ const x = i; const y = getPositionY(x); ctx.fillStyle = '#ff0'; ctx.fillRect(x, y, 2, 2); } for(let i = nextStartX; i<$canvas.width; i++){ const x = i; const y = getPositionY(x, nextStartX, nextStartY); ctx.fillStyle = '#0f0'; ctx.fillRect(x, y, 2, 2); } } |
ガイドをつくる
あとはランダムに羽ばたかせてこのような放物線を連続して描画して、その頂点部分に壁の穴を作ればいいですね。つまり先にお題を作ってしまうというわけです。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const guidePoints = []; const topPoints = []; window.onload = () => { $canvas.width = CANVAS_WIDTH; $canvas.height = CANVAS_HEIGHT; clearCanvas(); // 以下2つを新しく定義 createGuide() drawGuide() } |
createGuide関数は、複数の連続する放物線でできた点の座標と放物線の頂点の座標を配列に格納します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
function createGuide(){ let jumpX = undefined; let jumpY = undefined; for(let k=0; k<100; k++){ const nextStartX = jumpX == undefined ? 0 : jumpX; let lastX = 0; let lastY = 1000; for(let i = nextStartX; i < 10000; i++){ const x = i; const y = getPositionY(x, jumpX, jumpY); const point = {x: x, y:y}; guidePoints.push(point); if(lastY < y - 0.8 && y > 120 && (Math.random() > 0.98 || y > 400)){ lastX = x; lastY = y; break; } lastX = x; lastY = y; } jumpX = lastX; jumpY = lastY; const point = {x: jumpX + AXIS_X, y:jumpY - 64}; topPoints.push(point); } } |
drawGuide関数は、配列に格納された点を描画します。
1 2 3 4 5 6 7 8 9 10 11 12 |
function drawGuide(){ // 放物線を緑の点で描画する for(let i=0; i<guidePoints.length; i++){ ctx.fillStyle = '#0f0'; ctx.fillRect(guidePoints[i].x, guidePoints[i].y, 2, 2); } // 放物線の頂点に赤い点を描画する for(let i=0; i<topPoints.length; i++){ ctx.fillStyle = '#f00'; ctx.fillRect(topPoints[i].x -2 , topPoints[i].y -2, 4, 4); } } |
プレイヤーと壁をつくる
プレイヤーに関する定数と変数を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// プレイヤーのサイズ const PLAYER_WIDTH = 80; const PLAYER_HEIGHT = 48; // プレイヤーの初期座標 const INIT_PLAYER_X = 100; // プレイヤーの座標 let playerX = INIT_PLAYER_X; let playerY = 0; // プレイヤーが羽ばたいた地点の座標 let jumpX = undefined; let jumpY = undefined; |
羽ばたいたときの処理
羽ばたいたらその地点の座標を格納します。テストなのでキーをなにか押したら羽ばたいたことにします。
1 2 3 4 |
document.onkeydown = () => { jumpX = playerX; jumpY = playerY; } |
壁の生成
壁を生成する処理を示します。topPointsから座標を取得して通過時の自然落下分とさらにプラスアルファで壁に隙間を作ります。これらの値からWallオブジェクトを生成して配列に格納します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
class Wall{ constructor(x, min, max){ this.X = x; this.Min = min; this.Max = max; } } const walls = []; function createWalls(){ for(let i = 0; i < topPoints.length; i++){ let margin = 50 - i * 5; if(margin < 5) margin = 5; const min = topPoints[i].y - PLAYER_HEIGHT / 2 - margin; const max = min + PLAYER_HEIGHT + 30 + margin * 2; // 通過時の自然落下があるので30がない通れない walls.push(new Wall(topPoints[i].x, min, max)); } } <h3>壁の描画</h3> 配列に格納したオブジェクトをつかって壁を描画します。 function drawWalls(){ for(let i = 0; i < walls.length; i++){ // 縦に赤い壁を描画する ctx.fillStyle = '#f00'; ctx.fillRect(walls[i].X , 0, 4, $canvas.height); // 穴があいているように見えるように穴の部分は背景色と同じ色で塗りつぶす ctx.fillStyle = '#000'; ctx.fillRect(walls[i].X , walls[i].Min, 4, walls[i].Max - walls[i].Min); } } |
プレイヤーの描画
プレイヤーを描画する関数を定義します。ここでは(playerX, playerY)を中心とする矩形を描画します。
1 2 3 4 |
function drawPlayer(){ ctx.strokeStyle = '#fff'; ctx.strokeRect(playerX- PLAYER_WIDTH/2, playerY- PLAYER_HEIGHT/2, PLAYER_WIDTH, PLAYER_HEIGHT); } |
ガイドの描画
ゲームをするときは自機の左上ではなく右上をガイドに合わせるほうがやりやすいのでdrawGuide関数を定義しなおします。
1 2 3 4 5 6 7 8 |
function drawGuide(){ for(let i = 0; i < guidePoints.length; i++){ ctx.fillStyle = '#0f0'; // ctx.fillRect(guidePoints[i].x, guidePoints[i].y, 2, 2); // 全体を並行移動させる ctx.fillRect(guidePoints[i].x + PLAYER_WIDTH / 2, guidePoints[i].y - PLAYER_HEIGHT / 2, 2, 2); } } |
更新処理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
window.onload = () => { $canvas.width = CANVAS_WIDTH; $canvas.height = CANVAS_HEIGHT; clearCanvas(); createGuide() createWalls() } setInterval(() => { update(); }, 33); function update(){ clearCanvas(); drawGuide(); drawWalls(); playerX++; playerY = getPositionY(playerX, jumpX, jumpY) drawPlayer(); } |
当たり判定
壁に当たっていないかを調べ、当たっている場合はその部分に×を描画します。
1 2 3 4 5 6 7 8 9 |
function update(){ clearCanvas(); drawGuide(); drawWalls(); playerX++; playerY = getPositionY(playerX, jumpX, jumpY) drawPlayer(); check(); // 追加 } |
check関数は当たり判定の対象になる壁があるか調べ、当たり判定の対象になる壁がある場合、playerYとその壁のMinとMaxの値からプレイヤーが壁に接触していないかを調べます。
1 2 3 4 5 6 7 8 9 |
function check(){ const checkWalls = walls.filter(wall => wall.X < playerX + PLAYER_WIDTH / 2 && wall.X > playerX - PLAYER_WIDTH / 2); if(checkWalls.length > 0){ if(checkWalls[0].Min > playerY - PLAYER_HEIGHT / 2) onDead(checkWalls[0].X, playerY - PLAYER_HEIGHT / 2); if(checkWalls[0].Max < playerY + PLAYER_HEIGHT / 2) onDead(checkWalls[0].X, playerY + PLAYER_HEIGHT / 2); } } |
プレイヤーが壁に接触していた場合、その部分に×を描画します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function onDead(deadX, deadY){ if(deadX != undefined){ ctx.lineWidth = 4; ctx.strokeStyle = '#f00'; ctx.beginPath(); ctx.moveTo(deadX - 16, deadY - 16); ctx.lineTo(deadX + 16, deadY + 16); ctx.stroke(); ctx.beginPath(); ctx.moveTo(deadX + 16, deadY - 16); ctx.lineTo(deadX - 16, deadY + 16); ctx.stroke(); } ctx.lineWidth = 1; } |
とりあえずゲームをつくる準備はできたので、次回はゲームとして完成させます。