Godotたちのシングルトン
手段がGodotにせよ何にせよ、ゲーム作ってるとシングルトン要りますよね。ロードしたリソースをキープしたり、ゲームの進行状況を記録したり。
この場合、「常に1個しか存在しない」という本来の機能はそこまで重要ではなくって、
・参照を渡されなくてもどこからでもアクセスできる
・シーン(広義)が切り替わっても保持され続ける
というのがキーとなる要件だと思います。
んで、どうやってシングルトンを実装するかですけど、やり方が色々あるように思います。一長一短。
というわけで、そのアイデアを書き連ねてみます。これはそんな記事。
案A. AutoLoad機能を使う
Godot公式が推奨(?)するやり方です。Godotの「自動読み込み / AutoLoad」機能を使います。Godotさんがシングルトンと言ったらこれのことです。
プロジェクト設定から、自動読み込みリストにクラスを登録しておくと、ゲーム実行時に自動的にノードが作られます。ノードツリーのルート直下に置かれます。
# クラス定義 ============
class_name MySingleton
extends Node
# 普通のクラスのように書く
var val_a: int = -1
var val_b: int = -1
var val_c: int = -1
func do_something() -> void:
exec_something(val_a)
# 使う側 ============
# ノード名でインスタンスにアクセスできる。
# ノード名はクラス名と重複できないので、g(globalのg)を付けてます。
g_MySingleton.do_something()
メリット
プロジェクト内のシングルトンの存在が、GUI上でリストされます。管理がしやすい。どんなシングルトンが在るか一目瞭然。
仕組みがちょっと大げさなので、使用に慎重になって(ほんとに?)、乱用を控えることになります。どこからでもアクセスできるデータは、使いすぎるとプログラムがわけわからんくなります。
デメリット
どんなシーンを単独再生(F6キー)しても、プロジェクト内の全AutoLoadがインスタンス化されてしまいます。
シーンの入れ子構造でゲームを作り上げていってねというのがGodotの理念のはずです。でも小さなシーン(扇風機とか、HPバーとか)を再生したときも、ゲーム全体を管理するようなシングルトンが出来上がってツリー下に配置されてると、気になっちゃって仕方がない。
自動で駆動するようなシングルトンでもない限り、特に悪さするわけじゃないはずなので、気持ちの問題でしかないんですけどね。シングルトンを破棄できません。無駄なメモリを解放したり、リークを検知するために、データをバッサリ消したいことあると思うんです。そのときに、シングルトンノードごと消すということができない。
とはいえ、別にノード自体を消さなくても、それが保持してるデータを消せれば十分ではあります。
class_name MySingleton
# データだけDispose(破棄)できるようにした例
var dat: MySingletonDat = null # メンバ変数はこのクラスに集約する
func dispose() -> void:
if dat != null:
dat.dispose()
dat = null
案B. 特定名のノードに保持させる
永続的なデータの置き場としてノードを使うのなら、別にAutoLoadでエンジンに作ってもらわなくても、自分で作ればいいんじゃない?って案です。
この時カギになるのが、ノードの名前です。特定の名前(だいたいはクラス名)でツリー直下に置いておけば、存在の確認ができるし、取得できます。
class_name MySingleton
extands Node
var val_a: int = -1
static func _get_singleton() -> MySingleton
# 名前で取得する
var my_singleton: MySingleton = get_node("/root/MySingleton") as MySingleton
if my_singleton == null:
# 無ければ作ってルートに配置する
my_singleton = MySingleton.new()
my_singleton.name = "MySingleton"
var scene_tree: SceneTree = Engine.get_main_loop() as SceneTree
var root: Window = scene_tree.root
root.add_child(my_singleton)
return my_singleton
static func do_something() -> void:
MySingleton._get_singleton().do_something()
static func set_val_a(val: int) -> void:
MySingleton._get_singleton().val_a = val
メリット
static変数の無かった時代の、古いGodotでも使えます。
デメリット
static関数を呼ぶたびに、ツリーからノードを取得するので、動作が重いかもしれません。ノードの操作は重くて推奨されないイメージあります。Unityはそうでしたね。
1フレームに何度も処理をするような場合は、呼び出し側にノードを取らせて、非static関数を呼ばせるようにするといいかもしれません。
# ツリー上のノードの検索がcount回される
for i in range(0, count):
MySingleton.do_something()
# これならノードの検索が1度で済む(instはinstanceの略)
var my_singleton: MySingleton = MySingleton.get_inst()
for i in range(0, count):
my_singleton.do_something()
案C. static変数を使う
Godot 4.1では、static変数が使えるようになりました。今まで無かったのが不思議です。もうこれでいいじゃん。
Godotは長い歴史の中で、class_nameが導入されて、自作クラスにアクセスできるようになったのは最近(Godot 3.1 2019年3月)の話です。
多分ですけど、gdファイルがクラスとしてコンパイルされるようになったのはこのVer.以降で、それまでのGDScriptは名前通り、今よりもっともっと「スクリプト」的だったんじゃないかなと思います。事前にノードにアタッチしてシーン化するか、load()してアタッチして使うのが常で。だから仕組み的にstatic変数なんて作りようがなかったんだと思います。たぶん。
C-1. static変数にデータを置く
欲望に忠実な素直なやり方です。
class_name MySingleton
extands Node
# データを全部static変数にする
static var val_a: int = -1
static var val_b: int = -1
static var val_c: int = -1
static var node: MeshInstance3D = null
# それか別のクラスにまとめてしまう
static var dat: MySingletonData = null
メリット
わかりやすい~~!
デメリット
デバッグ中にインスペクタで内容が見れません。これはつらい。AutoLoadのノードはスタックトレースの変数一覧に表示されるんですけどね。static変数は対応してないようです。
C-2. ノード化してstatic変数で保持する
案Bとほぼ同じアイデアですが、作ったノードはツリーに配置するだけでなく、static変数で保持しておきます。こうすることで、ツリー上から毎回ノードを検索させるコストを解消できます。
# C-2-1
# 自動的にインスタンスが作成される案
# 使う側がインスタンスの存在を意識しなくていい
class_name MySingleton
extands Node
var _inst: MySingleton = null
# データは非static(dynamic)変数にする
var val_a: int = -1
var val_b: int = -1
var val_c: int = -1
var node: MeshInstance3D = null
static func _secure_inst() -> void:
if MySingleton._inst == null:
MySingleton._inst = MySingleton.new()
var scene_tree: SceneTree = Engine.get_main_loop() as SceneTree
var root: Window = scene_tree.root
root.add_child.call_deferred(MySingleton._inst)
# 呼び出される関数の冒頭で必ず_secure_inst()を呼ぶ
static func do_something() -> void:
MySingleton._secure_inst()
MySingleton._inst.exec_something()
# C-2-2
# 使う側が明示的に初期化する案
# データ管理の責任者をはっきりさせる&解放が可能になる
class_name MySingleton
extands Node
var _inst: MySingleton = null
# データは非static(dynamic)変数にする
var val_a: int = -1
var val_b: int = -1
var val_c: int = -1
var node: MeshInstance3D = null
# 使う側に初期化させる
# 引数を指定できるメリットがある
# 親をルート以外に指定できる
static func secure_inst(parent_node: Node, arg: int) -> void:
if MySingleton._inst == null:
MySingleton._inst = MySingleton.new(arg)
parent_node(MySingleton._inst)
# 破棄可能
static func abandon_inst() -> void:
if MySingleton._inst != null:
MySingleton._inst.free()
MySingleton._inst != null
# 初期化せずに呼び出すと_instが無いのでエラーとなる
static func do_something() -> void:
MySingleton._inst.exec_something()
メリット
関数内でメンバ変数/メンバ関数にアクセスする際には、必ず_inst.から始まるので、エディタのサジェスト機能がいい感じに働いてくれます。C#やC++で必ずthis付けるタイプの人に優しい。
デメリット
記述がちょっと面倒です。とはいえ、1つ作ってしまえばあとはコピペと置換でいけます。
以上です~~~!
Godotでゲーム作ろう!
この記事が気に入ったらサポートをしてみませんか?