ASP.NET Coreで3DのSpaceWar!のような対戦型ゲームをつくる(3)の続きです。クライアントサイドの処理を実装します。
Contents
cshtmlファイル
Pagesフォルダ内にSpaceWarフォルダを作り、そのなかにgame.cshtmlという名前でファイルを作成します。
Pages\SpaceWar\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 |
@page @{ ViewData["Title"] = "鳩でもわかるスペースウォーもどき"; Layout = "_Layout_none"; string baseurl = Global.BaseUrl; } <div id= "left"> <canvas id="can"></canvas> <p>遊び方</p> <p>移動:← → ↑ ↓キー<br> 攻撃:スペースキー </p> <input type="checkbox" value="音を出す" id="sound-checkbox">音を出す <label>ハンドルネーム</label> <input type="text" id="player-name" maxlength='16' /><br> <input type="checkbox" value="↑下降 ↓上昇とする" id="inversion-checkbox">↑下降 ↓上昇とする<br> <p id="can-start"></p> <input type="button" id="startButton1" value="ゲームスタート" onclick="GameStart()" style="margin-top:15px;margin-bottom:15px;"> <input type="button" id="showTop30Button1" onclick="location.href='./hi-score'" value="上位30位をチェック"> <p id = "conect-result"></p> </div> <div id= "right"> <p id = "score"></p> <canvas id = "radar-canvas"></canvas> <p>レーダーの()内は高度</p> <p id = "player-position"></p> <p id = "players-info"></p> <p id = "notify"></p> </div> <script> let connection = new signalR.HubConnectionBuilder().withUrl("@baseurl/SpaceWarHub").build(); let base_url = "@baseurl"; </script> <script src="https://unpkg.com/three@0.137.4/build/three.min.js"></script> <script src="@baseurl/space-war/space-war.js"></script> |
JavaScript部分
グローバル変数と定数
JavaScriptのグローバル変数と定数部分を示します。
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 |
// ゲームスタートのボタンの表示非表示を切り替えられるようにする let startButton1 = document.getElementById('startButton1'); let showTop30Button1 = document.getElementById('showTop30Button1'); startButton1.style.display = 'none'; showTop30Button1.style.display = 'none'; // メインの画面の大きさ let can = document.getElementById('can') const CANVAS_WIDTH = 480; const CANVAS_HEIGHT = 420; // レーダー部分の大きさ const RADAR_CANVAS_WIDTH = 400; const RADAR_CANVAS_HEIGHT = 300; const RADAR_CANVAS_INNER_WIDTH = 300; const RADAR_CANVAS_INNER_HEIGHT = 300; let ctxRadar; // シーンを作成 const scene = new THREE.Scene(); let renderer; let cameraDistance = 2000; // カメラの視界 let camera; let textFields = []; // 効果音 let bgm = new Audio(base_url + '/space-war/bgm.mp3'); // BGM let isPlayingBgm = false; // BGMは再生中か? let shotSound = new Audio(base_url + '/space-war/shot.mp3'); // 弾丸発射 let beingAttackedSound = new Audio(base_url + '/space-war/being-attacked.mp3'); // NPCからの攻撃 let enemyDeadSound = new Audio(base_url + '/space-war/enemy-dead.mp3'); // 敵を撃破 let damageSound = new Audio(base_url + '/space-war/damage.mp3'); // ダメージ let deadSound = new Audio(base_url + '/space-war/dead.mp3'); // 自機死亡時 let gameoverSound = new Audio(base_url + '/space-war/gameover.mp3'); // ゲームオーバー |
初期化の処理
ページが読み込まれたら初期化の処理をおこないます。
ここでやることはレイアウトの初期化、効果音やBGMをエンドレスで再生するためのイベントリスナーの追加、ThreeJSをつかって3D描画をするために必要な3Dオブジェクトの追加、カメラ、ライトの初期化、3D描画をするときに敵のプレイヤー名を表示するためのテキストフィールドの生成、レーダーを表示する部分の初期化などです。これらが完了したらAspNetCore.SignalRで接続を開始します。
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 |
window.addEventListener('load', Init); function Init() { // レイアウトの初期化 let leftElement = document.getElementById('left'); leftElement.style.marginLeft = '20px'; leftElement.style.marginTop = '20px'; leftElement.style.position = 'relative'; leftElement.style.float = 'left'; leftElement.style.overflow = 'hidden'; let rightElement = document.getElementById('right'); rightElement.style.float = 'left'; rightElement.style.marginLeft = '20px'; rightElement.style.marginTop = '20px'; // 効果音関連の初期化 bgm.addEventListener("ended", function () { isPlayingBgm = false; }, false); beingAttackedSound.addEventListener("ended", function () { beingAttackedSound.currentTime = 0; }, false); setInterval(() => { if (IsSound()) { if (!isPlayingBgm || bgm.currentTime > 80) { bgm.currentTime = 0; bgm.play(); isPlayingBgm = true; } } else StopBgm(); }, 500); SetVolumes(0.02); document.getElementById('sound-checkbox').checked = true; // ThreeJS関連の初期化 can.width = CANVAS_WIDTH; can.height = CANVAS_HEIGHT; // レンダラーを作成 renderer = new THREE.WebGLRenderer({ canvas: can }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(CANVAS_WIDTH, CANVAS_HEIGHT); AddPlayerShip(); AddEnemyShips(); AddBullets(); AddSparks(); AddLines(); AddLights(); // カメラを作成 camera = new THREE.PerspectiveCamera(45, CANVAS_WIDTH / CANVAS_HEIGHT, 1, cameraDistance); camera.position.set(0, 0, 0); camera.position.set(0, 0, 500); renderer.render(scene, camera); // テキストフィールドの初期化 for (let i = 0; i < ENEMIES_MAX; i++) { let textField = document.createElement('div'); textField.style.position = 'absolute'; textField.style.color = 'white'; textField.style.top = 0; textField.style.left = 0; textField.innerHTML = ''; leftElement.appendChild(textField); textFields.push(textField); } // レーダーの初期化 let radarCanvas = document.getElementById('radar-canvas'); radarCanvas.width = RADAR_CANVAS_WIDTH; radarCanvas.height = RADAR_CANVAS_HEIGHT; ctxRadar = radarCanvas.getContext('2d'); ctxRadar.fillStyle = "#000000"; ctxRadar.fillRect(0, 0, RADAR_CANVAS_WIDTH, RADAR_CANVAS_HEIGHT); ctxRadar.fillStyle = "#000080"; ctxRadar.fillRect(0, 0, RADAR_CANVAS_INNER_WIDTH, RADAR_CANVAS_INNER_HEIGHT); connection.start().catch(function (err) { document.getElementById("conect-result").innerHTML = '接続失敗'; }); } |
効果音を再生するときのボリュームの調整をしています。効果音が再生されるのは、プレイ中チェックボックスにチェックされている場合だけです。
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 SetVolumes(volume) { // 効果音の大きさに違いがあるので調整している let volume2 = volume * 3; if (volume2 > 1) volume2 = 1; shotSound.volume = volume; beingAttackedSound.volume = volume; deadSound.volume = volume2; damageSound.volume = volume2; enemyDeadSound.volume = volume2; gameoverSound.volume = volume; bgm.volume = volume; } let isPlaying = false; // 現在プレイ中か? // BGMと効果音を再生するかどうか? function IsSound() { return isPlaying && document.getElementById('sound-checkbox').checked; } // BGMを停止する function StopBgm() { bgm.currentTime = 0; bgm.pause(); isPlayingBgm = false; } |
必要な3Dオブジェクトをシーンに追加
必要な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 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 |
let playerShip; // 自機 function AddPlayerShip() { playerShip = new THREE.Group(); const material = new THREE.MeshStandardMaterial({ color: 0xffff00 }); // 胴体 const geometry1 = new THREE.CylinderGeometry(4, 12, 96, 32); const mesh1 = new THREE.Mesh(geometry1, material); mesh1.rotation.x = Math.PI / 2; playerShip.add(mesh1); // 翼 const geometry2 = new THREE.BoxGeometry(96, 2, 24); const mesh2 = new THREE.Mesh(geometry2, material); mesh2.rotation.z = -Math.PI / 6; playerShip.add(mesh2); const mesh3 = new THREE.Mesh(geometry2, material); mesh3.rotation.z = Math.PI / 6; playerShip.add(mesh3); // 後ろからジェット噴射しているように見えるもの const geometry4 = new THREE.CylinderGeometry(6, 6, 10, 32); const material4 = new THREE.MeshNormalMaterial({ color: 0x6699FF }); const mesh4 = new THREE.Mesh(geometry4, material4); mesh4.rotation.x = Math.PI / 2; mesh4.position.z = -60; playerShip.add(mesh4); playerShip.position.set(10000, 0, 0); // 最初はカメラの視界の外に配置する scene.add(playerShip); } 敵機をシーンに追加する関数を示します。 <pre class="lang:default decode:true " > let enemies = []; // 敵機 const ENEMIES_MAX = 8; function AddEnemyShips() { const material = new THREE.MeshStandardMaterial({ color: 0xff0000 }); const material4 = new THREE.MeshNormalMaterial({ color: 0x6699FF }); for (let i = 0; i < ENEMIES_MAX; i++) { const enemy = new THREE.Group(); { const geometry1 = new THREE.CylinderGeometry(4, 12, 96, 32); const mesh1 = new THREE.Mesh(geometry1, material); mesh1.rotation.x = Math.PI / 2; enemy.add(mesh1); const geometry2 = new THREE.BoxGeometry(96, 2, 24); const mesh2 = new THREE.Mesh(geometry2, material); mesh2.rotation.z = -Math.PI / 6; enemy.add(mesh2); const mesh3 = new THREE.Mesh(geometry2, material); mesh3.rotation.z = Math.PI / 6; enemy.add(mesh3); const geometry4 = new THREE.CylinderGeometry(6, 6, 80, 32); const mesh4 = new THREE.Mesh(geometry4, material4); mesh4.rotation.x = Math.PI / 2; mesh4.position.z = -20; enemy.add(mesh4); } enemy.position.z = 100000; // 最初はカメラの視界の外に配置する enemies.push(enemy); scene.add(enemy); } } |
弾丸をシーンに追加する処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
let bullets = []; // 弾丸 let BULLET_MAX = 160; // 合計8機で20連射制限なので160発分の3Dオブジェクトをつくれば充分 function AddBullets() { const bulletGeometry = new THREE.SphereGeometry(5, 32, 32); const bulletMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.1 }); for (let i = 0; i < BULLET_MAX; i++) { const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial); bullets.push(bullet); scene.add(bullet); } } |
火花をシーンに追加する処理を示します。
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 |
let spark1s = []; let spark2s = []; let spark3s = []; let spark4s = []; let spark5s = []; let spark6s = []; let SPARK_MAX = ENEMIES_MAX * 30; // これだけあれば足りるはず(本当はこんな考え方はダメ) function AddSparks() { const sparkMaterial1 = new THREE.SpriteMaterial({ map: new THREE.TextureLoader().load(base_url + '/space-war/spark1.png'), }); for (let i = 0; i < SPARK_MAX; i++) { const sprite = new THREE.Sprite(sparkMaterial1); sprite.scale.set(32, 32, 32); spark1s.push(sprite); scene.add(sprite); } const sparkMaterial2 = new THREE.SpriteMaterial({ map: new THREE.TextureLoader().load(base_url + '/space-war/spark2.png'), }); for (let i = 0; i < SPARK_MAX; i++) { const sprite = new THREE.Sprite(sparkMaterial2); sprite.scale.set(32, 32, 32); spark2s.push(sprite); scene.add(sprite); } const sparkMaterial3 = new THREE.SpriteMaterial({ map: new THREE.TextureLoader().load(base_url + '/space-war/spark3.png'), }); for (let i = 0; i < SPARK_MAX; i++) { const sprite = new THREE.Sprite(sparkMaterial3); sprite.scale.set(32, 32, 32); spark3s.push(sprite); scene.add(sprite); } const sparkMaterial4 = new THREE.SpriteMaterial({ map: new THREE.TextureLoader().load(base_url + '/space-war/spark4.png'), }); for (let i = 0; i < SPARK_MAX; i++) { const sprite = new THREE.Sprite(sparkMaterial4); sprite.scale.set(32, 32, 32); spark4s.push(sprite); scene.add(sprite); } const sparkMaterial5 = new THREE.SpriteMaterial({ map: new THREE.TextureLoader().load(base_url + '/space-war/spark5.png'), }); for (let i = 0; i < SPARK_MAX; i++) { const sprite = new THREE.Sprite(sparkMaterial5); sprite.scale.set(32, 32, 32); spark5s.push(sprite); scene.add(sprite); } const sparkMaterial6 = new THREE.SpriteMaterial({ map: new THREE.TextureLoader().load(base_url + '/space-war/spark6.png'), }); for (let i = 0; i < SPARK_MAX; i++) { const sprite = new THREE.Sprite(sparkMaterial6); sprite.scale.set(32, 32, 32); spark6s.push(sprite); scene.add(sprite); } } |
AddLines関数は3Dっぽく見せるためにX軸Y軸Z軸に平行な線をシーンに追加するためのものです。その処理を示します。
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 AddLines() { const lines = new THREE.Group(); const whiteMaterial = new THREE.LineBasicMaterial({ color: 0xffffff }); let lineGeometry;; for (let a = -44; a <= 44; a += 4) { for (let i = -44; i <= 44; i += 4) { const points = []; points.push(new THREE.Vector3(- 4800, 100 * i, 100 * a)); points.push(new THREE.Vector3(4800, 100 * i, 100 * a)); lineGeometry = new THREE.BufferGeometry().setFromPoints(points); const line = new THREE.Line(lineGeometry, whiteMaterial); lines.add(line); } } for (let a = -44; a <= 44; a += 4) { for (let i = -44; i <= 44; i += 4) { const points = []; points.push(new THREE.Vector3(100 * i, - 4800, 100 * a)); points.push(new THREE.Vector3(100 * i, + 4800, 100 * a)); lineGeometry = new THREE.BufferGeometry().setFromPoints(points); const line = new THREE.Line(lineGeometry, whiteMaterial); lines.add(line); } } for (let a = -44; a <= 44; a += 4) { for (let i = -44; i <= 44; i += 4) { const points = []; points.push(new THREE.Vector3(100 * i, 100 * a, - 4800)); points.push(new THREE.Vector3(100 * i, 100 * a, + 4800)); lineGeometry = new THREE.BufferGeometry().setFromPoints(points); const line = new THREE.Line(lineGeometry, whiteMaterial); lines.add(line); } } scene.add(lines); } |
シーンにライトを追加する処理を示します。平行光源だけだと影の部分が見えなくなってしまうので環境光もあわせて使用しています。
1 2 3 4 5 6 7 8 9 10 11 12 |
function AddLights() { // 平行光源 const directionalLight = new THREE.DirectionalLight( 0xffffff ); directionalLight.position.set(1, 1, -1); scene.add(directionalLight); // 環境光 const light = new THREE.AmbientLight(0xFFFFFF, 0.4); scene.add(light); } |
接続成功時の処理
AspNetCore.SignalRで接続することができたらサーバーサイドからSuccessfulConnectionToClientが送信されます。これを受信したらIDを表示させます。またゲームスタートのボタンが表示されるようにします。
1 2 3 4 5 6 7 8 9 |
let connectionID = ''; connection.on("SuccessfulConnectionToClient", function (result, id) { connectionID = id; document.getElementById("conect-result").innerHTML = `conect-result ${result}:${id}`; startButton1.style.display = 'block'; showTop30Button1.style.display = 'block'; }); |
接続に成功したとき、自分自身や他のユーザーがゲームに参加したときとゲームオーバーになったとき、離脱したときはサーバーサイドからNPCの数が送られてきます。この値が0でない場合はゲームを開始することができます。0のときは満員状態なので参加できません。この状態の変化にあわせてゲーム開始ボタンを表示させたり非表示にします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
connection.on("SendNpcCountToClient", function (npcCount) { if (isPlaying) { document.getElementById('can-start').innerHTML = '現在 プレイ中です'; } else if (npcCount == 0) { startButton1.style.display = 'none'; showTop30Button1.style.display = 'none'; document.getElementById('can-start').innerHTML = '現在 満員です。ゲームに参加できません。'; } else { startButton1.style.display = 'block'; showTop30Button1.style.display = 'block'; document.getElementById('can-start').innerHTML = '参加可能です'; } }); |
ゲームスタートボタンを押した場合はサーバーサイドにGameStartが送信されます。これがサーバーサイドで受信されたときはゲーム開始の処理がおこなわれます。
1 2 3 4 5 6 7 8 |
function GameStart() { if (connectionID != '') { let playerName = document.getElementById('player-name').value; connection.invoke("GameStart", connectionID, playerName).catch(function (err) { return console.error(err.toString()); }); } } |
ゲームスタートのボタンをおしてサーバーサイドで正常に処理がおこなわれた場合、サーバーサイドからEventGameStartToClientが送信されます。これを受信したらゲーム開始ボタンを非表示にしてisPlayingフラグをtrueにします。
1 2 3 4 5 |
connection.on("EventGameStartToClient", function () { startButton1.style.display = 'none'; showTop30Button1.style.display = 'none'; isPlaying = true; }); |
キーが押されたらこれをサーバーサイドに送信するのですが、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 |
function GetCommandFromKey(key) { if (key == "ArrowLeft") return 'Left'; else if (key == "ArrowRight") return 'Right'; else if (key == " ") return 'Shot'; else if (key == "ArrowUp" || key == "ArrowDown") { if (!document.getElementById('inversion-checkbox').checked) { if (key == "ArrowUp") return 'Up'; else if (key == "ArrowDown") return 'Down'; } else { if (key == "ArrowUp") return 'Down'; else if (key == "ArrowDown") return 'Up'; } } else return 'Other'; } |
キーが押されたり離されたときに実行される処理を示します。
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 |
// キーが押されているか? let isLeftKeyDown = false; let isRightKeyDown = false; let isUpKeyDown = false; let isDownKeyDown = false; let isSpaceKeyDown = false; document.onkeydown = function (e) { // ゲーム開始以降はデフォルトの動作を抑制する if (isPlaying && (e.key == "ArrowUp" || e.key == "ArrowDown" || e.key == "ArrowLeft" || e.key == "ArrowRight" || e.key == " ")) e.preventDefault(); // キーが押しっぱなしになっているときの二重送信を防ぐ if (e.key == "ArrowLeft" && isLeftKeyDown) return; if (e.key == "ArrowRight" && isRightKeyDown) return; if (e.key == "ArrowUp" && isUpKeyDown) return; if (e.key == "ArrowDown" && isDownKeyDown) return; if (e.key == ' ' && isSpaceKeyDown) return; if (e.key == "ArrowLeft") isLeftKeyDown = true; if (e.key == "ArrowRight") isRightKeyDown = true; if (e.key == "ArrowUp") isUpKeyDown = true; if (e.key == "ArrowDown") isDownKeyDown = true; if (e.key == " ") isSpaceKeyDown = true; // ゲーム中にプレイヤー名を変更可能なのでキーが押されたらそのつどプレイヤー名も送信する // 接続されていない場合はなにもしない if (connectionID != '') { let playerName = document.getElementById('player-name').value; connection.invoke("DownKey", GetCommandFromKey(e.key), playerName).catch(function (err) { return console.error(err.toString()); }); } } document.onkeyup = function (e) { if (e.key == "ArrowLeft") isLeftKeyDown = false; if (e.key == "ArrowRight") isRightKeyDown = false; if (e.key == "ArrowUp") isUpKeyDown = false; if (e.key == "ArrowDown") isDownKeyDown = false; if (e.key == " ") isSpaceKeyDown = false; // 接続されていない場合はなにもしない if (connectionID != '') { connection.invoke("UpKey", GetCommandFromKey(e.key)).catch(function (err) { return console.error(err.toString()); }); } } |
更新処理
更新処理が必要なときはサーバーサイドからクライアントサイドにUpdatePlayersToClient、UpdateBulletsToClientなどが送信されます。MoveShip関数は3Dオブジェクトを移動させたり回転させるためのものです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
function MoveShip(ship, x, y, z, headingAngle, attitudeAngle, bankAngle) { ship.position.x = x; ship.position.y = y; ship.position.z = z; ship.rotation.set(0, 0, 0); let mat = new THREE.Matrix4(); let mat1 = new THREE.Matrix4(); mat1.makeRotationY(headingAngle); mat.multiply(mat1); let mat2 = new THREE.Matrix4(); mat2.makeRotationX(-attitudeAngle); mat.multiply(mat2); let mat3 = new THREE.Matrix4(); mat3.makeRotationZ(bankAngle); mat.multiply(mat3); ship.matrix = mat; ship.rotation.setFromRotationMatrix(mat); } |
サーバーサイドからUpdatePlayersToClientが送信された場合は自機と敵機を上記のMoveShip関数を呼び出して移動させます。
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 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
// プレイヤー名は引数の都合で別に送られてくるので配列に格納しておく let playerNames = []; connection.on("UpdatePlayerNamesToClient", function (names) { playerNames = names.split(','); }); let updateCount = 0; // 前回の処理で自機が移動した座標 let curX = 0; let curY = 0; let curZ = 0; connection.on("UpdatePlayersToClient", function ( id, playerIds, playerXs, playerYs, playerZs, playerHeadingAngles, playerAttitudeAngles, playerBankAngles, playerIsDeads, playerInvincibleTimes) { if (playerIds == '') return; updateCount++; let ids = playerIds.split(','); let xs = playerXs.split(','); let ys = playerYs.split(','); let zs = playerZs.split(','); let headingAngles = playerHeadingAngles.split(','); let attitudeAngles = playerAttitudeAngles.split(','); let bankAngles = playerBankAngles.split(','); let isDeads = playerIsDeads.split(','); let invincibleTimes = playerInvincibleTimes.split(','); // 全敵機の3Dオブジェクトをカメラの視角の外に移動する for (let i = 0; i < ENEMIES_MAX; i++) enemies[i].position.set(10000, 0, 0); // レーダーをクリア(負荷を減らすためにレーダーの再描画は2回に1回とする) if (updateCount % 2 == 0) { ctxRadar.fillStyle = "#000000"; ctxRadar.fillRect(0, 0, RADAR_CANVAS_WIDTH, RADAR_CANVAS_HEIGHT); ctxRadar.fillStyle = "#000080"; ctxRadar.fillRect(0, 0, RADAR_CANVAS_INNER_WIDTH, RADAR_CANVAS_INNER_HEIGHT); ctxRadar.font = "12px Arial"; } for (let i = 0; i < ENEMIES_MAX; i++) { let isShow = true; // 無敵状態の敵は点滅させたいので2回に1回はカメラの視界の外から移動しない if (invincibleTimes[i] > 0 && invincibleTimes[i] % 2 == 0) isShow = false; // 死亡状態なら非表示にしたいので視界の外から移動しない if (isDeads[i] == 'true') { isShow = false; } if (ids[i] == id) { // ids[i] == idなら自機である showPlayerShip = true; if (isPlaying && isShow) { MoveShip(playerShip, xs[i], ys[i], zs[i], headingAngles[i], attitudeAngles[i], bankAngles[i]); // 現在の機体の角度とピッチ角を表示させる let headingAngle = headingAngles[i] * 180 / Math.PI; headingAngle = Math.round(headingAngle); if (headingAngle < 180) headingAngle = '左 ' + headingAngle; else headingAngle = '右 ' + (360 - headingAngle); let attitudeAngle = attitudeAngles[i] * 180 / Math.PI; attitudeAngle = Math.round(attitudeAngle); document.getElementById('player-position').innerHTML = ` X: ${xs[i]} Y: ${ys[i]} Z: ${zs[i]}<br> 機体角度: ${headingAngle} ピッチ角:${attitudeAngle} `; // レーダーに自分自身の位置を表示 if (updateCount % 2 == 0) { ctxRadar.fillStyle = "#ffffff"; let pointX = (xs[i] - 0 + 2000) / 4000 * RADAR_CANVAS_INNER_WIDTH; let pointY = (zs[i] - 0 + 2000) / 4000 * RADAR_CANVAS_INNER_HEIGHT; ctxRadar.fillRect(pointX, pointY, 4, 4); ctxRadar.fillText(playerNames[i] + ' (' + ys[i] + ')', pointX + 4, pointY); } } else playerShip.position.set(10000, 0, 0); // 非表示にしたいときはカメラの視界の外へ移動 curX = xs[i]; curY = ys[i]; curZ = zs[i]; } else { let x = xs[i] - 0; // xs[i]は文字列の数字なので値を加算すると文字列の結合になってしまう let y = ys[i] - 0; let z = zs[i] - 0; // ワープした敵でも見えるようにする if (xs[i] - curX > cameraDistance) x -= 4000; else if (curX - xs[i] > cameraDistance) x += 4000; if (ys[i] - curY > cameraDistance) y -= 4000; else if (curY - ys[i] > cameraDistance) y += 4000; if (zs[i] - curZ > cameraDistance) z -= 4000; else if (curZ - zs[i] > cameraDistance) z += 4000; if (isShow) { MoveShip(enemies[i], x, y, z, headingAngles[i], attitudeAngles[i], bankAngles[i]); // 敵のプレイヤー名を画面に表示させる // 3Dオブジェクトのワールド座標を取得する const worldPosition = enemies[i].getWorldPosition(new THREE.Vector3()); // スクリーン座標を取得する const projection = worldPosition.project(camera); const sx = (CANVAS_WIDTH / 2) * (+projection.x + 1.0); const sy = (CANVAS_HEIGHT / 2) * (-projection.y + 1.0); const sz = projection.z; if (updateCount % 2 == 0) { // レーダーに敵の位置とプレイヤー名を表示 ctxRadar.fillStyle = "#ff0000"; let pointX = (xs[i] - 0 + 2000) / 4000 * RADAR_CANVAS_INNER_WIDTH; let pointY = (zs[i] - 0 + 2000) / 4000 * RADAR_CANVAS_INNER_HEIGHT; ctxRadar.fillRect(pointX, pointY, 4, 4); ctxRadar.fillText(playerNames[i] + ' (' + ys[i] + ')', pointX + 4, pointY); // テキストフィールドにスクリーン座標を表示 if (sz < 0.99999 && 0 < Math.round(sx) && Math.round(sx) < CANVAS_WIDTH - 100 && 0 < Math.round(sy) && Math.round(sy) < CANVAS_HEIGHT - 50) { textFields[i].innerHTML = `?? ${playerNames[i]}`; textFields[i].style.transform = `translate(${sx + 20}px, ${sy}px)`; textFields[i].style.display = 'block'; } else { // 自分よりも手前にいる敵の場合はテキストフィールドを非表示にする textFields[i].style.display = 'none'; } } } else textFields[i].style.display = 'none'; } } }); |
カメラの座標がサーバーサイドから送られてきたら、カメラをこの座標に移動させ、自機が生きているなら存在する座標に向かせます。
1 2 3 4 |
connection.on("UpdateCameraPositionToClient", function (x, y, z) { camera.position.set(x, y, z); camera.lookAt(new THREE.Vector3(curX, curY, curZ)); }); |
弾丸の位置はUpdateBulletsToClientで送られてくるのでその位置に移動させます。
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 |
connection.on("UpdateBulletsToClient", function (bulletXs, bulletYs, bulletZs) { let xs = bulletXs.split(','); let ys = bulletYs.split(','); let zs = bulletZs.split(','); // すべての弾丸をいったんカメラの視角の外に移動する for (let i = 0; i < BULLET_MAX; i++) { bullets[i].position.x = 100000; bullets[i].position.y = 0; bullets[i].position.z = 0; } if (bulletXs == '') return; for (let i = 0; i < xs.length; i++) { // ワープしてしまった弾丸がある場合は移動させて描画させる if (xs[i] - curX > cameraDistance) xs[i] -= 4000; else if (curX - xs[i] > cameraDistance) xs[i] -= (-4000); if (ys[i] - curY > cameraDistance) ys[i] -= 4000; else if (curY - ys[i] > cameraDistance) ys[i] -= (-4000); if (zs[i] - curZ > cameraDistance) zs[i] -= 4000; else if (curZ - zs[i] > cameraDistance) zs[i] -= (-4000); bullets[i].position.x = xs[i]; bullets[i].position.y = ys[i]; bullets[i].position.z = zs[i]; } }); |
火花の座標はサーバーサイドからUpdateSparksToClientで送られてします。これを受信したら火花を移動させます。
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 |
connection.on("UpdateSparksToClient", function (sparkXs, sparkYs, sparkZs, sparkLifes) { for (let i = 0; i < spark1s.length; i++) spark1s[i].position.set(0, 0, 10000); for (let i = 0; i < spark2s.length; i++) spark2s[i].position.set(0, 0, 10000); for (let i = 0; i < spark3s.length; i++) spark3s[i].position.set(0, 0, 10000); for (let i = 0; i < spark4s.length; i++) spark4s[i].position.set(0, 0, 10000); for (let i = 0; i < spark5s.length; i++) spark5s[i].position.set(0, 0, 10000); for (let i = 0; i < spark6s.length; i++) spark6s[i].position.set(0, 0, 10000); if (sparkXs == '') return; let xs = sparkXs.split(','); let ys = sparkYs.split(','); let zs = sparkZs.split(','); let lifes = sparkLifes.split(','); let index1 = 0; let index2 = 0; let index3 = 0; let index4 = 0; let index5 = 0; let index6 = 0; for (let i = 0; i < xs.length; i++) { if (lifes[i] >= 11) { if (index1 < SPARK_MAX) spark1s[index1].position.set(xs[i], ys[i], zs[i]); index1++; } else if (lifes[i] >= 9) { if (index2 < SPARK_MAX) spark2s[index2].position.set(xs[i], ys[i], zs[i]); index2++; } else if (lifes[i] >= 7) { if (index3 < SPARK_MAX) spark3s[index3].position.set(xs[i], ys[i], zs[i]); index3++; } else if (lifes[i] >= 5) { if (index4 < SPARK_MAX) spark4s[index4].position.set(xs[i], ys[i], zs[i]); index4++; } else if (lifes[i] >= 3) { if (index5 < SPARK_MAX) spark5s[index5].position.set(xs[i], ys[i], zs[i]); index5++; } else if (lifes[i] >= 1) { if (index6 < SPARK_MAX) spark6s[index6].position.set(xs[i], ys[i], zs[i]); index6++; } } }); |
クライアントサイドにおける更新処理で必要なデータがすべてサーバーサイドから送信された場合、EndUpdateToClientが送信されます。これを受信したらレンダリングをおこないます。
1 2 3 |
connection.on("EndUpdateToClient", function () { renderer.render(scene, camera); }); |
スコアや残機にかんする状態が変化するときはサーバーサイドからUpdateScoreToClientが送信されます。これを受信したら表示を変更します。
1 2 3 4 |
connection.on("UpdateScoreToClient", function (score, rest, life) { document.getElementById('score').style.fontWeight = 'bold'; document.getElementById('score').innerHTML = 'Score ' + score + '<br>残 ' + rest + ' (耐 ' + life + ')'; }); |
自分自身を含むすべてのプレイヤーの状態が変更された場合はUpdatePlayerInfosToClientが送信されるので、これを受信したら表示を変更します。
1 2 3 |
connection.on("UpdatePlayerInfosToClient", function (str) { document.getElementById('players-info').innerHTML = str; }); |
プレイヤーが別のプレイヤーを撃破した場合はサーバーサイドからSendHitCheckToClientが送信されます。これを受信したら配列に格納し、3秒間その内容を表示します。3秒経過したら古い通知は削除して表示されないようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
let texts = []; connection.on("SendHitCheckToClient", function (str) { texts.unshift(str); setTimeout(() => { texts.pop(); }, 3000); }); setInterval(() => { let text = ''; for (let i = 0; i < texts.length; i++) text += texts[i] + '<br>'; document.getElementById('notify').innerHTML = text; }, 500); |
サーバーサイドで自機にかんするイベントが発生した場合、クライアントサイドにShotEventToClientやBeingAttackedEventToClientが送信されるので、対応する効果音を鳴らします。
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 |
connection.on("ShotEventToClient", function () { if (IsSound()) { shotSound.currentTime = 0; shotSound.play(); } }); connection.on("BeingAttackedEventToClient", function () { if (IsSound() && beingAttackedSound.currentTime == 0) { beingAttackedSound.currentTime = 0; beingAttackedSound.play(); } }); connection.on("AccelerateEventToClient", function () { if (IsSound()) { if (speedupSound.currentTime == 0) speedupSound.play(); } }); connection.on("HitEnemyToClient", function () { if (IsSound()) { // 敵を撃破したときの効果音。やや深みのある大きめの音にする。 enemyDeadSound.currentTime = 0; enemyDeadSound.play(); deadSound.currentTime = 0; deadSound.play(); } }); connection.on("DeadToClient", function () { if (IsSound()) { deadSound.currentTime = 0; deadSound.play(); } }); connection.on("DamageToClient", function () { if (IsSound()) { damageSound.currentTime = 0; damageSound.play(); } }); |
ゲームオーバーになったときはBGMが鳴っているときはこれを停止してゲームオーバーの効果音を鳴らします。また残機が表示されていた部分に「GAME OVER」の文字列を表示させます。また非表示にしていたスタートボタンを再表示させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
connection.on("GameOverEventToClient", function (score) { isPlaying = false; startButton1.style.display = 'block'; showTop30Button1.style.display = 'block'; document.getElementById('score').innerHTML = 'Score ' + score + '<br><span = style="color:#ff0000">GAME OVER</span>'; StopBgm(); if (document.getElementById('sound-checkbox').checked) { gameoverSound.currentTime = 0; gameoverSound.play(); } }); |