前回 ASP.NET Core版 戦車対戦ゲームをつくる(3)の続きです。
Contents
cshtmlファイル
最初にgame.cshtmlファイルの内容を示します。ファイルの場所ですが、(ウェブアプリのurl)/Tank/gameにしたいのでPages\Tankフォルダ内にgame.cshtmlという名前でファイルを作成します。
またPages\Shared\_Layout_none.cshtmlの内容はクライアントサイドの処理 ASP.NET Coreでクラッシュローラーをつくる(3)を参照してください。
Pages\Tank\game.cshtml
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 |
@page @{ ViewData["Title"] = "戦車対戦ゲーム"; Layout = "_Layout_none"; string baseurl = ウェブアプリのurl; } <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/110/three.min.js"></script> <div style="position: relative; overflow: hidden;"> <canvas id="can"></canvas> <div id="score" style="position: absolute; top: 0; left: 0; background: white"></div> <div id="info" style="position: absolute; top: 0; left: 0; background: white"></div> <br> <p>遊び方</p> <p>方向転換 ← と →<br> 前進 ↑ (後退はできません。方向転換して前進してください)<br> 砲撃 spaceキー(初期位置からは砲撃できません。一度前進してからspaceキーを押してください) </p> <form name="form1"> <input type="checkbox" value="音を出す" id="SoundCheckbox">音を出す <label>ハンドルネーム</label> <input type="text" id="player-name" maxlength='16' /><br> <input type="button" value="ゲームスタート" onclick="GameStart()" style="margin-top:15px;margin-bottom:15px;"> </form> <p><a href="./hi-score">トップ30を見る</a></p> <p id = "conect-result"></p> <p id = "pos-result"></p> </div> <script src="@baseurl/js/signalr.js"></script> <script> let connection = new signalR.HubConnectionBuilder().withUrl("@baseurl/TankHub").build(); </script> <script src="@baseurl/tank/app.js"></script> |
JavaScript部分
次にJavaScript部分ですが、最初にグローバル変数と定数部分を示します。
wwwroot\tank\app.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 |
const CANVAS_WIDTH = 640; const CANVAS_HEIGHT = 480; const MAX_PLAYERS = 7; const CHARACTER_SIZE = 24; const CAMERA_DISTANCE = 40; // シーン、カメラ、ライト、レンダラー let scene; let camera; let light; let renderer; // 3Dオブジェクト(自機、敵機、砲弾) let myTank; let rivalTanks = []; let bullets = []; let id; // 接続ID // 更新処理に必要な値を格納しておくための変数 // 自機の座標と回転角、プレイヤー名、死亡フラグ let x; let z; let ry; let name; let isDead; // 敵機の座標と回転角、プレイヤー名、死亡フラグ let xs = []; let zs = []; let rys = []; let isDeads = []; // 砲弾の座標 let bxs = []; let bzs = []; // 非表示にしたい壁の座標 let hideXs = []; let hideZs = []; // 壁(Wallオブジェクト)の配列 let walls = []; let explosions = []; // 効果音再生用 let soundShot = new Audio('../tank/shot.mp3'); let soundBomb = new Audio('../tank/bomb.mp3'); let soundDead = new Audio('../tank/dead.mp3'); // キーは押されているか? let isUpKeyDown = false; let isDownKeyDown = false; let isLeftKeyDown = false; let isRightKeyDown = false; // スコア、残機など let score = 0; let rest = 0; let isGameOver = false; |
次に壁と爆発の火花のようなものは表示と非表示を繰り替えすのでクラスを定義して管理します。
Wallクラス
Wallクラスを定義します。
this.Objectに格納するのはnew THREE.Mesh()で生成された3Dオブジェクトです。破壊されたオブジェクトはsceneから除去してしまって問題ないのですが、一時的に非表示にしたい場合はY座標を負の値にして地面の下に移動させて見えなくします。見えるようにするときはもとのY座標に移動させています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Wall { constructor(obj, x, z) { this.Object = obj; // obj は new THREE.Mesh()で生成された3Dオブジェクト this.X = x; this.Z = z; } Break() { scene.remove(this.Object); } Show() { this.Object.position.y = 0; } Hide() { this.Object.position.y = -50; } } |
Explosionクラス
Explosionクラスを定義します。
火花のようなものを正方形のオレンジ色の板で表現します。爆発が発生したら15個の板を生成してランダムな方向に飛ばします。そして24回(約1秒)移動させたらsceneからremoveします。
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 |
const explosionGeometry = new THREE.BoxGeometry(2, 0.5, 2); const explosionMaterial = new THREE.MeshStandardMaterial({ color: 0xff4500 }); class Explosion { constructor(x, z) { this.pieces = []; this.Finished = false; for (let i = 0; i < 15; i++) { const piece = new THREE.Mesh(explosionGeometry, explosionMaterial); piece.position.x = x; piece.position.y = -12; piece.position.z = z; this.pieces.push(piece); scene.add(piece); } this.MoveCount = 0; // それぞれにランダムな初速を与える this.explosionIVXs = []; this.explosionIVZs = []; this.explosionIRXs = []; this.explosionIRYs = []; this.explosionIRZs = []; for (let i = 0; i < this.pieces.length; i++) { this.explosionIVXs.push(Math.random() * 1.6 - 0.8); this.explosionIVZs.push(Math.random() * 1.6 - 0.8); this.explosionIRXs.push(Math.random() * Math.PI * 2); this.explosionIRYs.push(Math.random() * Math.PI * 2); this.explosionIRZs.push(Math.random() * Math.PI * 2); } } Move() { if (this.Finished) return; this.MoveCount++; for (let i = 0; i < this.pieces.length; i++) { // 周囲へ広がる this.pieces[i].position.x += this.explosionIVXs[i]; this.pieces[i].position.z += this.explosionIVZs[i]; // 最初は上へ移動させそのあと落下させる if (this.MoveCount < 8) this.pieces[i].position.y += 3; else this.pieces[i].position.y -= 3; // 爆発しているようにみせるために回転もいれる this.pieces[i].rotation.x += this.explosionIRXs[i]; this.pieces[i].rotation.y += this.explosionIRYs[i]; this.pieces[i].rotation.z += this.explosionIRZs[i]; } // 24回移動処理をしたら消滅させる if (this.MoveCount > 24) this.Finish(); } Finish() { if (this.Finished) return; for (let i = 0; i < this.pieces.length; i++) { scene.remove(this.pieces[i]); } this.Finished = true; } } |
戦車と壁、砲弾を描画するためのジオメトリとマテリアル
次に戦車と壁、砲弾を描画するためのジオメトリとマテリアルを生成する処理を示します。壁はレンガを積んで作られているようにみえるように適当な画像ファイルを事前に作成しておき、読み込めるようにしておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
const wallGeometry = new THREE.BoxGeometry(CHARACTER_SIZE / 2, CHARACTER_SIZE, CHARACTER_SIZE / 2); const bulletGeometry = new THREE.SphereGeometry(1, 32, 32); // 破壊できる壁のマテリアル const loader = new THREE.TextureLoader(); const wallTexture = loader.load('../tank/wall.png'); const wallMaterial = new THREE.MeshStandardMaterial({ map: wallTexture }); const borderTexture = loader.load('../tank/border.png'); // 破壊できない壁のマテリアル const borderMaterial = new THREE.MeshStandardMaterial({ map: borderTexture }); // 砲弾のマテリアル const bulletMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff }); |
ページが読み込まれたときの処理
ページが読み込まれたときの処理を示します。
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 |
window.addEventListener('load', Init); function Init() { // キャンバスの位置を微調整 document.getElementById('can').style.marginTop = '-100px'; // シーンを作成 scene = new THREE.Scene(); // カメラを作成 camera = new THREE.PerspectiveCamera(45, CANVAS_WIDTH / CANVAS_HEIGHT, 1, 10000); // 平行光源 light = new THREE.DirectionalLight(0xffffff); light.intensity = 2; // 光の強さを倍に light.position.set(1, 1, 1); scene.add(light); // 環境光源 const light2 = new THREE.AmbientLight(0xFFFFFF, 0.2); scene.add(light2); // レンダラーを作成 renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('can') }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(CANVAS_WIDTH, CANVAS_HEIGHT); // 戦車(自機)を生成し、証明がつねに自機の方向を向くようにする myTank = AddMyTank(scene); // 後述 myTank.rotation.y = Math.PI / 2; // 自機を回転させて後ろから撮影しているようにする light.target = myTank; // 戦車(敵機)を生成する rivalTanks = AddRivalTank(scene); // 後述 // 砲弾(各戦車1個)を生成する bullets = AddBullets(scene); // 後述 // 自機の後ろにカメラをセット camera.position.set(-CAMERA_DISTANCE, 12, 0); camera.rotateY(-90 / 180 * Math.PI); // 地面を作成する AddGround(scene); // 後述 renderer.render(scene, camera); SetVolume(0.06); // 効果音のボリューム 0.06あたりがよいらしい(0なら無音 最大値は1) } function SetVolume(volume) { if (volume < 0) volume = 0; if (volume > 1.0) volume = 1; soundShot.volume = volume; soundBomb.volume = volume; soundDead.volume = volume; } |
戦車を生成する
戦車を生成する関数を示します。自機と敵の戦車は形は同じ、色だけ変えます。
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 |
function CreateTank(isEnemy) { const tank = new THREE.Object3D(); let material; if (!isEnemy) material = new THREE.MeshStandardMaterial({ color: 0x0000ff }); else material = new THREE.MeshStandardMaterial({ color: 0xff0000 }); // 本体 { const geometry = new THREE.BoxGeometry(16.2, 7.8, 20); let tankPart = new THREE.Mesh(geometry, material); tankPart.position.set(0, -3.1, -1.3); tank.add(tankPart); // 任意のObject3Dを追加 } // 砲塔 { const geometry = new THREE.BoxGeometry(9.7, 6.5, 9.7); let tankPart = new THREE.Mesh(geometry, material); tankPart.position.set(0, 4.7, -1.6); tank.add(tankPart); } // 車体 { const geometry = new THREE.BoxGeometry(20.8, 0.65, 24); let tankPart = new THREE.Mesh(geometry, material); tankPart.position.set(0, -6.66, 0); tank.add(tankPart); } // 砲身 { const geometry = new THREE.CylinderGeometry(1.3, 1.3, 13, 32, 32, false); const cylinder = new THREE.Mesh(geometry, material); cylinder.position.set(0, 4.7, 8.1); cylinder.rotation.x = 1.5; tank.add(cylinder); } // 車輪 { const geometry = new THREE.CylinderGeometry(2.6, 2.6, 4.5, 16, 16, false); const material = new THREE.MeshStandardMaterial({ color: 0xcccccc }); let zs = [9.7, 3.3, -3.3, -9.7]; for (let i = 0; i < 4; i++) { for (let k = 0; k < 2; k++) { let a = k == 0 ? 1 : -1; const cylinder = new THREE.Mesh(geometry, material); cylinder.rotation.z = Math.PI / 2; cylinder.position.set(a * 7.8, -8.9, zs[i]); tank.add(cylinder); } } } // 底 { const geometry = new THREE.BoxGeometry(19.5, 0.16, 19.5); const material = new THREE.MeshStandardMaterial({ color: 0x000000 }); let tankPart = new THREE.Mesh(geometry, material); tankPart.position.set(0, -11.5, 0); tank.add(tankPart); } return tank; } |
自機と敵の戦車、砲弾を生成する関数を示します。いずれも一度しか呼び出されない関数です。一度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 |
function AddMyTank(scene) { let tank = CreateTank(false); tank.position.set(0, 0, 0); scene.add(tank); return tank; } function AddRivalTank(scene) { let tanks = []; for (let i = 0; i < MAX_PLAYERS - 1; i++) { let tank = CreateTank(true); tank.position.set(0, -100, 0); scene.add(tank); tanks.push(tank); } return tanks; } function AddBullets(scene) { let bullets = []; for (let i = 0; i < MAX_PLAYERS; i++) { let bullet = new THREE.Mesh(bulletGeometry, bulletMaterial); bullet.position.set(0, -100, 0); scene.add(bullet); bullets.push(bullet); } return bullets; } |
壁を生成する
sceneに壁を追加する関数を示します。これも一度しか呼び出されない関数です。戦車と砲弾同様、一度3Dオブジェクトをつくったら同じものを使い回します。また3DオブジェクトはWallクラス内に格納して表示、非表示の切り替えができるようにしておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// 破壊可能な壁 function AddWalls1(scene, wallPosXs, wallPosZs) { for (let i = 0; i < wallPosXs.length; i++) { let wall = new THREE.Mesh(wallGeometry, wallMaterial); let x = Number(wallPosXs[i]); let z = Number(wallPosZs[i]); wall.position.set(x, 0, z); scene.add(wall); walls.push(new Wall(wall, x, z)); } } // 破壊できない壁 function AddWalls2(scene, wallPosXs, wallPosZs) { for (let i = 0; i < wallPosXs.length; i++) { let wall = new THREE.Mesh(wallGeometry, borderMaterial); let x = Number(wallPosXs[i]); let z = Number(wallPosZs[i]); wall.position.set(x, 0, z); scene.add(wall); walls.push(new Wall(wall, x, z)); } } |
以下は引数で指定された座標にある壁を表示させる関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function ShowWalls(wallPosXs, wallPosZs) { for (let i = 0; i < wallPosXs.length; i++) { let x = Number(wallPosXs[i]); let z = Number(wallPosZs[i]); for (let k = 0; k < walls.length; k++) { if (walls[k].X == x && walls[k].Z == z) { walls[k].Show(); break; } } } } |
以下はhideXs配列とhideZs配列に格納されている座標の壁を非表示にする関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// 非表示にされている壁を保存する let hidenWalls = []; function HideBackWalls() { // いったん現在非表示の壁をすべて表示させる for (let i = 0; i < hidenWalls.length; i++) { hidenWalls[i].Show(); } // 配列を空にしてhideXsとhideZsに対応する壁を探してこれを非表示にするとともに配列に格納する hidenWalls = []; for (let i = 0; i < hideXs.length; i++) { let x = Number(hideXs[i]); let z = Number(hideZs[i]); for (let k = 0; k < walls.length; k++) { if (walls[k].X == x && walls[k].Z == z) { walls[k].Hide(); hidenWalls.push(walls[k]); break; } } } } |
地面を生成
以下は地面をsceneに追加する関数です。戦車のY座標を0にしているので地面のY座標は戦車の高さの半分だけ下に設定します。
1 2 3 4 5 6 7 8 |
function AddGround(scene) { const geometry = new THREE.BoxGeometry(1200, 1, 1200); const material = new THREE.MeshBasicMaterial({ color: 0xaaaaaa, side: THREE.DoubleSide }); const ground = new THREE.Mesh(geometry, material); ground.position.set(0, -CHARACTER_SIZE / 2, 0); scene.add(ground); } |
更新処理
描画されているものを更新するための関数を示します。ゲームオーバー以降は更新の必要がないのでなにもしません。またゲームが開始される前も更新処理は必要ないので最初の条件式でreturnしています。
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 |
function Update() { if (connectionID == '' || isGameOver) return; // スコア、残機を表示 const scoreText = 'SCORE ' + ZenkakuToHankaku(String(score).padStart(5, '0')); const tf1 = document.getElementById("score"); tf1.style.transform = "translate(30px, 10px)"; tf1.style.backgroundColor = "#00008B"; tf1.style.color = "white"; tf1.style.fontSize = "24px"; tf1.style.fontFamily = 'MS ゴシック'; tf1.innerHTML = scoreText + '<span style = "margin-left:100px;">残 ' + ZenkakuToHankaku(String(rest) + '</span>'); // ゲームオーバーが確定したら 'GAME OVER'と表示し、そのあとの更新処理はなにもしない if (rest <= 0) { const tf2 = document.getElementById("info"); tf2.style.transform = "translate(30px, 50px)"; tf2.style.backgroundColor = "#00008B"; tf2.style.color = "white"; tf2.style.fontSize = "18px"; tf2.style.fontFamily = 'MS ゴシック'; tf2.innerHTML = 'GAME OVER'; isDead = true; isGameOver = true; } // 自機の座標と回転角を変更 myTank.position.x = x; myTank.position.z = z; myTank.rotation.y = ry + Math.PI / 2; // 自機死亡時は非表示にしたいので地下に移動 if (!isDead) myTank.position.y = 0; else myTank.position.y = -100; // 自機を後ろから撮影しているように見せるためにカメラの位置を変更 camera.position.z = myTank.position.z + CAMERA_DISTANCE * Math.sin(myTank.rotation.y - Math.PI / 2); camera.position.x = myTank.position.x - CAMERA_DISTANCE * Math.cos(myTank.rotation.y - Math.PI / 2); camera.lookAt(new THREE.Vector3(myTank.position.x, 10, myTank.position.z)); camera.position.y = 14; // ライトも自機を後ろに移動 light.position.z = myTank.position.z + CAMERA_DISTANCE * Math.sin(myTank.rotation.y - Math.PI / 3); light.position.x = myTank.position.x - CAMERA_DISTANCE * Math.cos(myTank.rotation.y - Math.PI / 3); // 非表示にしたい壁を非表示に HideBackWalls(); // 爆発が発生しているのであれば移動させる // 爆発が終了しているのであればexplosions配列から取り除く let exists = []; for (let i = 0; i < explosions.length; i++) { if (!explosions[i].Finished) { explosions[i].Move(); exists.push(explosions[i]); } } explosions = exists; // 敵の戦車も自機と同様の処理で移動させる // xs[i] == undefinedなら存在しないので3Dオブジェクトは地下に移動する for (let i = 0; i < MAX_PLAYERS - 1; i++) { if (xs[i] != undefined) { rivalTanks[i].position.x = xs[i]; if(!isDeads[i]) rivalTanks[i].position.y = 0; else rivalTanks[i].position.y = -100; rivalTanks[i].position.z = zs[i]; rivalTanks[i].rotation.y = rys[i] + Math.PI / 2; } else rivalTanks[i].position.y = -100; } // 砲弾を移動させる // bxs[i] == undefinedならその砲弾は存在しないので3Dオブジェクトを地下に移動する for (let i = 0; i < MAX_PLAYERS; i++) { if (bxs[i] != undefined) { bullets[i].position.x = bxs[i]; bullets[i].position.y = 5; bullets[i].position.z = bzs[i]; } else bullets[i].position.y = -100; } // 自機の座標を表示する(デバッグ用) document.getElementById('pos-result').innerHTML = `X = ${myTank.position.x}, Z = ${myTank.position.z}`; // レンダリング renderer.render(scene, camera); } |