⇒ 動作確認はこちらからどうぞ。
Sキーでゲームスタート、← →キーで移動、スペースキーでミサイル発射です。
前回は自機を生成しましたが、今回は敵を生成します。
Contents
EnemyStatus列挙体
敵の状態は待機中、攻撃中、退却中、死亡の4つのどれかにあります。EnemyStatus列挙体で敵の状態を管理します。
1 2 3 4 5 6 |
enum EnemyStatus { Standby, Attack, Retreat, Dead, } |
Enemyクラス
プロパティを示します。敵は縦に5列、横に9列で配置されます。待機中の敵のうち前2列を前後に移動させるので、それが何列目なのかわかるようにしています。攻撃に参加してそのまま上空に退却した敵は編隊の最後尾に再配置されます。これによって何列目に配置されているかが変更されます。
敵が弾丸を発射するタイミング、機雷を投下するタイミングは攻撃に参加してからの時間で決まるのでMove関数を実行した回数を記憶させています。弾丸のうち3回目に発射するものは斜めに発射するため、何回弾丸を発射したかも記憶させています。
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 |
class Enemy { // 生成したオブジェクトはすべてこのなかにいれてまとめて処理する static Enemies: Enemy[] = []; // 敵は5行9列 static RowMax = 5; static ColumMax = 9; // マテリアルは4種類 static EnemyMaterial10: THREE.SpriteMaterial = null; static EnemyMaterial11: THREE.SpriteMaterial = null; static EnemyMaterial12: THREE.SpriteMaterial = null; static EnemyMaterial13: THREE.SpriteMaterial = null; // そのときに使用するスプライト Sprite: THREE.Sprite = null; // 敵の大きさ(半径) Radius: number = 1.0; // 敵の状態(待機中、攻撃中、退却中、死亡) EnemyStatus: EnemyStatus = EnemyStatus.Standby; // 何列目に配置されているか? Line: number = 0; // 最前列は何列目か? static FrontLine = 0; static NewPositionX = new Array(4, 3, 5, 2, 6, 1, 7, 0, 8); // 攻撃時は右方向か逆か? IsAttackRight: boolean = true; // 移動処理をした階数 MoveCount: number = 0; MoveDownCount: number = 0; MoveUpCount: number = 0; // 弾丸を何回発射したか? ShotCount: number = 0; } |
敵の初期化
コンストラクタを示します。マテリアルは一度取得すれば使い回すことができるので静的変数にしています。あとはJikiクラスとほとんど同じです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class Enemy { constructor(x: number, z: number) { if (Enemy.EnemyMaterial10 == null) Enemy.EnemyMaterial10 = this.GetMaterial(DataURLs.dataURL_Enemy10); if (Enemy.EnemyMaterial11 == null) Enemy.EnemyMaterial11 = this.GetMaterial(DataURLs.dataURL_Enemy11); if (Enemy.EnemyMaterial12 == null) Enemy.EnemyMaterial12 = this.GetMaterial(DataURLs.dataURL_Enemy12); if (Enemy.EnemyMaterial13 == null) Enemy.EnemyMaterial13 = this.GetMaterial(DataURLs.dataURL_Enemy13); this.Sprite = new THREE.Sprite(Enemy.EnemyMaterial10); this.Sprite.position.x = x; this.Sprite.position.y = 0; this.Sprite.position.z = z; this.Sprite.scale.set(this.Radius * 2, this.Radius * 2, this.Radius * 2); } } |
マテリアルを取得する関数を示します。Jikiクラスと同じです。
1 2 3 4 5 6 7 8 9 |
class Enemy { 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 }); } } |
敵全体を初期化する関数を示します。現在存在する敵をシーンからすべて取り除き、配列も空にします。新たにEnemy.RowMax行、Enemy.ColumMax列の敵を生成して初期座標を与え、シーンに追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class Enemy { static InitEnemies() { Enemy.RemoveAll(); Enemy.FrontLine = 0; updateCount = 0; for (let row = 0; row < Enemy.RowMax; row++) { for (let colum = 0; colum < Enemy.ColumMax; colum++) { // 敵に初期座標を与え、シーンに追加する let enemy = new Enemy(-12 + colum * 3, -16 - 3 * row); enemy.Line = row; Enemy.Enemies.push(enemy); scene.add(enemy.Sprite); } } } // 現在存在する敵をシーンからすべて取り除き、配列を空にする static RemoveAll() { Enemy.Enemies.forEach(enemy => scene.remove(enemy.Sprite)); Enemy.Enemies = []; } } |
敵の移動
移動に関するプロパティを示します。XとZに値を入れると描画位置が変更されるようにしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Enemy { get X(): number { return this.Sprite.position.x; } set X(value: number) { this.Sprite.position.x = value; } get Z(): number { return this.Sprite.position.z; } set Z(value: number) { this.Sprite.position.z = value; } } |
敵を移動させる関数を示します。最初に死んだ敵をシーンと配列のなかから取り除きます。updateCountの値で描画に使うスプライトを変更し、攻撃中であったり退却中である敵を移動させます。待機中の敵の移動や攻撃に参加させる処理は別の静的関数でおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class Enemy { Move() { if (this.EnemyStatus == EnemyStatus.Dead) { scene.remove(this.Sprite); Enemy.Enemies = Enemy.Enemies.filter(enemy => enemy != null); return; } let i = updateCount % 32; // updateCountはグローバル変数 if (i < 8) this.Sprite.material = Enemy.EnemyMaterial10; else if (i < 16) this.Sprite.material = Enemy.EnemyMaterial11; else if (i < 24) this.Sprite.material = Enemy.EnemyMaterial12; else this.Sprite.material = Enemy.EnemyMaterial13; if (this.EnemyStatus == EnemyStatus.Attack) this.MoveAttackingEnemy(); if (this.EnemyStatus == EnemyStatus.Retreat) this.MoveRetreatingEnemy(); } } |
攻撃中の敵を移動させる関数を示します。MoveDownCountが20の倍数であるときは弾丸を発射し、3発目は斜めに発射します。EnemyBurretクラスは後述します。EnemyBurretオブジェクトを生成して敵の弾丸が描画されるようにします。Z座標が15になるまで下降したら退却します。
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 |
class Enemy { MoveAttackingEnemy() { if (this.EnemyStatus == EnemyStatus.Attack) { this.MoveDownCount++; if (this.IsAttackRight) this.X += 0.05; else this.X -= 0.05; this.Z += 0.3; // 弾丸を発射する。 if (this.MoveDownCount % 20 == 0) { this.ShotCount++; if (this.ShotCount != 3) EnemyBurret.Add(new EnemyBurret(this.X, this.Z, 0, 0.7)); else { if (this.IsAttackRight) EnemyBurret.Add(new EnemyBurret(this.X, this.Z, 0.3, 0.5)); else EnemyBurret.Add(new EnemyBurret(this.X, this.Z, -0.3, 0.5)); } } // Z座標が15になるまで下降したら退却する if (this.Z > 15) { this.EnemyStatus = EnemyStatus.Retreat; } } } } |
退却中の敵を移動する処理を示します。退却時にときどき機雷を投下させます。機雷を投下するか判断するのは1秒間に1回です。実際に投下されるのは、X座標の絶対値が15より小さく(そうでないと自機で機雷を撃墜できない)、Z座標が-10以上である場合である場合で、確率は50%です。
Z座標が-30よりも上まで退却したら、EnemyStatusをEnemyStatus.Standbyに変更し、編隊の後部に再配置します。
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 |
class Enemy { MoveRetreatingEnemy() { if (this.Z > -30) { if (this.IsAttackRight) this.X += 0.05; else this.X -= 0.05; this.Z -= 0.3; this.MoveUpCount++; // 退却時にときどき機雷を投下させる。下記4つの条件を満たす場合はなにもしない。 if (this.MoveUpCount % 60 != 0) return; if (Math.abs(this.X) >= 15) return; if (this.Z < -10) return; if (Math.random() < 0.5) return; // 機雷投下。Mineクラスは後述 Mine.Add(new Mine(this.X, this.Z)); } else { // Z座標が-30まで退却したら編隊の後部に再配置する this.EnemyStatus = EnemyStatus.Standby; this.MoveDownCount = 0; this.MoveUpCount = 0; Enemy.SetNewPosition(this); } } } |
退却した敵を編隊の最後尾に再配置する関数を示します。現在編隊上に存在する敵のLineの値のなかから最大値を求めます。これが最後尾の列番号です。最後尾の列に空きがあればそこへ配置させ、ない場合はその後ろの列に配置されます。
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 |
class Enemy { // 再配置時の列 static NewPositionX = new Array(4, 3, 5, 2, 6, 1, 7, 0, 8); static SetNewPosition(enemy: Enemy) { let enemies: Enemy[] = Enemy.Enemies.filter(x => x.EnemyStatus == EnemyStatus.Standby); if (enemies.length > 0) { let line = Enemy.GetMaxLine(enemies); enemies = enemies.filter(x => x.Line == line); if (enemies.length < Enemy.ColumMax) { enemy.Line = line; enemy.Z = enemies[0].Z; enemy.X = -12 + 3 * Enemy.NewPositionX[enemies.length]; } else { enemy.Line = line + 1; enemy.Z = enemies[0].Z - 3; enemy.X = -12 + 3 * Enemy.NewPositionX[0]; } } else { enemy.Line = 1; enemy.Z = -16; enemy.X = -12 + 3 * Enemy.NewPositionX[0]; } enemy.EnemyStatus = EnemyStatus.Standby; } } |
Lineの最大値と最小値を求める関数を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Enemy { static GetMinLine(enemies: Enemy[]) { let line = Number.MAX_SAFE_INTEGER; for (let i = 0; i < enemies.length; i++) { if (enemies[i].Line < line) line = enemies[i].Line; } return line; } static GetMaxLine(enemies: Enemy[]) { let line = 0; for (let i = 0; i < enemies.length; i++) { if (enemies[i].Line > line) line = enemies[i].Line; } return line; } } |
待機中の敵を攻撃に参加させる関数を示します。updateCountが40の倍数のときに攻撃に参加させる敵を適当に選び、攻撃に参加した敵がいた場合は編隊全体を前進させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class Enemy { static EnemyBeginAttack() { let enemies: Enemy[] = Enemy.Enemies.filter(x => x.EnemyStatus == EnemyStatus.Standby); if (enemies.length == 0) return; let line = Enemy.GetMinLine(enemies); if (updateCount % 40 == 10) { enemies = Enemy.Enemies.filter(x => x.EnemyStatus == EnemyStatus.Standby && x.Line == line); if (enemies.length > 0) { // 攻撃に参加する敵はどれか? let r = Math.floor(Math.random() * (enemies.length)); enemies[r].EnemyStatus = EnemyStatus.Attack; enemies[r].IsAttackRight = Math.random() > 0.5 ? true : false; // 全体を前進させる enemies = Enemy.Enemies.filter(x => x.EnemyStatus == EnemyStatus.Standby); enemies.forEach(enemy => { enemy.Z += 3.0 / Enemy.ColumMax }); } } } } |
待機中の敵のうち前2列を前後に移動させる関数を示します。全体を1秒間下に移動、そのあと上に移動させるのでupdateCountが120の倍数のときに敵は一番上にきていることになります。このタイミングで最前列の敵のLineの最小値を取得し、保存しておきます。
最前列の敵のLineの最小値を更新する処理は前2列が一番上にきているタイミングでおこないます。任意のタイミングでおこなうと次の列以降にいる敵がへんなタイミングで「前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 |
class Enemy { static MoveStandbyEnemies() { let enemies: Enemy[] = Enemy.Enemies.filter(enemy => enemy.EnemyStatus == EnemyStatus.Standby); if (enemies.length == 0) return; let line = Enemy.GetMinLine(enemies); // 編隊の形が崩れないように前2列が一番上にきているタイミングで // 最前列の敵のLineの最小値を取得する if (updateCount % 120 == 0) Enemy.FrontLine = line; let frontLineEnemies: Enemy[] = Enemy.Enemies.filter(enemy => { return enemy.EnemyStatus == EnemyStatus.Standby && (enemy.Line == Enemy.FrontLine || enemy.Line == Enemy.FrontLine + 1); }); // 1秒間下に移動、そのあと上に移動 frontLineEnemies.forEach(enemy => { let cnt = updateCount % 120; if (cnt < 60) enemy.Z += 0.10; else enemy.Z -= 0.10; }); } } |
敵全体を移動させる関数を示します。
新たに敵を攻撃に参加させる必要があるならEnemyBeginAttack関数で処理をします。待機中の敵で必要なものは移動させ、攻撃中または退却中の敵がいるならMove関数で移動させます。ただし自機死亡時は新たに攻撃に参加するのをやめます。
1 2 3 4 5 6 7 8 9 |
class Enemy { static MoveAll() { if (!jiki.IsDead) Enemy.EnemyBeginAttack(); Enemy.MoveStandbyEnemies(); Enemy.Enemies.forEach(enemy => enemy.Move()); } } |
当たり判定
当たり判定の関数を示します。自機の弾丸が命中したら弾丸をDeadにしてtrueを返します。判定法は互いを球とみなして半径の和よりも距離が近ければ当たっているとしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Enemy { IsHited(jikiBurrets: JikiBurret[]): boolean { if (jikiBurrets.length == 0) return false; for (let i = 0; i < jikiBurrets.length; i++) { let a = Math.pow(this.Radius + jikiBurrets[i].Radius, 2); let b = Math.pow(this.X - jikiBurrets[i].X, 2) + Math.pow(this.Z - jikiBurrets[i].Z, 2); if (a > b) { jikiBurrets[i].Dead(); return true; } } return false; } } |
EnemyBurretクラス
敵の弾丸を管理するためのEnemyBurretクラスを作成します。
弾丸の生成
プロパティとコンストラクタを示します。敵の弾丸は直線で描画します。
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 |
class EnemyBurret { // 生成した弾丸はすべてここに格納してまとめて処理する static EnemyBurrets: EnemyBurret[] = []; // 弾丸の速度 VecX: number = 0; VecZ: number = 0; // 弾丸の座標 _x: number = 0; _z: number = 0; Line: THREE.Line = null; constructor(x: number, z: number, vecX: number, vecZ: number) { this.VecX = vecX; this.VecZ = vecZ; const line_material = new THREE.LineBasicMaterial({ color: 0xffffff }); // 弾丸として描画する直線の両端を求める let ang = Math.atan2(this.VecX, this.VecZ); let x0 = x + 2 * Math.sin(ang); let z0 = z + 2 * Math.cos(ang); //@ts-ignoreが必要 //@ts-ignore let line_geometry = new THREE.Geometry(); //頂点座標の追加 line_geometry.vertices.push( new THREE.Vector3(x, 0, z), new THREE.Vector3(x0, 0, z0), ); this.X = x; this.Z = z; //線オブジェクトの生成 this.Line = new THREE.Line(line_geometry, line_material); } } |
弾丸の移動
弾丸を移動させるプロパティと関数を示します。Z座標が20より大きいものはDead関数でシーンと配列から取り除きます。
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 |
class EnemyBurret { get X(): number { return this._x; } set X(value: number) { this._x = value; } get Z(): number { return this._z; } set Z(value: number) { this._z = value; } Move() { this.X += this.VecX; this.Z += this.VecZ; this.Line.position.x += this.VecX; this.Line.position.z += this.VecZ; if (this.Z > 20) this.Dead(); } // シーンと配列から取り除いて描画対象からはずす Dead() { scene.remove(this.Line); EnemyBurret.EnemyBurrets = EnemyBurret.EnemyBurrets.filter(burret => burret != this); } } |
追加と削除
新しい弾丸をシーンと配列に追加する関数を示します。
1 2 3 4 5 6 7 8 |
class EnemyBurret { static Add(burret: EnemyBurret) { EnemyBurret.EnemyBurrets.push(burret); //sceneに追加 scene.add(burret.Line); } } |
すべての弾丸をシーンと配列から取り除く関数を示します。
1 2 3 4 5 6 |
class EnemyBurret { static RemoveAll() { EnemyBurret.EnemyBurrets.forEach(burret => scene.remove(burret.Line)); EnemyBurret.EnemyBurrets = []; } } |
すべての弾丸を移動させる関数を示します。
1 2 3 4 5 |
class EnemyBurret { static MoveAll() { EnemyBurret.EnemyBurrets.forEach(burret => burret.Move()); } } |
Mineクラス
機雷を描画するためのクラスを作成します。
機雷の初期化
最初にプロパティとコンストラクタを示します。コンストラクタはEnemyクラスとほぼ同じです。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 |
class Mine { static Mines: Mine[] = []; static EnemyMaterial20: THREE.SpriteMaterial = null; static EnemyMaterial21: THREE.SpriteMaterial = null; static EnemyMaterial22: THREE.SpriteMaterial = null; static EnemyMaterial23: THREE.SpriteMaterial = null; Sprite: THREE.Sprite = null; Radius = 1.0; MoveCount = 0; constructor(x: number, z: number) { if (Mine.EnemyMaterial20 == null) Mine.EnemyMaterial20 = this.GetMaterial(DataURLs.dataURL_Enemy20); if (Mine.EnemyMaterial21 == null) Mine.EnemyMaterial21 = this.GetMaterial(DataURLs.dataURL_Enemy21); if (Mine.EnemyMaterial22 == null) Mine.EnemyMaterial22 = this.GetMaterial(DataURLs.dataURL_Enemy22); if (Mine.EnemyMaterial23 == null) Mine.EnemyMaterial23 = this.GetMaterial(DataURLs.dataURL_Enemy23); this.Sprite = new THREE.Sprite(Mine.EnemyMaterial20); this.Sprite.position.x = x; this.Sprite.position.y = 0; this.Sprite.position.z = z; this.Sprite.scale.set(this.Radius * 2, this.Radius * 2, this.Radius * 2); } 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 }); } } |
機雷の移動
移動に関するプロパティと関数を示します。
Z座標が21より大きくなると着弾したことになり、ダメージ1(自機死亡とは別概念)となります。
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 |
class Mine { 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++; let i = this.MoveCount % 32; if (i < 8) this.Sprite.material = Mine.EnemyMaterial20; else if (i < 16) this.Sprite.material = Mine.EnemyMaterial21; else if (i < 24) this.Sprite.material = Mine.EnemyMaterial22; else this.Sprite.material = Mine.EnemyMaterial23; this.Z += 0.2; if (this.Z > 21) { this.OnMineLanding(); this.Dead(); } } static MoveAll() { Mine.Mines.forEach(mine => mine.Move()); } } |
機雷の着弾
機雷着弾時の処理をする関数を示します。着弾したらダメージ1とし、ダメージが蓄積されて32以上になったら自機死亡とします。実際のレーダースコープではダメージがいっぱいになると「動作が遅くなる」そうですが、実際にどうなるのかは見たことがありません。
またゲームオーバー時は敵が攻撃と機雷投下を行ないますが、着弾時の処理はしないことにします。
1 2 3 4 5 6 7 8 9 |
class Mine { OnMineLanding() { if (rest > 0) { // restは残機を示すグローバル変数 damage += 1; // damageはダメージを示すグローバル変数 if (damage >= 32) OnJikiDead(); // ダメージが蓄積されて32以上になると自機死亡 } } } |
機雷の当たり判定
当たり判定処理をする関数と死んだ機雷をシーンから取り除く関数を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class Mine { IsHited(jikiBurrets: JikiBurret[]): boolean { if (jikiBurrets.length == 0) return false; for (let i = 0; i < jikiBurrets.length; i++) { let a = Math.pow(this.Radius + jikiBurrets[i].Radius, 2); let b = Math.pow(this.X - jikiBurrets[i].X, 2) + Math.pow(this.Z - jikiBurrets[i].Z, 2); if (a > b) { jikiBurrets[i].Dead(); return true; } } return false; } // 機雷をシーンと配列から取り除く Dead() { scene.remove(this.Sprite); Mine.Mines = Mine.Mines.filter(mine => mine != this); } } |
機雷の追加と削除
生成した機雷をシーンに追加する関数とすべての機雷をシーンから取り除く関数を示します。
1 2 3 4 5 6 7 8 9 10 11 |
class Mine { static Add(mine: Mine) { scene.add(mine.Sprite); Mine.Mines.push(mine); } static RemoveAll() { Mine.Mines.forEach(mine => scene.remove(mine.Sprite)); Mine.Mines = []; } } |