これまではランキングを偽装させないためにユーザーとサーバーの間で一対一の処理をおこなってきました。今回は複数のユーザーで情報を共有させます。
タイマーをつかってキーが押されているのであればその方向にキャラクタを移動させます。複数のユーザーでサーバサイドの同じ情報を共有するのでタイマーもそのイベントハンドラもひとつあれば十分なはずです。
ハマりどころ
以下のようなMultiMoveTestHubクラスを定義します。
| 
					 1 2 3 4 5 6  | 
						namespace SignalRChat.Hubs {     public class MultiMoveTestHub : Hub     {     } }  | 
					
以降は以下のように省略して書くことにします。
| 
					 1 2 3  | 
						public class MultiMoveTestHub : Hub { }  | 
					
Hubに状態を保存してはならない
キーが押されているかどうかをフラグにセットし、タイマーイベントが発生したときにフラグの状態に応じて処理ができるかどうかやってみたのですが、これではうまくいきません。フラグはつねにfalseです。またコンストラクタをつくって Console.WriteLine(“生成”);と書いておくとキーを操作するたびにコンストラクタが呼び出されていることがわかります。Hubに状態を保存してもこれでは意味をなさないのです。
| 
					 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  | 
						public class MultiMoveTestHub : Hub {     static System.Timers.Timer Timer = new System.Timers.Timer();     static bool IsFirstConnection = true;     // キーの状態を保存しておく??     bool IsUpKeyDown = false;     bool IsDownKeyDown = false;     bool IsLeftKeyDown = false;     bool IsRightKeyDown = false;     public override async Task OnConnectedAsync()     {         await Clients.Caller.SendAsync("ReceiveMessage", "接続成功", Context.ConnectionId, GetDateText());         await base.OnConnectedAsync();         ClientProxyMap.Add(Context.ConnectionId, Clients.Caller);         // イベントハンドラを追加するのは最初の1回だけ         if (IsFirstConnection)         {             IsFirstConnection = false;             Timer.Interval = 60;             Timer.Elapsed += Timer_Elapsed;             Timer.Start();         }         // 接続しているユーザーが0から1になったときだけタイマーをスタートさせる         if(ClientProxyMap.Count == 1)             Timer.Start();     }     public override async Task OnDisconnectedAsync(Exception? exception)     {         await base.OnDisconnectedAsync(exception);         ClientProxyMap.Remove(Context.ConnectionId);         // 接続しているユーザーが0になったらタイマーを止める         if(ClientProxyMap.Count <= 0)             Timer.Stop();     }     static private void Timer_Elapsed(object? sender, ElapsedEventArgs e)     {         Task.Run(async () =>         {             // IsUpKeyDown, IsDownKeyDown, IsLeftKeyDown, IsRightKeyDown をみて適切に対処する処理?         });     }     public void DownKey(string key)     {         // フィールド変数にキーの状態を保存?         if (key == "ArrowUp")             IsUpKeyDown = true;         if (key == "ArrowDown")             IsDownKeyDown = true;         if (key == "ArrowLeft")             IsLeftKeyDown = true;         if (key == "ArrowRight")             IsRightKeyDown = true;     }     public void UpKey(string key)     {         if (key == "ArrowUp")             IsUpKeyDown = false;         if (key == "ArrowDown")             IsDownKeyDown = false;         if (key == "ArrowLeft")             IsLeftKeyDown = false;         if (key == "ArrowRight")             IsRightKeyDown = false;     } }  | 
					
Playerクラスの定義とキャラクタの移動
そこでPlayerクラスを定義してキーの状態はここに保存します。またConnectionIdをキーにしたPlayerの辞書を静的変数としてMultiMoveTestHubクラス内に定義します。それからGameクラスも静的変数としてMultiMoveTestHubクラス内に定義します。
Playerクラスを示します。キャラクタの座標ですが、X座標とY座標も0以上80以下とします。外に出た場合は反対側にワープさせます。
| 
					 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  | 
						namespace MultiGameTest {     public class Player     {         public bool IsUpKeyDown = false;         public bool IsDownKeyDown = false;         public bool IsLeftKeyDown = false;         public bool IsRightKeyDown = false;         public Direct Direct = Direct.None;         public int X         {             private set;             get;         }         public int Y         {             private set;             get;         }         public void Move()         {             // キーがなにも押されていないときは最後に押されていたキーの方向に移動しつづける             if (IsLeftKeyDown)                 Direct = Direct.Left;             if (IsRightKeyDown)                 Direct = Direct.Right;             if (IsUpKeyDown)                 Direct = Direct.Up;             if (IsDownKeyDown)                 Direct = Direct.Down;             if (Direct == Direct.Left)             {                 X--;                 if (X < 0)                     X = 80;             }             if (Direct == Direct.Right)             {                 X++;                 if (X > 80)                     X = 0;             }             if (Direct == Direct.Up)             {                 Y--;                 if (Y < 0)                     Y = 80;             }             if (Direct == Direct.Down)             {                 Y++;                 if (Y > 80)                     Y = 0;             }         }     }     public enum Direct     {         None,         Up,         Right,         Down,         Left,     } }  | 
					
Gameクラスを独立させて定義する必要はないのですが、今後のために定義しておきます。
| 
					 1 2 3 4 5 6 7 8 9 10 11  | 
						namespace MultiGameTest {     public class Game     {         public void OnTimer(Dictionary<string, Player> players)         {             foreach (Player player in players.Values)                 player.Move();         }     } }  | 
					
MultiMoveTestHubクラスの定義
MultiMoveTestHubクラスを示します。
| 
					 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  | 
						public class MultiMoveTestHub : Hub {     static System.Timers.Timer Timer = new System.Timers.Timer();     static Dictionary<string, IClientProxy> ClientProxyMap = new Dictionary<string, IClientProxy>();     static Dictionary<string, Player> Players = new Dictionary<string, Player>();     static Game Game = new Game();     static bool IsFirstConnection = true;     public override async Task OnConnectedAsync()     {         await Clients.Caller.SendAsync("ReceiveMessage", "接続成功", Context.ConnectionId);         await base.OnConnectedAsync();         ClientProxyMap.Add(Context.ConnectionId, Clients.Caller);         Players.Add(Context.ConnectionId, new Player());         if (IsFirstConnection)         {             IsFirstConnection = false;             Timer.Interval = 60;             Timer.Elapsed += Timer_Elapsed;             Timer.Start();         }         if(ClientProxyMap.Count == 1)             Timer.Start();     }     static private void Timer_Elapsed(object? sender, ElapsedEventArgs e)     {         Task.Run(async () =>         {             Game.OnTimer(Players);             foreach (IClientProxy client in ClientProxyMap.Values)             {                 foreach (var pair in Players)                     await client.SendAsync("ReceiveUpdate", pair.Key, pair.Value.X, pair.Value.Y);             }         });     }     public override async Task OnDisconnectedAsync(Exception? exception)     {         if (Global.IsDebug)             Console.WriteLine("切断しました:" + Context.ConnectionId);         await base.OnDisconnectedAsync(exception);         ClientProxyMap.Remove(Context.ConnectionId);         Players.Remove(Context.ConnectionId);         if(ClientProxyMap.Count <= 0)             Timer.Stop();     }     public void DownKey(string key)     {         // 長大なデータが送りつけられるかもしれないので対策         if (key.Length > 16)             return;         Player player = Players[Context.ConnectionId];         if (key == "ArrowUp")             player.IsUpKeyDown = true;         if (key == "ArrowDown")             player.IsDownKeyDown = true;         if (key == "ArrowLeft")             player.IsLeftKeyDown = true;         if (key == "ArrowRight")             player.IsRightKeyDown = true;     }     public void UpKey(string key)     {         if (key.Length > 16)             return;         Player player = Players[Context.ConnectionId];         if (key == "ArrowUp")             player.IsUpKeyDown = false;         if (key == "ArrowDown")             player.IsDownKeyDown = false;         if (key == "ArrowLeft")             player.IsLeftKeyDown = false;         if (key == "ArrowRight")             player.IsRightKeyDown = false;     } }  | 
					
クライアントサイドの処理
次にクライアントサイドの処理ですが、以下のようにします。自分と他のプレイヤーとでは表示されるイメージを変えます。
Index.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  | 
						@page @{     ViewData["Title"] = "複数プレイヤーによる移動のテスト";     Layout = "_Layout3";     string baseurl = Global.BaseUrl; } <div class="container">     <p id = "conect-result"></p>     <p id = "move"></p>     <canvas id="can"></canvas>     <img src = "@baseurl/multi-move-test/player.png" id ="player">     <img src = "@baseurl/multi-move-test/other.png" id ="other"> </div> <script src="@baseurl/js/signalr.js"></script> <script>     "use strict";     let imgPlayer = document.getElementById('player');     let imgOther = document.getElementById('other');     let canvas = document.getElementById('can');     let ctx = canvas.getContext('2d');     canvas.width = 280;     canvas.height = 280;     function DrawPlayers()     {         DrawLines();         for(let i = 0; i < otherXs.length; i++)         {             ctx.drawImage(imgOther, otherXs[i] * 3, otherYs[i] * 3, 36, 36);         }         ctx.drawImage(imgPlayer, playerX * 3, playerY * 3, 36, 36);     }     function DrawLines()     {         ctx.strokeStyle = '#fff';         for(let x=0; x<280; x+=20*3)         {             ctx.beginPath();             ctx.moveTo(x + 18, 0);             ctx.lineTo(x + 18, canvas.height);             ctx.stroke();         }         for(let y=0; y<280; y+=20*3)         {             ctx.beginPath();             ctx.moveTo(0, y + 18);             ctx.lineTo(canvas.width, y + 18);             ctx.stroke();         }     } </script>  | 
					
サーバサイドに接続してデータを送信するときの処理を示します。
Index.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  | 
						<script>     let connection = new signalR.HubConnectionBuilder().withUrl("@baseurl/MultiMoveTestHub").build();     connection.start().then(function () {         console.log("connection.start()");     }).catch(function (err) {         document.getElementById("conect-result").innerHTML = '接続失敗';     });     let isUpKeyDown = false;     let isDownKeyDown = false;     let isLeftKeyDown = false;     let isRightKeyDown = false;     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;         connection.invoke("DownKey", e.key)             .catch(function (err) {                 return console.error(err.toString());             });     }     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;         connection.invoke("UpKey", e.key)             .catch(function (err) {                 return console.error(err.toString());             });     } </script>  | 
					
サーバサイドからのデータを受信したときの処理を示します。
Index.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  | 
						<script>     // 接続に成功したらメッセージが送られてくるのでconnectionIdを保存しておく     let connectionId = '';     connection.on("ReceiveMessage", function (result, id) {         connectionId = id;         document.getElementById("conect-result").innerHTML = `${result}:${datetime}<br>ConnectionId:${id}`;     });     // プレイヤーの座標が送られてくるので自分のconnectionIdと同じかどうか調べて     // グローバル変数または配列に保存する     connection.on("ReceiveUpdate", function (id, x, y) {         // 全体再描画するために必要なデータを保存する         if(connectionId == id)         {             playerX = x;             playerY = y;         }         else         {             otherXs.push(x);             otherYs.push(y);         }         // 自分のconnectionIdと同じ場合だけCanvasをクリアして全体を描画しなおす         if(connectionId == id)         {             document.getElementById("move").textContent = 'プレイヤーの座標:' + x + ", " + y;             ctx.fillStyle = '#000';             ctx.fillRect(0,0,canvas.width,canvas.height);             DrawPlayers();             otherXs = [];             otherYs = [];         }     }); </script>  | 
					
