ぬーぶのメモ帳

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

【Godot4】懲りずにマインスイーパーを作る4

カバータイルの設置と掘削判定を作成
前回: 【Godot4】懲りずにマインスイーパーを作る3 - ぬーぶのメモ帳




前回は爆弾と周囲に危険度を設置しました。

今回はカバータイルの生成、またそれらを掘れるようにしたいです。
よろしければお付き合いください。



カバータイルの作成

カバーは Node2D で作成しました、子は sprite2D のみとなります。
最初は音声とパーティクルも一緒でしたが、音声は別ノード、パーティクルもスクリプトでの生成となりました。

この時 sprite2D の Centered は外してください。
外さずにカバータイルを生成すると以下のようにズレて表示されます。



・パーティクルの作成

今回 CPUParticles2D を使用します。
作成したエフェクト用画像をインスペクタのDrawing → Texture に挿入し、各数値を設定しました。

パーティクルは個々の説明をするより各項目を弄って体験する方が早いと思いますが、個人的には…
・Time
・Direction
・Initial Velocity
・Scale

の辺りの項目を弄るとそれっぽくなるかと思います。
自分もまだまだ試行錯誤の段階です。

■ effect.gd

extends CPUParticles2D
class_name Effect

# 複数テクスチャを用意して破片にバリエーションをつけてみた
var texture_set := [
	preload("res://xxx1.png"),
	preload("res://xxx2.png")
]

# 生成されるタイミングで上の配列からランダムに取得
func _ready() -> void:
	texture = texture_set.pick_random()

# ノードタブから接続している
func _on_finished() -> void:
	queue_free()



・カバータイルにパーティクルを紐づける

作ったパーティクルをカバータイルで生成できるようにします。
ファイルシステムからパーティクルのシーンを探し、カバータイルのスクリプトに貼り付けます。

const EFFECT = preload("res://scene/effect/effect.tscn")

 
掘るとき親であるカバータイルが消えるとパーティクルも消えてしまうため少し工夫をしました。
カバータイルのスクリプト await を使用した単純な仕組みです。

	# パーティクルのインスタンスを作る
	var eff = EFFECT.instantiate()
	eff.global_position = rect.get_center()
	# エフェクトを再生
	eff.emitting = true
	# カバータイルの子として生成
	add_child(eff)
	# カバーを非表示にして掘ったように見せる
	test_blind.visible = false 
	# エフェクトが終わるまで待機
	await eff.finished 
	# エフェクト終了のシグナルを受け取ったカバーの削除処理を実行
	queue_free()

ここの await は子であるパーティクルの再生が終わるまで待機するものです。
これらを加えたカバータイルのコードが以下になります。

■ cover.gd

extends Node2D
@onready var test_blind: Sprite2D = $TestBlind
const EFFECT = preload("res://scene/effect/effect.tscn")

var rect :Rect2 # text_blind の texture の画像範囲を格納
var start # 上の四角範囲のスタート地点を取得 左上
var end # 上の四角範囲のゴール地点を取得 右下

func _ready() -> void:
	# 後で出てくるグローバルのシグナル
	SignalManager.dig.connect(on_dig)

	rect = test_blind.get_rect()
	start = rect.position + global_position
	end = rect.end + global_position
	

# シグナルを受け取ると実行される
func on_dig(pos:Vector2i)-> void:
	var tile_pos := Vector2i(position) / 64
	if tile_pos == pos:
		var effect = EFFECT.instantiate()
		effect.global_position = rect.get_center()
		effect.emitting = true
		add_child(effect)
		test_blind.visible = false # 非表示にして掘ったように見せる
		if not rock_break.playing:
			rock_break.play()
		await effect.finished # エフェクトが終わるまで待機
		queue_free()

シグナルについては次の項目で軽く説明をしています。


メインのコードの追記、グローバルの設定

まずカスタムシグナルを利用するためのスクリプトを作成します。
さらにこちらはグローバル設定にして利用します。
グローバル設定すると他のコード全てで参照、利用できるようになります。

 プロジェクト→プロジェクト設定→グローバル から設定できます

登録するコードはこれだけです
■ signal_manager.gd

extends Node

signal dig(pos:Vector2i)

シグナルをグローバルにした理由は別々のシーンでも発信、接続が楽になりコードがすっきりするためです。
インスタンス生成時にシグナル接続も可能ですが、自分は上の方式が好きです。

シグナル、信号なので使い方としては
 ・信号を飛ばすノード、シーン
 ・信号を受け取った時に接続し関数を実行させるノード、シーン
の2つが必要になり、自身で飛ばしたシグナルを自分が受け取ることも可能です。

今回だと main.gd で信号を飛ばし
cover.gd ready() で信号を接続し on_dig() が実行されます。


シグナルは一緒に値などを渡せるので使い方を間違えなければ非常に便利です。

■ main.gd の追加部分

func _input(event: InputEvent) -> void:
	if event is InputEventMouseButton:
		var mouse_pos = Vector2i(
			get_local_mouse_position().x/cell_size,
			get_local_mouse_position().y/cell_size
			)
		if event.button_index == 1 and event.pressed == true:
			# グローバルのシグナルを飛ばす。
			SignalManager.dig.emit(mouse_pos)

ポイントはマウスの取得座標をローカルにした事です。
グローバル座標はエンジン上に表示される 原点(0,0)が基準になり、ローカル座標は該当のノードの原点を基準とする解釈で間違っていないはず…

なので盤面を動かしてもマウスの取得する Vector2iがズレることがなくなります。
それにより 配列stage_data とカバータイルに正確な座標を返せます。




main の _input(event: InputEvent)内で先ほど設定したシグナルを発信します。
発信したシグナルをカバータイルが受け取り自身の座標とクリックした座標が一致した場合カバーの割れる演出が再生されます。

■ main.gd

extends Node2D

const BLIND = preload("res://scene/blind/blind.tscn")
# ナンバー設置や再帰処理で使う
const OFF_SET := [
	Vector2i(-1, -1), Vector2i(0, -1), Vector2i(1, -1), 
	Vector2i(-1, 0), Vector2i(1, 0), 
	Vector2i(-1, 1), Vector2i(0, 1), Vector2i(1, 1), 
]

@onready var tile_map: TileMap = $TileMap

# 大量に生成される壁と目隠しのコンテナ
@onready var wall_container: Node2D = $wall_container

@onready var blind_container: Node2D = $blind_container

# ステージの構成データ
@export var cell_size :int = 64
@export var stage_scale :Vector2i = Vector2i(10, 10)

# 盤面のデータを格納
var stage_array := []

# 白と黒のタイルを格納する配列
var floor_array := []
var wall_array := []

# 爆弾設置後と爆弾を数を格納する変数
var bomb_array := []
var bomb_count := 0

func _ready() -> void:
	get_stage()
	init_stage_array()
	set_stage()

	set_bomb()
	
func _input(event: InputEvent) -> void:
	if event is InputEventMouseButton:
		var mouse_pos = Vector2i(
			get_local_mouse_position().x/cell_size,
			get_local_mouse_position().y/cell_size
			)
		if event.button_index == 1 and event.pressed == true:
			SignalManager.dig.emit(mouse_pos)

func get_stage()-> void:
	# タイルマップの白黒タイルを取得
	floor_array = []
	wall_array = []
	wall_array = tile_map.get_used_cells_by_id(0,0,Vector2i.ZERO)
	floor_array = tile_map.get_used_cells_by_id(0,0,Vector2i(1, 0))

func init_stage_array()-> void:
	# 最初に全配列を初期化する
	for y in stage_scale.y:
		var y_array := []
		for x in stage_scale.x:
			y_array.append({"type": 0, "dig": false, "flag": false})
		stage_array.append(y_array)
	# 初期化した後、壁になる配列の値を変更
	for wall in wall_array:
		stage_array[wall.y][wall.x] = {"type": 99}
		

func set_stage()-> void:
	for i in wall_array: # 壁の配列に壁のグラフィックを設置
		var wall = Sprite2D.new()
		wall.texture= load("res://asset/art/tile_64x64.png")
		wall.global_position = i * cell_size
		wall.centered = false
		wall_container.add_child(wall)
		
	for i in floor_array:
		tile_map.set_cell(0, i, 1, Vector2i.ZERO)

		var blind = BLIND.instantiate()
		blind.global_position = i * cell_size
		blind_container.add_child(blind)

func set_bomb()-> void:
	# 爆弾の数をフロアの数から計算してみる
	bomb_count = int(floor_array.size() /10)
	for i in range(bomb_count):
		var pos = find_random_cell()
		stage_array[pos.y][pos.x].type = 9
		tile_map.set_cell(0, pos, 1, Vector2i(stage_array[pos.y][pos.x].type, 0))
		bomb_array.append(pos)
		
	set_number(bomb_array)

func set_number(array:Array)-> void:
	for i in array:
		for j in OFF_SET:
			var new_pos = i+j
			if is_bounds(new_pos) and stage_array[new_pos.y][new_pos.x].type <= 8:
				stage_array[new_pos.y][new_pos.x].type += 1
				tile_map.set_cell(0, new_pos, 1, Vector2i(stage_array[new_pos.y][new_pos.x].type, 0))
			else:
				continue
	#print(stage_array)


# パネル設置などで盤外を指定してエラーになるのを防ぐ
func is_bounds(pos:Vector2i)-> bool:
	return pos.x >= 0 and pos.x <= stage_scale.x-1 \
	and pos.y >= 0 and pos.y <= stage_scale.y-1

# 何もないポジションを取得する
func find_random_cell()-> Vector2i:
	var dx := 0
	var dy := 0
	while true:
		randomize()
		dx = randi() % stage_scale.x
		dy = randi() % stage_scale.y
		var cell = stage_array[dy][dx]
		if cell.type <= 8:
			break
	return Vector2i(dx, dy)

 
現在カバーはエフェクトの終了まで待機しているので、連続でクリックするとエフェクトが複数出てしまいます。

それに関しては次回に解決していきたいと思うのでひとまずこのままとなります。


まとめ

今回はメインでカバータイルを生成し、該当する盤面のカバーを取り除く所までできだんだんとゲームらしくなってきました。

次回は掘った部分のデータ更新、敗北判定、長くならないor余裕があれば、再帰処理までまとめて実装できるようにしたいと思います。

今回はここまでになります、次回もお付き合いいただけると嬉しいです



■ 次 :【Godot4】懲りずにマインスイーパーを作る5 - ぬーぶのメモ帳