前回 ASP.NET Coreで3Dっぽいカーレースをつくる(3)の続きです。今回はクライアントサイドの処理を解説します。
まず車とコース、コースの境界にある円錐状のものを描画する処理とその座標ですが、これはコースを描画する TypeScript/JavaScriptでもつくる3Dカーレースゲーム(3)で作成したものをそのまま使います。
以下のリンクからダウンロードできます。
create-car-functions.js
get-course-border.js
get-course-inside.js
このなかで車の描画に必要なCreateOrangeCar1関数、CreateBlueCar1関数、CreateBlueCar2関数、CreateRedCar1関数、CreateRedCar2関数、CreateYellowCar1関数、コースの内側を黒く塗りつぶすために必要なGetCourseInside関数、コースの境界線に円錐状のものを設置するためのGetCourseBorder1関数とGetCourseBorder2関数が定義されています。
Contents
cshtmlファイル
ページ数が増えてきてPagesフォルダ内のファイルが増えてきたのでCarRace3dというフォルダをつくってそこで公開します。そのためJavaScriptやmp3ファイルを指定するときに少し注意が必要です。
Pages\CarRace3d\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 43 44 45 46 47 48 49 50 51 52 53 54 |
@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="info1" style="position: absolute; top: 0; left: 0; background: white"></div> <div id="info2" style="position: absolute; top: 0; left: 0; background: white"></div> <!-- ゲームオーバー表示用 --> <div id="info3" style="position: absolute; top: 0; left: 0; background: white"></div> <div id="info4" style="position: absolute; top: 0; left: 0; background: white"></div> <!-- ライバルのプレイヤー名表示用 --> <div id="name_0" style="position: absolute; top: 0; left: 0; background: white"></div> <div id="name_1" style="position: absolute; top: 0; left: 0; background: white"></div> <div id="name_2" style="position: absolute; top: 0; left: 0; background: white"></div> <div id="name_3" style="position: absolute; top: 0; left: 0; background: white"></div> <div id="name_4" style="position: absolute; top: 0; left: 0; background: white"></div> <div id="name_5" style="position: absolute; top: 0; left: 0; background: white"></div> <div id="name_6" style="position: absolute; top: 0; left: 0; background: white"></div> <form name="form1"> <input type="checkbox" value="音を出す" id="SoundCheckbox">音を出す <label>ハンドルネーム</label> <input type="text" id="player-name" maxlength='16' /> </form> <p><a href="./hi-score">トップ30を見る</a></p> <p>遊び方</p> <p>Sキーでゲームスタート。ハンドル操作は← → キー、加速 ↑、ブレーキ ↓ です。</p> <!-- 接続状態の表示用 --> <p id = "conect-result"></p> </div> <script src="@baseurl/js/signalr.js"></script> <script> let connection = new signalR.HubConnectionBuilder().withUrl("@baseurl/CarRace3dHub").build(); </script> <script src="@baseurl/car-race-3d/get-course-border.js"></script> <script src="@baseurl/car-race-3d/get-course-inside.js"></script> <script src="@baseurl/car-race-3d/create-car-functions.js"></script> <script src="@baseurl/car-race-3d/app.js"></script> |
JavaScript グローバル変数
JavaScriptのファイルを読み込んでいますが、上の3つはすでに紹介したとおりなのでapp.jsについて解説します。
car-race-3d/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 |
// canvasのサイズ const width = 640; const height = 480; let scene; let camera; let renderer; // サーバサイドから渡された座標に対する拡大率 let expansionRate = 1.6; // BGM、効果音再生関連 let isGameOver = false; let intervalId = undefined; let soundCheckbox; let soundturn = new Audio('../car-race-3d/sounds/turn.mp3'); let isSoundturn = false; let soundBgm = new Audio('../car-race-3d/sounds/bgm.mp3'); let soundMiss = new Audio('../car-race-3d/sounds/miss.mp3'); // キーは押されているか? let isUpKeyDown = false; let isDownKeyDown = false; let isLeftKeyDown = false; let isRightKeyDown = false; // ライバルの車 let cars = []; // sceneに必要なものはすでに追加されているか? // ゲームオーバー後、もう一度ゲームをするときは追加の必要はない let isGameInited = false; |
ページが読み込まれたときの処理
ページが読み込まれたときの処理を示します。
car-race-3d/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 |
window.addEventListener('load', Init); function Init() { soundCheckbox = document.getElementById('SoundCheckbox'); soundCheckbox.addEventListener('change', soundCheckboxChanged); // シーンを作成 scene = new THREE.Scene(); // カメラを作成 camera = new THREE.PerspectiveCamera(45, width / height, 1, 10000); camera.position.set(0, 2, 12); camera.rotateX(-10 / 180 * Math.PI); // 平行光源 const light = new THREE.DirectionalLight(0xffffff); light.intensity = 2; // 光の強さを倍に light.position.set(1, 1, 1); scene.add(light); const light2 = new THREE.AmbientLight(0xFFFFFF, 0.4); scene.add(light2); // レンダラーを作成 renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('can') }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(width, height); ShowHowToGameStart(); myCar = CreateOrangeCar1(); myCar.position.x = 0; myCar.position.y = 2; myCar.position.z = 3; renderer.render(scene, camera); setInterval(UpdateBeforeGameStart, 1000 / 60); SetVolume(0.06); // 初期値は小さめの値に InitCars(); } |
上記関数内で呼び出されている関数を示します。
BGMを鳴らすかどうかを選択するチェックボックスの状態が変化したときにBGMの再生と停止処理をおこないます。
1 2 3 4 5 6 |
function soundCheckboxChanged() { if (soundCheckbox.checked && isGameInited && !isGameOver) PlayBgms(); else StopBgm(); } |
BGMの再生を再生します。102秒で1回の再生が終わるので102秒間隔で繰り返し再生をおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function PlayBgms() { if (soundCheckbox.checked) { intervalId = setInterval(PlayBGM, 102 * 1000); PlayBGM(); } } function PlayBGM() { if (soundCheckbox.checked) { soundBgm.currentTime = 0; soundBgm.play(); } } |
BGMの再生を停止します。
1 2 3 4 5 |
function StopBgm() { clearInterval(intervalId); soundBgm.pause(); soundBgm.currentTime = 0; } |
ボリュームをセットします。
1 2 3 4 5 6 7 8 9 10 |
function SetVolume(volume) { if (volume < 0) volume = 0; if (volume > 1.0) volume = 1; soundturn.volume = volume; soundBgm.volume = volume; soundMiss.volume = volume; } |
ユーザーがわかりやすいように操作方法を表示します。
1 2 3 4 5 6 7 8 |
function ShowHowToGameStart() { const tf1 = document.getElementById("info1"); tf1.innerHTML = "↑キー加速 ↓キー減速 ハンドル操作 ← →キー<br>Sキー:ゲーム開始(開始まで数秒かかります)"; tf1.style.transform = "translate(30px, 30px)"; tf1.style.backgroundColor = "#000000"; tf1.style.color = "white"; tf1.style.fontSize = "22px"; } |
ゲーム開始前は自車を回転させて表示させます。
1 2 3 4 5 6 7 8 9 10 11 12 |
function UpdateBeforeGameStart() { if (isGameInited) return; camera.position.set(0, 2, 12); camera.rotation.x = (-10 / 180 * Math.PI); myCar.position.x = 0; myCar.position.y = 0; myCar.position.z = 3; myCar.rotation.y += 0.03; renderer.render(scene, camera); } |
ライバル車を生成します。スタートボタンを押すまでは表示させないので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 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
function InitCars() { cars = []; let car = CreateBlueCar1(); car.position.x = 0; car.position.y = -10; car.position.z = 3; cars.push(car); car = CreateRedCar1(); car.position.x = 0; car.position.y = -10; car.position.z = 3; cars.push(car); car = CreateYellowCar1(); car.position.x = 0; car.position.y = -10; car.position.z = 3; cars.push(car); car = CreateBlueCar2(); car.position.x = 0; car.position.y = -10; car.position.z = 3; cars.push(car); car = CreateRedCar2(); car.position.x = 0; car.position.y = -10; car.position.z = 3; cars.push(car); car = CreateYellowCar1(); car.position.x = 0; car.position.y = -10; car.position.z = 3; cars.push(car); } |
コースをsceneに追加
ゲームが開始されたらコースをsceneに追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function InitGame() { // コースを作成 let geometry = new THREE.BoxGeometry(10000, 0.01, 10000); let material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); scene.add(new THREE.Mesh(geometry, material)); scene.add(CreateLeft()); scene.add(CreateRight()); scene.add(CreateInside()); // 自車の初期位置にスタートラインを引く AddStartLine(); renderer.setClearColor(0x00008B); renderer.render(scene, camera); PlayBgms(); } |
以下はInitGame関数内で呼び出されている関数です。
コースの左側と左側に円錐状のオブジェクトをつくります。
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 |
function CreateLeft() { let border1 = GetCourseBorder1().map(point => { return new Point(point.X * expansionRate, point.Y * expansionRate); }); let geometry = new THREE.Geometry(); let material = new THREE.MeshLambertMaterial({ color: 0xcccccc }); border1.forEach(point => { let geometryTemp = new THREE.CylinderGeometry(0.5, expansionRate, 1, 30); let meshTemp = new THREE.Mesh(geometryTemp); meshTemp.position.set(point.X, 0.5, point.Y); geometry.mergeMesh(meshTemp); }); return new THREE.Mesh(geometry, material); } function CreateRight() { let border2 = GetCourseBorder2().map(point => { return new Point(point.X * expansionRate, point.Y * expansionRate); }); let geometry = new THREE.Geometry(); let material = new THREE.MeshLambertMaterial({ color: 0xcccccc }); border2.forEach(point => { let geometryTemp = new THREE.CylinderGeometry(0.5, expansionRate, 1, 30); let meshTemp = new THREE.Mesh(geometryTemp); meshTemp.position.set(point.X, 0.5, point.Y); geometry.mergeMesh(meshTemp); }); return new THREE.Mesh(geometry, material); } |
コースの内部に黒い板を敷き詰めます。
1 2 3 4 5 6 7 8 9 10 11 |
function CreateInside() { let geometry = new THREE.Geometry(); GetCourseInside().forEach(point => { let geometryTemp = new THREE.BoxGeometry(expansionRate, 0.1, point.Depth * expansionRate); let meshTemp = new THREE.Mesh(geometryTemp); meshTemp.position.set(point.CenterX * expansionRate, 0.05, point.CenterY * expansionRate); geometry.mergeMesh(meshTemp); }); let material = new THREE.MeshBasicMaterial({ color: 0x333333 }); return new THREE.Mesh(geometry, material); } |
スタート地点がどこかわかるように赤いラインを表示させます。
1 2 3 4 5 6 7 8 |
function AddStartLine() { let line_geometry = new THREE.Geometry(); line_geometry.vertices.push(new THREE.Vector3(208, 1, 206.4), new THREE.Vector3(225.6, 1, 224)); //線オブジェクトの生成 const line_material = new THREE.LineBasicMaterial({ color: 0xff0000 }); scene.add(new THREE.Line(line_geometry, line_material)); } |
更新処理
ゲーム中の更新処理を示します。
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 |
// クラッシュしていないときはtrue let isAlive = true; function Update() { if (isGameOver) { ShowGameOverText(isGameOver); return; } myCar.position.x = x * expansionRate; myCar.position.y = y * expansionRate; myCar.position.z = z * expansionRate; myCar.rotation.x = rx; myCar.rotation.y = ry; myCar.rotation.z = rz; // 一定の速度を出して曲がろうとすると効果音を再生する PlaySoundIfTurning(); // クラッシュしていないときはカメラに自車の後ろを追わせる if (isAlive) FollowJikiCamera(); let i = 0; for (i = 0; i < otherXs.length; i++) { cars[i].rotation.x = otherRXs[i]; cars[i].rotation.y = otherRYs[i]; cars[i].rotation.z = otherRZs[i]; cars[i].position.x = otherXs[i] * expansionRate; cars[i].position.z = otherZs[i] * expansionRate; // ライバルのプレイヤーがクラッシュしている場合は吹っ飛ぶのではなくスピンさせる if (otherYs[i] > 0) cars[i].rotation.y += Math.PI / 8; else cars[i].position.y = 0; //otherYs[i] * expansionRate; // 前方に存在するライバルのプレイヤー名を表示する ShowNames(i); } // cars.lengthに対してotherXsが足りていない場合、その車オブジェクトは表示しない(地面の下に隠す) for (; i < cars.length; i++) cars[i].position.y = -10; // レンダリング renderer.render(scene, camera); } |
Update関数のなかで呼び出されている関数を示します。
一定の速度を出して曲がろうとすると効果音を再生します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function PlaySoundIfTurning() { if (isLeftKeyDown || isRightKeyDown) { if (soundCheckbox.checked && speed > 0.75 && !isSoundturn) { isSoundturn = true; soundturn.currentTime = 0; soundturn.play(); } } if (!isLeftKeyDown && !isRightKeyDown) { soundturn.pause(); isSoundturn = false; } } |
カメラに自車の後ろを追わせます。
1 2 3 4 5 6 7 8 9 10 11 |
function FollowJikiCamera() { let rotationY = myCar.rotation.y; if (isLeftKeyDown) rotationY -= 0.04; if (isRightKeyDown) rotationY += 0.04; camera.position.x = myCar.position.x - 8 * Math.sin(rotationY); camera.position.y = 3; camera.position.z = myCar.position.z - 8 * Math.cos(rotationY); camera.lookAt(new THREE.Vector3(myCar.position.x, myCar.position.y + 1, myCar.position.z)); } |
前方に存在するライバルのプレイヤー名を表示させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
function ShowNames(i) { // object3D は任意の3Dオブジェクト。 // オブジェクトのワールド座標を取得する const worldPosition = cars[i].getWorldPosition(new THREE.Vector3()); // スクリーン座標を取得する const projection = worldPosition.project(camera); const sx = (width / 2) * (projection.x + 1.0); const sy = (height / 2) * (- projection.y + 1.0); const sz = projection.z; const tf = document.getElementById('name_' + i); // ライバル車が自分よりも前方でcanvasをはみ出さない位置にいる場合だけ文字列を表示する if (sz < 1 && 0 < Math.round(sx) && Math.round(sx) < width * 0.8) tf.innerHTML = `(${otherNames[i]})`; else tf.innerHTML = ''; tf.style.transform = `translate(${sx}px, ${sy - 36}px)`; // 表示するY座標を-36してやや上方に表示させる tf.style.backgroundColor = "#00008B"; tf.style.color = "white"; } |
ゲームオーバーの時はその旨を表示させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
function ShowGameOverText(isGameOver) { const tf3 = document.getElementById("info3"); if (isGameOver) tf3.innerHTML = "GAME OVER"; else tf3.innerHTML = ""; tf3.style.transform = "translate(190px, 150px)"; tf3.style.backgroundColor = "#000000"; tf3.style.color = "white"; tf3.style.fontSize = "32px"; const tf4 = document.getElementById("info4"); if (isGameOver) tf4.innerHTML = "Press S key to Retry"; else tf4.innerHTML = ""; tf4.style.transform = "translate(230px, 200px)"; tf4.style.backgroundColor = "#000000"; tf4.style.color = "white"; tf4.style.fontSize = "18px"; } |
SignalRの接続と受信時の処理
SignalRの接続とデータを受信したときの処理を示します。
接続と切断
connectionID == ”でSキーが押されたときはSignalRで接続し、サーバサイドに対してゲームスタートの処理をする旨を送信します。そのための処理を示します。接続に失敗したときはその旨を表示させます。
1 2 3 4 5 |
function ConnectAndGameStart() { connection.start().catch(function (err) { document.getElementById("conect-result").innerHTML = '接続失敗'; }); } |
サーバーサイドから接続に成功した通知を受信したときの処理を示します。成功したらconnectionIDをグローバル変数に保存しておきます。
1 2 3 4 5 6 |
let connectionID = ''; connection.on("ReceiveConnected", function (result, id) { connectionID = id; document.getElementById("conect-result").innerHTML = `conect-result ${result}:${id}`; ShowGameOverText(false); }); |
切断された通知は受信したときの処理を示します。connectionIDを空文字列とし、切断を表示します。
1 2 3 4 |
connection.onclose(async () => { connectionID = ''; document.getElementById("conect-result").innerHTML = '切断'; }); |
ゲーム開始成功時の処理
サーバサイドからゲームが開始された通知を受信した場合、sceneにコースや車が追加されていない場合はInitGame関数を呼び出して追加の処理をおこないます。そしてチェックボックスにチェックがある場合はBGMを再生します。
1 2 3 4 5 6 7 8 9 10 |
connection.on("ReceiveGameStart", function () { isAlive = true; if (!isGameInited) { isGameInited = true; InitGame(); } isGameOver = false; PlayBgms(); }); |
キー操作への対応
キーがおされているときの処理を示します。まだ接続されていない場合、ゲームオーバー後に通信が切れている場合は前述のConnectAndGameStart関数を呼び出し、接続とゲームスタートの処理をおこなおうとします。
すでに接続されている場合は押されたキーとプレイヤー名をサーバサイドに送信します。
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 |
document.onkeydown = function (e) { if (e.key == "ArrowUp" || e.key == "ArrowDown" || e.key == "ArrowLeft" || e.key == "ArrowRight") e.preventDefault(); // キーが押しっぱなしになっているときの二重送信を防ぐ if (e.key == "ArrowUp" && isUpKeyDown) return; if (e.key == "ArrowDown" && isDownKeyDown) return; if (e.key == "ArrowLeft" && isLeftKeyDown) return; if (e.key == "ArrowRight" && isRightKeyDown) return; if (e.key == "ArrowUp") isUpKeyDown = true; if (e.key == "ArrowDown") isDownKeyDown = true; if (e.key == "ArrowLeft") isLeftKeyDown = true; if (e.key == "ArrowRight") isRightKeyDown = true; if (e.key == "s" && connectionID == '') ConnectAndGameStart(); if (connectionID != '') { let playerName = document.getElementById('player-name').value; if (playerName == '') playerName = '名無しさん'; connection.invoke("DownKey", e.key, playerName).catch(function (err) { return console.error(err.toString()); }); } } |
キーが離されたときの処理を示します。connectionIDが空文字列でない場合はサーバサイドに離されたキーを送信します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
document.onkeyup = function (e) { if (e.key == "ArrowUp") isUpKeyDown = false; if (e.key == "ArrowDown") isDownKeyDown = false; if (e.key == "ArrowLeft") isLeftKeyDown = false; if (e.key == "ArrowRight") isRightKeyDown = false; if (connectionID != '') { connection.invoke("UpKey", e.key).catch(function (err) { return console.error(err.toString()); }); } } |
更新処理
サーバーサイドから更新処理をおこなう旨を受信したときの処理を示します。
更新するのは自車だけでなくライバル車全体の座標、回転状態、プレイヤー名です。そこでライバル車の状態を一時保存するための配列を用意します。
ReceiveStartUpdateを受信したら配列を空にします。そしてReceiveUpdateを受信したら、それが自車のものかライバル車のものかを送られてきたconnectionIDとグローバル変数に保存したものとを比較して調べます。そして自車なら自車用の一時変数、ライバル車であればライバル車用の一時変数に保存します。ReceiveEndUpdateを受信したら前述のUpdate関数を呼び出して更新処理をおこないます。
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 |
// 自車用の一時的変数 let x = 0; let y = 0; let z = 0; let rx = 0; let ry = 0; let rz = 0; // ライバル車用の一時的配列 let otherXs = []; let otherYs = []; let otherZs = []; let otherRXs = []; let otherRYs = []; let otherRZs = []; let otherNames = []; connection.on("ReceiveStartUpdate", function () { otherXs = []; otherYs = []; otherZs = []; otherRXs = []; otherRYs = []; otherRZs = []; otherNames = []; }); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
connection.on("ReceiveUpdate", function (_id, _x, _y, _z, _rx, _ry, _rz, _speed, _name) { if (connectionID == _id) { x = _x; y = _y; z = _z; rx = _rx; ry = _ry; rz = _rz; speed = _speed; } else { otherXs.push(_x); otherYs.push(_y); otherZs.push(_z); otherRXs.push(_rx); otherRYs.push(_ry); otherRZs.push(_rz); otherNames.push(_name); } }); |
1 2 3 |
connection.on("ReceiveEndUpdate", function () { Update(); }); |
走行距離や時速などのプレイヤーの状態を受信したらこれをcanvas上部に表示させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
connection.on("ReceivePlayerInfo", function (text1, text2) { const tf1 = document.getElementById("info1"); tf1.style.transform = "translate(30px, 10px)"; tf1.style.backgroundColor = "#00008B"; tf1.style.color = "white"; tf1.style.fontSize = "18px"; tf1.innerHTML = text1; const tf2 = document.getElementById("info2"); tf2.style.transform = "translate(30px, 40px)"; tf2.style.backgroundColor = "#00008B"; tf2.style.color = "white"; tf2.style.fontSize = "18px"; tf2.innerHTML = text2; }); |
クラッシュ時の処理
サーバーサイドでイベントが通知されたらそれに対応した処理をおこないます。
クラッシュしたらクラッシュ時の効果音を鳴らします。
1 2 3 4 5 6 7 8 9 10 |
connection.on("ReceiveCrash", function (id) { if (connectionID == id) { isAlive = false; // isAliveフラグをクリアして吹っ飛んでいる自車をカメラが追わないようにする if (soundCheckbox.checked) { soundMiss.currentTime = 0; soundMiss.play(); } } }); |
復帰時の処理
クラッシュから復帰したらisAliveフラグをセットして再びカメラが自車を追うようにします。
1 2 3 4 5 |
connection.on("ReceiveRecover", function (id) { if (connectionID == id) { isAlive = true; } }); |
ReceiveGameOveredを受信したらゲームオーバーになったということなので、BGMを停止してcanvasにその旨を表示させます。このあと前述のとおりサーバーサイドで通信を切断する処理がおこなわれます。
ゲームオーバー時の処理
1 2 3 4 5 6 |
connection.on("ReceiveGameOvered", function (id) { if (connectionID == id) { StopBgm(); ShowGameOverText(true); } }); |
ゲーム参加を拒否されたときの処理
すでにプレイできる最高人数に達しているにもかかわらずゲームを開始しようとしたときはReceiveDenyNewPlayerを受信することになります。この場合はdeny-new-playerページにリダイレクトします。
1 2 3 |
connection.on("ReceiveDenyNewPlayer", function () { window.location.href = './deny-new-player'; }); |
deny-new-playerページの内容ですが、あまり作り込むようなものではないので以下のような簡単なものにしています。
Pages\CarRace3d\deny-new-player.cshtml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
@page @{ Layout = ""; } <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>プレイ不可</title> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> </head> <body> <h1>現在、混雑しています。</h1> <p>同時にプレイできるのは7人までです。空きができるまでしばらくお待ちください。</p> </body> </html> |
スコアランキングの表示
スコアランキングのページは以下のようになっています。
Pages\CarRace3d\hi-score.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 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 |
@page @{ Layout = ""; } <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>鳩でもわかるカーレース 上位30位</title> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"> <link rel="stylesheet" href="../css/hiscore.css"> </head> <body> @{ string path = "../hiscore-car-race.txt"; List<CarRace3d.Hiscore> hiscores = new List<CarRace3d.Hiscore>(); if (System.IO.File.Exists(path)) { System.IO.StreamReader sr = new StreamReader(path); string text = sr.ReadToEnd(); string[] vs1 = text.Split('\n'); foreach (string str in vs1) { try { string[] vs2 = str.Split(','); CarRace3d.Hiscore hiscore = new CarRace3d.Hiscore(vs2[0], long.Parse(vs2[1]), vs2[2]); hiscores.Add(hiscore); } catch { } } sr.Close(); } } <div id = "container"> <div id = "h1">鳩でもわかるカーレース 上位30位</div> <div id = "left"> <input type="button" id = "back" onclick="history.back()" value="戻る"> <div id = "result" > <table class="table" border="1" id="table"> @{ int num = 0; } @foreach(CarRace3d.Hiscore hiscore in hiscores) { num++; <tr> <td>@num 位</td> <td>@hiscore.Name</td> <td>@hiscore.Score</td> <td>@hiscore.Time</td> </tr> } @if (num < 30) { @for (num++; num <= 30; num++) { <tr> <td>@num 位</td> <td></td> <td></td> <td></td> </tr> } } </table> </div> </div> <div id = "right" > <div id = "h2">鳩がつくったその他のゲーム</div> <p class = "game"><a href="https://lets-csharp.com/samples/2204/aspnetcore-app-zero/CrashRoller" target="_blank" rel="noopener">クラッシュローラー</a></p> <p class = "game"><a href="https://lets-csharp.com/samples/2201/speed/" target="_blank" rel="noopener">カードゲーム スピード</a></p> <!-- ほかにも自作ゲームのページへのリンクを設置する --> </div> </div> </body> </html> |
CSSファイルですが、以下のようになっています。
wwwroot\css\hiscore.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 |
body { background-color: #444; color: #fff; } #container { width: 100%; max-width: 800px; margin-right: auto; margin-left: auto; } #back { margin-left: 10px; margin-bottom: 30px; width:100px; } #left { width: 50%; max-width: 400px; float:left; margin-left: 10px; } #right { width: 40%; max-width: 400px; float:right; } #h1 { font-size:160%; margin:20px; } #h2 { font-size:120%; margin:20px; } .game { font-size:110%; margin:10px; } a { color:#FFF; } a:hover { color:#FFF; } /* 600ピクセル以下の端末でアクセスしたときはカラムを左右にわけない */ @@media screen and (max-width: 600px) { #left { width: 100%; margin-left: 10px; margin-right: 10px; } #right { width: 100%; margin-left: 10px; margin-right: 10px; } } |