ぬーぶのメモ帳

フリーゲームエンジン「Godot」でプログラム経験のない素人の備忘録 最近は色々な動作を考えて試すのが好き。

【Godot】マインスイーパーを作ってみた5

カーソルで選択、タイル情報の取得ができるようになった前回
前: 【Godot】マインスイーパーを作ってみた4 - ぬーぶのメモ帳




前回カーソルと選択場所の変数を 二次元配列の Vector2 と連動させて任意のパネルの判定を行えるようになりました。


今回は再帰処理とそれに合わせた部分の機能を増やしていきたいです。

フラグを立てる

再帰処理に取りかかる前にフラグを立てられるようにしたいと思います。
マインスイーパでは自分が爆弾があると予想した場所へ旗を立てて目印をつけることができます。
また旗を立てるとその場所は開くことができなくなります。


まずは先頭付近にフラグ用の変数を作成

# map_cell をコピーして名前を変えただけ
var map_flag := [
	[],
	[],
	[],
	[],
	[],
	[],
	[],
	[],
	[],
	[],
]

# 上の配列にこのフラグを入れていく
var flag := false

最初に作った配列の map_cell はマス目の座標である Vector2 と一緒に 変数 flag も同じ配列内に加えることが可能です。

ですが今回は別々の配列の方が分かりやすいかと思いこのようにしています。
では _ready() 内へ作成した配列にフラグを追加していきます

func _ready() -> void:
	for x in cell_x:
		for y in cell_y:
#			var cell_position := Vector2(x, y)
#			map_cell[x].append(cell_position)

			# map_cell と同じ構造の配列に flag が追加されていく
			map_flag[x].append(flag)
			
#			cover_tile.set_cellv(cell_position, 1)
#			yield(get_tree().create_timer(0.1), "timeout")

	
#	set_mine()
#	num_plus()

これで準備ができたのでフラグをオンオフできる関数を作ります。



フラグ用関数

func set_flag(pos: Vector2)-> void:
		# 取得位置の flag が false かつ カバータイルがかかっているなら実行
		if !map_flag[pos.x][pos.y]  && cover_tile.get_cellv(pos) == 1:
			cover_tile.set_cellv(pos, 2)
			map_flag[pos.x][pos.y] = true

		elif map_flag[pos.x][pos.y] && cover_tile.get_cellv(pos) == 2:
			cover_tile.set_cellv(pos, 1)
			map_flag[pos.x][pos.y] = false

取得した位置のカバータイルと、フラグを切り替えます。

if文 !map_flag前「 ! 」 は 「○○ではない」という意味になります。
さらにその後に続く「 && 」記号の通りアンドです。


この場合if !map_flag[pos.x][pos.y] && cover_tile.get_cellv(pos) == 1
もし 指定場所に フラグが立ってないと確認 かつ 指定のタイル(cover_tile の 1番)の2つの条件を満たすと、初めてフラグタイル(cover_tile の 2番)に見た目が変わり内部の flag もオン(true)にできます。

フラグの処理だけ行うとタイルのない場所でもフラグを立たせる事が出来てしまうので、カバータイルの番号を指定する必要がありました。


また Godot では「 not 」というコマンドも「 ! 」と同じように使用できます。
同様に「 and 」と「 && 」も同じ動作になります。

	# 同じ働きをする
	!map_flag[pos.x][pos.y]
	not map_flag[pos.x][pos.y]

	# 条件は複数個設定できたりする
	A && B
	A and B and C



では set_flag() を _input() 内に追加して動作を確認してみます。
カッコには current_position が入ります。

func _input(event: InputEvent) -> void:
	## 真上に if game_over: がある ##

	# ui_select はスペースキー
	if event.is_action_pressed("ui_select"):	
		set_flag(current_position)
		return

	 if event.is_action_pressed("ui_accept"): 
		#タイル情報の取得を先頭に変更
		print("タイルNo : ", num_tile.get_cellv(current_position))
		print(current_position, " : ", map_flag[current_position.x][current_position.y])

	~~省略~~


まだタイル判定がないのでフラグを立てても消せてしまう

でも出力を確認するとちゃんとフラグはオンになっている。

スペースキーを押すことでカバーにフラグを立てたり、折ったりできるようになりました。
最後は _input() 内にフラグが立っていたらパネルを消せないようにします。

func _input(event: InputEvent) -> void:
	## 真上に if event.is_action_pressed("ui_select"): がある ##

	 if event.is_action_pressed("ui_accept"): 
		#タイル情報の取得を先頭に変更
		print("タイルNo : ", num_tile.get_cellv(current_position))
		print(current_position, " : ", map_flag[current_position.x][current_position.y])

		# 実行したい命令を先に持ってくること
		if map_flag[current_position.x][current_position.y] :
			print("旗がたってます")
			return


	~~省略~~


_input() 内では その命令だけで終わらせたい場合 return 等のコマンドを使って関数から出るようにしないと駄目なようです。
(他の関数でも?)

興味のある方は試しに print("旗がたってます") の下にある return を消してコードを実行して下さい。
警告文が出力されるにも関わらず tile_check() も実行されて、旗が消えてしまいます。

以上でフラグ関係の設定は完了しました、 return を元に戻して次に行きます。



再帰処理を作る

いよいよ再帰処理に手を加えていきます。


下が再帰処理などを加えた tile_check() 関数になります。

func tile_check(pos: Vector2) -> void:
#	cover_tile.set_cellv(pos, 0)
	
#	var number = num_tile.get_cellv(pos)
#	if number == 9:
#		print("あなたの負けです")
#		game_over = true
#		return
	
	
	# 選んだ所が数字パネルだった場合 再帰処理 は行わない
	if number != 0:
		print("数字パネル : ", number, " 近くに爆弾があるぞ!")
		return


	# 選択場所が0なら実行
	if number == 0:
		# 選んだ所の 周りを num_plus と同じ仕組みで取得 for で繰り返し処理を行う
		for i in get_offset(pos):
			yield(get_tree().create_timer(0.1),"timeout") # 段階処理のコマンド 後で消す

			# 周囲のカバーにフラグが立っていないかの確認
			if cover_tile.get_cellv(i) == 1:
				# 周りのパネルが この関数の最初から処理される
				tile_check(i)

if number != 0: では num_tile 0番 以外の数字がその場にあれば処理が終わります。
この条件では本来 num_tile 9番 も含んでしまいますがこの if条件 より先に 9番と一致する条件があるので問題ありません。




次に for文 に get_offset(pos) という関数を使っています。
前に num_plus() で爆弾の周囲に数字を足した事を覚えているでしょうか?

この関数はあれと同じく選択した周囲のタイル座標を入手して、それを配列として返します。

func get_offset(pos: Vector2)-> Array:
	var offset = [
		Vector2((pos.x -1), pos.y -1),
		Vector2(pos.x, pos.y -1),
		Vector2(pos.x +1, pos.y -1),
		Vector2(pos.x -1, pos.y),
		Vector2(pos.x +1, pos.y),
		Vector2(pos.x -1, pos.y +1),
		Vector2(pos.x, pos.y +1),
		Vector2(pos.x +1, pos.y +1),
	]

	return offset

※ この関数を作った後 num_plus() 関数内でも使えると気づいて置き換えました


return の横に offset がついています
関数が何かデータを返すには return の横に返したいデータを置く必要があります。


 func set_mine()-> void: 
 func get_offset(pos: Vector2)-> Array:


さらに関数の矢印の先( ->)に注目してください、voidArrayとなっています、この部分はその関数が返せるデータの型を表しています。
voidは何かというと、返すものが何もない、「無」という結果を返しています。

仮にvoid と指定された関数の return 横に Array を置いても型が違うと警告されます。
もしも return でデータを返そうと上手くいかない場合

 func get_offset(pos: Vector2):  

のように矢印とデータ型を消すとGodotが自動で型を選んでくれるので何とかなる事もあります。



get_offset(pos) から座標の配列が帰ってきて、if条件 に合致 していれば、今度は周囲のマスを基準として再度 tile_check(i) 実行を繰り返していきます。
最終的にマップ外のタイル番号-1 や数字パネルで関数を抜けてループは終わります。

上手くいけばこのように動いてくれると思います。


この tile_check() 関数の中でまた tile_check() 関数を実行させる方法が「再帰処理」であるようで、使い方が少し分かった気がします。
while文 のように再起処理もループから抜ける仕組みがないと無限ループでゲームがフリーズしてしまいます。

全体コード

現状のコードをすべて合わせるとこうなります。

extends Node2D

onready var num_tile: TileMap = $num_tile
onready var cover_tile: TileMap = $cover_tile
onready var cursor: Sprite = $Cursor


# vector2とかけるとマス目の位置を position で指定できる
const cell_size := 32

# マス目の数
var cell_x = 10
var cell_y = 10

# マス目用の配列 タイルマップと連動させる
var map_cell := [
	[],
	[],
	[],
	[],
	[],
	[],
	[],
	[],
	[],
	[],
]

#  フラグ用配列
var map_flag := [
	[],
	[],
	[],
	[],
	[],
	[],
	[],
	[],
	[],
	[],
]
# 上の配列に入れるフラグ
var flag := false

# 爆弾の数
export var mine := 10

# 現在選択しているパネルを格納する変数
var current_position := Vector2(0,0)

# ゲームオーバー判定
var game_over := false



func _ready() -> void:
	for x in cell_x:
		for y in cell_y:
			var cell_position := Vector2(x, y)
			map_cell[x].append(cell_position)
			map_flag[x].append(flag)
			
			cover_tile.set_cellv(cell_position, 1)

	
	set_mine()
	num_plus()


func set_mine()-> void:
	randomize()
	for i in mine:
		# ランダムなタイルを選択
		var rand_tile = map_cell[randi() % cell_x][randi() % cell_y]
		
		# 連動しているタイルマップのタイル番号を取得
		var get_tile = num_tile.get_cellv(rand_tile)
		# 選択したタイルの番号が 9 つまり爆弾だった場合 while 内部の処理を実行
		# もう一度ランダムなマスを選択して while で再度比較する 爆弾以外のマスが出るまで内部の処理を続ける
		while get_tile == 9:
			randomize()
			rand_tile = map_cell[randi() % cell_x][randi() % cell_y]
			get_tile = num_tile.get_cellv(rand_tile)
		
		# 爆弾でないマスが確定したので爆弾を配置
		num_tile.set_cellv(rand_tile, 9)


func num_plus()-> void:
	for x in cell_x:
		for y in cell_y:
			# 全マスを順番に取得
			var get_tile = num_tile.get_cellv(map_cell[x][y])
			# 取得したマスが爆弾だった場合処理をする
			if get_tile == 9:
				# 爆弾タイルのマップ位置を取得
				var current_tile = num_tile.to_global(map_cell[x][y])
				# 爆弾タイルの周辺の座標を取得
				# 座標の配列を for文 で処理する
				for pos in get_offset(current_tile):
					# pos に Vector2 の情報が入っている
					var vec = num_tile.get_cellv(pos)
					# 爆弾の周辺に別の爆弾があった場合は上書きしないように pass
					# 爆弾でない場合は 周辺タイルのステータス番号を +1する
					if vec == 9:
						pass
					else:
						num_tile.set_cellv(pos, vec +1)


func _input(event: InputEvent) -> void:
	# それぞれ対応するキーが押されると True を返す変数
	var up = event.is_action_pressed("ui_up")
	var down = event.is_action_pressed("ui_down")
	var right = event.is_action_pressed("ui_right")
	var left = event.is_action_pressed("ui_left")
	

	if game_over:
			return


	if event.is_action_pressed("ui_select"):	
		set_flag(current_position)
		return
	
	if event.is_action_pressed("ui_accept"):
		print("タイルNo : ", num_tile.get_cellv(current_position))
		print(current_position, " : ", map_flag[current_position.x][current_position.y])
		
		# 実行したい命令を先に持ってくること
		if map_flag[current_position.x][current_position.y]:
			print("旗がたってます")
			return


		tile_check(current_position)
		return


	if up or down:
		if up:
			# キーボードの 上キーを押すと vector2 Y軸 が上に1つズレる
			current_position.y -= 1
			# 変数の Y軸が 0以下になったら Y軸の 最大値までループする仕組み
			# else はこれと逆の処理を行っている
			if current_position.y < 0:
				current_position.y = (cell_y -1)
		else:
			current_position.y += 1
			if current_position.y > (cell_y -1):
				current_position.y = 0
			
	# 上の処理の X軸 バージョン
	if right or left:
		if left:
			current_position.x -= 1
			if current_position.x < 0:
				current_position.x = (cell_x -1)
		else:
			current_position.x += 1
			if current_position.x > (cell_x -1):
				current_position.x = 0
	
	
	cursor.position = current_position * cell_size


func set_flag(pos: Vector2)-> void:
		# 取得位置の flag が false かつ カバータイルがかかっているなら実行
		if !map_flag[pos.x][pos.y]  && cover_tile.get_cellv(pos) == 1:
			cover_tile.set_cellv(pos, 2)
			map_flag[pos.x][pos.y] = true

		elif map_flag[pos.x][pos.y] && cover_tile.get_cellv(pos) == 2:
			cover_tile.set_cellv(pos, 1)
			map_flag[pos.x][pos.y] = false


func tile_check(pos: Vector2) -> void:
	# 数字を隠しているカバータイルを透明にする 
	cover_tile.set_cellv(pos, 0)
	
	# pos 内の Vector2 からタイル番号を取得
	# タイル番号が 9 だった場合 game_over フラグを true にする
	var number = num_tile.get_cellv(pos)
	if number == 9:
		print("あなたの負けです")
		game_over = true
		return
	
	
	# 選んだ所が数字パネルだった場合 再帰処理 は行わない
	if number != 0:
		print("数字パネル : ", number, " 近くに爆弾があるぞ!")
		return


	# 選んだ所が0で パネルを再帰処理させる部分 
	if number == 0:
		# 選んだ所の周りを取得 for で繰り返し処理を行う
		for i in get_offset(pos):
#			yield(get_tree().create_timer(0.1),"timeout")
			
			if cover_tile.get_cellv(i) == 1:
				# 8方向のパネルが この関数の最初から処理される
				tile_check(i)


func get_offset(pos: Vector2)-> Array:
	var offset = [
		Vector2((pos.x -1), pos.y -1),
		Vector2(pos.x, pos.y -1),
		Vector2(pos.x +1, pos.y -1),
		Vector2(pos.x -1, pos.y),
		Vector2(pos.x +1, pos.y),
		Vector2(pos.x -1, pos.y +1),
		Vector2(pos.x, pos.y +1),
		Vector2(pos.x +1, pos.y +1),
	]

	return offset

機能でいえば十分遊べるので今回でほぼ完成といってもいいと思います。
再帰処理も最初何をしているのか分かりませんでしたが、最近は何となく分かる程度になりました。

次回に仕上げて最後にできたらと思っています、リセットや細かい部分をやりたいです。



次:【Godot】マインスイーパーを作ってみた6 - ぬーぶのメモ帳