今回はThree.jsでPOLAR STAR(ポーラースター)のようなゲームを作ります。
POLAR STARは1983年発売の疑似3Dシューティングゲームです。敵は後方画面外からも撃ってきます。レーダーがあり、画面外の敵の位置が表示されます。要塞を攻撃するためには連射できない特別な弾が必要で、これを撃つためにはエネルギーのチャージが必要。しかもそのあいだは一方的に敵の攻撃を受け続け、一切の反撃ができません。敵機の攻撃をよけながら要塞攻撃のチャンスを狙うという忍耐力が必要なゲームです。
ちなみにこれが本物のポーラースターの画面です。
Contents
HTML部分
HTML部分を示します。
index.html
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 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>鳩でもわかるポーラースター</title> <meta name = "viewport" content = "width=device-width, initial-scale = 1.0"> <link rel="stylesheet" href="./style.css"> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/110/three.min.js"></script> <style> </style> </head> <body> <div id = "container"> <div id = "field"> <div id = "score"></div> <div id = "rest"></div> <div id = "stage"></div> <div id = "distance"></div> <div><canvas id = "main-canvas"></canvas></div> <div> <canvas id = "radar-canvas"></canvas> </div> <div id = "start-buttons"> <p><button id = "start">START</button></p> <p>プレイヤー名:<input type="text" id = "player-name"></p> <p>遊び方は <a href="./how-to-play.html">こちらを参照</a> してください。</p> <p><a href="./ranking.html">スコアランキング</a> はこちら。</p> </div> <div id = "ctrl-buttons"> <p> <button id = "up" class = "ctrl-button">UP</button> </p> <p> <button id = "left" class = "ctrl-button">LEFT</button> <button id = "shot" class = "ctrl-button">SHOT</button> <button id = "right" class = "ctrl-button">RIGHT</button><br> </p> <p> <button id = "down" class = "ctrl-button">DOWN</button> </p> </div> </div> <div><input type="checkbox" id = "hide-ctrl-buttons"><label for="hide-ctrl-buttons">スマホ用操作用ボタンを非表示にする</label></div> <div style="font-size:0.9em;">PC なら 矢印キーで移動、スペースキーで弾丸の発射ができます。</div> <div id = "volume-ctrl"></div> </div> <script src = "./index.js"></script> </body> </html> |
style.css
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 121 122 123 124 |
body { background-color: black; color: white; line-height: 1.7; } #container { width: 360px; } #field { width: 360px; height: 500px; background-color: black; position: relative; margin-bottom: 10px; } #radar-canvas { margin-top: 10px; margin-left: 10px; margin-right: 20px; } #score, #rest, #stage, #distance { position: absolute; font-weight: bold; color: #0ff; } #score, #stage { top:0px; } #rest, #distance { top:24px; } #score { left:0px; width: 160px; } #rest { left:20px; width: 100px; } #stage { left:180px; width: 150px; text-align: right; } #distance { left:0px; width: 340px; text-align: right; } #start-buttons { position: absolute; width: 360px; height: 200px; top:250px; left:10px; text-align: center; } #start { width: 160px; height: 70px; } #ctrl-buttons { position: absolute; width: 360px; height: 200px; top:160px; left:10px; text-align: center; display: none; } .ctrl-button { color: white; background-color: transparent; font-weight: bold; font-size: large; } #up, #down { width: 140px; height: 80px; } #left, #shot, #right { width: 100px; height: 110px; margin-right: 10px; } #volume-ctrl { margin-top: 10px; } #hide-ctrl-buttons { vertical-align: middle; } .red { color: #f0f; font-weight: bold; } .right { text-align: right; } a { color: aqua; font-weight: bold; } a:hover { color: red; } |
グローバル変数と定数
グローバル変数と定数を示します。
index.js
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 |
// DOM要素 const $field = document.getElementById('field'); const $main_canvas = document.getElementById('main-canvas'); const $radar_canvas = document.getElementById('radar-canvas'); const $startButtons = document.getElementById('start-buttons'); const $ctrlButtons = document.getElementById('ctrl-buttons'); const $hideCtrlButtons = document.getElementById('hide-ctrl-buttons'); const $playerName = document.getElementById("player-name"); const radar_ctx = $radar_canvas.getContext('2d'); // 自機、敵、弾丸オブジェクトが取りうる座標の最大値・最小値 const MAX_X = 150; const MIN_X = 0; const MAX_Y = 200; const MIN_Y = 0; // 自機が移動できる座標 const MIN_PLAYER_X = MIN_X + 20; const MAX_PLAYER_X = MAX_X - 20; const MIN_PLAYER_Y = MAX_Y * 0.4; const MAX_PLAYER_Y = MAX_Y * 0.70; // 自機の初期座標 const INIT_PLAYER_X = MAX_X / 2; const INIT_PLAYER_Y = MAX_Y * 0.6; // 自機と敵を球に見立てたときの半径 const CHARCTER_RADIUS = 4; // 3Dオブジェクトが描画されるcanvasのサイズ const MAIN_WIDTH = 360; const MAIN_HEIGHT = 280; // レーダーが描画されるcanvasのサイズ const RADAR_WIDTH = MAX_X; const RADAR_HEIGHT = MAX_Y; // 操作用のキーまたはボタンが押下されているかどうか? let pressLeftKey = false; let pressRightKey = false; let pressUpKey = false; let pressDownKey = false; let pressShotButton = false; // 更新処理がおこなわれた回数 let updateCount = 0; // 自機、敵、弾丸オブジェクトを格納する変数 let player = null; let playerBullets = []; let enemies = []; let enemyBullets = []; let boss = null; let sparks = []; // 爆発で生じた火花 let lineEWs = []; // フィールドに描画される水平線 let isPlaying = false; // プレイ中かどうか? let isGameovered = true; // ゲームオーバーになったかどうか? let noCreateEnemy = false; // ゲームオーバーになったかどうか? let preventDefault = false; // trueのときは矢印キーとスペースキーのデフォルトの動作が抑制される let score = 0; // スコア let stage = 1; // 現在のステージ const initRest = 5; let rest = initRest; // 残機数 let charging = false; // 現在エネルギーのチャージ中か? let charged = 0; // エネルギーはどれだけチャージされたか? (0~100) // ThreeJS関連を作成する const renderer = new THREE.WebGLRenderer({ canvas: $main_canvas }); const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(45, MAIN_WIDTH / MAIN_HEIGHT); const light = new THREE.AmbientLight(0xFFFFFF, 2.4); // 敵、弾丸、火花の3Dオブジェクトを作成するさいのマテリアルを格納する変数 let bulletMaterial = null; let enemyBulletMaterial = null; let enemyMaterials = []; let enemyBulletMaterial = null; let sparkMaterials = []; // 効果音 const soundShot = new Audio('./sounds/shot.mp3'); const soundHit = new Audio('./sounds/hit.mp3'); const soundSpecialShot = new Audio('./sounds/special-shot.mp3'); const soundCanNotShot = new Audio('./sounds/can-not-shot.mp3'); const soundHitCore = new Audio('./sounds/hit-core.mp3'); const soundDead = new Audio('./sounds/dead.mp3'); const soundGameover = new Audio('./sounds/gameover.mp3'); const bgm1 = new Audio('./sounds/bgm1.mp3'); const bgm2 = new Audio('./sounds/bgm2.mp3'); const sounds = [soundShot, soundDead, soundHit, soundHitCore, soundSpecialShot, soundCanNotShot, soundGameover, bgm1, bgm2, ]; |
isPlaying と isGameovered の変数があります。プレイ中でないならゲームオーバーの状態ではないのか?ひとつにまとめられるのではないのかと思うかもしれませんが、ゲームオーバーになってもしばらく更新処理をおこなわないといけないので2変数にわけています。
マテリアルを生成する
敵と弾丸、火花の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 |
function getGeometriesMaterials(){ const bulletTexture = new THREE.TextureLoader().load('./images/bullet.png'); bulletTexture.colorSpace = THREE.SRGBColorSpace; bulletMaterial = new THREE.SpriteMaterial({ map: bulletTexture }); const enemyBulletTexture = new THREE.TextureLoader().load('./images/enemy-bullet.png'); enemyBulletTexture.colorSpace = THREE.SRGBColorSpace; enemyBulletMaterial = new THREE.SpriteMaterial({ map: enemyBulletTexture }); let enemiesImages = ['./images/enemy0.png', './images/enemy1.png']; for(let i=0; i<enemiesImages.length; i++){ const texture = new THREE.TextureLoader().load(enemiesImages[i]); texture.colorSpace = THREE.SRGBColorSpace; enemyMaterials.push(new THREE.SpriteMaterial({ map: texture })); } let sparksImages = [ './images/spark0.png', './images/spark1.png', './images/spark2.png', './images/spark3.png', './images/spark4.png', './images/spark5.png', ]; for(let i=0; i<sparksImages.length; i++){ const texture = new THREE.TextureLoader().load(sparksImages[i]); texture.colorSpace = THREE.SRGBColorSpace; sparkMaterials.push(new THREE.SpriteMaterial({ map: texture })); } } |
Playerクラスの定義
自機を操作できるようにPlayerクラスを定義します。
コンストラクタ
自機はつねにひとつしかないのでアプリケーションが開始されたらすぐに生成し、それを使い回すことにします。自機死亡時は一時的に見えない場所に移動させることにします。
自機は平板を生成してその上にテクスチャを貼り付けます。敵や弾丸はつねに正面を向いた状態で描画したいのでスプライトを使用しますが、自機はやや水平に傾いた感じで描画したいのでこの方法を取ることにします。
座標はレーダーに描画するときの座標です。3Dオブジェクトの座標はX座標は同じですが、Y座標は基本的に0であり、Z座標はPlayer.Yとなります。また生成した3Dオブジェクトはすぐにsceneに追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Player { constructor(){ // これはレーダーに描画するときの座標 this.X = INIT_PLAYER_X; this.Y = INIT_PLAYER_Y; this.IsDead = false; this.CanShot = true; // 連射制限用 const texture = new THREE.TextureLoader().load('./images/player.png'); texture.colorSpace = THREE.SRGBColorSpace; const material = new THREE.MeshStandardMaterial({ map: texture, transparent: true }); const geometry = new THREE.PlaneGeometry(CHARCTER_RADIUS * 2 * 2, CHARCTER_RADIUS * 2 * 2); this.Mesh = new THREE.Mesh(geometry, material); this.Mesh.position.set(INIT_PLAYER_X, 0, INIT_PLAYER_Y); this.Mesh.rotation.x = -1.5; scene.add(this.Mesh); } } |
初期位置に戻す
自機死亡時に初期位置に戻す処理を示します。
1 2 3 4 5 6 7 |
class Player { Init(){ this.X = INIT_PLAYER_X; this.Y = INIT_PLAYER_Y; this.IsDead = false; } } |
移動
自機を移動させる処理を示します。
移動可能範囲内であれば押下されたボタンの方向に移動させています。また3DオブジェクトのY座標を生存時は 0、死亡時は見えない座標(-10000 とか)に設定しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Player { Move(){ if(pressLeftKey && this.X > MIN_PLAYER_X) this.X--; if(pressRightKey && this.X < MAX_PLAYER_X) this.X++; if(pressUpKey && this.Y > MIN_PLAYER_Y) this.Y--; if(pressDownKey && this.Y < MAX_PLAYER_Y) this.Y++; if(!this.IsDead) this.Mesh.position.set(player.X, 0, player.Y); else this.Mesh.position.set(player.X, -10000, player.Y); } } |
描画
自機をレーダーに描画する処理を示します。生存時に小さな正方形で描画させています。
1 2 3 4 5 6 7 8 9 10 |
class Player { Draw(){ if(!this.IsDead){ radar_ctx.fillStyle = '#fff'; radar_ctx?.beginPath(); radar_ctx?.rect(this.X, this.Y, CHARCTER_RADIUS * 1.2, CHARCTER_RADIUS * 1.2); radar_ctx?.fill(); } } } |
弾丸の発射
弾丸を発射する処理を示します。
このままだとキーを押しっぱなしにしているときは弾丸が連射され、線上につながってしまうので、連射は0.2秒以上開けないとできないようにしています。
charging == false のときは自機が敵要塞のコアを破壊できる距離まで接近してできていないときなので連射可能です。charging == true のときは敵要塞のコアを破壊できる距離まで接近しているときなのでエネルギーチャージが完了しているときのみ発射処理をおこないます。
またスマホだとこういうゲームはやりにくいので発射ボタンを押しっぱなしにしているときは連射できるようにしています。
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 |
class Player { Shot(){ if(player.IsDead || !this.CanShot) return; this.CanShot = false; setTimeout(()=> this.CanShot = true, 200); if(!charging) { soundShot.currentTime = 0; soundShot.play(); playerBullets.push(new PlayerBullet(player.X, player.Y)); // ボタンを押下している状態ならrepeatShot関数を250ミリ秒おきに呼び出して // 連射できるようにした if(pressShotButton){ function repeatShot(){ soundShot.currentTime = 0; soundShot.play(); playerBullets.push(new PlayerBullet(player.X, player.Y)); setTimeout(() => { if(!pressShotButton || player.IsDead) return; repeatShot(); }, 250); } setTimeout(() => { if(!pressShotButton || player.IsDead) return; repeatShot(); }, 250); } } else { if(charged == 100){ charged = 0; soundSpecialShot.currentTime = 0; soundSpecialShot.play(); playerBullets.push(new PlayerBullet(player.X, player.Y)); } else { soundCanNotShot.currentTime = 0; soundCanNotShot.play(); } } } } |
PlayerBulletクラスの定義
自機から発射された弾丸を操作できるようにPlayerBulletクラスを定義します。
コンストラクタ
コンストラクタを示します。コンストラクタの引数はレーダーに描画されるときの座標であり、3Dオブジェクトの座標ではありません(Y座標とZ座標が入れ替わる)。グローバル変数に格納されているマテリアルをつかってスプライトを生成し、sceneに追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class PlayerBullet { constructor(x, y){ // これはレーダーに描画するときの座標 this.X = x; this.Y = y; this.IsDead = false; this.IsFalling = false; this.Height = 0; // スプライトを生成する this.Sprite = new THREE.Sprite(bulletMaterial); this.Sprite.scale.set(4, 4, 4); this.Sprite.position.set(this.X, 0, this.Y); scene.add(this.Sprite); } } |
移動
弾丸を移動させる処理を示します。
向こうまで飛んでいった弾丸は地平線の向こう側に落下していきます。これを表現するためにレーダー上のY座標(3DオブジェクトのZ座標)が0になったら落下フラグをセットします。1更新あたり高度を 0.5 ずつ下げ、-10 まで下がったら死亡フラグをセットしてsceneから取り除きます。
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 |
class PlayerBullet { Move(){ if(this.IsFalling){ this.Height -= 0.5; this.Y = -5; if(this.Height < -10){ this.Dead(); return; } } if(this.IsDead) return; this.Sprite.position.set(this.X, this.Height, this.Y); this.Y -= 0.8; if(this.Y < 0) this.IsFalling = true; } Dead(){ this.IsDead = true; scene.remove(this.Sprite); } } |
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
class Enemy { constructor(isMoveDown){ this.IsDead = false; this.IsMoveDown = isMoveDown; // true なら敵はこちらへ向かってくる動き this.Height = 0; // 3DオブジェクトのY座標 this.IsFalling = false; // 3Dオブジェクトは地平線の向こうに落下中 this.IsRaising = false; // 3Dオブジェクトは地平線の向こうから上昇中 this.UpdateCount = 0; // 更新回数(敵が弾丸を発射するタイミングを決定するときに必要:後述) this.Angle = 0; this.RadiusOfRotationX = 25; // 敵の回転運動の半径 this.RadiusOfRotationY = 20; // 敵の回転運動の中心座標を決める // ±10 でレーダーの左右から少し外へ出ることを許容しないと自機の安全地帯ができてしまう const ctrRotMinX = this.RadiusOfRotationX - 10; const ctrRotMaxX = MAX_X - this.RadiusOfRotationX + 10; const ctrRotX = Math.random() * (ctrRotMaxX - ctrRotMinX) + ctrRotMinX; // this.IsMoveDownのときは地平線の向こうから上昇してくる演出が必要 if(this.IsMoveDown){ this.IsRaising = true; this.Height = -5; this.Angle = Math.PI / 2 * 3; this.CenterOfRotationX = ctrRotX; this.CenterOfRotationY = this.RadiusOfRotationY; } else { this.Angle = Math.PI / 2 * 1; this.CenterOfRotationX = ctrRotX; this.CenterOfRotationY = MAX_Y - this.RadiusOfRotationY; } // 敵のレーダー上の初期座標が決定するsceneへの追加 this.X = this.RadiusOfRotationX * Math.cos(this.Angle) + this.CenterOfRotationX; this.Y = this.RadiusOfRotationY * Math.sin(this.Angle) + this.CenterOfRotationY; // スプライトの生成と if(Math.random() < 0.5) this.Sprite = new THREE.Sprite(enemyMaterials[0]); else this.Sprite = new THREE.Sprite(enemyMaterials[1]); this.Sprite.scale.set(12, 12, 12); this.Sprite.position.set(this.X, this.Height, this.Y); scene.add(this.Sprite); } } |
移動
上昇中の敵は上昇させ、降下中の敵は降下させます。地平線の向こうに消えた敵は死亡フラグをセットしてsceneから取り除きます。
上昇中でも降下中でもない敵は回転運動をさせるとともに向かってくるか後ろから迫ってくるかに応じで回転の中心座標を変更します。
レーダーの上側から外へ出た敵は降下フラグをセットして下側から外へ出た敵は死亡フラグをセットしてsceneから取り除きます。
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 { Move(){ this.UpdateCount++; if(this.IsRaising){ this.Height += 0.25; if(this.Height > 0){ this.Height = 0; this.IsRaising = false; return; } } if(this.IsFalling){ this.Height -= 0.5; this.Y = -5; if(this.Height < -10){ this.Dead(); return; } } this.Angle += 0.01; this.X = this.RadiusOfRotationX * Math.cos(this.Angle) + this.CenterOfRotationX; this.Y = this.RadiusOfRotationY * Math.sin(this.Angle) + this.CenterOfRotationY; if(this.IsMoveDown) this.CenterOfRotationY += 0.02; else this.CenterOfRotationY -= 0.02; if(this.Y > MAX_Y + 10) this.Dead(); if(this.Y < MIN_Y) this.IsFalling = true; if(this.IsDead) return; this.Sprite.position.set(this.X, this.Height, this.Y); } Dead(){ this.IsDead = true; scene.remove(this.Sprite); } } |
描画
レーダー上に描画する処理を示します。
1 2 3 4 5 6 7 8 9 10 11 |
class Enemy { Draw(){ if(this.IsDead) return; radar_ctx.fillStyle = '#f00'; radar_ctx?.beginPath(); radar_ctx?.rect(this.X, this.Y, CHARCTER_RADIUS * 1.2, CHARCTER_RADIUS * 1.2); radar_ctx?.fill(); } } |
EnemyBulletクラスの定義
敵弾を操作するためにEnemyBulletクラスを定義します。やることはPlayerBulletクラスと似ていますが、進行速度、弾丸の進行方向が上下存在すること、視認性を考慮して弾丸の大きさを変えながら進行する部分が異なります。
コンストラクタ
コンストラクタを示します。メンバ変数として IsMoveDown と UpdateCount が増えました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class EnemyBullet { constructor(x, y, isMoveDown){ this.X = x; this.Y = y; this.IsMoveDown = isMoveDown; this.IsDead = false; this.IsFalling = false; this.Height = 0; this.UpdateCount = 0; this.Sprite = new THREE.Sprite(enemyBulletMaterial); this.Sprite.scale.set(4, 4, 4); this.Sprite.position.set(this.X, 0, this.Y); scene.add(this.Sprite); } } |
移動
向こう側へ向かって移動する弾丸の処理はPlayerBulletクラスとほぼ同じです。また更新回数に応じで弾丸の描画サイズを大きくしたり小さくしたりします(描画のみで当たり判定は変更しない)。
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 |
class EnemyBullet { Move(){ this.UpdateCount++; // このあたりはPlayerBulletクラスとほぼ同じ if(this.IsFalling){ this.Height -= 0.5; this.Y = -5; if(this.Height < -10){ this.Dead(); return; } } if(this.Y < 0) this.IsFalling = true; if(this.IsMoveDown) this.Y += 0.5; else this.Y -= 0.5; if(this.Y > MAX_Y) this.Dead(); if(this.IsDead) return; if(this.UpdateCount % 16 < 8) this.Sprite.scale.set(4, 4, 4); else this.Sprite.scale.set(6, 6, 6); this.Sprite.position.set(this.X, this.Height, this.Y); } Dead(){ this.IsDead = true; scene.remove(this.Sprite); } } |
Sparkクラスの定義
爆発にともなって生じた火花を操作するためにSparkクラスを定義します。
コンストラクタ
コンストラクタの引数は火花の発生場所の座標と移動速度とタイプです。火花はレーダーには描画しないのでコンストラクタの引数の x, y, z は3DオブジェクトのXYZ座標です。
ザコ敵を倒したとき、敵要塞を破壊したとき、自機が破壊されたときで火花のタイプを変えます。ザコ敵を倒したときと比べて自機が破壊されたときは大きな爆発にします(テクスチャの切り替えを遅くすることで長時間表示されつづけるようにする)。また敵要塞が爆発するときは遠い場所なので火花自体を大きめのサイズにします。
爆発は同じテクスチャを使用するのではなく更新回数で別のものを使います。
なのでコンストラクタ内で一気に複数のスプライトを生成して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 |
class Spark{ constructor(x, y, z, vx, vy, vz, type){ this.X = x; this.Y = y; this.Z = z; this.VX = vx; this.VY = vy; this.VZ = vz; this.Type = type; this.UpdateCount = 0; this.IsDead = false; this.CurSpriteIndex = 0; // スプライトを生成する this.Sprites = []; for(let i=0; i<sparkMaterials.length; i++){ const sprite = new THREE.Sprite(sparkMaterials[i]); if(this.Type == 0 || this.Type == 2) sprite.scale.set(12, 12, 12); if(this.Type == 1) sprite.scale.set(24, 24, 24); this.Sprites.push(sprite); } this.Sprites[this.CurSpriteIndex].position.set(this.X, this.Y, this.Z); scene.add(this.Sprites[this.CurSpriteIndex]); } } |
移動
火花を移動させる処理を示します。更新回数によって使用するスプライトを変更します。コンストラクタ内で用意したすべてのスプライトを使い切ったらその火花は死亡フラグをセットしてsceneから取り除きます。
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 Spark{ Move(){ if(this.IsDead) return; this.UpdateCount++; this.X += this.VX; this.Y += this.VY; this.Z += this.VZ; let mod = 4; if(this.IsLong) mod = 8; // スプライトを入れ替える if(this.UpdateCount % mod == 0){ const oldIndex = this.CurSpriteIndex; this.CurSpriteIndex++; scene.remove(this.Sprites[oldIndex]); if(this.Sprites.length > this.CurSpriteIndex) scene.add(this.Sprites[this.CurSpriteIndex]); else this.IsDead = true; // 入れ替えるものがなくなったら消滅させる } if(!this.IsDead) this.Sprites[this.CurSpriteIndex].position.set(this.X, this.Y, this.Z); } Dead(){ this.IsDead = true; scene.remove(this.Sprites[this.CurSpriteIndex]); } } |
Bossクラスの定義
敵要塞を操作するためにBossクラスを定義します。
コンストラクタ
敵要塞は最初は地平線の向こうに隠れていて、自機が進行すると地平線の向こうから顔をだします。そこで地平線の向こう側にある敵要塞の下側が見えないように背景色(黒)と同じ色の板で隠します。
敵要塞にはコアがあります。この部分は自機が射撃可能な距離まで接近するまでは見えないように見えない位置にセットします。
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 |
class Boss { constructor(){ this.PositionZ = -50; const texture1 = new THREE.TextureLoader().load('./images/boss.png'); texture1.colorSpace = THREE.SRGBColorSpace; const material1 = new THREE.MeshStandardMaterial({ map: texture1, transparent: true }); const geometry1 = new THREE.PlaneGeometry(120, 120); this.Mesh = new THREE.Mesh(geometry1, material1); this.Mesh.position.set(MAX_X / 2, -150, this.PositionZ); scene.add(this.Mesh); const texture2 = new THREE.TextureLoader().load('./images/core.png'); texture2.needsUpdate = true; const material2 = new THREE.SpriteMaterial({ map: texture2}); this.CoreSprite = new THREE.Sprite(material2); this.CoreSprite.scale.set(12, 12, 12); this.CoreSprite.position.set(MAX_X / 2 + 0, -1000, -10); // -10 scene.add(this.CoreSprite); // 地平線の向こう側にある要塞の下部を隠す遮蔽物 const material3 = new THREE.MeshBasicMaterial({color: 0x000000}); const geometry3 = new THREE.PlaneGeometry(300, 300); this.CoverMesh = new THREE.Mesh(geometry3, material3); this.CoverMesh.position.set(MAX_X / 2, -155, 10); this.CoreMoveRight = true; scene.add(this.CoverMesh); this.CoreMoveRight = true; // コアは最初右方向に移動する } } |
初期化
Init関数は要塞の初期のY座標(3DオブジェクトのY座標)を設定します。ステージ数 + 1 に -50 を掛け合わせた値を設定します。
1 2 3 4 5 6 7 8 9 10 11 12 |
class Boss { Init(stage){ this.Mesh.position.set(MAX_X / 2, -50 * (1 + stage), this.PositionZ); // コアを見えない位置に移動(X座標は中央の値) this.CoreSprite.position.set(MAX_X / 2 + 0, -1000, -10); this.CoreMoveRight = true; // 遮蔽板をもとの位置に戻す this.CoverMesh.position.set(MAX_X / 2, -155, 10); } } |
移動
要塞を地平線の向こうから徐々に現れてくるようにY座標を増やす処理を示します。要塞のY座標が +10 に満たないときは自機が要塞のコアを射撃可能な位置まで接近できていないのでコアは見えない位置に配置します(3DオブジェクトのY座標を -10000 のような極端に小さい値にする)。射撃可能な場合は -10(要塞本体の中心よりも少し下)に配置します。そして更新処理がおこなわれるたびに左右に移動させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class Boss { Move(){ if(this.Mesh.position.y < 10){ this.Mesh.position.y += 0.04; this.CoreSprite.position.y = -1000; this.CoverMesh.position.set(MAX_X / 2, -155, 10); } else { this.Mesh.position.y = 10; this.CoreSprite.position.y = -10; this.CoverMesh.position.set(MAX_X / 2, -10000, 10); } if(this.CoreMoveRight && this.CoreSprite.position.x > MAX_X / 2 + 36) this.CoreMoveRight = false; if(!this.CoreMoveRight && this.CoreSprite.position.x < MAX_X / 2 - 36) this.CoreMoveRight = true; if(this.CoreMoveRight) this.CoreSprite.position.x += 0.8; else this.CoreSprite.position.x -= 0.8; } } |
自機が敵弾に当たってしまった場合は要塞から少し遠ざかった場所から再スタートとなります。KeepAway関数はそのときのために要塞を遠ざける処理をおこないます。
1 2 3 4 5 6 7 8 9 |
class Boss { KeepAway(disance){ // 要塞を遠ざける(地平線の位置から下げる) this.Mesh.position.y -= disance; // コアを射撃する距離よりも遠ざかってしまうのでコアは見えない位置に隠す this.CoreSprite.position.y = -1000; } } |
爆発の処理
要塞を爆発させる処理を示します。
要塞を爆発させるときは爆発の発生 × 2、要塞の消滅、爆発の発生の処理をおこないまs
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 Boss { async Explode(){ const x = this.CoreSprite.position.x; const z = this.CoreSprite.position.z; bossExplode(x, 0, z); // 爆発 await new Promise(resolve => setTimeout(resolve, 300)); // 待機 bossExplode(x + Math.random() * 100 - 50, Math.random() * 50, z); // 爆発 await new Promise(resolve => setTimeout(resolve, 300)); // 待機 // 本体とコアを見えない位置へ this.Mesh.position.set(MAX_X / 2, -1000, this.PositionZ); this.CoreSprite.position.y = -1000; bossExplode(x + Math.random() * 100 - 50, Math.random() * 50, z); // 爆発 function bossExplode(x, y, z){ for(let i=0; i<12; i++){ const speed = Math.random() + 0.5; const angle = Math.random() * Math.PI * 2; sparks.push(new Spark(x, y, z, speed * Math.cos(angle), speed * Math.sin(angle), 1, 1)); } } } } |
座標の取得
要塞本体とコアのX座標またはY座標を取得する関数を示します。この関数は外部から要塞本体とコアの座標を取得したいときに呼び出します。
1 2 3 4 5 6 7 8 9 10 11 |
class Boss { GetCoreX(){ return this.CoreSprite.position.x; } GetCoreHeight(){ return this.CoreSprite.position.y; } GetBossHeight(){ return this.Mesh.position.y; } } |
長くなってしまった(汗)。続きは次回とします。