ぬーぶのメモ帳

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

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

ほぼ完成といって良かった前回
前: 【Godot】マインスイーパーを作ってみた5 - ぬーぶのメモ帳




前回は再帰処理とそれに合わせてカバータイルに旗を立てられるようにしました。
今回は細かい部分を全て仕上げたいと思います。

リセット機能を追加

まずは一番簡単なリセットから追加します。

func _input(event: InputEvent) -> void:
#	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:
		# ゲームオーバー中にEscキーを押すとシーン全体がリロードされる
		if event.is_action_pressed("ui_cancel"):
			get_tree().reload_current_scene()
		else:
			return

		~~省略~~

is_action_pressed("ui_cancel") は何回か登場したキー入力コマンドで、対応している ui_cancel Escキーになります

get_tree().reload_current_scene() がリセット命令になります、正確には名前のとおりシーン最初から読み込み直しています。
これは Godot 備え付けの機能なのでシーンをリセットしたい場合お手軽に使えるコマンドです。



正解の位置を表示させたい

本物でも失敗した際にすべての爆弾の位置を教えてくれます。

そこで自分が作る時は旗が正解の位置にあれば「〇」旗の無い部分には「!」を付けたいと思いました。

第1回で設定したパネルの出番がやっと来ました

そのための関数を作成します。

func correct_flags()-> void:
	# 全マス取得
	for x in cell_x:
		for y in cell_y:
			var vec = map_cell[x][y]

			if game_over:
				# 旗を立てた所が正解だった場合
				if num_tile.get_cellv(vec) == 9 && cover_tile.get_cellv(vec) == 2:
					cover_tile.set_cellv(vec, 3)
				# 旗が立っていない所の正解
				elif num_tile.get_cellv(vec) == 9 && cover_tile.get_cellv(vec) == 1:
					cover_tile.set_cellv(vec, 4)
			elif game_clear:
				if num_tile.get_cellv(vec) == 9 && (cover_tile.get_cellv(vec) == 1 || cover_tile.get_cellv(vec) == 2):
					cover_tile.set_cellv(vec, 3)

仕組みは何度か行っている全マス調べて該当のマスを変化させる力技です。
タイルの変化も前回と同じで該当マスの num_tile cover_tile の両方が条件に一致していればタイルを変化させています。


game_clear については次の項目で作成するので先に作りました。
game_clear の条件で「 || 」 の謎の記号を使用していますが、これは「または」という意味になります。
前回の「&& = and」「!○○ = not ○○」と似たような感じですね。


この場合 該当マスに爆弾があり さらに(&&) カバータイルの 1番 または( || ) 2番 だったら
正解の「〇」パネルに変えるという条件になります 「 || 」は「or」という表記でも使えます。

やり方が悪いのかたまに「or」等の英字の表記で上手く機能しないことがあり、二つの表記方法を覚えておいても損はないのかなと思います。

	AA || BB
	AA or BB or CC



ではこの関数を tile_check()のゲームオーバー判定のところ に追加します。

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

		# ゲームオーバーフラグの立った後に入れないと機能しないので注意
		correct_flags()

		return

	~~省略~~

上手く機能すればこうなります

失敗しても自分の正解した場所が見れると個人的には嬉しいのでこのような形にしてみました。



クリア条件の追加

爆弾を全て回避して何もなしはさびしいものです、なのでクリア条件を作ります。
マインスイーパーのクリア条件は爆弾以外のパネルを全て開けることです。


なので 変数 cell_x と cell_y をかけてタイルの総数を出し、変数 mine で爆弾の数だけ引けばクリアに必要なタイル数が求められます。

次に 関数 tile_check() 内のカバータイルを消す命令を改良して上で求めた数を消したタイルの数から引くようにします。

最後はクリア条件として上で求めた数が0になったらクリアフラグを立てる関数を作成します。

クリア関係の変数を作ります。

# クリアフラグ
var game_clear := false
# クリア判定を格納する変数
var clear:= 0


まず _ready() 関数でクリア判定を計算して格納します。
フラグ用関数

func _ready() -> void:
	clear = cell_x * cell_y - mine
	print(clear) # 90になるはず、消してよい

	~~省略~~


次にクリア判定を行う関数を作成します。
clear_check(rem:int) のカッコ内に残りの数を入れて実行することでクリア判定を行います。
先ほどの項目で作成した 関数 correct_flags() も配置しています。

# rem = remaining
func clear_check(rem: int)-> void:
	if rem == 0:
		print("ゲームクリア! すごい!")
		game_clear = true
		# クリアフラグが立つと 全部マルで表示される
		correct_flags()
	else:
		print("残り ", rem, " 個です")


上で作成した関数を tile_check() 関数内に配置します。
また関数の先頭カバータイルを消す部分を改良して、タイルを消すたび 変数 clear から引いていきます。

clear_check(rem:int)の位置は数字パネルと空のパネルを判定した ifのすぐ下にしました。
カッコ内に変数 clear を入れ忘れると警告が出るので忘れないで下さい。

func tile_check(pos: Vector2) -> void:
	# 再帰処理でスルーできるように条件つきに変更
	if cover_tile.get_cellv(pos) == 1:
		# 指定カバーが1番だったらが消えて clear 内の数値が減っていく
		cover_tile.set_cellv(pos, 0)
		clear -= 1

#	var number = num_tile.get_cellv(pos)
#	if number == 9:
#		print("あなたの負けです")
#		game_over = true
#		correct_flags()
#		return

	# 選んだ所が数字パネルだった場合 再帰処理 は行わない
	if number != 0:

		clear_check(clear)

#		print("数字パネル : ", number, " 近くに爆弾があるぞ!")
		return


#	if !map_flag[pos.x][pos.y]:
		# 選んだ所が0で パネルを再帰処理させる部分 
	if number == 0:

		clear_check(clear)

		# 選んだ所の 周りを 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)


これでマインスイーパーに必要なコードは一通り揃ったと思います、機能すれば下のように動きます。
クリアパターン と 失敗パターンが連続して流れます。


リセット機能にクリアとゲームオーバーも認識し、正解位置も出るようになり完成と言える出来になったのでは無いでしょうか?




全体コード

コードを合わせるとこうなります。

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

# クリアフラグ
var game_clear := false
# クリア判定を格納する変数
var clear:= 0


func _ready() -> void:
	clear = cell_x * cell_y - mine
	print(clear) # 消してok
	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)
#			yield(get_tree().create_timer(0.1), "timeout")

	
	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 or game_clear:
		if event.is_action_pressed("ui_cancel"):
			get_tree().reload_current_scene()
		else:
			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:
	if !map_flag[pos.x][pos.y]  && cover_tile.get_cellv(pos) == 1:
		cover_tile.set_cellv(pos, 2)
			
	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] = not map_flag[pos.x][pos.y]
#	print(map_flag[pos.x][pos.y])


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


#	if !map_flag[pos.x][pos.y]:
		# 選んだ所が0で パネルを再帰処理させる部分 
	if number == 0:
		clear_check(clear)
		# 選んだ所の 周りを 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)
		

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

func clear_check(i: int)-> void:
	if i == 0:
		print("ゲームクリア! すごい!")
		game_clear = true
		correct_flags()
	else:
		print("残り ", i, " 個です")


# 爆弾を引いてしまった時の答え合わせ
func correct_flags()-> void:
	for x in cell_x:
		for y in cell_y:
			var vec = map_cell[x][y]
			if game_over:
				# 旗を立てた所が正解だった場合
				if num_tile.get_cellv(vec) == 9 and cover_tile.get_cellv(vec) == 2:
					cover_tile.set_cellv(vec, 3)
				# 旗が立っていない所の正解
				elif num_tile.get_cellv(vec) == 9 and cover_tile.get_cellv(vec) == 1:
					cover_tile.set_cellv(vec, 4)
			elif game_clear:
				if num_tile.get_cellv(vec) == 9 and (cover_tile.get_cellv(vec) == 1 || cover_tile.get_cellv(vec) == 2):
					cover_tile.set_cellv(vec, 3)


■ 最後に
コードも文章も間違いなく稚拙ですが、ゲームとして機能する物になったと思います。
自分でもこの関数は別ゲーで使いまわせると考えたり収穫があり楽しかったです。

マインスイーパーは比較的簡単に作れる題材ではありますが、Godotで望むような解説が見つからなかったので自身の試したことを無謀にも残しておこうと思いました。
万が一誰かのヒントなどになれば幸いです、最後までお付き合いいただきありがとうございました。



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