【Ace of Spades】basketballのアップグレード【script】

#Qiita始めました。
仕事や趣味のプログラミングでちょくちょくお世話になってたのになんとなく今まで登録はしていなかったんですが、ふと思い立って登録してみました。

###今後の用途
趣味でやってるMineCraft風（voxel形式）FPSのAce of Spadesのスクリプト開発の記録とか、仕事で勉強した知識とか、主に自分用備忘録としての運用を考えていますが、せっかく書くからには誰かの役に立てばいいなぁ。


前置きはこのぐらいにして、さっそく本題
#Basketballスクリプトを改修する
せっかくの初投稿なのでできれば0から作る系がよかったんだけど、今手元にアイディア無いし、昨日思いついたから自作ゲームモード、バスケットボールのスクリプト改修をしたいと思います。

###問題点
コート外の処理を追加したい

###現在の仕様
1. インテル（ボール）がマップの上下（Y軸）方向でコート外に出た時はスローインになる
2. マップの左右（X軸）方向でコート外に出た時は特に処理しない

###やりたいこと
仕様(2)だが、コート外にグレネード投げられないように高い壁用意してるけど、どんな投げ方すりゃそこまで高く飛ぶのか知らないが時々壁越えて外側に出てしまうことがある。
これ以上壁高くするのもスマートじゃないし、せっかくなので横方向もスクリプトで制限したい

###現在のソースコード
コート外の処理をしているのは以下の二ヶ所

ブロック破壊時の処理

```python:on_block_destroy(self,x,y,z,mode):

#god（サーバ管理者による編集モード）時の処理
if self.god
	（中略）
	
#通常時の処理
else:

	#グレネードによる破壊時の処理
	if mode == GRENADE_DESTROY:
	
		#敵側インテルの情報を変数に格納
		flag = self.team.other.flag
		
		#グレネードを投げたプレイヤーがインテル（ボール）を保持していた場合の処理
		#checkHolderは第一引数（プレイヤー）が第二引数（インテル）を保持しているときTrueを返す関数
			if checkHolder(self, flag):
			
			#インテルの出現座標にグレネードが爆発した座標をセット
			self.protocol.flag_spawn_pos = (x, y, z)
			
			#爆発した座標がゴール内かどうかを判定
			goal = self.protocol.is_goal(x, y, z)
			
			#ゴールに入った場合はゴール処理を行う
			if goal:
				self.goal_successed(goal.teamname)
			
			#爆発したy座標がコート領域の定義外の場合
			if y < self.protocol.coat_edge_u or y > self.protocol.coat_edge_d:
				
				#マップの半分より上ならコートの上端－１の座標
				if y < 256:
					dy = self.protocol.coat_edge_u - 1
				
				#下ならコート下端＋１の座標をセット
				else:
					dy = self.protocol.coat_edge_d + 1
				
				#インテルの出現座標をセットしてスローインモードに入る
				self.protocol.flag_spawn_pos = (x, dy, z)
				self.protocol.mode_throwin = True
				game_reset_roop(self, THROWIN_TIME)
				self.protocol.throwin_team = self.team.other
				message = MESSAGE_THROW_IN.format(team = self.protocol.throwin_team.name)
				self.protocol.send_chat(message, global_message = False)
			else:
				self.protocol.mode_throwin = False
			self.drop_flag()
			self.have_ball = False
	return False
```

プレイヤー位置情報更新時の処理

```python:on_position_update(self):

#敵側インテルの情報を変数に格納
flag = self.team.other.flag

#プレイヤーがインテルを保持している場合の処理
if self.have_ball:
	
	#プレイヤーの現在位置情報を取得
	x, y, z = self.world_object.position.get()
	
	#godモードの場合は何もせず後続処理へ引き継ぐ
	if self.god:
		return connection.on_pasition_update(self)
	
	#godでなく、かつスローイン中でない場合の処理
	elif not self.protocol.mode_throwin:
		
		#プレイヤーがコートの上下端から外に出ている場合
		if y <= self.protocol.coat_edge_u or y >= self.protocol.coat_edge_d:
			
			#マップの半分より上ならコート上端－１の座標
			if y < 256:
				dy = self.protocol.coat_edge_u - 1
			
			#下ならコート下端＋１の座標をセット
			else:
				dy = self.protocol.coat_edge_d + 1
			
			#インテルの出現座標をセットしてスローインモードに入り、コート外に出たプレイヤーはキルする
			self.protocol.flag_spawn_pos = (x, dy, z)
			self.drop_flag()
			self.have_ball = False
			self.kill(self, MELEE_KILL)
			self.protocol.mode_throwin = True
			game_reset_roop(self, THROWIN_TIME)
			self.protocol.throwin_team = self.team.other
			message = MESSAGE_THROW_IN.format(team = self.protocol.throwin_team.name)
			self.protocol.send_chat(message, global_message = False)
```

昔の自分が書いたコードながらイけてない。
何がいかんって、コートの外に出たかどうか判定し、コート外の場合にインテルを置く座標を計算する箇所が2か所にあるのに外部関数化していない。
しかもコートが必ずマップの真ん中を跨ぐとは限らないのに、マップのどちらの端を超えたかの判定にマップの中央の座標を使っている。
よし！直そう！！

本当はその後の『インテル座標をセットしてスローインモードに入る』ところも外部化したいが、これは後回しにする。

ということで、今回やることは以下の二つ

- コート外の判定とコート外のインテル座標の計算をする関数を作る
- X軸方向で場外に出た時の処理の追加


###1、場外判定と座標計算を関数化する。
これはそれほど時間かからなかった。

```python:basketball.py

#引数で渡された（x,y)座標がコート内ならTrueを返す、外ならFalseを返す
def checkCoatInside(connection, x, y):
	
	#Y上下端・X左右端の比較を一発でやりたかったが、条件式が長くなりすぎて読みづらいので2段階に分けた
	if connection.protocol.coat_edge_n < y and y < connection.protocol.coat_edge_s:
		if connection.protocol.coat_edge_w < x and x < connection.protocol.coat_edge_e:
			return True
	return False

#引数で渡された(x,y)座標がコート外の場合、最寄のコート端の座標を返す
#コート内ならそのままの座標を返す
def calcOutsidePosition(connection, x, y):
	
	#コートの東西南北の端の座標をマップ情報から取得
	n_edge = connection.protocol.coat_edge_n
	s_edge = connection.protocol.coat_edge_s
	w_edge = connection.protocol.coat_edge_w
	e_edge = connection.protocol.coat_edge_e
	
	#南北の比較と計算
	if y < n_edge:
		dy = n_edge - 1
	elif y > s_edge:
		dy = s_edge + 1
	else:
		dy = y
	
	#東西の比較と計算
	if x < w_edge:
		dx = w_edge - 1
	elif x > e_edge:
		dx = e_edge + 1
	else:
		dx = x
	
	return (dx, dy)

```

###2、X軸方向で場外に出た時の処理の追加

これ、簡単だと思ったら案外手間がかかった
なぜならこのbasketballスクリプト、いろいろなマップに適応できるようにゴールやコートの座標をJSONテキスト化してマップ情報メタファイルにread&writeする設計なのだ（結局輸出もしてないのに）

と、いうわけでメタファイルの読み書きに関する部分もちょこちょこ手を入れた

まずは変数定義の追加

```python:BasketBallProtocol(protocol)
coat_edge_n = None
coat_edge_s = None
coat_edge_w = None #追加
coat_edge_e = None #追加
（中略）

#マップ読み込み時
def on_map_change(self, map):
（中略）
	#初期値設定
	if not self.coat_edge_n:
		self.coat_edge_n = 240
		self.coat_edge_s = 271
		self.coat_edge_w = 240
		self.coat_edge_e = 271
```

メタファイル読み込み処理

```python:load_goal_json(self)

#メタファイルのパスの取得
path = self.get_goal_json_path()

#ファイルが見つからなければ終了
if not os.path.isfile(path):
	return

#読み込みモードでファイルを開く
with open(path, 'r') as file:
	data = json.load(file)

#ゴールのID格納用配列を用意
ids =[]

#メタデータの'goals'項目のデータを一つずつ読み出す
for goal_data in data['goals']:

	#ゴールのデータを読み込みながらIDを格納していく
	x1, x2 = goal_data['xpos']
	y1, y2 = goal_data['ypos']
	z1, z2 = goal_data['zpos']
	id = goal_data['id']
	teamname = goal_data['team']
	ids.append(id)
	
	#読み出した情報からGoalObjectを生成
	goal = GoalObject(id, teamname, x1, x2, y1, y2, z1, z2)
	goal.label = goal_data['label']
	self.goals[id] = goal

#コート座標の取得
self.coat_edge_n = data['north_edge']
self.coat_edge_s = data['south_edge']
self.coat_edge_w = data['west_edge'] #追加
self.coat_edge_e = data['east_edge'] #追加
self.blue_spawn_pos = data['blue_spawn_pos']
self.green_spawn_pos = data['green_spawn_pos']
ids.sort()
self.highest_id = ids[-1] if ids else -1
self.goal_json_dirty = True
```

メタファイル書き込み処理

```python:dump_goal_json(self)
#ゴール情報が無く、メタファイルの読み込みも行っていない（ゴールを作成したことのないマップの）場合、何も処理しない
if(not self.goals and not self.goal_json_dirty):
	return

#保存されているゴール情報をJSON形式に変換する
data = {
	'goals' : [goal.serialize() for goal in self.goals.values()],
	'north_edge' : self.coat_edge_u,
	'south_edge' : self.coat_edge_d,
	'west_edge' : self.coat_edge_w, #追加
	'east_edge' : self.coat_edge_e, #追加
	'blue_spawn_pos' : self.blue_team.spawn_pos,
	'green_spawn_pos' : self.green_team.spawn_pos
}

#メタファイルのパスを取得
path = self.get_goal_json_path()

#書き込みモードでファイルを開き、データを保存する
with open (path, 'w') as file:
	json.dump(data, file, indent = 4)
self.goal_json_dirty = True
```

```python:on_block_destroy(self,x,y,z,mode):

（前略）
#ゴールに入った場合はゴール処理を行う
if goal:
	self.goal_successed(goal.teamname)

#爆発したy座標がコート領域の定義外の場合
if y < self.protocol.coat_edge_u or y > self.protocol.coat_edge_d:
	
	#マップの半分より上ならコートの上端－１の座標
	if y < 256:
		dy = self.protocol.coat_edge_u - 1
	
	#下ならコート下端＋１の座標をセット
	else:
		dy = self.protocol.coat_edge_d + 1
	
	#インテルの出現座標をセットしてスローインモードに入る
	self.protocol.flag_spawn_pos = (x, dy, z)
	（攻略）

　↓　↓　↓　↓　↓　↓　↓　↓　↓　↓　

（前略）
#ゴールに入った場合はゴール処理を行う
if goal:
	self.goal_successed(goal.teamname)

#ゴールに入っていた場合はこの処理に入らない。
#爆発した座標が場外の場合（X-Y方向）
elif not checkCoatInside(self, x, y):
	dx, dy = calcOutsidePosition(self, x, y)
	
	#インテルの出現座標をセットしてスローインモードに入る
	self.protocol.flag_spawn_pos = (dx, dy, z)
    （攻略）

```

on_position_update(self)の方も同じ形でスッキリした。

これでテストしたところ、ちゃんと、横方向に場外してもちゃんと想定した座標にインテルが出現して、スローインになった。

今度は、スローインモードに入る時の処理の関数化とか、スローインの時の操作性とかを少し弄ろうと思う。