⇒ 動作確認はこちらからどうぞ。
Sキーでゲームスタート、← →キーで移動、スペースキーでミサイル発射です。
レーダースコープをJavaScript/TypeScriptで作成してきましたが、今回は完成させます。
Contents
Explosionクラス
まず爆発の描画をするExplosionクラスを作成します。
爆発の初期化
プロパティとコンストラクタを示します。マテリアルは6種類、生成後の時間経過とともに使用するものを変えます。GetMaterial関数はこれまでに作成したクラスと同じものです(だったら継承すれば?とも思うが・・・)。
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 |
class Explosion { // 生成したオブジェクトはすべてこのなかにいれてまとめて処理する static Explosions: Explosion[] = []; // マテリアルは6種類 static ExplosionMaterial0: THREE.SpriteMaterial = null; static ExplosionMaterial1: THREE.SpriteMaterial = null; static ExplosionMaterial2: THREE.SpriteMaterial = null; static ExplosionMaterial3: THREE.SpriteMaterial = null; static ExplosionMaterial4: THREE.SpriteMaterial = null; static ExplosionMaterial5: THREE.SpriteMaterial = null; Radius = 1.0; Sprite: THREE.Sprite = null; // 移動速度 VecX = 0; VecZ = 0; MoveCount = 0; constructor(x, z, vecX, vecZ) { this.VecX = vecX; this.VecZ = vecZ; if (Explosion.ExplosionMaterial0 == null) Explosion.ExplosionMaterial0 = this.GetMaterial(DataURLs.dataURL_Explosion0); if (Explosion.ExplosionMaterial1 == null) Explosion.ExplosionMaterial1 = this.GetMaterial(DataURLs.dataURL_Explosion1); if (Explosion.ExplosionMaterial2 == null) Explosion.ExplosionMaterial2 = this.GetMaterial(DataURLs.dataURL_Explosion2); if (Explosion.ExplosionMaterial3 == null) Explosion.ExplosionMaterial3 = this.GetMaterial(DataURLs.dataURL_Explosion3); if (Explosion.ExplosionMaterial4 == null) Explosion.ExplosionMaterial4 = this.GetMaterial(DataURLs.dataURL_Explosion4); if (Explosion.ExplosionMaterial5 == null) Explosion.ExplosionMaterial5 = this.GetMaterial(DataURLs.dataURL_Explosion5); this.Sprite = new THREE.Sprite(Explosion.ExplosionMaterial0); this.Sprite.position.x = x; this.Sprite.position.z = z; } GetMaterial(dataURL: string): THREE.SpriteMaterial { let img = new Image(); img.src = dataURL; var texture = new THREE.Texture(img); texture.needsUpdate = true; return new THREE.SpriteMaterial({ map: texture }); } } |
爆発の移動
移動に関するプロパティと関数を示します。MoveCountが24になったら消滅させます。そのため爆発が表示されるのは0.5秒未満です。
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 |
class Explosion { get X() { return this.Sprite.position.x; } set X(value: number) { this.Sprite.position.x = value; } get Z() { return this.Sprite.position.z; } set Z(value: number) { this.Sprite.position.z = value; } Move() { this.MoveCount++; this.X += this.VecX; this.Z += this.VecZ; if (this.MoveCount >= 24) this.Dead(); let i = this.MoveCount % 60; if (i < 4) this.Sprite.material = Explosion.ExplosionMaterial0; else if (i < 8) this.Sprite.material = Explosion.ExplosionMaterial1; else if (i < 12) this.Sprite.material = Explosion.ExplosionMaterial2; else if (i < 16) this.Sprite.material = Explosion.ExplosionMaterial3; else if (i < 20) this.Sprite.material = Explosion.ExplosionMaterial4; else if (i < 24) this.Sprite.material = Explosion.ExplosionMaterial5; } static MoveAll() { Explosion.Explosions.forEach(explosion => explosion.Move()); } } |
爆発の追加と削除
爆発をシーンと配列に追加する関数を示します。
1 2 3 4 5 6 |
class Explosion { static Add(explosion: Explosion) { scene.add(explosion.Sprite); Explosion.Explosions.push(explosion); } } |
爆発をシーンと配列から取り除く関数を示します。
1 2 3 4 5 6 |
class Explosion { Dead() { scene.remove(this.Sprite); Explosion.Explosions = Explosion.Explosions.filter(explosion => explosion != this); } } |
爆発を発生させる関数を示します。爆発の火花?に初速を与えて中心から広がるように移動させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Explosion { static CreateExplosion(x, z) { // 4方向へ広がるように let angle1 = Math.cos(Math.PI / 4); Explosion.Add(new Explosion(x, z, 0.1 * Math.cos(angle1), 0.1 * Math.sin(angle1))); let angle2 = Math.cos(Math.PI / 4 * 3); Explosion.Add(new Explosion(x, z, 0.1 * Math.cos(angle2), 0.1 * Math.sin(angle2))); let angle3 = Math.cos(Math.PI / 4 * 5); Explosion.Add(new Explosion(x, z, 0.1 * Math.cos(angle3), 0.1 * Math.sin(angle3))); let angle4 = Math.cos(Math.PI / 4 * 7); Explosion.Add(new Explosion(x, z, 0.1 * Math.cos(angle4), 0.1 * Math.sin(angle4))); // 上の処理だけでは単調な爆発描画しかできないので乱数で広がる量を決める for (let i = 0; i < 20; i++) { let angle = Math.random() * Math.PI * 2; Explosion.Add(new Explosion(x, z, 0.1 * Math.cos(angle), 0.1 * Math.sin(angle))); } } } |
その他の関数
次にクラスには所属しない関数を作成します。
フィールドを生成する
まずフィールドを生成する関数を示します。レーダースコープの縦横の直線を描画する処理をおこないます。
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 |
// フィールドを生成する function CreateField() { for (let i = -20; i <= 20; i += 4) AddLineEW(i); for (let i = -20; i <= 20; i += 5) AddLineNS(i); } function AddLineEW(z: number) { const line_material = new THREE.LineBasicMaterial({ color: 0x008000 }); // geometryの宣言と生成 @ts-ignoreが必要 //@ts-ignore var line_geometry = new THREE.Geometry(); //頂点座標の追加 line_geometry.vertices.push( new THREE.Vector3(-50, 0, z), new THREE.Vector3(50, 0, z), ); //線オブジェクトの生成 let line = new THREE.Line(line_geometry, line_material); //sceneにlineを追加 scene.add(line); } function AddLineNS(x: number) { const line_material = new THREE.LineBasicMaterial({ color: 0x008000 }); // geometryの宣言と生成 @ts-ignoreが必要 //@ts-ignore var line_geometry = new THREE.Geometry(); //頂点座標の追加 line_geometry.vertices.push( new THREE.Vector3(x, 0, -50), new THREE.Vector3(x, 0, 50), ); //線オブジェクトの生成 let line = new THREE.Line(line_geometry, line_material); //sceneにlineを追加 scene.add(line); } |
自機の移動と弾丸の発射
次に自機を移動させたり弾丸を発射する関数を示します。
移動は自機が画面の端よりも向こう側に移動して消えてしまわないように、移動の制限を加えます。弾丸発射の処理はJikiBurretオブジェクトを生成して、JikiBurret.Add関数でシーンに追加します。また発射時に音を鳴らし、連射はできないようにします。
1 2 3 4 5 6 7 8 |
function MoveJiki() { // 自機が画面の端まで移動できないように制限を加える // jiki left rightはグローバル変数(後述) if (left && jiki.X > -15) jiki.X -= 0.2; if (right && jiki.X < 15) jiki.X += 0.2; } |
1 2 3 4 5 6 7 8 9 10 11 12 |
// 自機から弾丸発射 function Shot() { // 自機死亡時には当然のことながら発射できない if (jiki.IsDead) return; if (JikiBurret.JikiBurrets.length < 1) { JikiBurret.Add(new JikiBurret(jiki.X, jiki.Z)); soundShot.currentTime = 0; // soundShotはグローバル変数(後述) soundShot.play(); } } |
当たり判定
当たり判定の処理をおこなう関数を作成します。
最初に自機からの弾丸が敵に命中したかどうかの判定をする関数を示します。
まずEnemy.Enemiesのなかから弾丸に命中した敵を取得します。敵を爆発させて敵の状態(待機中、攻撃中、退却中)に応じて点数を加算します。EnemyStatusをDeadにすると次にEnemy.Move関数が実行されたときにシーンから取り除かれます。処理をしたら最後に敵が全滅しているかどうかを調べます。
機雷に対しても同様の処理をおこないます。
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 |
function CheckJikiBurretsHit() { // 弾丸に命中した敵を取得 let hitedEnemies: Enemy[] = Enemy.Enemies.filter(x => x.IsHited(JikiBurret.JikiBurrets)); hitedEnemies.forEach(enemy => { // 敵に命中したら加算 scoreはグローバル変数(後述) if (enemy.EnemyStatus == EnemyStatus.Standby) score += 30; else if (enemy.EnemyStatus == EnemyStatus.Attack) score += 50; else if (enemy.EnemyStatus == EnemyStatus.Retreat) score += 30; // 音を鳴らす soundHitはグローバル変数(後述) soundHit.currentTime = 0; soundHit.play(); // 敵を死んだことにする enemy.EnemyStatus = EnemyStatus.Dead; // 爆発させる Explosion.CreateExplosion(enemy.X, enemy.Z); // 敵が全滅したかどうか調べる CheckClear(); }); let mines = Mine.Mines.filter(x => x.IsHited(JikiBurret.JikiBurrets)); mines.forEach(mine => { score += 30; soundHit.currentTime = 0; soundHit.play(); mine.Dead(); Explosion.CreateExplosion(mine.X, mine.Z); }); } |
敵が全滅したかどうかを調べる関数を示します。全滅しているときは3秒後にOnClear関数を実行します。OnClearでは敵が初期化されるとともに機雷によるダメージからも回復します。
1 2 3 4 5 6 7 8 9 10 11 |
function CheckClear() { let enemies = Enemy.Enemies.filter(enemy => enemy.EnemyStatus != EnemyStatus.Dead) if (enemies.length == 0) { setTimeout(OnClear, 3000); } } function OnClear() { damage = 0; // 機雷によるダメージから回復 Enemy.InitEnemies(); } |
自機死亡時の処理
次に自機が敵弾に当たっていないかを判定する処理を示します。各敵弾、機雷との距離と半径の合計を比較します。当たり判定とされた場合はOnJikiDead関数を呼びます。
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 |
function CheckJikiDead() { // すでに死亡しているときは処理をおこなわない if (jiki.IsDead) return; for (let i = 0; i < EnemyBurret.EnemyBurrets.length; i++) { // 各弾丸の距離と半径の合計を比較 let burret = EnemyBurret.EnemyBurrets[i]; let r = Math.pow(jiki.Radius, 2); let dis = Math.pow(jiki.X - burret.X, 2) + Math.pow(jiki.Z - burret.Z, 2); if (dis < r) { burret.Dead(); OnJikiDead(); return; } } for (let i = 0; i < Mine.Mines.length; i++) { // 各機雷の距離と半径の合計を比較 let mine = Mine.Mines[i]; let r = Math.pow(jiki.Radius + mine.Radius, 2); let dis = Math.pow(jiki.X - mine.X, 2) + Math.pow(jiki.Z - mine.Z, 2); if (dis < r) { mine.Dead(); OnJikiDead(); return; } } } |
自機が死亡した場合はjiki.IsDead = trueとすることで自機の描画を中断します。爆発音を鳴らすとともに残機を1減らします。残機が0になったらゲームオーバーです。
ゲームオーバーでない場合は4秒後に自機が復活してゲームが再開されますが、立て続けにやられないように自機死亡時は敵をいったん退却させます。そして自機が復活するまで新たに敵が攻撃をしかけてこないようにします。これに関してはEnemy.MoveAll関数でおこなわれます。
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 |
function OnJikiDead() { if (jiki.IsDead) return; Explosion.CreateExplosion(jiki.X, jiki.Z); // 自機の描画を停止 jiki.IsDead = true; rest--; // restはグローバル変数(後述) // 攻撃中の敵をすべて退却させる let enemies1 = Enemy.Enemies.filter(enemy => enemy.EnemyStatus == EnemyStatus.Attack); enemies1.forEach(enemy => enemy.EnemyStatus = EnemyStatus.Retreat); if (rest > 0) { // 爆発音を鳴らして4秒後にゲーム再開 soundDead.currentTime = 0; soundDead.play(); setTimeout(Resurrection, 4000); } else { // ゲームオーバーのときはやや大きめの爆発音を鳴らす soundGameOver.currentTime = 0; soundGameOver.play(); } // 背景を青く光らせる intervalId = setInterval(ChangeBackColor, 50); } |
自機が爆発するときに背景を光らせる処理をする関数を示します。0.05秒間隔でChangeBackColor関数が呼び出され、背景の色が変わります(色は2種類)。合計で40回呼び出されるとclearInterval関数を呼び出して処理を止めます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function ChangeBackColor() { changeBackColorCount++; if (changeBackColorCount % 2 == 0) renderer.setClearColor(0x00008B); else renderer.setClearColor(0x0000FF); if (changeBackColorCount == 40) { clearInterval(intervalId); changeBackColorCount = 0; renderer.setClearColor(0x00008B); } } |
自機死亡後に自機を復活させる処理をする関数を示します。ここでは自機をふたたび描画されるようにするとともに自機のX座標を0にして、敵弾と機雷をシーンから取り除いてゲームを再開します。またダメージが32になって自機が死亡した場合はダメージをリセットします。
1 2 3 4 5 6 7 8 9 10 11 |
function Resurrection() { if (damage >= 32) damage = 0; jiki.X = 0; Mine.RemoveAll(); EnemyBurret.RemoveAll(); jiki.IsDead = false; } |
スコア表示
スコアを描画するための関数を示します。テキストフィールドに点数と残機を表示させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function ShowScore() { const tf1 = document.getElementById("score"); // テキストフィールドに点数を表示 tf1.innerHTML = "Score " + ('00000' + score).slice(-5); tf1.style.transform = "translate(30px, 10px)"; tf1.style.backgroundColor = "#00008B"; tf1.style.color = "white"; tf1.style.fontSize = "18px"; const tf2 = document.getElementById("rest"); tf2.innerHTML = "残 " + rest.toString(); tf2.style.transform = "translate(170px, 10px)"; tf2.style.backgroundColor = "#00008B"; tf2.style.color = "white"; tf2.style.fontSize = "18px"; } |
ちなみにHTMLはこのようになっています。スコア、残機表示、ダメージメーター、ゲームオーバー時に表示される2種類の文字列を設定できるようになっています。
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 |
<!DOCTYPE html> <html> <head> <title>Three.jsでレーダースコープ Radar Scopeをつくる</title> <meta charset="UTF-8" /> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/110/three.min.js"></script> </head> <body> <canvas id="can"></canvas> <div id="score" style="position: absolute; top: 0; left: 0;"></div> <div id="rest" style="position: absolute; top: 0; left: 0;"></div> <div id="damagemeter" style="position: absolute; top: 0; left: 0;"></div> <div id="gameover" style="position: absolute; top: 0; left: 0;"></div> <div id="retry" style="position: absolute; top: 0; left: 0;"></div> <script src="data-urls.js"></script> <script src="jiki.js"></script> <script src="jiki-burret.js"></script> <script src="enemy.js"></script> <script src="enemy-burret.js"></script> <script src="mine.js"></script> <script src="explosion.js"></script> <script src="function.js"></script> <script src="app.js"></script> </body> </html> |
ダメージメーターの表示
ダメージメーターを表示させるための関数を示します。DAMAGE METERの文字と合計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 26 27 28 29 |
function ShowDamagemeter() { const tf1: HTMLElement = document.getElementById("damagemeter"); let red = "<div style='height:30px; width:10px; background: red;display: inline-block; margin-left:5px;'></div>"; let green = "<div style='height:30px; width:10px; background: green;display: inline-block; margin-left:5px;'></div>"; let str = ""; str = "<div style='text-align: center;font-weight: bold;'>DAMAGE METER</div>"; if (damage >= 32) damage = 32; str += "<div style='text-align: center;'>"; let i = 0; for (i = 0; i < damage; i++) // 赤い矩形から表示 str += red; for (; i < 32; i++) // 残りは緑に str += green; str += "</div>"; tf1.innerHTML = str; let x = (width - 15 * 32) / 2; tf1.style.transform = "translate(" + x.toString() + "px, 420px)"; tf1.style.backgroundColor = "#00008B"; tf1.style.color = "white"; } |
ゲームオーバー表示
ゲームオーバーのときにはゲームオーバー表示をする関数を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
function ShowIfGameOver() { let str1 = "<div style='text-align: center;font-weight: bold;' >GAME OVER</div>"; let str2 = "<div style='text-align: center;font-weight: bold;' >Press S key to Retry</div>"; if (rest > 0) { str1 = ""; str2 = ""; } let tf1: HTMLElement = document.getElementById("gameover"); tf1.innerHTML = str1; tf1.style.transform = "translate(185px, 140px)"; tf1.style.backgroundColor = "#00008B"; tf1.style.color = "white"; tf1.style.fontSize = "32px"; let tf2: HTMLElement = document.getElementById("retry"); tf2.innerHTML = str2; tf2.style.transform = "translate(200px, 190px)"; tf2.style.backgroundColor = "#00008B"; tf2.style.color = "white"; tf2.style.fontSize = "24px"; } |
ゲームの初期化とリトライ
ゲームの初期化とリトライをするための関数を示します。ゲームの初期化は敵の編隊を初期化するだけです。リトライをする処理はupdateCount、score、damageのリセット、残機をゲーム開始時の状態に戻す、敵の弾丸、機雷をクリアする、自機のX座標を0にセットして描画されるようにする・・・などです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function Init() { Enemy.InitEnemies(); } function GameRetry() { updateCount = 0; rest = restMax; score = 0; damage = 0; jiki.X = 0; jiki.IsDead = false; EnemyBurret.RemoveAll(); Mine.RemoveAll(); JikiBurret.RemoveAll(); Init(); } |
キー操作への対応
キーが押されたときの処理をする関数を示します。左右の方向キーがおされたらグローバル変数のleft、rightをtrueにして離されたらfalseにします。スペースキーで弾丸発射、Sキーはリトライです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function OnKeyDown(e: KeyboardEvent) { if (e.keyCode == 37) // Left key left = true; if (e.keyCode == 39) // Right key right = true; if (e.keyCode == 32) // Space key Shot(); if (e.keyCode == 83) // S key GameRetry(); } function OnKeyUp(e: KeyboardEvent) { if (e.keyCode == 37) left = false; if (e.keyCode == 39) right = false; } |
更新処理
データを更新する関数を示します。この関数は1秒間に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 |
function Update() { // 何回呼び出されたかカウントする updateCount++; // 自機と弾丸を移動させる MoveJiki(); JikiBurret.MoveAll(); // 敵と敵弾、機雷を移動させる Enemy.MoveAll(); EnemyBurret.MoveAll(); Mine.MoveAll(); // 爆発を移動させる Explosion.MoveAll(); // 当たり判定 CheckJikiBurretsHit(); CheckJikiDead(); // スコア、残機、ダメージメーターの表示 ShowScore(); ShowDamagemeter(); // ゲームオーバー時にゲームオーバー表示をする ShowIfGameOver(); // レンダリング renderer.render(scene, camera); } |
最後に
上記を組み合わせてゲームを完成させます。
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 |
// グローバル変数 // Three.jsで使うScene、Camera、Renderer let scene: THREE.Scene; let camera: THREE.PerspectiveCamera; let renderer: THREE.WebGLRenderer; let soundShot: HTMLAudioElement = new Audio('./sounds/shot.mp3'); let soundHit: HTMLAudioElement = new Audio('./sounds/hit.mp3'); let soundDead: HTMLAudioElement = new Audio('./sounds/dead.mp3'); let soundGameOver: HTMLAudioElement = new Audio('./sounds/gameover.mp3'); // フィールドの幅と高さ const width = 640; const height = 480; let jiki: Jiki = null; let updateCount = 0; // キーは押されているか let up = false; let down = false; let left = false; let right = false; // スコアと残機 let score = 0; let rest = 0; let restMax = 3; let damage = 0; // ミス時の背景を色を変えるための変数 let intervalId = null; let changeBackColorCount = 0; // 処理はここから document.onkeydown = OnKeyDown; document.onkeyup = OnKeyUp; // シーンを作成 scene = new THREE.Scene(); // カメラを作成 camera = new THREE.PerspectiveCamera(45, width / height, 1, 10000); camera.position.set(0, 36, 37); let cameraAngle = -48 / 180 * Math.PI; camera.rotateX(cameraAngle); // 平行光源 const light = new THREE.DirectionalLight(0xffffff); light.intensity = 2; // 光の強さを倍に light.position.set(1, 1, -1); scene.add(light); // レンダラーを作成 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); renderer.setClearColor(0x00008B); // フィールド等を作成 CreateField(); jiki = new Jiki(); jiki.AddScene(); jiki.IsDead = true; Init(); |
⇒ 動作確認はこちらからどうぞ。