前回はC# Windows Formsで昔なつかしいミサイルコマンドのようなゲームを作ってみましたが、今回はTypeScriptで同じようなゲームをつくってみました。
前回の記事
ミサイルコマンドのようなゲームをつくる(1)
ミサイルコマンドのようなゲームをつくる(2)
今回TypeScriptで作成したゲーム
動作確認はこちらからどうぞ。
ではコードを示します。C#をTypeScriptで書き換えただけです。
まずhtml部分をしめします。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<!DOCTYPE html> <html> <head> <title>TypeScriptでミサイルコマンドっぽいゲームをつくる</title> <meta charset="UTF-8" /> </head> <body> <canvas id="can"></canvas> <script src="./app.js"></script> </body> </html> |
これだけです。あとはapp.tsに以下のコードを書いてコンパイルすればapp.jsがつくられます。あと同じディレクトリに画像ファイルとmp3ファイルをおいておきましょう。
Contents
都市を描画するためのCityクラス
都市を描画するためのCityクラスを示します。考え方はC#のときとあまり変わりません。コンストラクタ内で都市が描画される矩形の上下左右の座標と中央の座標を計算しています。
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 |
class City { _left: number; _right: number; _top: number; _bottom: number; _width: number; _height: number; static _image: HTMLImageElement constructor(x: number, y: number, width: number, height: number) { this._left = x; this._top = y; this._width = width; this._height = height; this._right = x + width; this._bottom = y + height; this.CenterX = x + width / 2; this.CenterY = y + height / 2; let image: HTMLImageElement = new Image(); image.src = "city.png"; City._image = image; } _centerX: number; set CenterX(value) { this._centerX = value; } get CenterX(): number { return this._centerX; } _centerY: number; set CenterY(value) { this._centerY = value; } get CenterY(): number { return this._centerY; } _isDead: boolean; get IsDead() { return this._isDead; } set IsDead(value) { this._isDead = value; } // 引数で渡されたPointは矩形の内部かどうか? IsInsidePoint(x: number, y: number) { if (x < this._left) return false; if (y < this._top) return false; if (this._right < x) return false; if (this._bottom < y) return false; return true; } // 都市が壊滅していなければ描画する Draw(con: CanvasRenderingContext2D) { if (!this.IsDead) con.drawImage(City._image, this._left, this._top, this._width, this._height); } } |
敵のミサイルを描画するためのEnemyMissileクラス
次に敵のミサイルを描画するためのEnemyMissileクラスを示します。これもC#をTypeScriptに置き換えただけです。
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 EnemyMissile { _startX = 0; _startY = 0; _vX = 0; _vY = 0; X = 0; Y = 0; TargetX = 0; TargetY = 0; IsDead = false; SpeedUp = false; constructor(x: number, y: number, targetX: number, targetY: number, speed: number) { this.X = x; this.Y = y; this._startX = x; this._startY = y; this.TargetX = targetX; this.TargetY = targetY; let angle = Math.atan2(targetY - y, targetX - x); this._vX = Math.cos(angle) * speed; this._vY = Math.sin(angle) * speed; } Update() { this.X += this._vX; this.Y += this._vY; if (this.SpeedUp) { this.X += this._vX * 5; this.Y += this._vY * 5; } } Draw(con: CanvasRenderingContext2D) { con.strokeStyle = '#ff0000'; con.lineWidth = 3; con.beginPath(); con.moveTo(this._startX, this._startY); con.lineTo(this.X, this.Y); con.closePath(); con.stroke(); } } |
迎撃ミサイルを描画するためのMyMissileクラス
迎撃ミサイルを描画するためのMyMissileクラスを示します。
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 |
class MyMissile { _startX = 0; _startY = 0; _targetX = 0; _targetY = 0; X = 0; Y = 0; VX = 0; VY = 0; IsReached = false; IsDead = false; // 発射位置と目標からXY座標の移動量を求める constructor(startX, startY, targetX, targetY) { // 現在位置を設定 this.X = startX; this.Y = startY; // 初期位置を保存 this._startX = startX; this._startY = startY; // 最終位置を保存 this._targetX = targetX; this._targetY = targetY; // 移動方向と移動量を算出する let angle = Math.atan2(targetY - startY, targetX - startX); this.VX = Math.cos(angle) * 20; this.VY = Math.sin(angle) * 20; this.IsReached = false; } Update() { // 移動させて目標を通過していたら目標と同じ座標にすると同時にIsReached = trueにする this.X += this.VX; this.Y += this.VY; if (this.Y < TargetY) { this.X = TargetX; this.Y = TargetY; this.IsReached = true; } } // ミサイルの描画処理 Draw(con: CanvasRenderingContext2D) { con.strokeStyle = '#0000ff'; con.lineWidth = 3; con.beginPath(); con.moveTo(this._startX, this._startY); con.lineTo(this.X, this.Y); con.closePath(); con.stroke(); } } |
爆発を描画するExplosionクラス
爆発を描画するExplosionクラスを示します。前回のプログラムでは敵ミサイルが爆発に巻き込まれたら撃墜成功ということで無条件に得点にしていましたが、敵ミサイルが都市に着弾して爆発、そこで別の敵ミサイルがきた場合、これも誘爆して得点としてカウントされるという問題がありました。そこで敵ミサイルが都市に着弾して爆発した場合はこれを判別するフラグをつくって得点にはならないようにしています。
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 |
class Explosion { X: number = 0; Y: number = 0; Radius: number = 0; Life: number = 0; UpdateCount: number = 0; IsDead: boolean = false; static colors: string[] = []; Damage: boolean = false; // これがtrueだと都市に着弾したことによる爆発 // 得点としては処理されない constructor(x: number, y: number, life: number) { if (Explosion.colors.length == 0) { Explosion.colors.push('#ff0000'); Explosion.colors.push('#ffff00'); Explosion.colors.push('#00ff00'); Explosion.colors.push('#00ffff'); Explosion.colors.push('#0000ff'); Explosion.colors.push('#ff00ff'); } this.X = x; this.Y = y; this.Life = life; this.Radius = 0; } Update() { this.UpdateCount++; this.Radius = this.UpdateCount * 2; if (this.UpdateCount >= this.Life) { this.IsDead = true; } } Draw(con: CanvasRenderingContext2D) { let index = UpdateCount % Explosion.colors.length; con.beginPath(); con.arc(this.X, this.Y, this.Radius, 0, 2 * Math.PI, false); con.fillStyle = "black"; con.fill(); con.strokeStyle = Explosion.colors[index]; con.lineWidth = 3; con.closePath(); con.stroke(); } // 引数で渡されたPointは円の内部かどうか? public IsContactPoint(x: number, y: number) { let distance2: number = Math.pow(this.X - x, 2) + Math.pow(this.Y - y, 2); let radius2: number = Math.pow(this.Radius, 2); if (distance2 < radius2) return true; return false; } } |
ここからはクラスは使いません。
Init関数はゲームの初期化をするための関数です。都市のイメージを読み込み、ターゲットを中心にもっていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function Init() { can.width = Screen_W; // 640ピクセル can.height = Screen_H; // 480ピクセル let spacing = (Screen_W - 50 * 2 - 45) / 5; Cities.push(new City(50 + spacing * 0, 350, 45, 30)); Cities.push(new City(50 + spacing * 1, 350, 45, 30)); Cities.push(new City(50 + spacing * 2, 350, 45, 30)); Cities.push(new City(50 + spacing * 3, 350, 45, 30)); Cities.push(new City(50 + spacing * 4, 350, 45, 30)); Cities.push(new City(50 + spacing * 5, 350, 45, 30)); TargetX = Screen_W / 2; TargetY = Screen_H / 2; setInterval(Update, 1000/60); } |
1秒間に60回Update関数が呼び出されます。各オブジェクトの状態が変更されてDraw関数が呼び出されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
let UpdateCount = 0; function Update() { MyMissiles = MyMissiles.filter(x => !x.IsDead); Explosions = Explosions.filter(x => !x.IsDead); EnemyMissiles = EnemyMissiles.filter(x => !x.IsDead); MoveTarget(); MoveEnemyMissile(); CreateEnemyMissile(); MoveMyMissiles(); UpdateExplosions(); CheckEnemyMissileLanding(); CheckHitEnemyMissile(); Draw(); UpdateCount++; } |
Draw関数では敵ミサイルとプレイヤーの迎撃ミサイル、残存都市、爆発、迎撃目標を選択するためのターゲット(+マークのもの)を描画します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function Draw() { con.fillStyle = "black"; con.fillRect(0, 0, Screen_W, Screen_H); EnemyMissiles.forEach(missile => missile.Draw(con)); MyMissiles.forEach(missile => missile.Draw(con)); Cities.forEach(city => city.Draw(con)); Explosions.forEach(explosion => explosion.Draw(con)); DrawTarget(con); DrawScore(con); DrawStringIfGameOver(con); } |
キーが押されたときの動作です。ターゲットがどちら側に移動するかを示すグローバル変数 MoveLeftなどをtrueにします。迎撃ミサイルの発射やゲームオーバー後の再挑戦の処理もここでおこなわれます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function OnKeyDown(e: KeyboardEvent) { if (e.keyCode == 37) // 左 MoveLeft = true; if (e.keyCode == 38) // 上 MoveUp = true; if (e.keyCode == 39) // 右 MoveRight = true; if (e.keyCode == 40) // 下 MoveDown = true; if (e.keyCode == 32) // Keys.Space Shot(); if (e.keyCode == 83 && Cities.filter(x => !x.IsDead).length == 0) // Keys.S Retry(); } |
キーが離されたときの動作です。ターゲットの動きが停止します。
1 2 3 4 5 6 7 8 9 10 |
function OnKeyUp(e: KeyboardEvent) { if (e.keyCode == 37) MoveLeft = false; if (e.keyCode == 38) MoveUp = false; if (e.keyCode == 39) MoveRight = false; if (e.keyCode == 40) MoveDown = false; } |
Update関数内で呼び出されます。方向キーをおしたままだとターゲットが移動しますが、画面の外には出ないようにしています。
1 2 3 4 5 6 7 8 9 10 |
function MoveTarget() { if (MoveLeft && TargetX > 0) TargetX -= 5; if (MoveUp && TargetY > 0) TargetY -= 5; if (MoveRight && TargetX < Screen_W) TargetX += 5; if (MoveDown && TargetY < MyMissileLaunchPointY - 10) TargetY += 5; } |
DrawTarget関数は、グローバル変数のTargetXとTargetYをもとにターゲットを描画する処理を行ないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function DrawTarget(con: CanvasRenderingContext2D) { con.strokeStyle = '#00ffff'; con.lineWidth = 3; con.beginPath(); con.moveTo(TargetX - 5, TargetY); con.lineTo(TargetX + 5, TargetY); con.closePath(); con.stroke(); con.beginPath(); con.moveTo(TargetX, TargetY - 5); con.lineTo(TargetX, TargetY + 5); con.closePath(); con.stroke(); } |
MoveEnemyMissile関数は1秒間に60回、敵を移動させる処理をおこないます。
1 2 3 4 5 6 7 8 |
function MoveEnemyMissile() { EnemyMissiles.forEach(missile => { if (Cities.filter(city => !city.IsDead).length == 0) missile.SpeedUp = true; missile.Update(); }); } |
CreateEnemyMissile関数は敵ミサイルを新たに出現させる処理をおこないます。
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 CreateEnemyMissile() { let i = 50 - UpdateCount / 100; // 敵ミサイルが生成される間隔 5より短くはならない if (i < 5) i = 5; i = Math.floor(i); // 敵ミサイルを生成する if (UpdateCount % i == 0) { // 敵ミサイルの落下速度 let speed = 0.7 + UpdateCount / 2000; // 敵ミサイルの出現場所 let startX = Math.random() * Screen_W; // 敵ミサイルの落下ポイント // ランダムに選んでいてはいつまでたっても終わらないので生き残っている都市を目指すが // ときどきそうではない場所にも落下させる let cities = Cities.filter(x => !x.IsDead); if (cities.length != 0) { let city; if (Math.floor(Math.random() * 3) == 0) city = cities[Math.floor(Math.random() * cities.length)]; // 生き残っている都市が目標 else city = Cities[Math.floor(Math.random() * Cities.length)]; // 6箇所から適当に選ぶ EnemyMissiles.push(new EnemyMissile(startX, 0, city.CenterX, city.CenterY, speed)); // 落下中のミサイルのどれかを分岐させる if (EnemyMissiles.length != 0) { let enemyMissile = EnemyMissiles[Math.floor(Math.random() * EnemyMissiles.length)]; if (enemyMissile.TargetX != city.CenterX) EnemyMissiles.push( new EnemyMissile(enemyMissile.X, enemyMissile.Y, city.CenterX, city.CenterY, speed)); } } } } |
Shot関数は敵ミサイルを迎撃するためのミサイルを発射する処理をおこないます。
1 2 3 4 5 6 7 8 9 10 |
function Shot() { if (Cities.filter(city => !city.IsDead).length != 0) { MyMissiles.push(new MyMissile(MyMissileLaunchPointX, MyMissileLaunchPointY, TargetX, TargetY)); // 音を鳴らす seShot.currentTime = 0; seShot.play(); } } |
MoveMyMissiles関数は迎撃ミサイルを移動させる処理をおこないます。そして目標地点に到達すると爆発処理にはいります。
1 2 3 4 5 6 7 8 9 10 11 |
function MoveMyMissiles() { MyMissiles.forEach(missile => { missile.Update(); if (missile.IsReached && !missile.IsDead) { Explosions.push(new Explosion(missile.X, missile.Y, 20)); missile.IsDead = true; } }); MyMissiles = MyMissiles.filter(missile => !missile.IsDead); } |
UpdateExplosions関数はExplosionクラスのUpdate関数を呼び出して、爆発を状態を変更させます。
1 2 3 |
function UpdateExplosions() { Explosions.forEach(x => x.Update()); } |
CheckEnemyMissileLanding関数は敵のミサイルが着弾したかどうかを調べます。
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 |
function CheckEnemyMissileLanding() { EnemyMissiles = EnemyMissiles.filter(x => !x.IsDead); EnemyMissiles.forEach(missile => { let city: City = null; // 敵ミサイルのY座標が都市の中心Y座標より大きければ貫通している // その場合はどの都市を貫通しているかを調べる if (missile.TargetY < missile.Y) city = Cities.find(x => x.CenterX == missile.TargetX); // 敵ミサイルが貫通している都市があればミサイルを爆発させて都市を消滅させる if (city != null) { city.IsDead = true; missile.IsDead = true; // Explosionsにオブジェクトを追加 let explosion = new Explosion(city.CenterX, city.CenterY, 20) explosion.Damage = true; Explosions.unshift(explosion); // 爆発の音を鳴らす seDamage.currentTime = 0; seDamage.play(); } }); } |
CheckHitEnemyMissile関数は迎撃ミサイルの爆発で敵のミサイルを撃墜できたかを調べます。撃墜成功の場合はOnHitEnemyMissile関数を呼び出して得点を加算します。しかし敵のミサイルが都市に着弾した場合は近くにある敵のミサイルを誘爆させません。
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 CheckHitEnemyMissile() { // foreach文のなかでExplosionsに要素を追加すると例外が発生するので // 追加したいデータはいったんhitedEnemyMissilesに格納する let hitedEnemyMissiles = []; let enemyMissileLen = EnemyMissiles.length; Explosions.forEach(explosion => { for (let i = 0; i < enemyMissileLen; i++) { if (EnemyMissiles[i].IsDead) continue; let x = EnemyMissiles[i].X; let y = EnemyMissiles[i].Y; if (explosion.IsContactPoint(x, y) && !explosion.Damage) { hitedEnemyMissiles.push(EnemyMissiles[i]); EnemyMissiles[i].IsDead = true; OnHitEnemyMissile(); break; } } }); // 追加するときは一番最後ではなく最初に hitedEnemyMissiles.forEach(missile => { Explosions.unshift(new Explosion(missile.X, missile.Y, 15)); }); } |
OnHitEnemyMissile関数は敵のミサイルを撃墜したときに呼び出されます。25点を追加します。また爆発音を鳴らします。
1 2 3 4 5 6 7 8 |
let Score = 0; function OnHitEnemyMissile() { Score += 25; // 音を鳴らす seHit.currentTime = 0; seHit.play(); } |
DrawScore関数はスコアを表示するための関数です。
1 2 3 4 5 6 7 8 9 |
function DrawScore(con: CanvasRenderingContext2D) { con.fillStyle = "white"; con.font = "16pt sans-serif"; let ret = ('00000' + Score).slice(-5); ret = "Score " + ret; con.fillText(ret, 10, 24); } |
DrawStringIfGameOver関数はゲームオーバーになっているときにDraw関数のなかで呼び出されます。文字が中央にくるようにCanvasRenderingContext2D.measureText関数を使用して中心に描画するために適切な座標を調べています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function DrawStringIfGameOver(con: CanvasRenderingContext2D) { if (Cities.filter(x => !x.IsDead).length == 0) { // 実際に文字を描画してX座標は中心にY座標は中心よりも少し上の座標を取得する con.fillStyle = "white"; con.font = "32pt sans-serif"; let textMetrics1 = con.measureText("Game Over"); let width1 = textMetrics1.width; let height1 = textMetrics1.emHeightAscent; con.fillText("Game Over", (Screen_W - width1) / 2, (Screen_H) / 3); con.font = "20pt sans-serif"; let textMetrics2 = con.measureText("Press S key to try again"); let width2 = textMetrics2.width; con.fillText("Press S key to try again", (Screen_W - width2) / 2, (Screen_H) / 2); } } |
Retry関数はゲームオーバーになったあと、もう一度ゲームをするときに呼び出される関数です。得点と敵ミサイルの落下速度の鍵となるUpdateCountをリセットしています。そして破壊された都市を復元し、ターゲットを中心に戻してゲームを再開させます。
1 2 3 4 5 6 7 8 9 |
function Retry() { MyMissiles = []; UpdateCount = 0; Score = 0; Cities.map(x => x.IsDead = false); TargetX = Screen_W / 2; TargetY = Screen_H / 2; } |
あとはグローバル変数だけです。これで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 |
let Screen_W: number = 640; let Screen_H: number = 480; document.onkeydown = OnKeyDown; document.onkeyup = OnKeyUp; let can: HTMLCanvasElement = <HTMLCanvasElement>document.getElementById('can'); let con: CanvasRenderingContext2D = can.getContext("2d"); let Cities: City[] = []; let EnemyMissiles: EnemyMissile[] = []; let MyMissiles: MyMissile[] = []; let Explosions: Explosion[] = []; let MoveRight = false; let MoveLeft = false; let MoveUp = false; let MoveDown = false; let TargetX = 0; let TargetY = 0; let MyMissileLaunchPointX = Screen_W / 2; let MyMissileLaunchPointY = 400; var seDamage = new Audio('damage.mp3'); var seHit = new Audio('hit.mp3'); var seShot = new Audio('shot.mp3'); Init(); // ここからスタート! |