Playerクラス ASP.NET Core版 戦車対戦ゲームをつくる(2)
tank-dotnet-player
前回 ASP.NET Core版 戦車対戦ゲームをつくる(1) の続きです。今回はPlayerクラスを定義して戦車を動かすことができるようにします。
VIDEO
ほんとうは以下のように書かなければならないのですが、
namespace TankGame
{
public class Player
{
}
}
以降は省略して
と書きます。
プロパティと初期化
まずコンストラクタとプロパティを示します。
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
public class Player
{
// X座標とZ座標
public int X
{
set ;
get ;
}
public int Z
{
set ;
get ;
}
// 回転角(フィールド変数_directは度数法だが RotationYプロパティは弧度法)
public double RotationY
{
get { return ( double ) _direct / 180 * Math . PI ; }
}
// 接続したときのID。これが空文字列ならNPC
public string ConnectionID
{
private set ;
get ;
}
// プレイヤーの名前
// NPCの場合はNPCXXXのような名前にする
string _name = "" ;
public string Name
{
set { _name = value ; }
get
{
if ( ConnectionID ! = "" )
return _name ;
else
return String . Format ( "NPC{0:000}" , _npcNumber ) ;
}
}
// 戦車が撃破された場合は残機1減らして2秒後に復活させる
bool _isDead = false ;
public bool Dead
{
set
{
_isDead = value ;
if ( value == true )
{
Rest --;
System . Timers . Timer timer = new System . Timers . Timer ( ) ;
timer . Interval = 2000 ;
timer . Elapsed += RevivePlayer ; // 後述
timer . Start ( ) ;
}
}
get { return _isDead ; }
}
// 残機
public int Rest
{
set ;
get ;
}
// スコア
public int Score
{
set ;
get ;
}
// ゲーム進行上、非表示にしたい壁
public List < Wall > WallsToHide
{
private set ;
get ;
}
}
コンストラクタを示します。残機数をMAX_RESTにしてプロパティをセットします。
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 Player
{
const int MAX_REST = 5 ;
int _npcNumber ;
static int _nextNpcNumber = 1 ;
public Player ( string connectionId )
{
SetInitPosition ( ) ; // 後述
ConnectionID = connectionId ;
Name = "" ;
Dead = false ;
Rest = MAX_REST ;
Score = 0 ;
WallsToHide = new List < Wall > ( ) ;
_npcNumber = _nextNpcNumber ;
_nextNpcNumber ++;
}
}
ゲームが始まったらプレイヤーの戦車をフィールドの端に出現させます。この位置は破壊できない壁の内部で他のプレイヤーからは見えません。戦車の初期座標を決定するのがSetInitPositionメソッドです。
戦車を出現させる座標は一度取得したら同じものを使いまわせるので最初の1回だけ取得してフィールド変数に格納しておきます。あとは戦車の初期の方向をランダムに設定して、それに応じたX座標とZ座標を設定します。
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
public class Player
{
int _upperInitPositionX = 0 ;
int _lowerInitPositionX = 0 ;
int _upperInitPositionZ = 0 ;
int _lowerInitPositionZ = 0 ;
void SetInitPosition ( )
{
if ( _upperInitPositionX == 0 )
{
// 戦車の初期のX座標とZ座標は破壊できない壁のhalfWallSize分だけ外側である
int halfWallSize = Game . CHARACTER_SIZE / 4 ;
_upperInitPositionX = Game . IndestructibleWalls . Max ( wall => wall . X ) + halfWallSize ;
_lowerInitPositionX = Game . IndestructibleWalls . Min ( wall => wall . X ) - halfWallSize ;
_upperInitPositionZ = Game . IndestructibleWalls . Max ( wall => wall . Z ) + halfWallSize ;
_lowerInitPositionZ = Game . IndestructibleWalls . Min ( wall => wall . Z ) - halfWallSize ;
}
int [ ] angles = { 0 , 90 , 180 , 270 } ;
int r = _random . Next ( 4 ) ;
int angle = angles [ r ] ;
if ( angle == 0 )
{
// 戦車が上向きの場合、X座標は固定だがZ座標は-15から+15であればどこでもよい
X = _lowerInitPositionX ;
Z = Game . CHARACTER_SIZE * ( _random . Next ( 30 ) - 15 ) ;
}
if ( angle == 90 )
{
X = Game . CHARACTER_SIZE * ( _random . Next ( 30 ) - 15 ) ;
Z = _upperInitPositionZ ;
}
if ( angle == 180 )
{
X = _upperInitPositionX ;
Z = Game . CHARACTER_SIZE * ( _random . Next ( 30 ) - 15 ) ;
}
if ( angle == 270 )
{
X = Game . CHARACTER_SIZE * ( _random . Next ( 30 ) - 15 ) ;
Z = _lowerInitPositionZ ;
}
// 戦車の初期の方向をフィールド変数に格納する(値は0以上360未満の整数)
_angle = angle ;
}
}
プレイヤーの手前にある壁は非表示に
戦車を描画するときに手前になる壁が存在すると見えないので非表示とします。非表示にさせたいWallオブザーバーのリストを取得するGetWallsToHideメソッドを示します。
方向転換するときに10度刻みで変更されるので315度~45度、45度~135度、135度~225度、225度~315度の4パターンにわけて取得しています。
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
public class Player
{
List < Wall > GetWallsToHide ( )
{
// _angleを0以上360未満の整数にする
if ( _angle < 0 )
_angle += 360 ;
_angle = _angle % 360 ;
int halfWallSize = Game . CHARACTER_SIZE / 4 ;
List < Wall > walls = Game . IndestructibleWalls . Where (
wall => X - halfWallSize * 1 < = wall . X && wall.X <= X + halfWallSize * 1 &&
Z - halfWallSize * 1 <= wall.Z && wall.Z <= Z + halfWallSize * 1
).ToList();
if ( _angle < = 45 | | _angle > = 315 )
{
List < Wall > walls1 = Game . ExistingWalls . Where (
wall =>
X > = wall . X && Z - halfWallSize <= wall.Z && wall.Z <= Z + halfWallSize
).ToList();
List < Wall > walls3 = Game . IndestructibleWalls . Where (
wall =>
X > = wall . X && Z - halfWallSize <= wall.Z && wall.Z <= Z + halfWallSize
).ToList();
walls . AddRange ( walls1 ) ;
walls . AddRange ( walls3 ) ;
}
if ( _angle > = 45 && _angle <= 135)
{
List<Wall> walls1 = Game.ExistingWalls.Where(
wall =>
Z <= wall.Z && X - halfWallSize <= wall.X && wall.X <= X + halfWallSize
).ToList();
List < Wall > walls3 = Game . IndestructibleWalls . Where (
wall =>
Z < = wall . Z && X - halfWallSize <= wall.X && wall.X <= X + halfWallSize
).ToList();
walls . AddRange ( walls1 ) ;
walls . AddRange ( walls3 ) ;
}
if ( _angle > = 135 && _angle <= 225)
{
List<Wall> walls1 = Game.ExistingWalls.Where(
wall =>
X <= wall.X && Z - halfWallSize <= wall.Z && wall.Z <= Z + halfWallSize
).ToList();
List < Wall > walls3 = Game . IndestructibleWalls . Where (
wall =>
X < = wall . X && Z - halfWallSize <= wall.Z && wall.Z <= Z + halfWallSize
).ToList();
walls . AddRange ( walls1 ) ;
walls . AddRange ( walls3 ) ;
}
if ( _angle > = 225 && _angle <= 315)
{
List<Wall> walls1 = Game.ExistingWalls.Where(wall =>
Z >= wall.Z && X - halfWallSize <= wall.X && wall.X <= X + halfWallSize
).ToList();
List < Wall > walls3 = Game . IndestructibleWalls . Where (
wall =>
Z > = wall . Z && X - halfWallSize <= wall.X && wall.X <= X + halfWallSize
).ToList();
walls . AddRange ( walls1 ) ;
walls . AddRange ( walls3 ) ;
}
return walls ;
}
}
方向転換と移動
つぎに方向転換と移動の処理を考えます。
方向転換
方向転換するためのRotateメソッドを示します。方向転換するときは_directChangingLまたは_directChangingRに1をセットします。更新処理を行なおうとしたときにこれらのフィールド変数が0以外であれば方向転換をおこなっている最中であることがわかります。また1回の更新処理で10度ずつ回転させるので9回処理をおこなえば方向転換の処理は完了したことになります。
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
public class Player
{
int _directChangingL = 0 ;
int _directChangingR = 0 ;
void Rotate ( )
{
if ( _directChangingL > 0 )
{
_angle += 10 ;
_directChangingL ++;
}
if ( _directChangingR > 0 )
{
_angle -= 10 ;
_directChangingR ++;
}
if ( _directChangingL > = 9 | | _directChangingR > = 9 )
{
_directChangingL = 0 ;
_directChangingR = 0 ;
}
}
}
移動処理
Moveメソッドは場所を移動するためのメソッドです。移動処理をおこなうときは_positionChangingに1をセットします。更新処理を行なおうとしたときに戦車の方向に応じてX座標とZ座標を変更し、_positionChangingをインクリメントします。_positionChangingがGame.CHARACTER_SIZEに一致したとき、隣のマスに移動したことになるので移動処理を終了させます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Player
{
int _positionChanging = 0 ;
void Move ( )
{
if ( _angle == 0 )
X ++;
if ( _angle == 90 )
Z --;
if ( _angle == 180 )
X --;
if ( _angle == 270 )
Z ++;
_positionChanging ++;
if ( _positionChanging > = Game . CHARACTER_SIZE )
_positionChanging = 0 ;
}
}
戦車を前方に移動させることができるかどうかを調べるCanMoveForwardメソッドを示します。
戦車を移動させようとした先に壁が存在する場合は移動させることができません。戦車を移動させようとした先に壁が存在する場合、その座標は戦車の中心座標に戦車のサイズの4分の3または4分の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
public class Player
{
bool CanMoveForward ( int direct )
{
if ( direct < 0 )
direct += 360 ;
direct = direct % 360 ;
int halfWallSize = Game . CHARACTER_SIZE / 4 ;
if ( direct == 0 )
{
if ( Game . IndestructibleWalls . Any ( wall => (
wall . X > = X + 3 * halfWallSize && wall.X <= X + 5 * halfWallSize &&
wall.Z >= Z - halfWallSize && wall.Z <= Z + halfWallSize
)))
return false;
return ! Game . ExistingWalls . Any ( wall => (
wall . X > = X + 3 * halfWallSize && wall.X <= X + 5 * halfWallSize &&
wall.Z >= Z - halfWallSize && wall.Z <= Z + halfWallSize
));
}
if ( direct == 90 )
{
if ( Game . IndestructibleWalls . Any ( wall => (
wall . X > = X - halfWallSize && wall.X <= X + halfWallSize &&
wall.Z >= Z - 5 * halfWallSize && wall.Z <= Z - 3 * halfWallSize
)))
return false;
return ! Game . ExistingWalls . Any ( wall => (
wall . X > = X - halfWallSize && wall.X <= X + halfWallSize &&
wall.Z >= Z - 5 * halfWallSize && wall.Z <= Z - 3 * halfWallSize
));
}
if ( direct == 180 )
{
if ( Game . IndestructibleWalls . Any ( wall => (
wall . X > = X - 5 * halfWallSize && wall.X <= X - 3 * halfWallSize &&
wall.Z >= Z - halfWallSize && wall.Z <= Z + halfWallSize
)))
return false;
return ! Game . ExistingWalls . Any ( wall => (
wall . X > = X - 5 * halfWallSize && wall.X <= X - 3 * halfWallSize &&
wall.Z >= Z - halfWallSize && wall.Z <= Z + halfWallSize
));
}
if ( direct == 270 )
{
if ( Game . IndestructibleWalls . Any ( wall => (
wall . X > = X - halfWallSize && wall.X <= X + halfWallSize &&
wall.Z >= Z + 3 * halfWallSize && wall.Z <= Z + 5 * halfWallSize
)))
return false;
return ! Game . ExistingWalls . Any ( wall => (
wall . X > = X - halfWallSize && wall.X <= X + halfWallSize &&
wall.Z >= Z + 3 * halfWallSize && wall.Z <= Z + 5 * halfWallSize
));
}
return false ;
}
}
戦車はフィールド内に存在するか?
戦車がフィールド内に存在するかどうかを調べるIsOutOfFieldメソッドを示します。戦車をhalfSizeWall分移動させようとすると破壊できない壁と重なってしまうかどうかを調べています。
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
public class Player
{
int _posXMax = 0 ;
int _posXMin = 0 ;
int _posZMax = 0 ;
int _posZMin = 0 ;
bool IsOutOfField ( )
{
if ( _posXMax == 0 )
{
int halfSizeWall = Game . CHARACTER_SIZE / 4 ;
_posXMax = Game . IndestructibleWalls . Max ( wall => wall . X ) - halfSizeWall ;
_posXMin = Game . IndestructibleWalls . Min ( wall => wall . X ) + halfSizeWall ;
_posZMax = Game . IndestructibleWalls . Max ( wall => wall . Z ) - halfSizeWall ;
_posZMin = Game . IndestructibleWalls . Min ( wall => wall . Z ) + halfSizeWall ;
}
if ( X > _posXMax )
return true ;
if ( X < _posXMin )
return true ;
if ( Z > _posZMax )
return true ;
if ( Z < _posZMin )
return true ;
return false ;
}
}
更新処理がおこなわれるときにキーが押されているかどうかで前進または方向転換処理をおこなうUpdatePlayerメソッドを示します。
LeftKeyまたはRightKeyが押されている場合は方向転換を開始し、UpKeyが押されている場合は前進処理を開始します。すでになんらかの処理がおこなわれている場合はその処理を継続し、それ以外の処理は開始させません。
またフィールドの外にいる場合はフィールド内に前進する以外の処理はできないものとします。
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 Player
{
public bool IsUpKeyDown = false ;
public bool IsDownKeyDown = false ;
public bool IsLeftKeyDown = false ;
public bool IsRightKeyDown = false ;
void UpdatePlayer ( )
{
BulletUpdate ( ) ;
if ( Dead )
return ;
WallsToHide = GetWallsToHide ( ) ;
if ( _directChangingL > 0 | | _directChangingR > 0 )
Rotate ( ) ;
else if ( _positionChanging > 0 )
Move ( ) ;
else if ( IsUpKeyDown )
{
// UpKeyが押されている場合は前進処理を開始する
BeginMove ( ) ;
}
else if ( IsLeftKeyDown && !IsOutOfField())
{
// フィールド内においてキーが押されている場合は回転処理を開始する
_directChangingL = 1;
_angle += 10 ;
}
else if ( IsRightKeyDown && !IsOutOfField())
{
_directChangingR = 1;
_angle -= 10 ;
}
IsLeftKeyDown = false ;
IsRightKeyDown = false ;
}
// 移動処理を開始する
void BeginMove ( )
{
if ( ! CanMoveForward ( _angle ) )
return ;
if ( _angle == 0 )
X ++;
if ( _angle == 90 )
Z --;
if ( _angle == 180 )
X --;
if ( _angle == 270 )
Z ++;
_positionChanging = 1 ;
}
}
撃破されたときの処理
敵によって撃破された戦車を初期位置に戻すための処理をおこなうResetメソッドを示します。このメソッドはゲームオーバーになったときにその戦車をNPCにするときにも呼び出されます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Player
{
public void Reset ( string connectionId )
{
SetInitPosition ( ) ;
ConnectionID = connectionId ;
WallsToHide = new List < Wall > ( ) ;
// フィールド変数を初期化
_directChangingL = 0 ;
_directChangingR = 0 ;
_positionChanging = 0 ;
// NPCの場合は新しい名前をつける
_npcNumber = _nextNpcNumber ;
_nextNpcNumber ++;
Dead = false ;
}
}
撃破された戦車を復活させる処理を示します。これはTimer.Elapsedイベントによって呼び出されるイベントハンドラです。そこで最初にタイマーを停止させて戦車の復活場所を求めています。また残機が0になった場合はゲームオーバーのイベントを発生させます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Player
{
public delegate void GameHandler ( Player player ) ;
public event GameHandler ? GameOverEvent ;
private void RevivePlayer ( object ? sender , System . Timers . ElapsedEventArgs e )
{
System . Timers . Timer ? timer = ( System . Timers . Timer ? ) sender ;
timer ? . Stop ( ) ;
timer ? . Dispose ( ) ;
Reset ( ConnectionID ) ;
if ( ConnectionID ! = "" && Rest <= 0)
GameOverEvent?.Invoke(this);
}
}
ゲームを開始するための処理を示します。スコアをリセットして残機数を最大値に設定しています。
public class Player
{
public void GameStart ( )
{
Score = 0 ;
Rest = MAX_REST ;
}
}
砲撃にかんする処理
次に砲撃に関する処理を示します。
Bulletクラスの定義
最初に弾丸を管理するためのBulletクラスを示します。
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
namespace TankGame
{
public class Bullet
{
int _vx , _vz ;
public Bullet ( Player player , int x , int z , int vx , int vz )
{
// 発射したのは誰か?
Player = player ;
// 発射位置
X = x ;
Z = z ;
// 弾丸の初速
_vx = vx ;
_vz = vz ;
}
public Player Player
{
private set ;
get ;
}
public int X
{
private set ;
get ;
}
public int Z
{
private set ;
get ;
}
public void Update ( )
{
X += _vx ;
Z += _vz ;
}
}
}
砲撃にかんするイベントの定義
次にPlayerクラスの砲撃にかんするイベントを定義します。壁に砲弾が命中したらクライアントサイドで爆破の演出と壁を非表示にする処理をおこなわなければなりません。また戦車に命中した場合も効果音を鳴らすためにクライアントサイドに知らせる必要があります。そのため引数で座標も同時に渡しています。
public class Player
{
public delegate void BrokenWallHandler ( Player player , int x , int z ) ;
public event BrokenWallHandler ? BrokenWall ;
public event BrokenWallHandler ? Explosion ;
// 再掲 public delegate void GameHandler(Player player);
public event GameHandler ? ShotEvent ;
public event GameHandler ? BombEvent ;
}
Bulletプロパティ
砲弾が壁か戦車にあたるまで次の弾は発射できません。発射した弾丸を管理するためのBulletプロパティを示します。連射できないのでリストにする必要はありません。
public class Player
{
Bullet ? _bullet = null ;
public Bullet ? Bullet
{
set
{
_bullet = value ;
if ( value == null )
BombEvent ? . Invoke ( this ) ;
}
get { return _bullet ; }
}
}
砲弾を発射する
砲弾を発射する処理を示します。前回発射した弾丸が着弾していない場合は発射できません。また戦車がフィールドの外にあたり死亡している場合も発射できません。発射処理をおこなうときは戦車の向きに応じて適切な初速を与えます。そしてクライアントサイドに通知するためのイベントを発生させます。
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 Player
{
public void Shot ( )
{
if ( Bullet ! = null | | IsOutOfField ( ) | | this . Dead )
return ;
if ( _angle < 0 )
_angle += 360 ;
_angle = _angle % 360 ;
if ( _angle == 0 )
Bullet = new Bullet ( this , X , Z , 8 , 0 ) ;
if ( _angle == 90 )
Bullet = new Bullet ( this , X , Z , 0 , -8 ) ;
if ( _angle == 180 )
Bullet = new Bullet ( this , X , Z , -8 , 0 ) ;
if ( _angle == 270 )
Bullet = new Bullet ( this , X , Z , 0 , 8 ) ;
ShotEvent ? . Invoke ( this ) ;
}
}
砲弾の更新処理
弾丸を移動させる処理を示します。
砲弾の移動可能範囲を取得し、再利用できるようにフィールド変数に格納しておきます。そして砲弾の移動可能範囲を超えたら爆発させて次の砲弾を発射可能にします。また破壊可能な壁に命中した場合はWall.Breakメソッドを呼び出してその壁を破壊します。
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
public class Player
{
int _bulletPosXMax = 0 ;
int _bulletPosZMax = 0 ;
int _bulletPosXMin = 0 ;
int _bulletPosZMin = 0 ;
void BulletUpdate ( )
{
if ( Bullet == null )
return ;
int beforeX = Bullet . X ;
int beforeZ = Bullet . Z ;
Bullet . Update ( ) ;
if ( _bulletPosXMax == 0 )
{
// 砲弾の移動可能範囲を取得(再利用可)
_bulletPosXMax = Game . IndestructibleWalls . Max ( wall => wall . X ) ;
_bulletPosZMax = Game . IndestructibleWalls . Max ( wall => wall . Z ) ;
_bulletPosXMin = Game . IndestructibleWalls . Min ( wall => wall . X ) ;
_bulletPosZMin = Game . IndestructibleWalls . Min ( wall => wall . Z ) ;
}
// 砲弾の移動可能範囲を超えたら爆発させて次の砲弾を発射可能にする
if ( Bullet . X < _bulletPosXMin - Game . CHARACTER_SIZE )
{
Explosion ? . Invoke ( this , Bullet . X + Game . CHARACTER_SIZE , Bullet . Z ) ;
Bullet = null ;
return ;
}
if ( Bullet . Z < _bulletPosZMin - Game . CHARACTER_SIZE )
{
Explosion ? . Invoke ( this , Bullet . X , Bullet . Z + Game . CHARACTER_SIZE ) ;
Bullet = null ;
return ;
}
if ( Bullet . X > _bulletPosXMax + Game . CHARACTER_SIZE )
{
Explosion ? . Invoke ( this , Bullet . X - Game . CHARACTER_SIZE , Bullet . Z ) ;
Bullet = null ;
return ;
}
if ( Bullet . Z > _bulletPosZMax + Game . CHARACTER_SIZE )
{
Explosion ? . Invoke ( this , Bullet . X , Bullet . Z - Game . CHARACTER_SIZE ) ;
Bullet = null ;
return ;
}
// 砲弾が破壊可能な壁に命中したときの処理
int half = Game . CHARACTER_SIZE / 4 ;
List < Wall > walls1 = Game . ExistingWalls . Where (
wall => ( Bullet . X - 4 < = wall . X && wall.X <= Bullet.X + 4) && (Bullet.Z - 4 <= wall.Z - half && wall.Z - half <= Bullet.Z + 4)
).ToList();
List < Wall > walls2 = Game . ExistingWalls . Where (
wall => ( Bullet . X - 4 < = wall . X && wall.X <= Bullet.X + 4) && (Bullet.Z - 4 <= wall.Z + half && wall.Z + half <= Bullet.Z + 4)
).ToList();
List < Wall > walls3 = Game . ExistingWalls . Where (
wall => ( Bullet . X - 4 < = wall . X - half && wall.X - half <= Bullet.X + 4) && (Bullet.Z - 4 <= wall.Z && wall.Z <= Bullet.Z + 4)
).ToList();
List < Wall > walls4 = Game . ExistingWalls . Where (
wall => ( Bullet . X - 4 < = wall . X + half && wall.X + half <= Bullet.X + 4) && (Bullet.Z - 4 <= wall.Z && wall.Z <= Bullet.Z + 4)
).ToList();
List < Wall > walls = new List < Wall > ( ) ;
walls . AddRange ( walls1 ) ;
walls . AddRange ( walls2 ) ;
walls . AddRange ( walls3 ) ;
walls . AddRange ( walls4 ) ;
if ( walls . Count > 0 )
{
foreach ( Wall wall in walls )
{
// 砲弾が命中した壁を破壊する
BrokenWall ? . Invoke ( this , wall . X , wall . Z ) ;
wall . Break ( ) ;
}
Bullet = null ;
}
}
}
NPCの動作
次にNPCを動作させる処理を示します。
すでに砲弾を発射している場合は砲弾を移動させます。そのあと64分の1の確率で砲撃可能であれば砲弾を発射させます。またすでに方向転換処理または移動処理が開始されている場合はこれを継続し、開始されていない場合は前進できるか、左右に移動可能かをしらべてそのなかからランダムに次の行動を選択させます。
ただし方向転換したあとまた方向転換をくりかえし、同じところをグルグル回り続けることを避けるため、前回の行動が方向転換の場合、つぎに前進できる場合は前進処理をおこなわせます。
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
public class Player
{
bool _isLastTimeRotation = false ;
void UpdateNPC ( )
{
BulletUpdate ( ) ;
if ( Dead )
return ;
if ( _random . Next ( 64 ) == 0 )
Shot ( ) ;
if ( _directChangingL > 0 | | _directChangingR > 0 )
Rotate ( ) ;
else if ( _positionChanging > 0 )
Move ( ) ;
else if ( CanMoveForward ( _angle ) | | ( ! IsOutOfField ( ) && CanMoveForward(_angle + 90)) || (!IsOutOfField() && CanMoveForward(_angle - 90)))
{
List<int> vs = new List<int>();
if ( CanMoveForward ( _angle ) )
vs . Add ( 1 ) ;
if ( ! IsOutOfField ( ) && CanMoveForward(_angle + 90))
vs.Add(2);
if ( ! IsOutOfField ( ) && CanMoveForward(_angle - 90))
vs.Add(3);
int r = _random . Next ( vs . Count ) ;
int index = vs [ r ] ;
// 方向転換した直後の場合は、前進できる場合は必ず前進処理をおこなわせる
if ( index == 1 | | ( CanMoveForward ( _angle ) && _isLastTimeRotation))
{
BeginMove();
_isLastTimeRotation = false ;
}
else if ( index == 2 )
{
_directChangingL = 1 ;
_angle += 10 ;
_isLastTimeRotation = true ;
}
else if ( index == 3 )
{
_directChangingR = 1 ;
_angle -= 10 ;
_isLastTimeRotation = true ;
}
}
else
{
// 袋小路にはまってしまった場合は右回転させる
_directChangingR = 1 ;
_angle -= 10 ;
_isLastTimeRotation = true ;
}
}
}
最後に 仕上げ
最後にプレイヤーとNPC共通の更新処理を示します。ConnectionIDが空文字列でないなら普通のプレイヤーなのでUpdatePlayerメソッドで処理をおこないます。ConnectionIDが空文字の場合はNPCなので上記で定義したUpdateNPCメソッドを呼び出して処理をおこなわせます。
public class Player
{
public void Update ( )
{
if ( _angle < 0 )
_angle += 360 ;
_angle = _angle % 360 ;
if ( ConnectionID ! = "" )
UpdatePlayer ( ) ;
else
{
UpdateNPC ( ) ;
}
}
}
VIDEO