JavaScriptで作成したクラッシュローラーもどきのゲームにランキング機能を追加します。
このゲームをある方に見せたら「ランキング機能を追加したほうがよい」といわれました。たしかにランキング機能があるとなんとしてもランクインさせたいという気持ちになってしまいます。別のゲームで10位まで表示させているものがあるのですが、今回は30位まで表示させます。我こそは!という方がいるなら自分の名前で全部埋めてしまってもかまいません。
追記:このページに書かれている方法ではランキングを偽装することができてしまいます。問題点と偽装の方法はゲームのランキング偽装するのがチョロすぎて草な件を参照してください。またランキング偽装対策をしたものも公開しています。ASP.NET Coreでクラッシュローラーをつくるもみていただけるとありがたいです。
Contents
ハンドル名設定のためのテキストボックスを設置
まずプレイヤーが自分の名前を登録できるようにします。ユーザーがハンドルネームを指定できるように、テキストボックスをゲームのページに設置します。そしてゲームオーバー時に30位以内に入っている場合は、ここに入力されている名前がスコアとそのときの時刻とともにランキングに掲載されるようにします。
テキストボックスを表示しなければならないので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 |
<!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"> <style> body { background-color: #444; color: #fff; } #container { width: 100%; max-width: 960px; margin-right: auto; margin-left: auto; } .display-none { display:none; } #player-name { width: 180px; } </style> </head> <body> <div id = "container"> <canvas id = "can"></canvas> <!-- 追加したのはこの部分 --> <div> <label for="player-name">ハンドルネーム (12文字まで):</label><br> <input type="text" id="player-name" name="player-name" maxlength="12" size="10"> <input type="button" value="ゲームスタート" onclick="GameStart()"><br> <input type="button" onclick="location.href='./high-score.html'" value="上位30位をチェック"> </div> <!-- 追加部分 ここまで --> <div class = "display-none"> <img id = "player" src="./player.png" /> <img id = "enemy1" src="./enemy1.png" /> <img id = "enemy2" src="./enemy2.png" /> </div> </div> <script type='text/javascript' src='./map.js'></script> <script type='text/javascript' src='./main.js'></script> </body> </html> |
Canvasの大きさを変更
それからCanvasの下にあれこれ追加したのでCanvasのサイズを変更しないとディスプレイのなかにうまく表示されなくなります。そこでCanvasの大きさを変更するために必要な関数を修正します。
map.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
const TOP_MARGIN = 0; // 10から0に変更 function GetCanvasSize() { const a = window.innerWidth / 2; const b = window.innerHeight / 3; if(a < b) { CANVAS_WIDTH = a * 2 - CANVAS_WIDTH / 32; CANVAS_HEIGHT = a * 3 - CANVAS_WIDTH / 32 * 3 - 40; // 高さを40ピクセル短くする } else { CANVAS_WIDTH = b * 2 - CANVAS_WIDTH / 32; CANVAS_HEIGHT = b * 3 - CANVAS_WIDTH / 32 * 3 - 40; // 高さを40ピクセル短くする } EXPANSION_RATE = CANVAS_WIDTH / 200; CHARACTOR_SIZE = CANVAS_WIDTH / 16; BORDER_WIDTH = CANVAS_WIDTH / 160; } |
ゲームオーバー時にランキングに反映させる
あとはゲームオーバーになったときにSendData関数を実行するだけです。プレイヤーの名前とスコアが取得されてsave-data.phpにPOSTされます。ハンドルネームの欄になにも入力されていなければ「名無しさん」とします。このあたりは2ちゃんねるの影響をうけています。いまは2ちゃんねるではなく5ちゃんねるですけど。
save-data.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function SendData() { const textbox1 = document.getElementById("player-name"); let phpurl = "./save-data.php"; // ハンドルネームの欄になにも入力されていなければ「名無しさん」とする let name = textbox1.value; if(name == "") name = "名無しさん"; $.post(phpurl, { name:name, score:score, }); } |
上記の関数をゲームオーバーになったタイミングで呼べばいいですね。
map.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 |
function OnDeadPlayer() { StopTimer1(); StopTimer2(); deadSound.play(); rest--; addPointCrashEnemy = INIT_ADD_POINT_CRASH_ENEMY; setTimeout(() => { if(rest <= 0) { isGaming = false; StopBGM(); gameOverSound.play(); Draw(); SendData(); // ここで呼び出す return; } playerX = INIT_PLAYER_X; playerY = INIT_PLAYER_Y; playerDirect = Direct.Stop; enemies[0].Reset(); enemies[1].Reset(); // 停止させていたタイマーをStartさせてゲームを続行 StartTimer1(interval); StartTimer2(); }, 2000); } |
Canvasの縦サイズが短くなったのでこのままではGameOverの文字がうまく表示されなくなりました。
ところで残機0になったらゲームオーバーなのですから、残機0の表示とゲームオーバーの表示を別にする必要はないのではないでしょうか? そこでDrawRest関数を変更します。
map.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function DrawRest() { if(isGaming) { // ゲームオーバーでないとき ctx.font="bold 20px MS ゴシック"; ctx.fillStyle = "rgb(255, 255, 222)"; let scoreText = '残 ' + rest ctx.fillText(scoreText, 10, 410 * CANVAS_WIDTH / 320); } else { // ゲームオーバー時の処理 ctx.font="bold 20px MS ゴシック"; ctx.fillStyle = "rgb(255, 0, 0)"; ctx.fillText('GAME OVER',10,410 * CANVAS_WIDTH / 320); } } |
Draw関数のなかでDrawGameOverIfNeed関数を呼び出していましたが、これは不要なので削除します。
以上でJSでやる処理はおわりです。
ゲームオーバー時にスコアをPostする
つぎにPHPでやらなければならない処理があります。
ゲームオーバーになったらユーザー名とスコアなどがPostされるのですが、これだけだと外部から不正なPostリクエストをして最高スコアを偽造することができてしまいます。そこでPostされたデータを受け取ったときはそれが自分のドメインからかどうかを確認します。これでランキング偽装は防げると思います。
ランキングはデータベースをつかってもいいのですが、格納されているデータにパスワードのようなものは存在せず、格納されているデータはすべて表示してしまうのであまりセキュリティは考えなくてよいと思います。そこでcsvファイルに保存し、それを表示するという安易な方法を採用しています。
csvファイルも以下に示すsave-data.phpも同じディレクトリ内に設置します。
save-data.php
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 |
<?php // 上位30位まで保存 $MaxCount = 30; saveData(); function GetFileName(){ $filename = "./highscore.csv"; return $filename; } function sortByKey($key_name, $sort_order, $array) { foreach ($array as $key => $value) { $standard_key_array[$key] = $value[$key_name]; } array_multisort($standard_key_array, $sort_order, $array); return $array; } function saveData(){ // https://lets-csharp.com/* 以外からのリクエストは拒否する $referer = $_SERVER['HTTP_REFERER']; if(strpos($referer,'https://lets-csharp.com/') === false) return; $stack = array(); // csvファイルが存在するならデータを配列に変換 if(file_exists(GetFileName())){ $allData = file_get_contents(GetFileName()); $lines = explode("\n", $allData); foreach ( $lines as $line ) { $words = explode(",", $line); if($words[0] == "") continue; $newArray = array( 'name'=> $words[0], 'score'=> $words[1], 'now'=> $words[2], ); $stack[] = $newArray; } } // 配列に送られてきたデータを追加 $name = ""; $score = 0; $name = $_POST["name"]; $score = $_POST["score"]; $now = date("Y-m-d H:i:s"); // 不適切なデータは処理しない // nameがない、長すぎるなど if(mb_strlen($name) > 32) return; if(mb_strlen($score) > 32) return; if($name == "") return; $newArray = array( 'name'=> $name, 'score'=> $score, 'now'=> $now, ); $stack[] = $newArray; // socreが大きい順に配列をソート $sorted_array = sortByKey('score', SORT_DESC, $stack); // 上位からMaxCountだけデータを取得してcsvファイルとして保存する global $MaxCount; $dataCount = count($sorted_array); $str = ""; for($i = 0; $i < $dataCount; $i++){ if($i >= $MaxCount) break; $str .= join(",", $sorted_array[$i]); $str .= "\n"; } file_put_contents( GetFileName(), $str, LOCK_EX ); } |
ランキング情報を表示する
ランキング情報をファイルとして保存したら、これを表示する手段を用意しなければなりません。同じディレクトリ内にあるhigh-score.htmlにアクセスしたらランキング情報が表示されるようにします。
まずhigh-score.htmlを示します。
アクセスされたら後述するget-data.phpでランキング情報を取得してランキングの表示をおこないます。あと他にも自作したゲームをしてほしいのでそのページへのリンクも右側のカラムに表示させます。スマートフォンのような横幅の狭い端末の場合はカラムを左右にわけるのではなく、ランキングの下にリンクを表示させます(レスポンスウェブデザイン)。またゲーム画面に戻るためのボタンも設置します。
high-score.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 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 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
<!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"> <link rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css'> <style> body { background-color: #444; color: #fff; } #container1 { 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; } } </style> </head> <body> <div id = "container1"> <div id = "h1">鳩でもわかるクラッシュローラーもどき 上位30位</div> <div id = "left"> <input type="button" id = "back" onclick="history.back()" value="戻る"> <div id = "result" ></div> </div> <div id = "right" > <div id = "h2">鳩がつくったその他のゲーム</div> <p class = "game"><a href="https://lets-csharp.com/samples/2201/speed/" target="_blank" rel="noopener">カードゲーム スピード</a></p> <p class = "game"><a href="https://lets-csharp.com/samples/2112/shoplifting-boy/" target="_blank" rel="noopener">レトロなゲーム 万引き少年</a></p> <!-- ほかにも自作ゲームのページへのリンクを設置する --> </div> </div> <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script> <script type="text/javascript"> let urldata = "./get-data.php"; let request = createXMLHttpRequest(); request.open("GET", urldata, true); request.send(""); request.onreadystatechange = function() { if (request.readyState == 4 && request.status == 200) { //受信完了時の処理 var result = document.getElementById("result"); var text = document.createTextNode(decodeURI(request.responseText)); let str = text.textContent; const lines = str.split("\n"); $(result).append("<table class=\"table\" border=\"1\" id=\"table\">\n</table>"); var table = document.getElementById("table"); let len = lines.length; let num = 0; for(let i=0; i<len; i++) { if(lines[i] == "") break; let trid = "tr-id" + i.toString(); $(table).append("<tr id = " + trid + "></tr>\n"); const words = lines[i].split(","); let len2 = words.length; var tr = document.getElementById(trid); $(tr).append("<td>" + (i + 1).toString() + " 位</td>"); for(let j=0; j<len2; j++) { $(tr).append("<td>" + words[j] + "</td>"); } $(table).append("</tr>"); } $(result).append(""); } } function createXMLHttpRequest() { if (window.XMLHttpRequest) { return new XMLHttpRequest(); } else if (window.ActiveXObject) { try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e) { try { return new ActiveXObject("Microsoft.XMLHTTP"); } catch (e2) { return null; } } } else { return null; } } </script> </body> </html> |
ランキング情報をcsvファイルから取得する
最後にランキング情報が格納されているcsvファイルからランキングを取得するget-data.phpを示します。
get-data.php
1 2 3 4 5 6 7 8 |
<?php function GetFileName(){ $filename = "./highscore.csv"; return $filename; } $allData = file_get_contents(GetFileName()); echo $allData; |
追記:この方法ではランキングを偽装することができてしまいます。問題点と偽装の方法はゲームのランキング偽装するのがチョロすぎて草な件を参照してください。またランキング偽装対策をしたものも公開しています。ASP.NET Coreでクラッシュローラーをつくるもみていただけるとありがたいです。