恋愛シミュレーションを作ろう!
Part 3: メインスクリプトの作成
Godot Engine バージョン4.4
このパートでは、メインとなるスクリプトの中身を丁寧に解説します。ゲーム中のセリフ表示、選択肢、立ち絵、BGM、背景などの演出がどのように制御されているかを学びましょう。これを理解することで、自作のシナリオを思い通りに演出できるようになります!
メインスクリプト
下記のスクリプトを作成して
Main
に付与します。
@toolextends Control
# --- UI Export Configuration ---@export_category("SCENARIO LIST")@export_tool_button("シーンを読み込む")var button = load_scenarios_from_directory
@export var scenarios: Array[Scenario]@export_category("ENDING LIST")@export var endings: Array[PackedScene]@export_category("OTHERS")@export var option_btn: PackedScene
# --- Node References ---@onready var character_speak_panel = $CharacterSpeakPanel@onready var character_name = $CharacterSpeakPanel/CharacterName@onready var character_text = $CharacterSpeakPanel/CharacterText@onready var background: TextureRect = $Background@onready var bgm: AudioStreamPlayer2D = $Bgm@onready var audio: AudioStreamPlayer2D = $Audio@onready var pos_left: TextureRect = $PositionBox/PosLeft@onready var pos_center: TextureRect = $PositionBox/PosCenter@onready var pos_right: TextureRect = $PositionBox/PosRight@onready var option_box_container: VBoxContainer = $OptionPanel/OptionBoxContainer@onready var option_panel: Panel = $OptionPanel@onready var next = $CharacterSpeakPanel/Next@onready var text_delay = $TextDelay
# --- Runtime Variables ---var current_scenario := 0var current_scenario_group := "begin"var scenario_list_loaded := []var is_dialog_mode := falsevar text_to_display := []var current_index := 0var current_text_index := 0var has_option := falsevar ready_to_next := false
# --- Constants ---const SAVE_SCENARIO_DIRECTORY := "res://scenes/scenarios/"const SAVE_ENDING_DIRECTORY := "res://scenes/endings/"
var loaded_list := {}
# 初期化処理func _ready(): load_scenario_list()
# シーンをソートするためのカスタムソート関数func natural_sort(a: String, b: String) -> bool: var regex = RegEx.new() regex.compile(r"^([A-Z]?)-?(\d+)$") var match_a = regex.search(a) var match_b = regex.search(b) if match_a and match_b: var prefix_a = match_a.get_string(1) var num_a = int(match_a.get_string(2))
var prefix_b = match_b.get_string(1) var num_b = int(match_b.get_string(2))
if prefix_a == prefix_b: return num_a < num_b else: return prefix_a < prefix_b elif match_a and not match_b: return true elif not match_a and match_b: return false return a < b
# --- シナリオとエンディングファイルをフォルダから読み込む ---func load_scenarios_from_directory(): scenario_list_loaded.clear() var dir := DirAccess.open(SAVE_SCENARIO_DIRECTORY) if dir: dir.list_dir_begin() var file_name := dir.get_next() while file_name != "": if file_name.ends_with(".tres"): scenario_list_loaded.append(file_name.replace(".tres", "")) file_name = dir.get_next() dir.list_dir_end()
if scenario_list_loaded.size() > 0: scenario_list_loaded.sort_custom(natural_sort) for file in scenario_list_loaded: scenarios.append(load(SAVE_SCENARIO_DIRECTORY + file + ".tres") as Scenario)
# トリック:エディタ反映用に一度代入し直し var temp = scenarios scenarios = [] scenarios = temp
# エンディングシーンも同様に読み込み var dir_ending = DirAccess.open(SAVE_ENDING_DIRECTORY) if dir_ending: dir_ending.list_dir_begin() var file_name = dir_ending.get_next() while file_name != "": if file_name.ends_with(".tscn"): endings.append(load(SAVE_ENDING_DIRECTORY + file_name)) file_name = dir_ending.get_next() dir_ending.list_dir_end() # インスペクターを更新 notify_property_list_changed()
# --- シナリオのプレフィックスごとに分類 ---func load_scenario_list(): for file in scenarios: var prefix := file.prefix_scenario if file.prefix_scenario != "" else "begin" if loaded_list.has(prefix): loaded_list[prefix].append(file) else: loaded_list[prefix] = [file]
if loaded_list.has("begin"): current_scenario = 0 load_scenario(loaded_list[current_scenario_group][current_scenario]) else: printerr("シナリオのプレフィックス 'begin' が見つかりません。")
# --- シナリオを読み込み、UIや音声、背景を設定 ---func load_scenario(scenario: Scenario): option_panel.hide() audio.stop()
# プレフィックスが変わったら最初から再開 if scenario.prefix_scenario != "" and scenario.prefix_scenario != current_scenario_group: current_scenario_group = scenario.prefix_scenario current_scenario = 0
# 背景変更時にフェード if background.texture != scenario.background: character_speak_panel.hide() Fade.start_fade(0.5, change_background.bind(scenario.background)) await get_tree().create_timer(1.0).timeout
# キャラ名とセリフをセット character_name.text = scenario.text_name text_to_display = [scenario.text_speak]
# BGM処理 if scenario.text_bgm != "" and (bgm.stream == null or scenario.text_bgm != bgm.stream.resource_path): bgm.stream = load(scenario.text_bgm) bgm.play()
# 音声処理 if scenario.text_audio != "": audio.stream = load(scenario.text_audio) audio.play()
# 立ち絵をセット pos_left.texture = scenario.exp_pos_left pos_center.texture = scenario.exp_pos_center pos_right.texture = scenario.exp_pos_right
# 発言パネル表示 if scenario.text_name != "": character_speak_panel.show()
# 選択肢がある場合はボタンを生成 has_option = scenario.option_list.size() > 0 if has_option: for option in scenario.option_list: var option_btn_instance = option_btn.instantiate() option_btn_instance.get_child(0).text = option.option_text option_btn_instance.pressed.connect(find_scenario_by_name.bind(option.option_value)) option_box_container.add_child(option_btn_instance) else: option_panel.hide()
start_dialog() await get_tree().create_timer(0.5).timeout ready_to_next = true
# --- テキストを1文字ずつ表示するタイマー処理 ---func _on_text_delay_timeout() -> void: if current_index < text_to_display.size(): if current_text_index < text_to_display[current_index].length(): character_text.text = text_to_display[current_index].substr(0, current_text_index + 1) current_text_index += 1 else: text_delay.stop() next.visible = true if has_option: is_dialog_mode = false option_panel.show() else: option_panel.hide() else: # テキスト終了後、次のシナリオへ text_delay.stop() is_dialog_mode = false ready_to_next = false if current_scenario < loaded_list[current_scenario_group].size() - 1: current_scenario += 1 load_scenario(loaded_list[current_scenario_group][current_scenario]) else: var ending_scene: String = loaded_list[current_scenario_group][current_scenario].ending_scenario if ending_scene != "": for ending in endings: if ending.resource_path.get_file().get_basename() == ending_scene: get_tree().change_scene_to_file(SAVE_ENDING_DIRECTORY + ending_scene + ".tscn") return else: print("シナリオが終了しました。")
# --- 入力イベント処理(クリックで次へ) ---func _input(event: InputEvent) -> void: if event.is_action_pressed("next") and is_dialog_mode and ready_to_next: if current_index < text_to_display.size(): if current_text_index == 0 or current_text_index == text_to_display[current_index].length(): text_delay.start() current_text_index = 0 current_index += 1 next.visible = false else: # スキップして全文表示 character_text.text = text_to_display[current_index] current_text_index = 0 text_delay.stop() next.visible = true if has_option: is_dialog_mode = false option_panel.show() else: option_panel.hide()
# --- ダイアログ開始処理(テキスト初期化+タイマー開始) ---func start_dialog(): character_text.text = "" current_text_index = 0 current_index = 0 text_delay.start() is_dialog_mode = true
# --- 背景変更処理(フェード用) ---func change_background(new_background: Texture): background.texture = new_background
# --- 選択肢からシナリオを検索して読み込む ---func find_scenario_by_name(scenario_name: String) -> void: var prefix := scenario_name.split("-")[0] if scenario_name.contains("-") else "begin"
if loaded_list.has(prefix): for scenario in loaded_list[prefix]: if scenario.scenario_name == scenario_name: Se.play() load_scenario(scenario) return
解説内容
1. スクリプトの全体構造
このスクリプトは
@tool
付きで、Godot エディタ上でもシナリオをロード可能になっています。UIや音響の再生、選択肢の処理など、ギャルゲーの核となる処理が詰まった一枚岩です。@toolextends Control
2. エディタ上でシナリオをロードする
@export_tool_button("シーンを読み込む")
を使うことで、ボタン一つでシナリオフォルダから .tres
ファイルを読み込めます。これは開発の効率化にとても便利です。
@export_tool_button("シーンを読み込む")var button = load_scenarios_from_directory
3. シナリオのロード処理
func load_scenarios_from_directory(): ...
この関数では、
res://scenes/scenarios/
フォルダ内の .tres
を全てスキャンして scenarios
に追加し、エディタに反映します。エンディングも res://scenes/endings/
から同様にロードされます。natural_sort()
で自然な順番にソート(例:A-1, A-2, A-10)
- 読み込んだシナリオを
prefix_scenario
で分類し、ゲーム中のシナリオ切り替えに使用
4. シナリオの表示ロジック
func load_scenario(scenario: Scenario):
この関数は1つのシナリオを読み込んで表示の準備をします。
- キャラ名やセリフの設定
- 背景画像やBGMの変更
- キャラ立ち絵の設定(左右中央)
- 選択肢がある場合はボタンを生成し、押されたら該当シナリオに分岐
_on_text_delay_timeout()
で1文字ずつセリフを表示
5. テキスト表示タイミングと入力制御
func _on_text_delay_timeout():
- 指定時間ごとに1文字ずつテキスト表示
- テキストが終わると「次へ」ボタン表示 or 選択肢出現
func _input(event: InputEvent):
- 入力があれば「次へ」進む
- 表示中でもクリックで全表示(スキップ)可能
6. 選択肢の実装
func find_scenario_by_name(scenario_name: String):
選択肢で選んだシナリオ名をキーにして、シナリオをロード。これによりマルチエンディングや分岐が実現できます。
このパートでできるようになること
- シナリオのロードと表示の流れを理解
- セリフや背景、音楽などの演出をコントロール
- 分岐や選択肢の追加によるマルチルート化
.tres
によるデータ駆動型シナリオ管理
次回予告
次のパートでは、実際にシナリオ(
.tres
)ファイルを作成し、ゲーム内で再生する方法を紹介します。分岐やエンディングの演出もここでカバー!
🔗 補足
Scenario
はカスタム Resource。Part 2で作成したもの。 テキストアニメーションや背景フェードもこのスクリプト内で制御。
1
100%