ASP.NET Core版 ボンバーマンのような対戦型ゲームをつくる(4)の続きですが、ここからは延長戦です。
実は一度プログラミング実況配信系YouTuberのT.Umezawaさんの配信で紹介していただいたのですが、そこで問題点がいくつか発生。一応、それらは改善してもう一度レビューしていただくことにしました。これがその配信動画です。
35:00あたりから辛口レビューが始まります。
Contents
浮上した課題
新たにプレイヤーが参戦したときにキャラクタがワープしてしまう問題について
これに対しては新たにプレイヤーが参戦したことを通知するメッセージを表示する。プレイヤーを点滅させることで違和感を減らせるのではないかと考えていたのですが、やっぱり見る人によっては違和感はなくならないようです。しかもそのさいに設置されている爆弾が消えてしまうのも違和感があるとのこと。
ではどうすればいいのか? 爆弾を消さない代わりに無敵状態を長くする? それだと長時間プレイヤーが点滅するのでこれも変に見えてしまうかもしれません。
そこで対案として出てきたのが
最初は壁のなかなど攻撃されない場所に登場させる
またこれだけだとそのままの状態で放置されると他の人がいつまで経ってもプレイに参加できないという問題がおきるのですが、それについては
一定時間経過したら強制的に通路に出される
ようにすることでよいのではないかと助言をいただくことができました。これで違和感があるかどうかは実際につくって動かしてみないとわかりません。
また斜め移動のバグへの対策ですが、
動作が鈍くなったのではないか
という指摘も受けました。これは移動可能フラグをその都度リセットしているからなのですが、やり方がよくなかったようです。
ほかにも
同時にプレイできるのが4人までは少なすぎる
とか
壁がなくなってしまってもそのままなのは改善すべき
というありがたい意見をいただくことができました。
これは突然壁が出現すると違和感必至なので、壁が出現する前に出現する壁を点滅させるとかの演出をいれればそれほど違和感はないかもしれません。そのあたり改良してみようかと考えています。
今日はここまで。修正のコードが書けたら掲載します。過去記事は修正しないでここに加筆していこうと思います。
同時にプレイできる人数を増やす
簡単にできそうなものからやっていくことにします。
同時にプレイできる人数を増やす場合、最初に登場する位置も変えたほうがいいかもしれません。正方形のフィールドの各頂点だけでなく各辺の中点にも登場させるようにすれば8人でプレイできるゲームに作りかえることができます。
Playerクラスの修正
まずPlayerクラスに変更を加えます。
ここではResetメソッドの引数が0~3から0~7に増えるのでその場合の初期座標を設定しています。それからプレイヤーの登場場所はフィールドの外の壁のなかとします。それからいつまでもフィールドの外にいると他の人がプレイに参加できなくなるので5秒経過しても壁のなかにいる場合は強制的にフィールドのなかに移動させます。
| 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 | public class Player {     int TimeToStart = 0; // 強制的にフィールド内に移動させるまでの時間     const int TIME_TO_START = 24 * 5; // 上記の初期値     bool NoMove = true; // リセット後一度も移動していない場合は true     public void Reset(int i)     {         // プレイヤーの場合は5秒間フィールドの外にいてかまわない         if (ConnectionID != "")         {             TimeToStart = TIME_TO_START;             NoMove = true;         }         if (i == 0)         {             X = 32;             Y = 32;             // プレイヤーの場合はフィールドの外に配置する             if (ConnectionID != "")                 X -= 32;         }         else if (i == 1)         {             X = 32 * 13;             Y = 32;             if (ConnectionID != "")                 X += 32;         }         else if (i == 2)         {             X = 32;             Y = 32 * 13;             if (ConnectionID != "")                 X -= 32;         }         else if (i == 3)         {             X = 32 * 13;             Y = 32 * 13;             if (ConnectionID != "")                 X += 32;         }         else if (i == 4)         {             X = 32 * 13;             Y = 32 * 7;             if (ConnectionID != "")                 X += 32;         }         else if (i == 5)         {             X = 32 * 7;             Y = 32 * 13;             if (ConnectionID != "")                 Y += 32;         }         else if (i == 6)         {             X = 32 * 7;             Y = 32;             if (ConnectionID != "")                 Y -= 32;         }         else if (i == 7)         {             X = 32;             Y = 32 * 7;             if (ConnectionID != "")                 X -= 32;         }         ResetNumber = i;         _beforeDirect = Direct.None;         _movingDown = false;         _movingUp = false;         _movingLeft = false;         _movingRight = false;         NextColumn = CurrentColumn = X / Game.CHARACTER_SIZE;         NextRow = CurrentRow = Y / Game.CHARACTER_SIZE;         IsDead = false;     } } | 
強制的にプレイヤーをフィールド内に移動させるメソッドを示します。
| 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 | public class Player {     public void ForceMove()     {         // まだ移動していないことを意味するフラグをクリア         NoMove = false;         if (ResetNumber == 0)         {             // フィールドのなかに移動             if (ConnectionID != "")                 X = 32;         }         else if (ResetNumber == 1)         {             if (ConnectionID != "")                 X = 32 * 13;         }         else if (ResetNumber == 2)         {             if (ConnectionID != "")                 X = 32;         }         else if (ResetNumber == 3)         {             if (ConnectionID != "")                 X = 32 * 13;         }         else if (ResetNumber == 4)         {             if (ConnectionID != "")                 X = 32 * 13;         }         else if (ResetNumber == 5)         {             if (ConnectionID != "")                 Y = 32 * 13;         }         else if (ResetNumber == 6)         {             if (ConnectionID != "")                 Y = 32;         }         else if (ResetNumber == 7)         {             if (ConnectionID != "")                 X = 32;         }     } } | 
UpdatePlayerメソッドが呼び出されたらTimeToStartをデクリメントします。強制的に移動させる処理をおこなうのはTimeToStartが0になったときで、それまで移動処理が行なわれていない場合です。この場合はForceMoveメソッドを呼び出して移動処理をおこないます。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public class Player {     public void UpdatePlayer()     {         if(_movingDown || _movingUp || _movingLeft || _movingRight)             NoMove = false;         // TimeToStartが0になっても移動していないのであれば         // 上記のForceMoveメソッドでフィールド内に移動させる         // リセット後、移動処理がおこなわれているのであればなにも起きない         TimeToStart--;         if (TimeToStart <= 0 && NoMove)         {             ForceMove();             return;         }         // あとは既存のものと同じ     } } | 
それからこれまでは外に破壊できない壁が存在したため場外に移動することはできなかったのですが、初期位置が変更されたことで場外への移動が可能になってしまいました。しかも一度場外で出てしまうと戻ってくることができません。これでは困るので上下左右に移動できるかを調べるメソッドを修正します。
| 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 | public class Player {     bool CanMoveUp(int col, int row)     {         if (row > 0 && !Game.IndestructibleWalls.Any(wall => wall.Column == col && wall.Row == row - 1))             return !Game.Walls.Any(wall => wall.Column == col && wall.Row == row - 1);         return false;     }     bool CanMoveDown(int col, int row)     {         if (row + 1 < Game.RowMax && !Game.IndestructibleWalls.Any(wall => wall.Column == col && wall.Row == row + 1))             return !Game.Walls.Any(wall => wall.Column == col && wall.Row == row + 1);         return false;     }     bool CanMoveLeft(int col, int row)     {         if (col > 0 && !Game.IndestructibleWalls.Any(wall => wall.Column == col - 1 && wall.Row == row))             return !Game.Walls.Any(wall => wall.Column == col - 1 && wall.Row == row);         return false;     }     bool CanMoveRight(int col, int row)     {         if (col + 1 < Game.ColMax && !Game.IndestructibleWalls.Any(wall => wall.Column == col + 1 && wall.Row == row))             return !Game.Walls.Any(wall => wall.Column == col + 1 && wall.Row == row);         return false;     } } | 
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | public class Game {     public static void Init()     {         // この部分は既存のものと同じ。破壊できない壁を追加する         IndestructibleWalls.Clear();         string[] vs1 = mapText;         for (int row = 0; row < RowMax; row++)         {             string str = vs1[row];             char[] vs2 = str.ToArray();             for (int col = 0; col < ColMax; col++)             {                 if (vs2[col] == '1')                 {                     IndestructibleWalls.Add(new Wall(col, row));                 }             }         }         // プレイヤーの数が増えたので破壊できる壁の位置を変更する         Walls.Clear();         for (int row = 0; row < RowMax; row++)         {             string str = vs1[row];             char[] vs2 = str.ToArray();             for (int col = 0; col < ColMax; col++)             {                 if (row == 1 && (col == 1 || col == 2 || col == 6 || col == 7 || col == 8 || col == 12 || col == 13))                     continue;                 if (row == 2 && (col == 1 || col == 7 || col == 13))                     continue;                 if (row == 6 && (col == 1 || col == 13))                     continue;                 if (row == 7 && (col == 1 || col == 2 || col == 12 || col == 13))                     continue;                 if (row == 8 && (col == 1 || col == 13))                     continue;                 if (row == 12 && (col == 1 || col == 7 || col == 13))                     continue;                 if (row == 13 && (col == 1 || col == 2 || col == 6 || col == 7 || col == 8 || col == 12 || col == 13))                     continue;                 if (vs2[col] == '0')                     Walls.Add(new Wall(col, row));             }         }         Bombs.Clear();         Fires.Clear();         BrokenWalls.Clear();     } } | 
BomberHubクラスの修正
プレイヤーの総数が変更されたのでASP.NET Core 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 | public class BomberHub : Hub {     public override async Task OnConnectedAsync()     {         if (Global.IsDebug)             Console.WriteLine("接続しました:" + Context.ConnectionId);         await Clients.Caller.SendAsync("ReceiveConnected", "接続成功 ゲームの準備をしています", Context.ConnectionId);         if (IsFirstConnection)         {             IsFirstConnection = false;             Timer.Interval = 1000 / 24;             Timer.Elapsed += Timer_Elapsed;             Game.BombExploded += Game_BombExploded;         }         await base.OnConnectedAsync();         await Clients.Caller.SendAsync("ReceiveConnected", "接続成功", Context.ConnectionId);         ClientProxyMap.Add(Context.ConnectionId, Clients.Caller);         if (ClientProxyMap.Count == 1)         {             InitField();             Timer.Start();         }         await SendWalls();     }     void InitField()     {         Game.Init();         NPCs.Clear();         for (int i = 0; i < 8; i++)         {             Player npc = new Player("");             NPCs.Add(npc);             npc.Reset(i);         }     }     async Task SendWalls()     {         foreach (string key in ClientProxyMap.Keys)         {             if (!ClientProxyMap.ContainsKey(key))                 continue;             // 壁をクライアントサイドに送信する             string xs1 = String.Join(",", Game.Walls.Select(wall => wall.X.ToString()).ToArray());             string ys1 = String.Join(",", Game.Walls.Select(wall => wall.Y.ToString()).ToArray());             await ClientProxyMap[key].SendAsync("ReceiveWalls", xs1, ys1);             // 破壊できない壁をクライアントサイドに送信する             string xs2 = String.Join(",", Game.IndestructibleWalls.Select(wall => wall.X.ToString()).ToArray());             string ys2 = String.Join(",", Game.IndestructibleWalls.Select(wall => wall.Y.ToString()).ToArray());             await ClientProxyMap[key].SendAsync("ReceiveIndestructibleWalls", xs2, ys2);         }     } } | 
ゲームスタートのとき自分が一人目のプレイヤーの場合は壁をリセットします。この場合、変更された壁の座標をすべてのユーザーに送信します。
| 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 | public class BomberHub : Hub {     public async Task GameStart(string id, string playerName)     {         if (Players.ContainsKey(id))             return;         // 自分がはじめてのプレイヤーの場合のみフィールドをリセット         if (Players.Count == 0)             InitField();         // NPCのひとつをPlayerオブジェクトに入れ替える         if (NPCs.Count > 0)         {             playerName = playerName.Replace(",", "_");             int resetNumber = NPCs[0].ResetNumber;             NPCs.RemoveAt(0);             Player player = new Player(id);             player.GameOverEvent += Player_GameOverEvent;             player.Reset(resetNumber);             player.New();             player.Name = playerName;             Players.Add(id, player);             playerName = playerName.Replace(",", "_").Replace("<", "<").Replace(">", ">");             foreach (string key in ClientProxyMap.Keys)             {                 if (!ClientProxyMap.ContainsKey(key))                     continue;                 if (id != key)                     await ClientProxyMap[key].SendAsync("ReceiveNotification", playerName + "が参戦しました");                 if (id == key)                     await ClientProxyMap[key].SendAsync("ReceiveNotification", playerName + "として参戦しました");             }             await SendWalls();         }     } } | 
クライアントサイドの処理の修正
クライアントサイドの処理で変更になる部分を示します。
プレイヤー増にともなって表示するキャラクタと項目が増えました。
Pages\Bomber\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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | @page @{     ViewData["Title"] = "対戦ボンバーマン";     Layout = "_Layout_none";     string baseurl = Global.BaseUrl; } <div class = "display-none">     <img src = "@baseurl/bomber/player1.png" id ="player1">     <img src = "@baseurl/bomber/player2.png" id ="player2">     <img src = "@baseurl/bomber/player3.png" id ="player3">     <img src = "@baseurl/bomber/player4.png" id ="player4">     <img src = "@baseurl/bomber/wall1.png" id ="wall1">     <img src = "@baseurl/bomber/wall2.png" id ="wall2">     <img src = "@baseurl/bomber/bomb1.png" id ="bomb1">     <img src = "@baseurl/bomber/bomb2.png" id ="bomb2">     <img src = "@baseurl/bomber/fire1.png" id ="fire1">     <img src = "@baseurl/bomber/fire2.png" id ="fire2">     <img src = "@baseurl/bomber/fire3.png" id ="fire3">     <img src = "@baseurl/bomber/fire4.png" id ="fire4">     <img src = "@baseurl/bomber/fire5.png" id ="fire5">     <img src = "@baseurl/bomber/fire6.png" id ="fire6"> </div> <div style="position: relative; overflow: hidden; margin-left:20px;margin-top:20px;float:left">     <canvas id="can"></canvas>     <br>     <p>遊び方</p>     <p>移動:↑→↓←キー<br>     爆弾のセット:SPACEキー<br>     「音を出す」にチェックをいれてもゲームに参加していない場合は音はでません。</p>     <form name="form1">     <input type="checkbox" value="音を出す" id="sound-checkbox">音を出す     <label>ハンドルネーム</label>     <input type="text" id="player-name" maxlength='16' /><br>     <input type="button" id="startButton1" value="ゲームスタート" onclick="GameStart()" style="margin-top:15px;margin-bottom:15px;">     </form>     <p><a href="./hi-score">トップ30を見る</a></p>     <p id = "conect-result"></p>     <p id = "pos-result"></p> </div> <div style="margin-left:0px;margin-top:20px;float:left;width:300px;">     <div id = "gameStatus"></div>     <input type="button" id="startButton2" value="ゲームスタート" onclick="GameStart()" style="margin-top:15px;margin-bottom:15px;">     <br>     <div id = "score"></div>     <div id = "rest"></div>     <br>     <img src = "@baseurl/bomber/player1.png"> <span id = "playerName1" style = "color:white; font-weight:bold"></span><br>     <img src = "@baseurl/bomber/player2.png"> <span id = "playerName2" style = "color:white; font-weight:bold"></span><br>     <img src = "@baseurl/bomber/player3.png"> <span id = "playerName3" style = "color:white; font-weight:bold"></span><br>     <img src = "@baseurl/bomber/player4.png"> <span id = "playerName4" style = "color:white; font-weight:bold"></span><br>     <img src = "@baseurl/bomber/player1.png"> <span id = "playerName5" style = "color:white; font-weight:bold"></span><br>     <img src = "@baseurl/bomber/player2.png"> <span id = "playerName6" style = "color:white; font-weight:bold"></span><br>     <img src = "@baseurl/bomber/player3.png"> <span id = "playerName7" style = "color:white; font-weight:bold"></span><br>     <img src = "@baseurl/bomber/player4.png"> <span id = "playerName8" style = "color:white; font-weight:bold"></span><br>     <div id = "info"></div> </div> <script src="@baseurl/js/signalr.js"></script> <script>     let connection = new signalR.HubConnectionBuilder().withUrl("@baseurl/BomberHub").build(); </script> <script src="@baseurl/bomber/app.js"></script> | 
wwwroot\bomber\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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | function DrawPlayers() {     for (let i = 0; i < 8; i++) {         if (playersX[i] != undefined) {             let imgPlayer = null;             if (i == 0)                 imgPlayer = imgPlayer2;             else if (i == 1)                 imgPlayer = imgPlayer3;             else if (i == 2)                 imgPlayer = imgPlayer4;             else if (i == 3)                 imgPlayer = imgPlayer1;             else if (i == 4)                 imgPlayer = imgPlayer2;             else if (i == 5)                 imgPlayer = imgPlayer3;             else if (i == 6)                 imgPlayer = imgPlayer4;             else if (i == 7)                 imgPlayer = imgPlayer1;             let show = true;             if (isPlayersDead[i] || (invincibleTimes[i] > 0 && invincibleTimes[i] % 2 == 0))                 show = false;             if (show)                 ctx.drawImage(imgPlayer, playersX[i] - 5, playersY[i] - 5, 32 + 10, 32 + 10);         }     } } function ShowPlayerInfos() {     for (let i = 0; i < 8; i++) {         if (playersX[i] != undefined) {             let playerNameElement = null;             let imgPlayer = null;             if (i == 0)                 playerNameElement = document.getElementById('playerName2');             else if (i == 1)                 playerNameElement = document.getElementById('playerName3');             else if (i == 2)                 playerNameElement = document.getElementById('playerName4');             else if (i == 3)                 playerNameElement = document.getElementById('playerName5');             else if (i == 4)                 playerNameElement = document.getElementById('playerName6');             else if (i == 5)                 playerNameElement = document.getElementById('playerName7');             else if (i == 6)                 playerNameElement = document.getElementById('playerName8');             else if (i == 7)                 playerNameElement = document.getElementById('playerName1');             let score = '';             if (scores[i] != "")                 score = ' (' + scores[i] + ' - ' + rests[i] + ')';             if (playerNameElement != null)                 playerNameElement.innerText = playersName[i] + score;         }     } } | 
