ASP.NET Core版 デジタルインベーダーをつくる(1) の続きです。
VIDEO
DigitalInvaderHubクラスの定義
using Microsoft . AspNetCore . SignalR ;
namespace DigitalInvader
{
public class DigitalInvaderHub : Hub
{
}
}
以降は名前空間を省略して以下のように書きます。
public class DigitalInvaderHub : Hub
{
}
接続時の処理
OnConnectedAsyncメソッドが初めて実行されたときはタイマーの初期化もおこないます。配列DigitalInvaderGame.Intervalsから値を取得してIntervalプロパティが異なるタイマーを配列の要素数個だけつくります。
以降は共通の処理です。Context.ConnectionIdをキーにしてClients.CallerとDigitalInvaderGameクラスのインスタンスを辞書に登録します。またDigitalInvaderGameクラスにおけるイベントに対応できるようにイベントハンドラを追加します。
ClientProxyMap.Countが0のときはサーバーへの負荷を考えてタイマーを停止させることにしています。もしClientProxyMap.Countが1になった場合は停止しているタイマーをスタートさせなければならないのでその処理を再開させます。
最後にクライアントサイドに接続に成功したことを通知するために”SuccessfulConnectionToClient”を送信します。
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
public class DigitalInvaderHub : Hub
{
static bool IsFirstConnection = true ;
static List < System . Timers . Timer > Timers = new List < System . Timers . Timer > ( ) ;
static Dictionary < string , IClientProxy > ClientProxyMap = new Dictionary < string , IClientProxy > ( ) ;
static Dictionary < string , DigitalInvaderGame > Games = new Dictionary < string , DigitalInvaderGame > ( ) ;
public override async Task OnConnectedAsync ( )
{
if ( IsFirstConnection )
{
IsFirstConnection = false ;
// 初回実行時だけ処理をする
foreach ( int interval in DigitalInvaderGame . Intervals )
{
System . Timers . Timer timer = new System . Timers . Timer ( ) ;
timer . Interval = interval ;
timer . Elapsed += Timer_Elapsed ;
Timers . Add ( timer ) ;
}
}
await base . OnConnectedAsync ( ) ;
ClientProxyMap . Add ( Context . ConnectionId , Clients . Caller ) ;
DigitalInvaderGame game = new DigitalInvaderGame ( Context . ConnectionId ) ;
game . SendEnemiesEvent += Game_SendEnemiesEvent ; // イベントハンドラを追加(定義は後述)
game . ChangeTargetEvent += Game_ChangeTargetEvent ; ;
game . HitEvent += Game_HitEvent ;
game . HitUfoEvent += Game_HitUfoEvent ;
game . StageClearEvent += Game_StageClearEvent ;
game . ChangeScoreEvent += Game_ChangeScoreEvent ;
game . ChangeStageEvent += Game_ChangeStageEvent ;
game . ChangeLifeEvent += Game_ChangeLifeEvent ;
game . MissEvent += Game_MissEvent ;
game . GameOverEvent += Game_GameOverEvent ;
Games . Add ( Context . ConnectionId , game ) ;
if ( ClientProxyMap . Count == 1 )
{
foreach ( System . Timers . Timer timer in Timers )
timer . Start ( ) ;
}
await Clients . Caller . SendAsync ( "SuccessfulConnectionToClient" , "接続成功" , Context . ConnectionId ) ;
}
}
切断時の処理
通信が切断されたときはClientProxyMapとGamesからキーを削除します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class DigitalInvaderHub : Hub
{
public override async Task OnDisconnectedAsync ( Exception ? exception )
{
await base . OnDisconnectedAsync ( exception ) ;
if ( ClientProxyMap . ContainsKey ( Context . ConnectionId ) )
ClientProxyMap . Remove ( Context . ConnectionId ) ;
if ( Games . ContainsKey ( Context . ConnectionId ) )
Games . Remove ( Context . ConnectionId ) ;
if ( ClientProxyMap . Count == 0 )
{
foreach ( System . Timers . Timer timer in Timers )
timer . Stop ( ) ;
}
try { System . GC . Collect ( ) ; } catch { Console . WriteLine ( "GC.Collect失敗" ) ; }
}
}
各イベントハンドラについて
DigitalInvaderGameクラス内イベント発生時の処理を示します。いずれもクライアントサイドに通知しているだけです。
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
public class DigitalInvaderHub : Hub
{
// インベーダーが前進した。または撃墜されて状態が変わった。
private void Game_SendEnemiesEvent ( DigitalInvaderGame game , string text )
{
if ( ClientProxyMap . ContainsKey ( game . ConnectionId ) )
{
Task . Run ( async ( ) => {
await ClientProxyMap [ game . ConnectionId ] . SendAsync ( "SendEnemiesEventToClient" , text ) ;
} ) ;
}
}
// ユーザーの処理によってターゲットが変更された
private void Game_ChangeTargetEvent ( DigitalInvaderGame game , string target )
{
if ( ClientProxyMap . ContainsKey ( game . ConnectionId ) )
{
Task . Run ( async ( ) =>
{
await ClientProxyMap [ game . ConnectionId ] . SendAsync ( "ChangeTargetEventToClient" , target ) ;
} ) ;
}
}
// UFOを撃墜した
private void Game_HitUfoEvent ( DigitalInvaderGame game )
{
if ( ClientProxyMap . ContainsKey ( game . ConnectionId ) )
{
Task . Run ( async ( ) =>
{
await ClientProxyMap [ game . ConnectionId ] . SendAsync ( "HitUfoEventToClient" ) ;
} ) ;
}
}
// 数字のインベーダーを撃墜した
private void Game_HitEvent ( DigitalInvaderGame game )
{
if ( ClientProxyMap . ContainsKey ( game . ConnectionId ) )
{
Task . Run ( async ( ) =>
{
await ClientProxyMap [ game . ConnectionId ] . SendAsync ( "HitEventToClient" ) ;
} ) ;
}
}
// ステージをクリアした
private void Game_StageClearEvent ( DigitalInvaderGame game )
{
if ( ClientProxyMap . ContainsKey ( game . ConnectionId ) )
{
Task . Run ( async ( ) =>
{
await ClientProxyMap [ game . ConnectionId ] . SendAsync ( "StageClearEventToClient" ) ;
} ) ;
}
}
// スコアが変更された
private void Game_ChangeScoreEvent ( DigitalInvaderGame game , int score )
{
if ( ClientProxyMap . ContainsKey ( game . ConnectionId ) )
{
Task . Run ( async ( ) =>
{
await ClientProxyMap [ game . ConnectionId ] . SendAsync ( "ChangeScoreEventToClient" , score ) ;
} ) ;
}
}
// ステージが変更された
private void Game_ChangeStageEvent ( DigitalInvaderGame game , int stage )
{
if ( ClientProxyMap . ContainsKey ( game . ConnectionId ) )
{
Task . Run ( async ( ) =>
{
await ClientProxyMap [ game . ConnectionId ] . SendAsync ( "ChangeStageEventToClient" , stage ) ;
} ) ;
}
}
// Lifeが変更された
private void Game_ChangeLifeEvent ( DigitalInvaderGame game , int life )
{
if ( ClientProxyMap . ContainsKey ( game . ConnectionId ) )
{
Task . Run ( async ( ) =>
{
await ClientProxyMap [ game . ConnectionId ] . SendAsync ( "ChangeLifeEventToClient" , life ) ;
} ) ;
}
}
// プレイヤーがミスをした
private void Game_MissEvent ( DigitalInvaderGame game )
{
if ( ClientProxyMap . ContainsKey ( game . ConnectionId ) )
{
Task . Run ( async ( ) =>
{
await ClientProxyMap [ game . ConnectionId ] . SendAsync ( "MissEventToClient" ) ;
} ) ;
}
}
// ゲームオーバー時の処理は後述
}
ゲームスタート時の処理
ゲームをスタートするときに呼び出されるGameStartメソッドを示します。
辞書のなかにキー(引数id)に対応するDigitalInvaderGameを取得して、DigitalInvaderGame.GameStartメソッドを呼び出します。それと同時にgame.Nameプロパティにプレイヤー名をセットします。このとき名前にカンマが入っているとスコアランキングへの登録の処理で支障がでるので別の文字(”_”)に置き換えています。
処理が正常におこなわれたらクライアントサイドに”EventGameStartToClient”を送信します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class DigitalInvaderHub : Hub
{
public void GameStart ( string id , string playerName )
{
if ( ! ClientProxyMap . ContainsKey ( id ) | | ! Games . ContainsKey ( id ) )
return ;
DigitalInvaderGame game = Games [ id ] ;
string name = playerName . Length > 16 ? playerName . Substring ( 0 , 16 ) : playerName ;
game . Name = name . Replace ( "," , "_" ) ;
Games [ id ] . GameStart ( ) ;
Task . Run ( async ( ) => {
await ClientProxyMap [ id ] . SendAsync ( "EventGameStartToClient" ) ;
} ) ;
}
}
インベーダーを前進させる処理
Timer.Elapsedイベントが発生したらインベーダーを前進させる処理を行なうのですが、ステージによって対応するタイマーが違います。そこでDigitalInvaderGame.Stageステージの値とDigitalInvaderGame.Intervalsから適切なintervalを取得します。そのあとsenderからTimerを取得してIntervalプロパティが同じ場合だけDigitalInvaderGame.Updateメソッドを呼び出して更新処理をおこないます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class DigitalInvaderHub : Hub
{
static private void Timer_Elapsed ( object ? sender , System . Timers . ElapsedEventArgs e )
{
Task . Run ( ( ) =>
{
foreach ( DigitalInvaderGame game in Games . Values )
{
int interval = DigitalInvaderGame . Intervals . First ( ) ;
if ( game . Stage == 0 ) // game.Stage == 0のときはゲームが始まっていないので何もしない
continue ;
else if ( game . Stage - 1 > = DigitalInvaderGame . Intervals . Length )
interval = DigitalInvaderGame . Intervals . Last ( ) ;
else
interval = DigitalInvaderGame . Intervals [ game . Stage - 1 ] ;
System . Timers . Timer ? timer = ( System . Timers . Timer ? ) sender ;
if ( timer ! = null && timer.Interval == interval)
game.Update();
}
} ) ;
}
}
ゲームオーバー時の処理
ゲームオーバーになったらイベントハンドラGame_GameOverEventが呼び出されます。
ここではDigitalInvaderGameオブジェクトからNameプロパティとScoreプロパティを抜き取ってスコアランキングに登録する処理をおこなっています。そのあとクライアントサイドに”GameOverEventToClient”を送信しています。
public class DigitalInvaderHub : Hub
{
private void Game_GameOverEvent ( DigitalInvaderGame game )
{
HiscoreManager . Save ( "../hiscore-digitalInvader.txt" , game . Name , game . Score ) ;
if ( ClientProxyMap . ContainsKey ( game . ConnectionId ) )
{
Task . Run ( async ( ) =>
{
await ClientProxyMap [ game . ConnectionId ] . SendAsync ( "GameOverEventToClient" ) ;
} ) ;
}
}
}
キーが押下されたときの処理
ユーザーがキーを押下するたびにDownKeyメソッドが呼び出されます。このとき引数として渡された文字列が長すぎる場合は処理をおこないません(プレイヤー名は16文字までに制限しているのでスパム的なことをしない限り、ここで弾かれることはない)。
プレイヤー名のなかにカンマがある場合はスコアランキングへの登録で支障がでるので別の文字に置き換えています。そのあとDigitalInvaderGame.Nameプロパティにセットしています。そのあと第一引数を調べて”Z”であればターゲットの変更、”X”であればインベーダーを攻撃する処理をおこなわせています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class DigitalInvaderHub : Hub
{
public void DownKey ( string key , string name )
{
// 長大なデータが送りつけられるかもしれないので対策
if ( key . Length > 16 | | name . Length > 16 )
return ;
if ( ! Games . ContainsKey ( Context . ConnectionId ) )
return ;
DigitalInvaderGame game = Games [ Context . ConnectionId ] ;
game . Name = name . Replace ( "," , "_" ) ;
key = key . ToUpper ( ) ;
if ( key == "Z" )
game . ChangeTarget ( ) ;
if ( key == "X" )
game . Shot ( ) ;
}
}
VIDEO
鳩でも分かるC#管理人からのお願い
できる仕事であれば請け負います。鳩でもわかるC#管理人はクラウドワークスに在宅ワーカーとして登録しています。お仕事の依頼もお待ちしております。
⇒ 仕事を依頼する
コメントについて
コメントで英語などの外国語でコメントをされる方がいますが、管理人は日本語以外はわからないので基本的に内容が理解できず、承認することもありません。それからへんな薬を売っているサイトやリンク先のサイトが存在しないというスパムコメントも多々あります。
Some people make comments in foreign languages such as English, but since the manager does not understand anything other than Japanese, he basically cannot understand the content and does not approve it. Please use Japanese when making comments.
そんななか日本語のコメントもいただけるようになりました。「○○という変数はどこで宣言されているのか?」「××というメソッドはどこにあるのか」「例外が発生する」「いっそのことソース丸ごとくれ」という質問ですが、管理人としては嬉しく思います。「自分が書いた記事は読まれているんだな」と。疑問点には可能な限り答えます。記事に問題があれば修正いたします。
そのうえでお願いがあります。「匿名」という味も素っ気もない名前ではなく、捨てハンでいいのでなにかハンドルネームをつくってほしいと思います。
管理人のモチベーションアップのために
よろしければご支援お願いします。
⇒ 管理人の物乞いリスト