⇒ 動作確認はこちらからどうぞ。
←↑→↓キーで移動、Zキーで穴を堀り、Xキーで埋めます。黄色いやさぐれひよこは落とせば死にますが、ピンク色のものは2段、青いやさぐれひよこは3段連続で落とさないと死にません。
前回はPlayerクラスを作成しましたが、今回はEnemyクラスを作成します。
Enemyクラス
最初にプロパティを示します。
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 Enemy { // 雑魚敵は1。2だと二段、3なら三段連続で落とさないと死なない Life: number = 1; // 現在位置 X: number = 0; Y: number = 0; // 最初の位置 InitX: number = 0; InitY: number = 0; EnemySpeed: number = 2.0; // 移動する方向 MoveDirect: MoveDirect = MoveDirect.Right; // 穴に落ちた敵が穴から這い出すまでの時間 static TimeInTheHole1: number = 10 * 1000; static TimeInTheHole2: number = 2 * 1000; // 敵がハマっている穴と深さ Hole: Hole = null; HoleDepth: number = 0; // 穴にハマる前の敵のY座標 OldY: number = 0; // ハシゴの座標(移動できるかどうかの判定で必要) LadderPositions: Point[]; LadderUpperPositions: Point[]; // setTimeoutを無効化するために必要なID TimeoutID: NodeJS.Timeout = undefined; } |
コンストラクタ
コンストラクタを示します。引数は最初のX座標とY座標です。コンストラクタ内で上下の階に移動できるハシゴの座標を取得しています。
1 2 3 4 5 6 7 8 9 10 11 |
class Enemy { constructor(x: number, y: number) { this.X = x; this.Y = y; this.InitX = x; this.InitY = y; this.LadderPositions = GameManager.GetLadderPositions(); this.LadderUpperPositions = this.GetLadderUpperPositions(this.LadderPositions); } } |
描画する
敵を描画するための関数を示します。描画のたびにDrawCountを増やして使用するイメージを変えて動いているように見せかけています。ただし穴に落ちているときはびっくりしているようなイメージを使用します。
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 { DrawCount = 0; Draw(con: CanvasRenderingContext2D) { let point = GameManager.GetPointForShow(this.X, this.Y); let size = GameManager.CharctorSize; con.fillStyle = "red"; this.DrawCount++; let index = 0; let division = this.DrawCount % 16; if (division < 4) index = 0; else if (division < 8) index = 1; else if (division < 12) index = 2; else if (division < 16) index = 3; if (this.Hole != null) index = 4; if (this.Life == 2) index += 5; if (this.Life == 3) index += 10; con.drawImage(GameManager.EnemiesImages[index], point.X, point.Y, GameManager.CharctorSize, GameManager.CharctorSize); } } |
移動させる
次に移動の処理を考えます。
上下左右に移動できるか確認するために、まずハシゴの座標を取得します。以下は上端の座標を取得する関数です。下端はコンストラクタ内でGameManagerクラスから取得します。
1 2 3 4 5 6 7 8 9 |
class Enemy { GetLadderUpperPositions(ladderPositions: Point[]) { let ret: Point[] = []; ladderPositions.forEach(pt => { ret.push(new Point(pt.X, pt.Y - GameManager.CharctorSize * 3)); }); return ret; } } |
敵を移動させる関数を示します。敵はプレイヤーがいる階に向かって移動します。同じ階にいる場合はフロアを左右に往復します。移動先に穴があるときはハマりますが、他の敵がハマっていると穴の前で引き返します。
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 |
class Enemy { Move() { // 穴にハマっている敵は動けない if (this.Hole != null) return; this.ChangeDirection(); if (this.MoveDirect == MoveDirect.Left) this.X -= 2; if (this.MoveDirect == MoveDirect.Right) this.X += 2; if (this.MoveDirect == MoveDirect.Down) this.Y += 2; if (this.MoveDirect == MoveDirect.Up) this.Y -= 2; // フロアの端に来たら折り返す if (this.X > 14 * GameManager.CharctorSize) this.MoveDirect = MoveDirect.Left; if (this.X < 0) this.MoveDirect = MoveDirect.Right; // 他の敵がハマっている穴の前では引き返す if (this.TurnBackIfEnemyFallingHole()) return; // 移動先にハマっていない穴があるならその穴にハマる this.EnterHoleIfInHole(); } } |
敵をプレイヤーと同じ階に移動するために進行方向を変更させる関数を示します。別の階に移動するためにはハシゴを使うしかありません。
水平移動している敵がプレイヤーが自分よりも下の階にいる場合、ハシゴの上端に来たのであれば下に方向転換します。プレイヤーが上の階にいる場合でハシゴの下端にいる場合は上に方向転換します。
ハシゴを下りてきた敵がハシゴの下端に来たらハシゴがさらにつながっているのであればそのまま下に移動します。そうでない場合は水平移動に切り替え、プレイヤーがいる方向に方向転換します。
ハシゴを上って敵がハシゴの上端に来たらハシゴがさらにつながっているのであればそのまま上に移動し、そうでない場合は水平移動に切り替えます。
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 |
class Enemy { ChangeDirection() { // ここは水平移動しているモンスターにとってハシゴの上端 let b1 = this.MoveDirect == MoveDirect.Left || this.MoveDirect == MoveDirect.Right; let b2 = this.LadderUpperPositions.filter(x => x.X == this.X && x.Y == this.Y).length > 0 let b3 = this.Y < GameManager.Player.Y; if (b1 && b2 && b3) { this.MoveDirect = MoveDirect.Down; return; } // ここは水平移動しているモンスターにとってハシゴの下端 b2 = this.LadderPositions.filter(x => x.X == this.X && x.Y == this.Y).length > 0; b3 = this.Y > GameManager.Player.Y; if (b1 && b2 && b3) { this.MoveDirect = MoveDirect.Up; return; } // ここはハシゴを下りてきた敵にとってハシゴの下端 b1 = this.MoveDirect == MoveDirect.Down; b2 = this.LadderPositions.filter(x => x.X == this.X && x.Y == this.Y).length > 0; b3 = this.X < GameManager.Player.X; if (b1 && b2) { // ハシゴが繋がっていてプレイヤーがさらに下の階にいるのであればそのまま下に移動する if (this.LadderUpperPositions.filter(x => x.X == this.X && x.Y == this.Y).length > 0 && this.Y < GameManager.Player.Y) return; if (b3) this.MoveDirect = MoveDirect.Right; else this.MoveDirect = MoveDirect.Left; return; } // ここはハシゴを上ってきた敵にとってハシゴの上端 b1 = this.MoveDirect == MoveDirect.Up; b2 = this.LadderUpperPositions.filter(x => x.X == this.X && x.Y == this.Y).length > 0; b3 = this.X < GameManager.Player.X; if (b1 && b2) { // ハシゴが繋がっていてプレイヤーがさらに上の階にいるのであればそのまま上に移動する if (this.LadderPositions.filter(x => x.X == this.X && x.Y == this.Y).length && this.Y > GameManager.Player.Y) return; if (b3) this.MoveDirect = MoveDirect.Right; else this.MoveDirect = MoveDirect.Left; return; } // 敵同士が重なっていたら別方向に移動させる let enemies: Enemy[] = GameManager.Enemies.filter(x => x != this); let enemy = enemies.filter(x => (x.X < this.X && this.X < x.X + GameManager.CharctorSize) && (x.Y == this.Y)); if (enemy.length > 0 && this.MoveDirect == enemy[0].MoveDirect) { if (this.MoveDirect == MoveDirect.Left) enemy[0].MoveDirect = MoveDirect.Right; if (this.MoveDirect == MoveDirect.Right) enemy[0].MoveDirect = MoveDirect.Left; } } } |
別の敵がすでにハマっている穴があったら引き返します。そのための関数を示します。
最初にその階の敵がハマっている穴のリストを取得します。もし現在の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 |
class Enemy { TurnBackIfEnemyFallingHole(): boolean { // その階の敵がハマっている穴のリストを取得する let holes: Hole[] = GameManager.Holes.filter(x => x.Enemy != null && x.Rectangle.Y == this.Y); // 現在位置はすでに敵がハマっている穴の縁よりも内側ではないのか? let hole: Hole[] = holes.filter(x => x.Rectangle.X - GameManager.CharctorSize < this.X && this.X < x.Rectangle.Right); if (hole.length == 1) { let enemy: Enemy = hole[0].Enemy; if (this.MoveDirect == MoveDirect.Left) { // 穴の縁よりも右側4ピクセルに移動させて右方向へ移動させる this.X = hole[0].Rectangle.Right + 4; this.MoveDirect = MoveDirect.Right; } else if (this.MoveDirect == MoveDirect.Right) { // 穴の縁よりも左側4ピクセルに移動させて左方向へ移動させる this.X = hole[0].Rectangle.Left - GameManager.CharctorSize - 4; this.MoveDirect = MoveDirect.Left; } return true; } return false; } } |
穴にハマる
敵の現在位置にどの敵もハマっていない穴がある場合はその穴にハマります。そのときに穴にハマる前のY座標を記憶しておきます。そしてsetTimeout関数を実行し、指定された時間が経過したにもかかわらず埋められなかった場合は穴から這い出ることができるようにしておきます。またゲームの進行上、setTimeoutを無効にする場合に備えてTimeoutIDを保存しておきます。
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 |
class Enemy { EnterHoleIfInHole() { // 敵がいる位置は穴がある位置か? let count = GameManager.Holes.length; let hole: Hole = null; for (let i = 0; i < count; i++) { let hole0: Hole = GameManager.Holes[i]; if (hole0.Rectangle.X == this.X && hole0.Rectangle.Y == this.Y) { hole = hole0; break; } } if (hole != null) { // 穴から出るときのためにY座標を記憶しておく this.OldY = this.Y; if (hole.Depth == 1) { this.Y += 7; this.TimeoutID = setTimeout(GetoutHole, Enemy.TimeInTheHole2, this); } if (hole.Depth == 2) { this.Y += 14; this.TimeoutID = setTimeout(GetoutHole, Enemy.TimeInTheHole2, this); } if (hole.Depth == 3) { this.Y += GameManager.CharctorSize; this.TimeoutID = setTimeout(GetoutHole, Enemy.TimeInTheHole1, this); } this.MoveDirect = MoveDirect.Stop; this.Hole = hole; hole.Enemy = this; this.HoleDepth = hole.Depth; } } } |
穴から這い出す
一定時間が経過したのに埋められなかった穴から敵が這い出す処理をする関数を示します。
自分がハマっていた穴の深さを調べてループを回して座標を上に移動させます。GameManager.CharctorSizeが3で割り切れない場合があるので誤差をなくすため、最後に記憶しておいたOldYをY座標に指定します。また敵は一ランク上の敵に昇格します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
async function GetoutHole(enemy: Enemy): Promise<number> { let depth = enemy.HoleDepth; enemy.HoleDepth = 0; enemy.Hole.Depth = 0; enemy.Hole = null; for (let i = 0; i < depth; i++) { await sleep(500); enemy.Y -= GameManager.CharctorSize / 3; } enemy.Y = enemy.OldY; // モンスターはボスに、ボスはドンに昇格する if (depth == 3 && (enemy.Life == 1 || enemy.Life == 2)) enemy.Life++; if (Math.random() > 0.5) enemy.MoveDirect = MoveDirect.Left; else enemy.MoveDirect = MoveDirect.Right; return 0; } |
穴から落ちる
敵が落ちる処理をする関数を示します。
落とされた敵を倒すことができるかどうかは敵のLifeによります。Lifeと同じか高いところから落とさないと敵を倒すことはできません。
穴は縦に連続して掘られているかもしれません。またその穴に別の敵がハマっている場合は、何段落とされたかに関わりなく相手も巻き込んで一緒に死ぬことになります。また敵を倒したときの点数を計算するとともに、それを表示させるために敵が死んだ位置を記憶しておきます。
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 |
class Enemy { AddScore: number = 0; DeadPoint: Point public async Fall() { let first = true; let holesPassedThrough: Hole[] = []; // 他の敵を巻き込んだ場合 let isDead = false; // 追加される点数 let addScore: number = 0; while (true) { // 落ちた先のY座標を計算する let down: number = first ? 2 : 3; let nextY: number = this.Y + GameManager.CharctorSize * down; // 一気に落とすのではなくGameManager.CharctorSize / 8ずつ移動させて // 落ちていくように見せかける for (let i = 0; i < 2 * 8; i++) { await sleep(20); this.Y += GameManager.CharctorSize / 8; } this.Y = nextY; // 穴は垂直に並んでいないか?落ちた先にさらに穴は存在しないか? let holes: Hole[] = GameManager.Holes.filter(x => x.Rectangle.X == this.X && x.Rectangle.Y == this.Y); if (holes.length == 0) break; let hole: Hole = holes[0]; holesPassedThrough.push(hole); first = false; if (hole.Enemy != null) { // 通過した穴にも敵がいた場合、巻き込みで同時に倒すことができる isDead = true; addScore += this.GetBaseScore(hole.Enemy); hole.Enemy.Life = 0; GameManager.Enemies = GameManager.Enemies.filter(x => x != hole.Enemy); } } this.HoleDepth = 0; holesPassedThrough.forEach(x => x.Depth = 0); // 敵のLifeと同じか高いところから落とした場合のみ倒すことができる // 通過した穴にも敵がいた場合は、Lifeに関係なく巻き込みで同時に倒すことができる if (isDead || this.Life <= holesPassedThrough.length + 1) { let downCount = holesPassedThrough.length + 1; addScore += this.GetAddScore(downCount); this.Life = 0; GameManager.Enemies = GameManager.Enemies.filter(x => x != this); this.DeadPoint = new Point(this.X, this.Y); this.AddScore = addScore; } else { // 敵のLifeよりも低いところからしか落とせなかった場合は穴だけ埋めて通常移動させる this.Hole = null; if (Math.random() > 0.5) this.MoveDirect = MoveDirect.Left; else this.MoveDirect = MoveDirect.Right; } } } |
敵を倒したときの点数を計算するための関数を示します。雑魚敵は100点、ボスは300点、ドンは800点ですが、高いところから落とした場合はさらに高得点になります。
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 |
class Enemy { GetBaseScore(enemy: Enemy): number { if (enemy.Life == 1) return 100; if (enemy.Life == 2) return 300; if (enemy.Life == 3) return 800; return 0; } GetAddScore(downCount: number): number { if (this.Life == 1) { if (downCount == 1) return 100; if (downCount == 2) return 200; if (downCount == 3) return 300; if (downCount == 4) return 400; if (downCount == 5) return 800; } if (this.Life == 2) { if (downCount == 1 || downCount == 2) return 300; if (downCount == 3) return 400; if (downCount == 4) return 600; if (downCount == 5) return 1200; } if (this.Life == 3) { if (downCount == 1 || downCount == 2 || downCount == 3) return 800; if (downCount == 4) return 1000; if (downCount == 5) return 2000; } return 0; } } |
当たり判定
プレイヤーとの当たり判定をする関数を示します。敵とプレイヤーの座標同士の距離がCharctorSize以下なら接触していると判定します。この関数がtrueを返すということはミスをしたことになります。
1 2 3 4 5 6 7 8 9 10 |
class Enemy { IsPlayerDead(): boolean { let a = Math.pow(GameManager.Player.X - this.X, 2) + Math.pow(GameManager.Player.Y - this.Y, 2); let b = Math.pow(GameManager.CharctorSize, 2); if (a < b) return true; else return false; } } |
ミス時の処理
ミスをしたらゲームを再開する前に敵をもとの位置に戻します。そのための関数を示します。
敵を最初の位置・状態に戻すだけでなく、穴にハマった状態であればその状態も解消します。これと同じタイミングで穴におちた敵が這い上がるとY座標がズレてしまいます。そこでsetTimeout関数が実行されていた場合はclearTimeout関数を実行することでこれを無効化します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class Enemy { // 敵を最初の位置・状態に戻す MoveInitPosition() { // 穴にハマった状態であれば元に戻す this.Hole = null; this.HoleDepth = 0; clearTimeout(this.TimeoutID); this.X = this.InitX; this.Y = this.InitY; // 最初に移動する方向を決める if (Math.random() > 0.5) this.MoveDirect = MoveDirect.Left; else this.MoveDirect = MoveDirect.Right; } } |