動作確認はこちらからどうぞ。
以前、Unityでブロック崩しをつくりましたが、今回はThree.jsで3Dのブロック崩しをつくります。
ブロックだけクラスにしてそれ以外のクラスはつくりません。
最初にグローバル変数を示します。
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 |
// フィールドの幅と高さ const width = 640; const height = 480; // Three.jsで使うScene、Camera、Renderer let scene: THREE.Scene; let camera: THREE.PerspectiveCamera; let renderer: THREE.WebGLRenderer; // 床 let floor: THREE.Mesh; // 左右と上側の壁 let leftWall: THREE.Mesh; let topWall: THREE.Mesh; let RightWall: THREE.Mesh; // ボールを跳ね返すパドルとパドルの幅 let paddle: THREE.Mesh; let paddleWidth = 8; // ボール。ボールの半径、移動速度、X方向、Z方向の速度 let ball: THREE.Mesh; let ballRadius = 0.5; let ballSpeed = 0.5; let ballVX = ballSpeed * Math.cos(45 * Math.PI / 180); let ballVZ = -ballSpeed * Math.sin(45 * Math.PI / 180); // 方向キーが押されているかどうか let isUpKeyDown = false; let isDownKeyDown = false; let isLeftKeyDown = false; let isRightKeyDown = false; // スコア let score = 0; // ブロックの配列 let Blocks: Block[] = []; |
Blockクラスのメンバー変数とコンストラクタを示します。
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
class Block { // 一番左を0としたとき何列目か? Colum = 0; // 一番上を0としたとき何行目か? Row = 0; // X座標とZ座標 X = 0; Z = 0; // ブロックの奥行きと幅(明示していないが高さは1.0) Depth = 0.9; Width = 1.9; // このブロックを壊したときの点数 Score = 0; block: THREE.Mesh = null; constructor(row: number, colum: number) { // 引数からX座標とZ座標を算出する this.Colum = colum; this.Row = row; this.X = 2 * this.Colum + 1 - 16; this.Z = -16 * 1 + this.Row + 3; // ジオメトリ(形状)とマテリアル(素材)からメッシュをつくる // 色は虹の色と同じ。上段のブロックほど点数が高い let geometry = new THREE.BoxGeometry(this.Width, 1, this.Depth); if (this.Row == 0) { this.block = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ color: 0xff0000 })); this.Score = 80; } if (this.Row == 1) { this.block = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ color: 0xff4500 })); this.Score = 70; } if (this.Row == 2) { this.block = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ color: 0xffff00 })); this.Score = 60; } if (this.Row == 3) { this.block = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ color: 0x00ff00 })); this.Score = 50; } if (this.Row == 4) { this.block = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ color: 0x00ffff })); this.Score = 40; } if (this.Row == 5) { this.block = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ color: 0x0000ff })); this.Score = 30; } if (this.Row == 6) { this.block = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ color: 0xff00ff })); this.Score = 20; } // 算出した座標にブロックを移動する this.block.position.x = this.X; this.block.position.y = 0; this.block.position.z = this.Z; } } |
ブロックが壊されたかどうかを示すプロパティです。falseが設定されたらそのブロックを非表示にします。
1 2 3 4 5 6 7 8 9 10 |
class Block { _isBroken = false; get IsBroken() { return this._isBroken; } set IsBroken(value) { this._isBroken = value; this.block.visible = this._isBroken ? false : true; } } |
ボールがどこに当たったかを調べる処理を示します。上辺、左辺、右辺、下辺、4つの角のどこに当たったかでボールの跳ね返り方を変えなければならないので、単に当たり判定をするだけでなく、どこに当たったかも調べています。
ボールに一定の大きさがあるのでボールの半径も考慮しなければなりません。ブロックとボールの中心の座標とボールの半径からどこに当たったのかを調べています。
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
class Block { IsHitTop(ballX: number, ballZ: number): boolean { let b1 = this.Z - this.Depth / 2 - ballRadius <= ballZ; let b2 = ballZ <= this.Z - this.Depth / 2; let b3 = this.X - this.Width / 2 <= ballX; let b4 = ballX <= this.X + this.Depth / 2; if (b1 && b2 && b3 && b4) return true; return false; } IsHitBottom(ballX: number, ballZ: number): boolean { let b1 = this.Z + this.Depth / 2 + ballRadius >= ballZ; let b2 = ballZ >= this.Z + this.Depth / 2; let b3 = this.X - this.Width / 2 <= ballX; let b4 = ballX <= this.X + this.Depth / 2; if (b1 && b2 && b3 && b4) return true; return false; } IsHitLeft(ballX: number, ballZ: number): boolean { let b1 = this.Z - this.Depth / 2 <= ballZ; let b2 = ballZ <= this.Z + this.Depth / 2; let b3 = ballX >= this.X - this.Width / 2 - ballRadius; let b4 = ballX <= this.X - this.Depth / 2; if (b1 && b2 && b3 && b4) return true; return false; } IsHitRight(ballX: number, ballZ: number): boolean { let b1 = this.Z - this.Depth / 2 <= ballZ; let b2 = ballZ <= this.Z + this.Depth / 2; let b3 = ballX <= this.X + this.Width / 2 + ballRadius; let b4 = ballX >= this.X + this.Depth / 2; if (b1 && b2 && b3 && b4) return true; return false; } IsHitLeftTop(ballX: number, ballZ: number) { let x = this.X - this.Width / 2; let z = this.Z - this.Depth / 2; if (Math.pow(ballX - x, 2) + Math.pow(ballZ - z, 2) <= Math.pow(ballRadius, 2)) return true; return false; } IsHitRightTop(ballX: number, ballZ: number) { let x = this.X + this.Width / 2; let z = this.Z - this.Depth / 2; if (Math.pow(ballX - x, 2) + Math.pow(ballZ - z, 2) <= Math.pow(ballRadius, 2)) return true; return false; } IsHitLeftBottom(ballX: number, ballZ: number) { let x = this.X - this.Width / 2; let z = this.Z + this.Depth / 2; if (Math.pow(ballX - x, 2) + Math.pow(ballZ - z, 2) <= Math.pow(ballRadius, 2)) return true; return false; } IsHitRightBottom(ballX: number, ballZ: number) { let x = this.X + this.Width / 2; let z = this.Z + this.Depth / 2; if (Math.pow(ballX - x, 2) + Math.pow(ballZ - z, 2) <= Math.pow(ballRadius, 2)) return true; return false; } } |
ページが読み込み終わったらInit関数が実行されます。
Init関数ではシーン、カメラを作成し、光源を設定します。そのあとフィールドを構成する壁やボール、ブロックを生成します。そのあとレンダラーを作成してレンダリングします。
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 |
window.addEventListener('load', Init); function Init() { document.onkeydown = OnKeyDown; document.onkeyup = OnKeyUp; // シーンを作成 scene = new THREE.Scene(); // カメラを作成 camera = new THREE.PerspectiveCamera(45, width / height, 1, 10000); camera.position.set(0, 30, 28); let cameraAngle = -50 / 180 * Math.PI; camera.rotateX(cameraAngle); // 平行光源 const light = new THREE.DirectionalLight(0xffffff); light.intensity = 2; // 光の強さを倍に light.position.set(1, 1, -1); scene.add(light); // フィールド等を作成 CreateField(); CreateBall(); CreatePaddle(); CreateBlocks(); // レンダラーを作成 renderer = new THREE.WebGLRenderer({ canvas: <HTMLCanvasElement>document.getElementById('can') }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(width, height); renderer.render(scene, camera); setInterval(Update, 1000 / 60); } |
CreateField関数は床と壁を生成してシーンに追加する処理をおこないます。フィールドの幅と奥行きは32です。
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 |
function CreateField() { let thickness: number = 0.4; let geometry1 = new THREE.BoxGeometry(32, thickness, 32); let material1 = new THREE.MeshStandardMaterial({ color: 0xFFffff }); floor = new THREE.Mesh(geometry1, material1); floor.position.y = -thickness; scene.add(floor); let geometry2 = new THREE.BoxGeometry(thickness, 1, 32); RightWall = new THREE.Mesh(geometry2, material1); RightWall.position.x = 16 + thickness / 2; RightWall.position.y = 0; leftWall = new THREE.Mesh(geometry2, material1); leftWall.position.x = -16 - thickness / 2; leftWall.position.y = 0; scene.add(RightWall); scene.add(leftWall); let geometry3 = new THREE.BoxGeometry(32 + thickness*2, 1, thickness); topWall = new THREE.Mesh(geometry3, material1); topWall.position.z = -16 - thickness / 2; topWall.position.y = 0; scene.add(topWall); } |
CreateBall関数はボールを生成する処理をおこないます。
1 2 3 4 5 6 7 8 9 |
function CreateBall() { const geometry = new THREE.SphereGeometry(ballRadius, 32, 32); const material = new THREE.MeshStandardMaterial({ color: 0xFF0000 }); ball = new THREE.Mesh(geometry, material); ball.position.x = 8; ball.position.z = 0; ball.position.y = 0; scene.add(ball); } |
ResetBall関数はミスをしたあとボールを最初の位置に戻してゲームを再開する準備をおこなう関数です。
1 2 3 4 5 6 7 |
function ResetBall() { ball.position.x = 8; ball.position.z = 0; ball.position.y = 0; ballVX = ballSpeed * Math.cos(45 * Math.PI / 180); ballVZ = -ballSpeed * Math.sin(45 * Math.PI / 180); } |
CreatePaddle関数はボールを跳ね返すパドルを生成してシーンに追加する処理をおこないます。
1 2 3 4 5 6 7 8 9 |
function CreatePaddle() { let geometry = new THREE.BoxGeometry(paddleWidth, 1, 0.5); let material = new THREE.MeshStandardMaterial({ color: 0x0000ff }); paddle = new THREE.Mesh(geometry, material); paddle.position.x = 0; paddle.position.z = 13; paddle.position.y = 0; scene.add(paddle); } |
CreateBlocks関数はブロックオブジェクトを生成してメッシュをシーンに追加する処理をおこないます。またオブジェクトはBlockの配列にも追加しておきます。
1 2 3 4 5 6 7 8 9 |
function CreateBlocks() { for (let row = 0; row < 7; row++) { for (let colum = 0; colum < 16; colum++) { let block = new Block(row, colum); scene.add(block.block); // Blockオブジェクトのなかのメッシュをシーンに追加 Blocks.push(block); // Blockオブジェクトを配列に追加 } } } |
ResetBlocks関数は壊されたブロックをすべて元の状態に戻します。
1 2 3 4 5 |
function ResetBlocks() { Blocks.forEach(block => { block.IsBroken = false; }); } |
アニメーションさせるためにUpdate関数を呼びます。ここでやることはボールを移動する、方向キーが押されている場合はパドルをその方向に移動させる、ミスの判定です。
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 Update() { if (ball.visible && ball.position.z >= 16 - ballRadius) CheckMiss(); // 方向キーが押されているならパドルを移動させる MovePaddle(); // ボールはパドルに当たったか? 当たっているなら進行方向を変更する OnBallHitWall(); // ボールはパドルに当たったか? CheckBallHitPaddle(); // ボールはブロックに当たったか? CheckBallHitBlock(); // ボールの座標を変更する ball.position.x += ballVX; ball.position.z += ballVZ; // スコアの表示 ShowScore(); // レンダリング renderer.render(scene, camera); } |
ボールがフィールドの手前から外へ出たらミスとなります。この場合はボールをいったん止めて見えない状態にします。3秒後にボールの位置を初期位置に戻してゲームを再開します。
1 2 3 4 5 6 7 8 9 10 |
function CheckMiss() { ballVX = 0; ballVZ = 0; ball.visible = false; setTimeout(() => { ball.visible = true; ResetBall(); }, 3000); } |
MovePaddle関数は方向キーが押されているならパドルを移動させる関数です。壁に当たっている場合はそれ以上移動することはできません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
function MovePaddle() { if (isLeftKeyDown) { paddle.translateX(-0.5); if (paddle.position.x < -16 + paddleWidth / 2) paddle.position.x = -16 + paddleWidth / 2; } if (isRightKeyDown) { paddle.translateX(0.5); if (paddle.position.x > 16 - paddleWidth / 2) paddle.position.x = 16 - paddleWidth / 2; } if (isUpKeyDown) { paddle.translateZ(-0.5); if (paddle.position.z < 4) paddle.position.z = 4; } if (isDownKeyDown) { paddle.translateZ(0.5); if (paddle.position.z > 15) paddle.position.z = 15; } } |
OnBallHitWall関数はボールが壁に当たった場合はボールの進行方向を変える関数です。符号を反転するだけでは壁のなかで振動して動けなくなることがあるので絶対値に符号をつけて壁のなかで複数回方向転換してボールが動けなくならないようにしています。
1 2 3 4 5 6 7 8 |
function OnBallHitWall() { if (ball.position.x >= 16 - ballRadius) ballVX = -Math.abs(ballVX); if (ball.position.x <= -16 + ballRadius) ballVX = Math.abs(ballVX); if (ball.position.z <= -16 + ballRadius) ballVZ = Math.abs(ballVZ); } |
CheckBallHitPaddle関数はパドルにボールが当たったときにボールの進行方向を変える関数です。端に当たると反発して横に大きく移動するようにしています。反射の角度は左右それぞれ30度、45度、60度です。
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 |
function CheckBallHitPaddle() { let b1: boolean = paddle.position.z - 0.25 <= ball.position.z + ballRadius; let b2: boolean = ball.position.z - ballRadius <= paddle.position.z + 0.25; let b3: boolean = paddle.position.x - paddleWidth / 2 <= ball.position.x; let b4: boolean = ball.position.x <= paddle.position.x + paddleWidth / 2; if (b1 && b2 && b3 && b4) { ballVZ = -Math.abs(ballVZ); let i: number = paddle.position.x - ball.position.x; if (ballVX < 0) ballVX = ballSpeed * Math.cos(120 * Math.PI / 180); else ballVX = ballSpeed * Math.cos(60 * Math.PI / 180); if (i > 0) { if (i > paddleWidth / 3) ballVX = ballSpeed * Math.cos(150 * Math.PI / 180); else if (i > paddleWidth / 6) { if (ballVX > 0) ballVX = ballSpeed * Math.cos(45 * Math.PI / 180); else ballVX = ballSpeed * Math.cos(135 * Math.PI / 180); } } else { if (i < -paddleWidth / 3) ballVX = ballSpeed * Math.cos(30 * Math.PI / 180); else if (i < -paddleWidth / 6) { if (ballVX > 0) ballVX = ballSpeed * Math.cos(45 * Math.PI / 180); else ballVX = ballSpeed * Math.cos(135 * Math.PI / 180); } } // パドルでボールを跳ね返したら10点追加 score += 10; } } |
CheckBallHitBlock関数はボールがブロックに当たったときに、ブロックを消すとともにボールの進行方向を変える関数です。ブロックの上面に当たったときはZ方向を負数に、下面に当たったときはZ方向を正数に、左面に当たったときはX方向を負数に、右面に当たったときはX方向を正数に変えています。
上下左右の面に当たっていない場合は角に当たっている可能性を疑わないといけません。角に当たったときはそのときのボールの進行方向で挙動を変えています。
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
function CheckBallHitBlock() { let blocksLen = Blocks.length; for (let i = 0; i < blocksLen; i++) { if (Blocks[i].IsBroken) continue; if (Blocks[i].IsHitTop(ball.position.x, ball.position.z)) { ballVZ = -Math.abs(ballVZ); Blocks[i].IsBroken = true; OnBlockBreak(Blocks[i]); break; } if (Blocks[i].IsHitBottom(ball.position.x, ball.position.z)) { ballVZ = Math.abs(ballVZ); Blocks[i].IsBroken = true; OnBlockBreak(Blocks[i]); break; } if (Blocks[i].IsHitLeft(ball.position.x, ball.position.z)) { ballVX = -Math.abs(ballVX); Blocks[i].IsBroken = true; OnBlockBreak(Blocks[i]); break; } if (Blocks[i].IsHitRight(ball.position.x, ball.position.z)) { ballVX = Math.abs(ballVX); Blocks[i].IsBroken = true; OnBlockBreak(Blocks[i]); break; } let hitLeftTop = Blocks[i].IsHitLeftTop(ball.position.x, ball.position.z); let hitRightTop = Blocks[i].IsHitRightTop(ball.position.x, ball.position.z); let hitLeftBottom = Blocks[i].IsHitLeftBottom(ball.position.x, ball.position.z); let hitRightBottom = Blocks[i].IsHitRightBottom(ball.position.x, ball.position.z); if (hitLeftTop) { if (ballVX > 0) { if (ballVZ > 0) { ballVX = -Math.abs(ballVX); ballVZ = -Math.abs(ballVZ); } else { ballVX = -Math.abs(ballVX); } } else { if (ballVZ > 0) { ballVZ = -Math.abs(ballVZ); } } Blocks[i].IsBroken = true; OnBlockBreak(Blocks[i]); break; } if (hitRightTop) { if (ballVX > 0) { if (ballVZ > 0) { ballVZ = -Math.abs(ballVZ); } } else { if (ballVZ > 0) { ballVX = Math.abs(ballVX); ballVZ = -Math.abs(ballVZ); } else { ballVX = Math.abs(ballVX); } } Blocks[i].IsBroken = true; OnBlockBreak(Blocks[i]); break; } if (hitLeftBottom) { if (ballVX > 0) { if (ballVZ > 0) { ballVX = -Math.abs(ballVX); } else { ballVX = -Math.abs(ballVX); ballVZ = Math.abs(ballVZ); } } else { if (ballVZ < 0) { ballVZ = Math.abs(ballVZ); } } Blocks[i].IsBroken = true; OnBlockBreak(Blocks[i]); break; } if (hitRightBottom) { if (ballVX > 0) { if (ballVZ < 0) { ballVZ = Math.abs(ballVZ); } } else { if (ballVZ > 0) { ballVX = Math.abs(ballVX); } else { ballVX = Math.abs(ballVX); ballVZ = Math.abs(ballVZ); } } Blocks[i].IsBroken = true; OnBlockBreak(Blocks[i]); break; } } } |
ブロックが壊れたときは点数を追加する処理をおこないます。
1 2 3 |
function OnBlockBreak(block: Block) { score += block.Score; } |
スコアを表示する処理を示します。HTMLは以下のようになっています。id=”score”の部分にスコアを表示させます。
1 2 3 |
<canvas id="can"></canvas> <div id="score" style="position: absolute; top: 0; left: 0; background: white"></div> <script src="app.js"></script> |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function ShowScore() { const tf = document.getElementById("score"); // テキストフィールドに点数を表示 let ret = ('00000' + score).slice(-5); ret = "Score " + ret; tf.innerHTML = ret; tf.style.transform = "translate(30px, 30px)"; tf.style.backgroundColor = "black"; tf.style.color = "white"; tf.style.fontSize = "18px"; } |
方向キーが押されたときと離されたときの処理を示します。キーが押されているフラグを変更しているだけです。フラグの状態でパドルが適切な方向に移動します。
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 |
function OnKeyDown(e: KeyboardEvent) { if (e.keyCode == 37) // 左キー isLeftKeyDown = true; if (e.keyCode == 38) // 上キー isUpKeyDown = true; if (e.keyCode == 39) // 右キー isRightKeyDown = true; if (e.keyCode == 40) // 下キー isDownKeyDown = true; } function OnKeyUp(e: KeyboardEvent) { if (e.keyCode == 37) { isLeftKeyDown = false; } if (e.keyCode == 39) { isRightKeyDown = false; } if (e.keyCode == 38) { isUpKeyDown = false; } if (e.keyCode == 40) { isDownKeyDown = false; } } |
動作確認はこちらからどうぞ。