MENU

Godot 4 でWiz系ダンジョンRPG開発 (街・拠点の枠組み作り) [010]

今回は「街・拠点の枠組み作り」を行います。これまでの段階では、街・拠点に戻っても冒険者ギルドや酒場のメニューはあるものの選択しても何も起こらない状態でした。そして、ダンジョンへ進む以外は選択できないようになっていました。今回は冒険者ギルドや酒場に行くことができるようにし、メニューも用意するようにします。(上の画像をクリックすると、イメージが沸くはずです) また、街のBGMも流れるようになりました。ぜひ最後までご確認ください。

前回は敵のデータから複数体表示処理と敵の名前の言語対応を行いました。今まで作成してきたのは以下の通りです。詳しくは過去記事をご確認ください。

  • ダンジョンの壁と床の設置
  • プレイヤーの現在座標とその表記
  • イベントメッセージの表示
  • 敵とのランダムエンカウント
  • ダンジョンの扉と階段の設置
  • ダンジョンのB1から街に戻る処理
  • ダンジョンのB1からB2に降りる処理
  • ダンジョンの画像置き換え
  • 座標データ管理 (階段)
  • 敵データ作成 (JSON)
  • 敵の複数体ランダム出現処理
  • 敵の名前切り替え処理 (英語 / 日本語)

ここでは説明しやすいものだけを街に配置しています。「街に何を配置するか」は各々好みがあるのではないでしょうか。適宜読み替えていただけたら幸いです。

目次

街から冒険が始まる処理

ゲームをスタートしたときに「街から冒険を始める」ようにします。人によっては必要ない処理かもしれません。その点ご注意ください。

メイン処理用コード修正

# ==============================================================================
# main.gd : ゲーム全体の状態を統括するコントローラー
# ==============================================================================
extends Node

enum GameState { EXPLORE, BATTLE, TOWN }
var current_state: GameState = GameState.EXPLORE

var town_system: TownSystem
var battle_system: BattleSystem

# 型定義を自作のクラス(dungeon.gdのclass_name)に変更
@onready var dungeon_explore: DungeonExplore = $Camera3D

func _ready() -> void:
	print("【Main】コントローラー起動。")
	
	# ゲーム起動時のデフォルト言語を「英語」に強制設定
	TranslationServer.set_locale("en")
	
	town_system = TownSystem.new(self)
	battle_system = BattleSystem.new(self)
	
	# シグナル結合
	dungeon_explore.battle_triggered.connect(_on_battle_triggered)
	dungeon_explore.town_entered.connect(_on_town_entered)
	
	town_system.dungeon_entered.connect(_on_dungeon_entered)
	battle_system.battle_finished.connect(_on_battle_finished)
	
	# 💡 【変更】初期状態を「探索」から「街モード」に変更して起動します
	_change_state(GameState.TOWN)

func _change_state(next_state: GameState) -> void:
	current_state = next_state
	
	if town_system: town_system.end_town_mode()
	if battle_system: battle_system.end_battle_mode()
	if dungeon_explore: dungeon_explore.set_explore_active(false)
	
	match current_state:
		GameState.EXPLORE:
			dungeon_explore.set_explore_active(true)
		GameState.TOWN:
			town_system.start_town_mode()
		GameState.BATTLE:
			# 現在の階層(整数)を取得
			var current_floor_num: int = dungeon_explore.current_floor
			
			# dungeon_explore経由で、JSON(map_data.json)内の出現数設定を取得
			var limits: Dictionary = dungeon_explore.get_floor_enemy_limits(current_floor_num)
			var min_count: int = limits["min"]
			var max_count: int = limits["max"]
			
			# 登場可能な敵ID(文字列)を格納する配列の作成
			var available_enemy_ids: Array[String] = []
			
			# ロード済みの enemy_master_data から、この階層に出る敵をすべて抽出
			for enemy_id in battle_system.enemy_master_data.keys():
				var enemy_data = battle_system.enemy_master_data[enemy_id]
				
				if typeof(enemy_data) == TYPE_DICTIONARY and enemy_data.has("appear_floors"):
					var floors_list: Array = enemy_data["appear_floors"]
					for f in floors_list:
						if int(f) == current_floor_num:
							available_enemy_ids.append(enemy_id)
							break
			
			# 安全装置
			if available_enemy_ids.is_empty():
				available_enemy_ids.append("goblin")
			
			# 選別された配列と、JSONから動的に引っ張ってきた出現数範囲を渡してバトル開始
			battle_system.start_battle(available_enemy_ids, min_count, max_count)

func _on_battle_triggered() -> void:
	_change_state(GameState.BATTLE)

func _on_battle_finished() -> void:
	_change_state(GameState.EXPLORE)

func _on_town_entered() -> void:
	_change_state(GameState.TOWN)

func _on_dungeon_entered() -> void:
	# 💡 街からダンジョンへ入るタイミングで、プレイヤーをB1の開始座標に配置します
	dungeon_explore.reset_to_dungeon_start()
	_change_state(GameState.EXPLORE)

メイン処理用コード (main.gd) を上記のように修正します。これは、街のメニューで「ダンジョン」を押した際に発行される town_system.dungeon_entered シグナルを受け取る _on_dungeon_entered() の中で、確実に reset_to_dungeon_start() が実行されるものです。これで街からゲームが始まるようになりました。

街の各場所に移動する

今までは街・拠点に戻ってもメニューは「ダンジョン」以外は未実装で、反応しませんでした。これを街の中にある場所に移動できるようにします。この記事では以下のメニューになるものとします。

冒険者ギルド冒険者の作成などを行う
酒場クエスト受注・情報収集
武器・防具・道具を購入・売却できる
教会呪い・石化・死の状態異常を回復する
図書館記録確認・チュートリアル
ダンジョンダンジョンへ進む

各場所の画像を用意する

各場所の画像を用意しましょう。用意出来たら Godot の Filesystem にある res://image/town にインポートしておきます。

town_background.png
冒険者ギルドguild.png
酒場tavern.png
shop.png
教会temple.png
図書館library.png

街処理用コード修正

# ==============================================================================
# town.gd : 拠点(街)のUI操作とフォーカス管理を強化したバージョン
# ==============================================================================
class_name TownSystem
extends RefCounted

signal dungeon_entered

var town_graphic: TextureRect
var town_menu: Control
var dungeon_button: Button
var guild_button: Button
var tavern_button: Button
var shop_button: Button
var temple_button: Button
var library_button: Button

# 💡 各場所の画像定義
const TOWN_IMAGES = {
	"base": "res://image/town/town_background.png",
	"guild": "res://image/town/guild.png",
	"tavern": "res://image/town/tavern.png",
	"shop": "res://image/town/shop.png",
	"temple": "res://image/town/temple.png",
	"library": "res://image/town/library.png"
}

func _init(main_node: Node) -> void:
	town_graphic = main_node.get_node_or_null("CanvasLayer/TownGraphic")
	town_menu = main_node.get_node_or_null("CanvasLayer/TownGraphic/TownMenu")
	
	dungeon_button = main_node.get_node_or_null("CanvasLayer/TownGraphic/TownMenu/DungeonButton")
	guild_button = main_node.get_node_or_null("CanvasLayer/TownGraphic/TownMenu/GuildButton")
	tavern_button = main_node.get_node_or_null("CanvasLayer/TownGraphic/TownMenu/TavernButton")
	shop_button = main_node.get_node_or_null("CanvasLayer/TownGraphic/TownMenu/ShopButton")
	temple_button = main_node.get_node_or_null("CanvasLayer/TownGraphic/TownMenu/TempleButton")
	library_button = main_node.get_node_or_null("CanvasLayer/TownGraphic/TownMenu/LibraryButton")
	
	# ボタンの配列を作る(上から順に配置されている前提)
	var buttons = [guild_button, tavern_button, shop_button, temple_button, library_button, dungeon_button]
	
	# フォーカスの循環設定
	for i in range(buttons.size()):
		var prev = buttons[(i - 1 + buttons.size()) % buttons.size()]
		var next = buttons[(i + 1) % buttons.size()]
		
		if buttons[i]:
			# 上キーを押した時、前のボタンへ移動
			buttons[i].focus_neighbor_top = prev.get_path()
			# 下キーを押した時、次のボタンへ移動
			buttons[i].focus_neighbor_bottom = next.get_path()

	# シグナル接続
	if dungeon_button: dungeon_button.pressed.connect(_on_dungeon_button_pressed)
	
	if guild_button: guild_button.pressed.connect(func(): _move_to("guild"))
	if tavern_button: tavern_button.pressed.connect(func(): _move_to("tavern"))
	if shop_button: shop_button.pressed.connect(func(): _move_to("shop"))
	if temple_button: temple_button.pressed.connect(func(): _move_to("temple"))
	if library_button: library_button.pressed.connect(func(): _move_to("library"))

func start_town_mode() -> void:
	if town_graphic: town_graphic.show()
	_reset_to_main_town()

func end_town_mode() -> void:
	if town_graphic: town_graphic.hide()
	_remove_return_button()

# 🛠️ メインメニューに戻る
func _reset_to_main_town() -> void:
	_update_graphic("base")
	if town_menu:
		town_menu.show()
		# フォーカスをメインメニューのいずれかに戻す(今回はダンジョンボタンへ)
		if dungeon_button: dungeon_button.grab_focus()
	_remove_return_button()

# 🛠️ 場所移動の処理
func _move_to(location: String) -> void:
	_update_graphic(location)
	if town_menu:
		town_menu.hide()
	_show_return_button()

# 🛠️ 画像更新
func _update_graphic(location: String) -> void:
	if town_graphic and TOWN_IMAGES.has(location):
		town_graphic.texture = load(TOWN_IMAGES[location])

# 🛠️ 「街に戻る」ボタンを生成して表示
func _show_return_button() -> void:
	if town_graphic.has_node("ReturnButton"): return
	
	var btn = Button.new()
	btn.name = "ReturnButton"
	btn.text = "Back to Town"
	# ボタンの配置場所を調整してください
	btn.position = Vector2(800, 500) 
	
	btn.pressed.connect(_reset_to_main_town)
	town_graphic.add_child(btn)
	
	# 💡 重要:動的に生成したボタンにゲームパッドのフォーカスを移す
	btn.grab_focus()

# 🛠️ 「街に戻る」ボタンを削除
func _remove_return_button() -> void:
	var btn = town_graphic.get_node_or_null("ReturnButton")
	if btn:
		btn.queue_free()

func _on_dungeon_button_pressed() -> void:
	dungeon_entered.emit()

街処理用のコード (town.gd) を上記のように修正します。これは街の各メニューを選択したときに、各画像を表示するだけのものです。たとえば、冒険者ギルドを選択したら冒険者ギルド画像が出て “Back to Town” のメニューだけが表示されます。これで、街の各メニューを選択し街の各場所に行くことができるようになりました。

この街処理用のコードの _init メソッドの箇所も変更しています。これは街のメニューのカーソルが上下ループするようにするものです。

たとえば、一番上の「冒険者ギルド」にカーソルがあるときにゲームパッドの上キーを押すと、一番下の「ダンジョン」にカーソルが当たります。逆に一番下の「ダンジョン」にカーソルがあるとき下キーを押すと、一番上の「冒険者ギルド」にカーソルが動くようになっています。

街のメニューの言語切り替え (英語 / 日本語)

上記のコードだと、言語が日本語に固定されます。英語やそのほかの言語でも表示されるようにしたいのであれば、言語切り替え (ローカライゼーション) は必須です。ここでは英語と日本語の切り替えだけをできるようにします。

翻訳データCSVファイルに追記

TOWN_MENU_GUILD,Adventurers Guild,冒険者ギルド
TOWN_MENU_TAVERN,Tavern,酒場
TOWN_MENU_SHOP,Shop,店
TOWN_MENU_TEMPLE,Temple,教会
TOWN_MENU_LIBRARY,Library,図書館
TOWN_MENU_DUNGEON,Dungeon,ダンジョン

敵データで使用したCSVファイル (localization.csv) をテキストで開き、上記メニューの言語データを追記してください。追記後、CSV形式で保存します。追加したら忘れずにインポートしてください。

街処理用のコード修正

# ==============================================================================
# town.gd : 拠点(街)のUI操作とフォーカス管理を強化したバージョン
# ==============================================================================
class_name TownSystem
extends RefCounted

signal dungeon_entered

var town_graphic: TextureRect
var town_menu: Control
var dungeon_button: Button
var guild_button: Button
var tavern_button: Button
var shop_button: Button
var temple_button: Button
var library_button: Button

# 💡 各場所の画像定義
const TOWN_IMAGES = {
	"base": "res://image/town/town_background.png",
	"guild": "res://image/town/guild.png",
	"tavern": "res://image/town/tavern.png",
	"shop": "res://image/town/shop.png",
	"temple": "res://image/town/temple.png",
	"library": "res://image/town/library.png"
}

func _init(main_node: Node) -> void:
	town_graphic = main_node.get_node_or_null("CanvasLayer/TownGraphic")
	town_menu = main_node.get_node_or_null("CanvasLayer/TownGraphic/TownMenu")
	
	dungeon_button = main_node.get_node_or_null("CanvasLayer/TownGraphic/TownMenu/DungeonButton")
	guild_button = main_node.get_node_or_null("CanvasLayer/TownGraphic/TownMenu/GuildButton")
	tavern_button = main_node.get_node_or_null("CanvasLayer/TownGraphic/TownMenu/TavernButton")
	shop_button = main_node.get_node_or_null("CanvasLayer/TownGraphic/TownMenu/ShopButton")
	temple_button = main_node.get_node_or_null("CanvasLayer/TownGraphic/TownMenu/TempleButton")
	library_button = main_node.get_node_or_null("CanvasLayer/TownGraphic/TownMenu/LibraryButton")
	
	# ボタンの配列を作る(上から順に配置されている前提)
	var buttons = [guild_button, tavern_button, shop_button, temple_button, library_button, dungeon_button]
	
	# フォーカスの循環設定
	for i in range(buttons.size()):
		var prev = buttons[(i - 1 + buttons.size()) % buttons.size()]
		var next = buttons[(i + 1) % buttons.size()]
		
		if buttons[i]:
			# 上キーを押した時、前のボタンへ移動
			buttons[i].focus_neighbor_top = prev.get_path()
			# 下キーを押した時、次のボタンへ移動
			buttons[i].focus_neighbor_bottom = next.get_path()

	# ローカライゼーション
	if guild_button: guild_button.text = tr("TOWN_MENU_GUILD")
    if tavern_button: tavern_button.text = tr("TOWN_MENU_TAVERN")
    if shop_button: shop_button.text = tr("TOWN_MENU_SHOP")
    if temple_button: temple_button.text = tr("TOWN_MENU_TEMPLE")
    if library_button: library_button.text = tr("TOWN_MENU_LIBRARY")
    if dungeon_button: dungeon_button.text = tr("TOWN_MENU_DUNGEON")

	# 各ボタンのシグナル接続
	if dungeon_button: dungeon_button.pressed.connect(_on_dungeon_button_pressed)
	if guild_button: guild_button.pressed.connect(func(): _move_to("guild"))
	if tavern_button: tavern_button.pressed.connect(func(): _move_to("tavern"))
	if shop_button: shop_button.pressed.connect(func(): _move_to("shop"))
	if temple_button: temple_button.pressed.connect(func(): _move_to("temple"))
	if library_button: library_button.pressed.connect(func(): _move_to("library"))

func start_town_mode() -> void:
	if town_graphic: town_graphic.show()
	_reset_to_main_town()

func end_town_mode() -> void:
	if town_graphic: town_graphic.hide()
	_remove_return_button()

# 🛠️ メインメニューに戻る
func _reset_to_main_town() -> void:
	_update_graphic("base")
	if town_menu:
		town_menu.show()
		# フォーカスをメインメニューのいずれかに戻す(今回はダンジョンボタンへ)
		if dungeon_button: dungeon_button.grab_focus()
	_remove_return_button()

# 🛠️ 場所移動の処理
func _move_to(location: String) -> void:
	_update_graphic(location)
	if town_menu:
		town_menu.hide()
	_show_return_button()

# 🛠️ 画像更新
func _update_graphic(location: String) -> void:
	if town_graphic and TOWN_IMAGES.has(location):
		town_graphic.texture = load(TOWN_IMAGES[location])

# 🛠️ 「街に戻る」ボタンを生成して表示
func _show_return_button() -> void:
	if town_graphic.has_node("ReturnButton"): return
	
	var btn = Button.new()
	btn.name = "ReturnButton"
	btn.text = "Back to Town"
	# ボタンの配置場所を調整してください
	btn.position = Vector2(800, 500) 
	
	btn.pressed.connect(_reset_to_main_town)
	town_graphic.add_child(btn)
	
	# 💡 重要:動的に生成したボタンにゲームパッドのフォーカスを移す
	btn.grab_focus()

# 🛠️ 「街に戻る」ボタンを削除
func _remove_return_button() -> void:
	var btn = town_graphic.get_node_or_null("ReturnButton")
	if btn:
		btn.queue_free()

func _on_dungeon_button_pressed() -> void:
	dungeon_entered.emit()

街処理用のコード (town.gd) を上記のように修正します。これでメイン処理用コード (main.gd) の記述で、英語と日本語切り替え対応ができるようになりました。

現時点では、メイン処理用コードに言語情報(どの言語を使うか)が直書きされています。これは後の工程で設定画面から切り替えられるように修正する予定です。

街の各場所のメニュー表示

上記のコードだと、各場所には行けても「街に戻る」メニューしかありませんでした。ここでは「街に戻る」メニュー以外のメニューを表示し、選択できるようにしていきます。冒険者ギルド、酒場、店、教会、図書館のメニューを以下のようにします。

冒険者ギルド冒険者をよびだすJoin Party
冒険者ギルド冒険者をあずけるRemove from Party
冒険者ギルド冒険者を作成するCreate Character
冒険者ギルドクラスを変えるChange Class
冒険者ギルド冒険者を削除するDelete Character
冒険者ギルド名前を変えるChange Name
冒険者ギルド見た目を変えるChange Appearance
冒険者ギルド街へ戻る Back to Town
酒場クエストを受けるAccept Quest
酒場クエストの報告をするReport Quest
酒場情報を集めるGather Info
酒場街に戻るBack to Town
武器を買うWeapons
防具を買うArmors
道具を買うItems
売るSell
街に戻るBack to Town
教会のろいをとくBenediction
教会石化の治療Depetrify
教会生き返らせるResurrection
教会街へ戻るBack to Town
図書館記録を調べるCheck Records
図書館冒険譚を読むRead Stories
図書館チュートリアルTutorial
図書館街へ戻るBack to Town

翻訳データCSVファイルに追記

GUILD_MENU_JOIN,Join Party,冒険者をよびだす
GUILD_MENU_REMOVE,Remove from Party,冒険者をあずける
GUILD_MENU_CREATE,Create Character,冒険者を作成する
GUILD_MENU_CHANGE_CLASS,Change Class,クラスを変える
GUILD_MENU_DELETE,Delete Character,冒険者を削除する
GUILD_MENU_CHANGE_NAME,Change Name,名前を変える
GUILD_MENU_CHANGE_APPEARANCE,Change Appearance,見た目を変える
GUILD_MENU_BACK,Back to Town,街へ戻る
TAVERN_MENU_ACCEPT,Accept Quest,クエストを受ける
TAVERN_MENU_REPORT,Report Quest,クエストの報告をする
TAVERN_MENU_TALK,Gather Info,情報を集める
TAVERN_MENU_BACK,Back to Town,街へ戻る
SHOP_MENU_WEAPON,Weapons,武器を買う
SHOP_MENU_ARMOR,Armors,防具を買う
SHOP_MENU_ITEM,Items,道具を買う
SHOP_MENU_SELL,Sell,売る
SHOP_MENU_BACK,Back to Town,街へ戻る
TEMPLE_MENU_BENEDICT,Benediction,のろいをとく
TEMPLE_MENU_DEPETRIFY,Depetrify,石化の治療
TEMPLE_MENU_RESURRECT,Resurrection,生き返らせる
TEMPLE_MENU_BACK,Back to Town,街へ戻る
LIBRARY_MENU_RECORDS,Check Records,記録を調べる
LIBRARY_MENU_STORIES,Read Stories,冒険譚を読む
LIBRARY_MENU_TUTORIAL,Tutorial,チュートリアル
LIBRARY_MENU_BACK,Back to Town,街へ戻る

翻訳データCSVファイル (localization.csv) に上記を追記してください。そのあと、ファイルをインポートします。

街処理用コード修正

# ==============================================================================
# town.gd : 拠点(街)のUI操作と各場所のメニュー管理(完全版)
# ==============================================================================
class_name TownSystem
extends RefCounted

signal dungeon_entered

var town_graphic: TextureRect
var town_menu: Control
var dungeon_button: Button
var guild_button: Button
var tavern_button: Button
var shop_button: Button
var temple_button: Button
var library_button: Button

# 💡 ギルド専用のメニュー項目
const GUILD_MENU_KEYS = [
	"GUILD_MENU_JOIN", 
	"GUILD_MENU_REMOVE", 
	"GUILD_MENU_CREATE", 
	"GUILD_MENU_CHANGE_CLASS", 
	"GUILD_MENU_DELETE", 
	"GUILD_MENU_CHANGE_NAME", 
	"GUILD_MENU_CHANGE_APPEARANCE", 
	"GUILD_MENU_BACK"
]

# 💡 酒場専用のメニュー項目キー
const TAVERN_MENU_KEYS = [
	"TAVERN_MENU_ACCEPT",
	"TAVERN_MENU_REPORT",
	"TAVERN_MENU_TALK",
	"TAVERN_MENU_BACK"
]

# 💡 店専用のメニュー項目キー
const SHOP_MENU_KEYS = [
	"SHOP_MENU_WEAPON",
	"SHOP_MENU_ARMOR",
	"SHOP_MENU_ITEM",
	"SHOP_MENU_SELL",
	"SHOP_MENU_BACK"
]

# 💡 教会専用のメニュー項目キー
const TEMPLE_MENU_KEYS = [
	"TEMPLE_MENU_BENEDICT",
	"TEMPLE_MENU_DEPETRIFY",
	"TEMPLE_MENU_RESURRECT",
	"TEMPLE_MENU_BACK"
]

# 💡 図書館専用のメニュー項目キー
const LIBRARY_MENU_KEYS = [
	"LIBRARY_MENU_RECORDS",
	"LIBRARY_MENU_STORIES",
	"LIBRARY_MENU_TUTORIAL",
	"LIBRARY_MENU_BACK"
]

const TOWN_IMAGES = {
	"base": "res://image/town/town_background.png",
	"guild": "res://image/town/guild.png",
	"tavern": "res://image/town/tavern.png",
	"shop": "res://image/town/shop.png",
	"temple": "res://image/town/temple.png",
	"library": "res://image/town/library.png"
}

func _init(main_node: Node) -> void:
	town_graphic = main_node.get_node_or_null("CanvasLayer/TownGraphic")
	town_menu = main_node.get_node_or_null("CanvasLayer/TownGraphic/TownMenu")
	
	dungeon_button = main_node.get_node_or_null("CanvasLayer/TownGraphic/TownMenu/DungeonButton")
	guild_button = main_node.get_node_or_null("CanvasLayer/TownGraphic/TownMenu/GuildButton")
	tavern_button = main_node.get_node_or_null("CanvasLayer/TownGraphic/TownMenu/TavernButton")
	shop_button = main_node.get_node_or_null("CanvasLayer/TownGraphic/TownMenu/ShopButton")
	temple_button = main_node.get_node_or_null("CanvasLayer/TownGraphic/TownMenu/TempleButton")
	library_button = main_node.get_node_or_null("CanvasLayer/TownGraphic/TownMenu/LibraryButton")
	
	# ボタンの配列を作る(エディタ上の配置順序に合わせてください)
	var buttons = [guild_button, tavern_button, shop_button, temple_button, library_button, dungeon_button]
	
	# フォーカスの循環設定
	for i in range(buttons.size()):
		var prev = buttons[(i - 1 + buttons.size()) % buttons.size()]
		var next = buttons[(i + 1) % buttons.size()]
		if buttons[i]:
			buttons[i].focus_neighbor_top = prev.get_path()
			buttons[i].focus_neighbor_bottom = next.get_path()
	
	# ローカライゼーション
	if guild_button: guild_button.text = tr("TOWN_MENU_GUILD")
	if tavern_button: tavern_button.text = tr("TOWN_MENU_TAVERN")
	if shop_button: shop_button.text = tr("TOWN_MENU_SHOP")
	if temple_button: temple_button.text = tr("TOWN_MENU_TEMPLE")
	if library_button: library_button.text = tr("TOWN_MENU_LIBRARY")
	if dungeon_button: dungeon_button.text = tr("TOWN_MENU_DUNGEON")
	
	
	# 各ボタンのシグナル接続
	if dungeon_button: dungeon_button.pressed.connect(_on_dungeon_button_pressed)
	if guild_button: guild_button.pressed.connect(func(): _move_to("guild"))
	if tavern_button: tavern_button.pressed.connect(func(): _move_to("tavern"))
	if shop_button: shop_button.pressed.connect(func(): _move_to("shop"))
	if temple_button: temple_button.pressed.connect(func(): _move_to("temple"))
	if library_button: library_button.pressed.connect(func(): _move_to("library"))

func start_town_mode() -> void:
	if town_graphic: town_graphic.show()
	_reset_to_main_town()

func end_town_mode() -> void:
	if town_graphic: town_graphic.hide()
	_cleanup_dynamic_menus()

# 🛠️ メインメニューに戻る
func _reset_to_main_town() -> void:
	_update_graphic("base")
	if town_menu:
		town_menu.show()
		dungeon_button.grab_focus()
	_cleanup_dynamic_menus()

# 🛠️ 場所移動とメニュー生成
func _move_to(location: String) -> void:
	_update_graphic(location)
	if town_menu:
		town_menu.hide()
	
	_cleanup_dynamic_menus()
	
	match location:
		"guild":
			_show_dynamic_menu(GUILD_MENU_KEYS)
		"tavern":
			_show_dynamic_menu(TAVERN_MENU_KEYS)
		"shop": 
			_show_dynamic_menu(SHOP_MENU_KEYS)
		"temple": 
			_show_dynamic_menu(TEMPLE_MENU_KEYS)
		"library":
			_show_dynamic_menu(LIBRARY_MENU_KEYS)
		_:
			_show_return_button()
			

# 🛠️ 汎用的なメニュー生成メソッド(共通利用)
func _show_dynamic_menu(menu_keys: Array) -> void:
	var center_container = CenterContainer.new()
	center_container.name = "DynamicMenu"
	center_container.set_anchors_preset(Control.PRESET_FULL_RECT)
	town_graphic.add_child(center_container)
	
	var menu_panel = VBoxContainer.new()
	center_container.add_child(menu_panel)
	
	var buttons = []
	for key in menu_keys:
		var btn = Button.new()
		btn.text = tr(key)
		btn.pressed.connect(func(): _on_menu_selected(key))
		menu_panel.add_child(btn)
		buttons.append(btn)
	
	# 循環フォーカス設定
	for i in range(buttons.size()):
		var prev = buttons[(i - 1 + buttons.size()) % buttons.size()]
		var next = buttons[(i + 1) % buttons.size()]
		buttons[i].focus_neighbor_top = prev.get_path()
		buttons[i].focus_neighbor_bottom = next.get_path()
	
	buttons[0].grab_focus()

# 🛠️ ギルド専用メニューの生成
func _show_guild_menu() -> void:
# 💡 中心配置専用のコンテナを挟む
	var center_container = CenterContainer.new()
	center_container.name = "DynamicMenu"
	center_container.set_anchors_preset(Control.PRESET_FULL_RECT) # 画面いっぱいに広げて...
	town_graphic.add_child(center_container)
	
	var menu_panel = VBoxContainer.new()
	center_container.add_child(menu_panel)
	
	var buttons = []
	for key in GUILD_MENU_KEYS:
		var btn = Button.new()
		# 💡 ここで翻訳を適用
		btn.text = tr(key)
		btn.pressed.connect(func(): _on_menu_selected(key)) # キーを渡す
		menu_panel.add_child(btn)
		buttons.append(btn)
	
	# ギルドメニュー内の循環フォーカス設定
	for i in range(buttons.size()):
		var prev = buttons[(i - 1 + buttons.size()) % buttons.size()]
		var next = buttons[(i + 1) % buttons.size()]
		buttons[i].focus_neighbor_top = prev.get_path()
		buttons[i].focus_neighbor_bottom = next.get_path()
	
	buttons[0].grab_focus()

# 🛠️ 簡易的な戻るボタン
func _show_return_button() -> void:
	var btn = Button.new()
	btn.name = "DynamicMenu"
	btn.text = "街へ戻る"
	btn.set_anchors_preset(Control.PRESET_CENTER)
	btn.pressed.connect(_reset_to_main_town)
	town_graphic.add_child(btn)
	btn.grab_focus()

# 🛠️ メニュー選択時の共通処理
func _on_menu_selected(key: String) -> void:
	var back_keys = [
		"GUILD_MENU_BACK", 
		"TAVERN_MENU_BACK", 
		"SHOP_MENU_BACK", 
		"TEMPLE_MENU_BACK", 
		"LIBRARY_MENU_BACK"
	]
	
	if key in back_keys:
		_reset_to_main_town()
	else:
		print(tr(key) + " が選択されました")

# 🛠️ 動的生成したメニューを削除
func _cleanup_dynamic_menus() -> void:
	var menu = town_graphic.get_node_or_null("DynamicMenu")
	if menu:
		menu.queue_free()

func _update_graphic(location: String) -> void:
	if town_graphic and TOWN_IMAGES.has(location):
		town_graphic.texture = load(TOWN_IMAGES[location])

func _on_dungeon_button_pressed() -> void:
	dungeon_entered.emit()

街処理用のコード (town.gd) を上記のように修正します。town.gd 内に冒険者ギルド・酒場・店・教会・図書館のメニューキーを定義し、_move_to メソッドで酒場が選ばれた際の処理を追加します。これで、冒険者ギルド・酒場・店・教会・図書館のメニューに行くと、それぞれのメニューが出るようになりました。

冒険者ギルド・酒場・店・教会・図書館の各メニューの実装は、後の工程で行います。

街のBGM処理

街にいる間、BGMを流します。後の工程で戦闘BGMなどにも対応することを考えて、BGMを統括する流れを作っていきます。

音楽処理用ノード (AudioManager) の作成

Main シーンを右クリックし、Add Child Node を選択します。

Node を追加し、名前を AudioManager に変更します。

その AudioManager の子ノードとして AudioStreamPlayer を追加し、名前を BGMPlayer にします。

次に、BGMを管理するスクリプトを作ります。AudioManager を選択し、Node – Script 欄から New Script をクリックしてください。

audio_manager.gd を作成してください。

次に音楽ファイルを置いておく場所を作ります。FileSystem を右クリックして Create New から Folder を選択して、music フォルダを作ってください。

music フォルダを作成したら、街のBGMファイルをドラッグ&ドロップでインポートしてください。この記事では town.mp3 とします。

曲のループ処理対応

曲をループで流すためには、まず Godot Engine にインポートされた音楽ファイルをダブルクリックします。ウィンドウが開くので Loop – Enable にチェックを入れてください。Reimport して完了です。これだけで、ゲーム内の曲がループで流れるようになります。

おそらくダンジョンRPGの曲はループ処理するはずです。デフォルトではループしないので、忘れずにチェックしておきましょう。

音楽処理用コードの実装

音楽やBGM を管理するスクリプト (audio_manager.gd) の実装に入ります。

# audio_manager.gd
extends Node

@onready var bgm_player: AudioStreamPlayer = $BGMPlayer

# 再生するBGMのリスト(将来的な管理用)
const BGM_FILES = {
	"town": preload("res://music/town.mp3"), # パスは実際のものに置き換えてください
	# "dungeon": preload("res://music/dungeon_theme.ogg"),
	# "battle": preload("res://music/battle_theme.ogg")
}

func play_bgm(key: String) -> void:
	if not BGM_FILES.has(key):
		print("BGM key not found: ", key)
		return
	
	# すでにその曲が流れていれば何もしない
	if bgm_player.stream == BGM_FILES[key] and bgm_player.playing:
		return
		
	bgm_player.stream = BGM_FILES[key]
	bgm_player.play()

func stop_bgm() -> void:
	bgm_player.stop()

audio_manager.gd に書かれていたものをすべて消して、上記のように記述してください。このスクリプトは、曲の再生と停止をハンドリングするものです。

メイン処理用コードの修正

# ==============================================================================
# main.gd : ゲーム全体(街 / 探索 / 戦闘)の状態を統括するコントローラー
# ==============================================================================
extends Node

enum GameState { EXPLORE, BATTLE, TOWN }
var current_state: GameState = GameState.EXPLORE

var town_system: TownSystem
var battle_system: BattleSystem

# 型定義を自作のクラス(dungeon.gdのclass_name)に変更
@onready var dungeon_explore: DungeonExplore = $Camera3D

# AudioManagerの参照
@onready var audio_manager = $AudioManager

func _ready() -> void:
	print("【Main】コントローラー起動。")
	
	# ゲーム起動時のデフォルト言語を「英語」に設定
	TranslationServer.set_locale("en")
	
	town_system = TownSystem.new(self)
	battle_system = BattleSystem.new(self)
	
	# シグナル結合
	dungeon_explore.battle_triggered.connect(_on_battle_triggered)
	dungeon_explore.town_entered.connect(_on_town_entered)
	
	town_system.dungeon_entered.connect(_on_dungeon_entered)
	battle_system.battle_finished.connect(_on_battle_finished)
	
	# 【変更】初期状態を「探索」から「街モード」に変更して起動します
	_change_state(GameState.TOWN)

func _change_state(next_state: GameState) -> void:
	current_state = next_state
	
	if town_system: town_system.end_town_mode()
	if battle_system: battle_system.end_battle_mode()
	if dungeon_explore: dungeon_explore.set_explore_active(false)
	
	match current_state:
		GameState.EXPLORE:
			# 曲を止める
			audio_manager.stop_bgm()
			dungeon_explore.set_explore_active(true)
		GameState.TOWN:
			# 街のBGMを再生
			audio_manager.play_bgm("town")
			town_system.start_town_mode()
		GameState.BATTLE:
			# 曲を止める
			audio_manager.stop_bgm()
			
			# 現在の階層(整数)を取得
			var current_floor_num: int = dungeon_explore.current_floor
			
			# dungeon_explore経由で、JSON(map_data.json)内の出現数設定を取得
			var limits: Dictionary = dungeon_explore.get_floor_enemy_limits(current_floor_num)
			var min_count: int = limits["min"]
			var max_count: int = limits["max"]
			
			# 登場可能な敵ID(文字列)を格納する配列の作成
			var available_enemy_ids: Array[String] = []
			
			# ロード済みの enemy_master_data から、この階層に出る敵をすべて抽出
			for enemy_id in battle_system.enemy_master_data.keys():
				var enemy_data = battle_system.enemy_master_data[enemy_id]
				
				if typeof(enemy_data) == TYPE_DICTIONARY and enemy_data.has("appear_floors"):
					var floors_list: Array = enemy_data["appear_floors"]
					for f in floors_list:
						if int(f) == current_floor_num:
							available_enemy_ids.append(enemy_id)
							break
			
			# 安全装置
			if available_enemy_ids.is_empty():
				available_enemy_ids.append("goblin")
			
			# 選別された配列と、JSONから動的に引っ張ってきた出現数範囲を渡してバトル開始
			battle_system.start_battle(available_enemy_ids, min_count, max_count)

func _on_battle_triggered() -> void:
	_change_state(GameState.BATTLE)

func _on_battle_finished() -> void:
	_change_state(GameState.EXPLORE)

func _on_town_entered() -> void:
	_change_state(GameState.TOWN)

func _on_dungeon_entered() -> void:
	# 💡 街からダンジョンへ入るタイミングで、プレイヤーをB1の開始座標に配置します
	dungeon_explore.reset_to_dungeon_start()
	_change_state(GameState.EXPLORE)

メイン処理用のコード (main.gd) を上記のように修正します。これで街にいる間は街専用のBGMが流れます。冒険者ギルドに行っても教会に行っても同じ曲が流れますが、ダンジョンに入ると曲が止まります。

ダンジョン内の曲や戦闘曲は、後の工程で実装を行います。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

CAPTCHA


目次