仕上げとしてクライアント サーバー通信の処理をおこないます。これでランキングを偽装できないWebアプリの完成です。
Contents
CrashRollerHubクラスを定義する
PagesフォルダのなかにCrashRoller.cshtml.csという名前のファイルを作成して以下のように書きます。
Pages\CrashRoller.cshtml.cs
1 2 3 4 5 6 7 8 9 |
using Zero; // プロジェクト名 using CrashRoller; // Gameクラスはこの名前空間にある namespace SignalRChat.Hubs { public class CrashRollerHub : Hub { } } |
インデントが深くなるので以降は名前空間を省略して書きます。
フィールド変数ですが、ConnectionIDをつかって対応するGameオブジェクトを取得できるようにしておきます。
1 2 3 4 5 6 7 8 |
public class CrashRollerHub : Hub { string GroupName = "CrashRollerHub"; string ConnectionID = ""; static Dictionary<string, IClientProxy> ClientProxyMap = new Dictionary<string, IClientProxy>(); static Dictionary<string, Game> Games = new Dictionary<string, Game>(); } |
接続成功時の処理
接続に成功したときの処理を示します。
接続に成功したらクライアントにこれを伝えます。そしてGameオブジェクトを生成してリストに追加します。
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 |
public class CrashRollerHub : Hub { public override async Task OnConnectedAsync() { // 接続に成功したことをクライアントサイドに通知する await Clients.Caller.SendAsync("ReceiveMessage", "接続成功", Context.ConnectionId, GetDateText()); await Groups.AddToGroupAsync(Context.ConnectionId, GroupName); await base.OnConnectedAsync(); // Clients.Callerを辞書に登録 ClientProxyMap.Add(Context.ConnectionId, Clients.Caller); ConnectionID = Context.ConnectionId; // Gameオブジェクトを生成して辞書に登録 Game game = new Game(); Games.Add(Context.ConnectionId, game); // イベントが発生したら処理できるようにイベントハンドラを追加する game.ChangeStatus += Game_ChangeStatus; game.StageClear += Game_StageClear; game.NextStage += Game_NextStage; game.HitEmemy += Game_HitEmemy; game.DeadPlayer += Game_DeadPlayer; game.GameStarted += Game_GameStarted; game.GameOvered += Game_GameOvered; Game_ChangeStatus(); // 後述 } string GetDateText() { DateTime now = DateTime.Now; return String.Format("{0:00}:{1:00}:{2:00}", now.Hour, now.Minute, now.Second); } } |
切断されたときの処理
接続が切断されたときの処理を示します。辞書からGameオブジェクトを削除するとともにGameオブジェクト内でタイマーイベントが発生してもなにも起きないようにイベントハンドラを削除します。イベントハンドラの削除はGameクラスのDestroyGameメソッド内でおこなわれます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class CrashRollerHub : Hub { public override async Task OnDisconnectedAsync(Exception? exception) { await Groups.RemoveFromGroupAsync(Context.ConnectionId, GroupName); await base.OnDisconnectedAsync(exception); Game game = Games[Context.ConnectionId]; game.DestroyGame(); // 辞書内からContext.ConnectionIdキーを削除する ClientProxyMap.Remove(Context.ConnectionId); Games.Remove(Context.ConnectionId); } } |
キーが押されたり離されたりしたら、それをGameオブジェクトで処理させます。
クライアントサイドのイベントをサーバーサイドに送る
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class CrashRollerHub : Hub { public void DownKey(string key) { // 長大なデータが送りつけられるかもしれないので対策 if (key.Length > 16) return; Game game = Games[Context.ConnectionId]; game.OnKeyDown(key); } public void UpKey(string key) { // 長大なデータが送りつけられるかもしれないので対策 if (key.Length > 16) return; Game game = Games[Context.ConnectionId]; game.OnKeyUp(key); } } |
クライアントサイドでゲームスタートの処理がおこなわれたらGameクラスのGameStartメソッドを呼び出します。またこれによってゲームスタートの処理がサーバサイドで正常におこなわれたときはこれをクライアントサイドに通知します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class CrashRollerHub : Hub { public void GameStart() { Game game = Games[Context.ConnectionId]; game.GameStart(); } private void Game_GameStarted() { Task.Run(async () => { await ClientProxyMap[ConnectionID].SendAsync( "ReceiveGameStarted" ); }); } } |
サーバーサイドのデータをクライアントサイドに通知する
Gameオブジェクトのなかではタイマーによりイベントが発生します。イベントが発生したらその都度Gameオブジェクト内のデータを取得してクライアントサイドに通知します。
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 |
public class CrashRollerHub : Hub { private void Game_ChangeStatus() { Task.Run(async () => { Game game = Games[ConnectionID]; await ClientProxyMap[ConnectionID].SendAsync( "ReceiveUpdatePlayer", game.PlayerX, game.PlayerY, game.PlayerDirect ); await ClientProxyMap[ConnectionID].SendAsync( "ReceiveUpdateEnemy", game.Enemy1X, game.Enemy1Y, game.Enemy1Direct, game.Enemy2X, game.Enemy2Y, game.Enemy2Direct ); await ClientProxyMap[ConnectionID].SendAsync( "ReceiveUpdateScore", game.EatX, game.EatY, game.Score, game.Rest, game.StageNumber ); bool isRollerHold = (game.IsHoldRollerE || game.IsHoldRollerW || game.IsHoldRollerN || game.IsHoldRollerS); await ClientProxyMap[ConnectionID].SendAsync( "ReceiveUpdateRoller", game.RollerNS?.X, game.RollerNS?.Y, game.RollerWE?.X, game.RollerWE?.Y, isRollerHold ); }); } } |
敵を倒したりミスをしたりゲームオーバーになったときは、これをクライアントサイドに通知します。
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 |
public class CrashRollerHub : Hub { private void Game_HitEmemy() { Task.Run(async () => { Game game = Games[ConnectionID]; await ClientProxyMap[ConnectionID].SendAsync( "ReceiveHitEmemy", game.PositionHitEmemyX, game.PositionHitEmemyY, game.AddPointCrashEnemy ); }); } private void Game_DeadPlayer() { Task.Run(async () => { await ClientProxyMap[ConnectionID].SendAsync( "ReceiveDeadPlayer" ); }); } private void Game_GameOvered() { Task.Run(async () => { await ClientProxyMap[ConnectionID].SendAsync( "ReceiveGameOvered" ); }); } } |
ステージクリア時とじっさいに新しいステージに移動したときも、これをクライアントサイドに通知します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class CrashRollerHub : Hub { private void Game_StageClear() { Task.Run(async () => { await ClientProxyMap[ConnectionID].SendAsync( "ReceiveStageClear" ); }); } private void Game_NextStage() { Task.Run(async () => { await ClientProxyMap[ConnectionID].SendAsync( "ReceiveNextStage" ); }); } } |
ランキングへの登録
ゲームオーバーになったらそのときのスコアとランキングを比較して必要ならランキングにプレイヤー名とスコア、時刻を登録します。以下はプレイヤー名とスコア、時刻を扱うためのクラスです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
namespace CrashRoller { public class Hiscore { public Hiscore(string name, long score, string time) { Name = name; Score = score; Time = time; } public string Name = ""; public long Score = 0; public string Time = ""; } } |
ゲームオーバーに伴いクライアントサイドからデータが送られてきたらランキングに反映させます。現在のランキングにクライアントサイドから送られてきたデータを追加してスコアでソートします。上位から30位までを取得してテキストファイルとして保存します。
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 |
public class CrashRollerHub : Hub { public void SendData(string name) { // プレイヤー名は12文字まで。それ以上の長さの文字列が送りつけられた場合は切り詰める if(name.Length > 12) name = name.Substring(0, 12); // プレイヤー名とスコア、時刻はカンマ区切り。 // プレイヤー名のなかにカンマがある場合は別の文字に置換する // 保存場所が同じディレクトリでないのは間違って上書きアップロードしてしまわないようにするという // 管理人の個人的な都合による name.Replace(",", "_"); string path = "../hiscore-crash-roller.txt"; List<Hiscore> hiscores = new List<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(','); Hiscore hiscore = new Hiscore(vs2[0], long.Parse(vs2[1]), vs2[2]); hiscores.Add(hiscore); } catch { } } sr.Close(); } Game game = Games[Context.ConnectionId]; DateTime dt = DateTime.Now; string time = String.Format( "{0}/{1:00}/{2:00} {3:00}:{4:00}:{5:00}", dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second ); Hiscore newScore = new Hiscore(name, game.Score, time); hiscores.Add(newScore); hiscores = hiscores.OrderByDescending(_ => _.Score).Take(30).ToList(); System.Text.StringBuilder sb = new System.Text.StringBuilder(); foreach (Hiscore hiscore in hiscores) { sb.Append(hiscore.Name + "," + hiscore.Score.ToString() + "," + hiscore.Time + "\n"); } System.IO.StreamWriter sw = new StreamWriter(path); sw.Write(sb); sw.Close(); } } |
それから実際にクライアント サーバー通信の処理ができるようにするにはProgram.csの最後のほう、app.Run();の直前あたりに以下を記述しなければなりません。
1 2 3 4 |
// 省略 app.MapHub<CrashRollerHub>("/CrashRollerHub"); // この行を追加 app.Run(); |
スコアランキングを表示させる
あとはこのファイルを読み取って内容を表示できるようにすれば完成です。
新しいページCrashRollerHiscoreを作成します。
Pages\CrashRollerHiscore.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 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 |
@page @{ Layout = ""; string baseurl = Global.BaseUrl; } <!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'> <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> @{ string path = "../hiscore-crash-roller.txt"; List<CrashRoller.Hiscore> hiscores = new List<CrashRoller.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(','); CrashRoller.Hiscore hiscore = new CrashRoller.Hiscore(vs2[0], long.Parse(vs2[1]), vs2[2]); hiscores.Add(hiscore); } catch { } } sr.Close(); } } <div id = "container1"> <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(CrashRoller.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/2201/speed/" target="_blank" rel="noopener">カードゲーム スピード</a></p> <!-- ほかにも自作ゲームのページへのリンクを設置する --> </div> </div> </body> </html> |