【ゲーム開発備忘録】畑管理システム編（Godot4.2）

# 実装機能

![ezgif-2-18f985a8b2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3771613/bb80ae0a-c3c0-79bc-3cdb-c25a49ff9f4f.gif)

### 機能説明・操作説明
・畑エリアを**左クリック**で畑情報がポップアップ

・畑情報とは、以下の4つのこと
　1．そのエリアに植えている（or 植える予定）の作物
　2．前回、そのエリアに植えてあった作物
　3．メモ
　4．畑の状態（耕して、植えれる、成長中、収穫可能、休耕地）

・ポップアップ中の「収穫後に押すボタン」を押して畑情報1が畑情報2に移動
・ポップアップ下部、「上」「下」「左」「右」ボタンを押して、畑エリアの縦横比と面積を変更
・ポップアップ右下部、「削除」ボタンを押して畑エリアを削除

・畑エリアを**右クリック**でドラッグアンドドロップすることで配置移動

・画面左上、「畑エリアを追加」ボタンで新しい畑エリアを追加
・画面右上、「保存」ボタンで変更内容を保存

・矢印キーでカメラの上下左右移動、マウスホイールでカメラの拡大縮小

ダウンロードは[こちら](https://ux.getuploader.com/hatakedemo/download/1)から

# ソースコード
畑情報シーン、畑エリアシーン、メインシーンの3つで管理しています
シーンツリーとスクリプトを紹介した後に詳細な説明を入れます

### 畑情報シーン
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3771613/2e5bfea5-acc1-ad08-7199-481e26effe50.png)
&nbsp;  
**スクリプト（FieldInformation）**
```GDScript
extends VBoxContainer
func is_field_node():
	pass

# Called when the node enters the scene tree for the first time.
func _ready():
	$OptionButton.text = "畑の状態"
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	pass

func _on_button_pressed():
	$LineEdit2.text = $LineEdit.text
	$LineEdit.text = ""
```
**説明**
is_field_node()って何ですかね……？
試行錯誤しながらやったので、その消し忘れでしょう

_ready()関数は「シーン内の全ノードが準備できたタイミングで実行される関数」ですね
今回は、子ノードのオプションボタンに「畑の状態」とデフォルトで表示される命令にしてます

_process(delta)関数は1フレームごとに呼び出される関数ですね
今回はpassですが、こいつは有用なので他シーンでは多用します

_on_button_pressed()は「収穫後に押すボタン」の制御ですね
畑情報1が畑情報2に移動します
いちいちタイプしなくていいので便利ですね！ってだけです

### 畑エリアシーン
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3771613/993c27a0-e552-6fc8-a30c-10b0b05193fc.png)
&nbsp;  
**スクリプト（Area000）**
```GDScript
extends Button
#セーブ
func save():
	return {
		"filename" : get_scene_file_path(),
		"parent" : get_parent().get_path(),
		"pos_x": position.x,
		"pos_y": position.y,
		"size_x":size.x,
		"size_y":size.y,
		"area_name":text,
		"nowplanted_name":$Popup/FieldInformation/LineEdit.text,
		"lateplanted_name":$Popup/FieldInformation/LineEdit2.text,
		"note":$Popup/FieldInformation/TextEdit.text,
		"field_situation":$Popup/FieldInformation/OptionButton.selected,
	}
# Called when the node enters the scene tree for the first time.
func _ready():
	add_to_group("Persist")
	mouse_default_cursor_shape = Control.CURSOR_ARROW
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	var x = $Popup/FieldInformation/OptionButton.selected
	var normal_style = StyleBoxFlat.new() #通常状態
	var hover_style = StyleBoxFlat.new() #ホバー時
	var pressed_style = StyleBoxFlat.new() #押されている状態
	match x:
		0: # 茶色
			normal_style.bg_color = Color("#F6BF7F80")
			add_theme_stylebox_override("normal", normal_style)
			hover_style.bg_color = Color("#F6BF7F80")
			add_theme_stylebox_override("hover", hover_style)
			pressed_style.bg_color = Color("#F6BF7F80")
			add_theme_stylebox_override("pressed", pressed_style)
			$Plow.visible = true
			$Follow.visible = false
		1: # 茶色
			normal_style.bg_color = Color("#F6BF7F80")
			add_theme_stylebox_override("normal", normal_style)
			hover_style.bg_color = Color("#F6BF7F80")  # 青色
			add_theme_stylebox_override("hover", hover_style)
			pressed_style.bg_color = Color("#F6BF7F80")
			add_theme_stylebox_override("pressed", pressed_style)
			$Plow.visible = false
			$Follow.visible = false
		2: # 緑色
			normal_style.bg_color = Color("#CFD0A080")
			add_theme_stylebox_override("normal", normal_style)
			hover_style.bg_color = Color("#CFD0A080")  # 青色
			add_theme_stylebox_override("hover", hover_style)
			pressed_style.bg_color = Color("#CFD0A080")
			add_theme_stylebox_override("pressed", pressed_style)
			$Plow.visible = false
			$Follow.visible = false
		3: # オレンジ色
			normal_style.bg_color = Color("#F6E8A280")
			add_theme_stylebox_override("normal", normal_style)
			hover_style.bg_color = Color("#F6E8A280")  # 青色
			add_theme_stylebox_override("hover", hover_style)
			pressed_style.bg_color = Color("#F6E8A280")
			add_theme_stylebox_override("pressed", pressed_style)
			$Plow.visible = false
			$Follow.visible = false
		4: # 茶色
			normal_style.bg_color = Color("#F6BF7F80")
			add_theme_stylebox_override("normal", normal_style)
			hover_style.bg_color = Color("#F6BF7F80")  # 青色
			add_theme_stylebox_override("hover", hover_style)
			pressed_style.bg_color = Color("#F6BF7F80")
			add_theme_stylebox_override("pressed", pressed_style)
			$Plow.visible = false
			$Follow.visible = true
	self.text = $Popup/FieldInformation/LineEdit.text
#サイズ変更
func _on_upper_pressed():
	size.y -= 50
func _on_downer_pressed():
	size.y += 50
func _on_bigger_pressed():
	size.x -= 50
func _on_smaller_pressed():
	size.x += 50

func _on_hide_button_pressed():
	$Popup.hide()

#DD移動
var dragging = false
var drag_start_position = Vector2()
func _gui_input(event):
	if event is InputEventMouseButton:
		if event.button_index == MOUSE_BUTTON_RIGHT:
			if event.pressed:
				dragging = true
				drag_start_position = get_global_mouse_position() - global_position
				mouse_default_cursor_shape = Control.CURSOR_MOVE
			else:
				dragging = false
				mouse_default_cursor_shape = Control.CURSOR_ARROW
		elif event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
			# 左クリックの処理をここに移動
			emit_signal("pressed")
	
	if event is InputEventMouseMotion and dragging:
		global_position = get_global_mouse_position() - drag_start_position

func _on_yes_button_pressed():
	queue_free()

#ポップアップ表示
func _on_pressed():
	$Popup.popup_centered()

```
**説明**
save()で情報を辞書形式で格納します
ユーザーによる変更はほとんど保存されます
カメラの移動は保存されません
最初の2つの"filename" : get_scene_file_path(),"parent" : get_parent().get_path()はよくわかんないけどあったほうがいいらしいです
どのシーンにどのノードがあるかとか、そんな感じの情報みたい？
分かる人がいたらコメントお願いします☆

_ready()関数で、このシーンを"Persist"グループに追加しています
"Persist"は保存すべきシーンのグループです
その下の
mouse_default_cursor_shape = Control.CURSOR_ARROW
これはマウスカーソルの形を決めてますね（普通に標準的な矢印カーソルです）

_process(delta)では畑の状態に応じて畑エリアのビジュアルを変えています
畑の状態が5種類あるので、それぞれ一つずつ書いてます
もっと簡単に書ける方法があるはず
Plow.visible = trueで、子ノードの画像の表示・非表示を管理してますね
あと
self.text = Popup/FieldInformation/LineEdit.text
で畑情報1（現在植えている作物の名前）を畑エリアに表示するようにしてます

_on_upper_pressed()みたいなのが4つあるんですが、コメントに書いてあるとおりです
畑エリアのサイズが変更できます
関数名は試行錯誤の結果よくわからなくなってます

で、_on_hide_button_pressed()関数にポップアップを消す機能を実装してます
まあポップアップ外を押せば消えるんですけどね

そしてドラッグアンドドロップ機能ですね
Godotこれ苦手みたいで、カメラ動かしちゃうと判定がバグっちゃうみたいで大変でした
正直、訳わかんなくてAIに聞いたら動いただけなので、説明できる人を待ってます☆
多分global_positionとかが重要だと思う
後半の左クリックの処理ってのはいらないかも？
元々buttonノードを改造して作ってるわけだから、そもそも左クリックされたら〇〇をするってのはデフォルトの機能
逆に、button以外で同じような機能を実装する時には役立つかも

_on_yes_button_pressed()ってのは畑エリアの完全削除ですね
hide()だと見えなくなるだけ情報は残りますのでqueue_free()にしてます

_on_pressed()で、この畑エリア（ボタン）が左クリックされたときポップアップが表示されます

**スクリプト（deleteButton）**
```GDScript
extends Button


# Called when the node enters the scene tree for the first time.
func _ready():
	pass # Replace with function body.


# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	pass

func _on_no_button_pressed():
	$Popup.hide()

func _on_pressed():
	$Popup.popup_centered()
```
**説明**
子ノードにスクリプトをアタッチしてます
あんま推奨される操作ではないみたいですが、小規模なシステムなので問題ないでしょう
これは畑エリアを「削除」するっていうボタンを押した時の確認のポップアップを出すためものです
YESを押すと完全削除ですね
NOを押すと確認のポップアップは消えます

**スクリプト（PlowとFollow）**
```GDScript
extends TextureRect


# Called when the node enters the scene tree for the first time.
func _ready():
	pass # Replace with function body.


# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	pass

func _on_bigger_pressed():
	position.x -= 25
func _on_smaller_pressed():
	position.x += 25
```
**説明**
これは畑の状態に合わせて表示される画像の制御です
畑エリアの大きさを変えた時に、エリアの真ん中上辺に表示されるよう一緒に移動してくれます

### メインシーン
![スクリーンショット 2024-07-30 171121.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3771613/b74897cd-76d8-c90a-5198-8a947cf6e575.png)
&nbsp;  
**スクリプト（Main）**
```GDScript
extends Node2D
var areas = []

# Called when the node enters the scene tree for the first time.
func _ready():
	add_to_group("Persist")
	call_deferred("deferred_load")

func deferred_load():
	load_game()


# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	pass

#畑を追加
func _on_add_area1_pressed():
	var new_area = preload("res://Area000.tscn").instantiate()
	new_area.add_to_group("Persist")  # Persistグループに追加
	new_area.name = "Area_" + str(get_child_count())  # ユニークな名前を設定
	add_child(new_area)
	areas.append({"position":new_area.position})

#カメラ
func _input(event):
	if event is InputEventMouseButton:
		if event.button_index == MOUSE_BUTTON_WHEEL_UP:
			$Camera2D.zoom -= Vector2(0.1, 0.1) # ズームイン
		elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
			$Camera2D.zoom += Vector2(0.1, 0.1) # ズームアウト

#セーブ
func save():
	return {
		"filename" : get_scene_file_path(),
		"parent" : get_parent().get_path(),
		"areas": areas,
		"node_name": name,
		"pos_x": position.x,
		"pos_y": position.y,
		}
func save_game():
	var save_game = FileAccess.open("user://savegame.save", FileAccess.WRITE)
	var save_nodes = get_tree().get_nodes_in_group("Persist")
	for node in save_nodes:
		# ノードにsave()メソッドがあることを確認
		if !node.has_method("save"):
			print("Persistent node '%s' is missing a save() function, skipped" % node.name)
			continue
		
		# ノードのsave()メソッドを呼び出し
		var node_data = node.call("save")
		node_data["node_name"] = node.name
		# データをJSONに変換して保存
		var json_string = JSON.stringify(node_data)
		save_game.store_line(json_string)
	
	save_game.close()
func load_game():
	if not FileAccess.file_exists("user://savegame.save"):
		return # セーブファイルが存在しない場合は何もしない
	
	var save_game = FileAccess.open("user://savegame.save", FileAccess.READ)
	while save_game.get_position() < save_game.get_length():
		var json_string = save_game.get_line()
		var node_data = JSON.parse_string(json_string)
		
		var node_name = node_data["node_name"]
		var target_node = get_node_or_null(node_data["parent"] + "/" + node_name)
		
		if target_node:
			if target_node.name == "Main":
				update_main_node(target_node, node_data)
			else:
				update_area_node(target_node, node_data)
		else:
			create_area_node(node_data)
	
	save_game.close()

func update_main_node(node, data):
	node.position.x = data["pos_x"]
	node.position.y = data["pos_y"]
	node.areas = data["areas"]

func update_area_node(node, data):
	node.position.x = data["pos_x"]
	node.position.y = data["pos_y"]
	node.size.x = data["size_x"]
	node.size.y = data["size_y"]
	node.text = data["area_name"]
	node.get_node("Popup/FieldInformation/LineEdit").text = data["nowplanted_name"]
	node.get_node("Popup/FieldInformation/LineEdit2").text = data["lateplanted_name"]
	node.get_node("Popup/FieldInformation/TextEdit").text = data["note"]
	node.get_node("Popup/FieldInformation/OptionButton").selected = data["field_situation"]

func create_area_node(data):
	var new_area = load(data["filename"]).instantiate()
	get_node(data["parent"]).add_child(new_area)
	new_area.name = data["node_name"]
	update_area_node(new_area, data)

func _on_save_pressed():
	save_game()
	$CanvasLayer/SaveMessage.show()
	await get_tree().create_timer(2.0).timeout
	$CanvasLayer/SaveMessage.hide()

func _on_loas_deug_pressed():
	call_deferred("deferred_load")
```
**説明**
このシーンは実際にユーザーが操作するシーンですね
areasリストに畑エリアを格納します
保存したいシーンなので"Persist"グループに追加しています
call_deferred("deferred_load")は確かデバッグ機能なので気にしなくていいです

deferred_load()関数はロード時にちょっとディレイを入れます
これはGodotのready関数が、子ノードが準備できたタイミングで実行されるからですね
今回はなくても大丈夫でしたが、これを入れたほうがバグはないらしいです（ホントかよ）

_on_add_area1_pressed()で、「畑エリアを追加」ボタンを押した時の処理をしています
新しい畑エリアをインスタンス化して Persistグループに追加します
個別に名前をつけて、ロードした時に混同しないようにしてあげます
そいつをこのノードの子ノードとして追加します
最後にareasリストに追加します

_input(event)でカメラを制御しています
マウスホイールで拡大縮小です

save()には保存したい情報を辞書形式で記入します

save_game()にセーブ機能が記述されています
書き込みのためのjsonファイルを開いて、セーブすべきノードにあるsave()関数を巡回します
それから、save()関数の辞書情報をjsonに変換して保存します

load_game()にロード機能が記述されています
セーブファイルが存在しない場合は「保存する」ボタンを押しても何も起きません
まずjsonファイルを読み取ります
そこに保存された情報を元にノードを再構築していきます

update_main_node関数~update_area_node関数は試行錯誤の成れの果てです
なぜか畑情報の情報をロードしてくれなかったので、直接「ここから読み取ってね！」って指定してます
もっと大規模なシステムだとこの方法は大変ですね
シーンの子ノードのsave()関数は巡回してくれないのかな？
詳しい人いたら教えて～☆

create_area_node関数はなんかよくわかんないですけど、ロードした時に新しいエリアを作ってるみたいですね……

_on_save_pressed()
これは「保存する」ボタンを押した時に「保存しました」って表示されるやつです
やっぱりこういう文字が出てくれないと不安になりますからね

_on_loas_deug_pressed()はデバッグ機能です
loasはloadのタイプミスです

**スクリプト（Camera2D）**
```GDScript
extends Camera2D


# Called when the node enters the scene tree for the first time.
func _ready():
	pass # Replace with function body.


# Called every frame. 'delta' is the elapsed time since the previous frame.

var speed = 500 # カメラの移動速度

func _process(delta):
	var direction = Vector2.ZERO
	if Input.is_action_pressed("CAMERA_RIGHT"):
		direction.x += 1
	if Input.is_action_pressed("CAMERA_LEFT"):
		direction.x -= 1
	if Input.is_action_pressed("CAMERA_DOWN"):
		direction.y += 1
	if Input.is_action_pressed("CAMERA_UP"):
		direction.y -= 1
	position += direction * speed * delta
```
**説明**
ここはカメラ操作ですね
移動速度と、矢印ボタンを押した時にカメラが移動する方向を定義してます
メインシーンにアタッチしろって感じですよね

***
### 終
ここまで見てくださってありがとうございました
コメント等あればお気軽にお願いします☆