<PR>『ミオの○○な一日』【体験版】を公開しました!!。
前回の記事では、開発中のゲーム「ミオの〇〇な一日」の心臓部である「ComponentEngine」の全体像についてお話ししました。
ComponentEngineの最大の目的は、「文字列の配列を投げるだけで、ゲーム内のあらゆる要素を連携させて動かすこと」です。
シンプルにまとめた ComponentEngine
ComponentEngineのソースコード(engin.gd)を紹介します。
extends Node
class_name ComponentEngine
@export var speak_dialog:SpeakDialog
@export var menu_dialog:MenuDialog
@export var flag_manager:FlagManager
@export var phase_manager:PhaseManager2D
@export var character_manager:CharacterManager
@export var background_Manager:BackgroundManager
@export var sprite_manager:SpriteManager
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
if not menu_dialog == null:
menu_dialog.hide()
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(_delta: float) -> void:
pass
func exec_command_line(_command:Array) -> void:
match _command[0][0]:
"_":
var _c:String = _command[0].erase(0,1).to_lower()
await self.background_Manager.call("exec_command_line_"+_c, _command)
"@":
var _c:String = _command[0].erase(0,1).to_lower()
await self.character_manager.call("exec_command_line_"+_c, _command)
">":
var _c:String = _command[0].erase(0,1).to_lower()
self.phase_manager.call("exec_command_line_"+_c, _command)
"-":
var _c:String = _command[0].erase(0,1).to_lower()
await self.sprite_manager.call("exec_command_line_"+_c, _command)
_:
self.speak_dialog.show()
if _command.size() < 3:
await self.speak_dialog.call("exec", character_manager.name_list[_command[0]], _command[1])
else:
await self.speak_dialog.call("exec", character_manager.name_list[_command[0]], _command[1], _command[2])
func start_script(_script:Array) -> void:
#speak_dialog.hide()
for _command:Array in _script:
await self.exec_command_line(_command)
speak_dialog.hide()このように、非常にシンプルなソースコードで、各マネージャーおよびダイアログを操作できるようにしています。
ゲーム内のあらゆる要素を連携させて動かすことを実現しているのが、 exec_command_line というメソッドです。
すべてのコマンドを捌く「ルーティング」機能
ComponentEngineは、背景、キャラクター、会話UIなど、様々な「マネージャー」や「ダイアログ」への参照を持っています。
そして、渡された配列データ(簡易スクリプト)の「1番目の要素の先頭文字(プレフィックス)」を見て、どのマネージャーに指示を出すかを決定します。
実際のコードを見てみましょう。
# engine.gd (一部抜粋)
func exec_command_line(_command:Array) -> void:
match _command[0][0]:
"_": # 背景マネージャーへ
var _c:String = _command[0].erase(0,1).to_lower()
await self.background_Manager.call("exec_command_line_"+_c, _command)
"@": # キャラクターマネージャーへ
var _c:String = _command[0].erase(0,1).to_lower()
await self.character_manager.call("exec_command_line_"+_c, _command)
">": # フェイズマネージャーへ
var _c:String = _command[0].erase(0,1).to_lower()
self.phase_manager.call("exec_command_line_"+_c, _command)
"-": # スプライトマネージャーへ
var _c:String = _command[0].erase(0,1).to_lower()
await self.sprite_manager.call("exec_command_line_"+_c, _command)
_: # デフォルト(プレフィックスなし)は会話ダイアログへ
self.speak_dialog.show()
if _command.size() < 3:
await self.speak_dialog.call("exec", character_manager.name_list[_command[0]], _command[1])
else:
await self.speak_dialog.call("exec", character_manager.name_list[_command[0]], _command[1], _command[2])このシンプルな match 文による振り分け(ルーティング)が、ComponentEngineの根幹です。
例えば、["@FadeIn", "Mio"] という配列が渡された場合、以下の処理が行われます。
- 先頭文字が
@なので、対象はcharacter_managerに決定する。 @を取り除き、小文字に変換してfadeinというコマンド名を作る。character_managerのexec_command_line_fadeinメソッドを呼び出して、ミオの画像をフェードインで表示する。
UNIX哲学に基づいた「疎結合」な設計
ここで重要なポイントが2つあります。
1. call メソッドによる動的呼び出し
Godotの call メソッドを使用することで、メソッド名を文字列で動的に組み立てて呼び出しています(メタプログラミング的なアプローチ)。
これにより、ComponentEngine自体は「キャラクターのフェードイン処理がどう実装されているか」を知る必要がありません。「こういうコマンドが来たから、お前に任せたぞ!」と丸投げするだけです。
2. デフォルト動作としての「会話」
特別なプレフィックスが付いていない場合(例えば ["Mio", "こんにちは"])、それは自動的に SpeakDialog への命令として解釈されます。
アドベンチャーゲームにおいて最も頻繁に発生するのは「会話」です。そのため、会話に関するコマンドにはあえてプレフィックスを付けない仕様にすることで、スクリプトの記述量を減らすことができました。
まとめ
ComponentEngine は、巨大で複雑な処理を内包しているわけではありません。
「シンプルなルールの文字解析」と「適切なマネージャーへの委譲」を行っているだけの、いわば「コンダクター(指揮者)」です。
このように「単一の責任を持つ小さなモジュールを組み合わせて大きなシステムを作る」というUNIX哲学的な設計にすることで、バグが起きにくく、拡張しやすい環境を構築できました。
この仕組みを理解した上で、次回は「デフォルト動作」として処理される会話ダイアログ『SpeakDialog』の実装について、さらに深く掘り下げていきたいと思います!



コメント