📝

Godot で Unity の ScriptableObject のようなことをする方法(エディター拡張の実装方法を添えて)

2023/12/22に公開

対象者

  • Unity をある程度触っていて、これから Godot に挑戦しようという人
  • GodotEditor 上でマスターデータを管理したい人

また、今回 Unity からの移行者向けということで基本的には C# を利用して実装をしていきます。

結論

  • Resource を継承したクラスを作ろう
  • GlobalClassTool 属性をクラスにつけよう
  • プロパティには Export 属性をつけよう

環境

  • Godot 4.2 .NET

また、今回の実装物をまとめたリポジトリを以下リンクにて公開してます。

https://github.com/R-Sudo/PracticeCreateResourceAtGodot

前提として、ScriptableObject とは?

https://docs.unity3d.com/ja/2022.3/Manual/class-ScriptableObject.html

ScriptableObject は、クラスのインスタンスとは独立した大量のデータを保存するためのデータコンテナです。

とあるように、データを保存することに特化したコンポーネントです。
マスターデータのローカル管理やエディタ拡張の設定の保存など、様々な用途に利用することができます。

この記事では Godot でこの機能のようなことを実現するための方法を解説します。

リソースとは?

https://docs.godotengine.org/ja/4.x/classes/class_resource.html

Resourceはデータの格納に使います。それ自身はなにもしませんが、代わりにノードが、データの入ったリソースを使用します。

とあるように、データの格納の機能を提供するクラスです。
上記リンク先にも書いてありますが、

  • Texture
  • Mesh
  • Animation

などが該当するようです。
このあたりは Unity の ScriptableObject とは違う点で、各データは Resource を継承して作られています。

そして、Resource クラスを継承して実装することで、独自のリソースを作ることが可能です。

独自リソースを作ってみよう

https://docs.godotengine.org/ja/4.x/tutorials/scripting/resources.html#creating-your-own-resources

こちらを参考に、独自のリソースを作成します。
今回は以下のように実装をしました。

using Godot;

namespace PracticeCreateResourceAtGodot;

[GlobalClass]
[Tool]
public partial class CustomResource : Resource
{
    [Export]
    public int IntProp { get; set; }

    [Export]
    public float FloatProp { get; set; }

    [Export]
    public bool BoolProp { get; set; }

    [Export]
    public string StringProp { get; set; }

    [Export]
    public Vector2 Vector2Prop { get; set; }

    [Export]
    public Color ColorProp { get; set; }

    [Export]
    public Curve CurveProp { get; set; }
}

C# の基本的な型には対応できていて、かつ Godot で定義された Vector2・Color といった型、
そして、Resource を継承したクラス(今回だと Curve) にも対応しています。

実装にあたってのポイントは、

  • Resource クラスを継承する
  • partial クラスにする
  • GlobalClass をクラスにつける
  • Tool をクラスにつける(プラグインの項で解説)
  • Export をプロパティにつける

です。
partial をつけるのは、GodotObject から派生するクラスを作る際には必須 となります。
つけない場合以下のようなエラーが表示されます。
https://docs.godotengine.org/ja/4.x/tutorials/scripting/c_sharp/diagnostics/GD0001.html

早速このクラスを使ってリソースの作成をしていきます。

リソースを作る@GlobalClass 属性の効果

リソースの作り方は、
右クリックメニュー > 新規作成 > リソース
です。

選択するとリソースの一覧が表示されます。

今回は先ほど定義した CustomResource クラスのリソースを作りたいので、
Custom で検索をかけます。

一覧に表示されているのが確認できるので、選択します。
この一覧に表示するためには、GlobalClass をつけておく必要があります。

選択後、任意の名前で保存します。
拡張子に tresres の2種類ありますが、今回は tres にします。
t は text の t で、res にするとバイナリでの保存となり、読み込み速度向上やファイルサイズの削減になるようです。

インスペクターの表示を確認@Export 属性の効果

先ほど作成した tres のファイルを選択します。
すると、以下のような表示になります。

このように、各プロパティの型に応じた編集用の UI が表示されます。

配列の対応

独自リソースは配列も対応しています。
以下のように定義しました。

using Godot;

namespace PracticeCreateResourceAtGodot;

[GlobalClass]
[Tool]
public partial class CustomResourceArray : Resource
{
    [Export]
    public CustomResource[] Data { get; set; }
}

先ほどの CustomResource 型と同様に Export 属性をつければ完了です。
こちらは以下のような表示になります。

要素の追加や削減が可能になっていて、かつそれぞれの要素で CustomResource 型のプロパティの編集が可能になっています。

読み込みの動作確認

配列の定義が可能なことを確認したので、読み込みの挙動を確認していきます。
確認ように以下のような実装を追加しました。

using System.Linq;
using Godot;
using PracticeCreateResourceAtGodot;

public partial class Test : Node
{
    [Export] private Button loadButton;
    [Export] private Sprite2D target;

    public override void _Ready()
    {
        loadButton.Pressed += OnPressed;
    }

    private void OnPressed()
    {
        var res = GD.Load<CustomResourceArray>("res://addons/my_custom_resource/res/array.tres");
        var first = res?.Data.FirstOrDefault();

        if (first is null)
        {
            return;
        }

        target.Position = first.Vector2Prop;
        target.Modulate = first.ColorProp;
    }
}

ボタンが押された時にリソースを読み込む実装を追加しました。
GD.Load メソッドで読み込みすることができます。
読み込んだリソースのプロパティを使って座標と色を変更するようにしてみました。

アプリケーション動作中に編集したリソースを読み込んで、位置や色が変わることが確認できました。
実行中にデータを編集して、再読み込みしてその結果を確認できることはゲーム制作の効率化につながりそうです。
(いわゆるホットリロード)

独自リソースに対するエディター拡張をしてみよう

ここまででマスターデータのホットリロードに活用できそうと分かりました。
ここは更に一歩踏み込んで、独自リソースに対するエディター拡張の実装を試みます。

Godot における『エディター拡張』とは?

Unity ではエディター拡張という名称ですが、Godot では エディタプラグイン という名称がつけられています。
コミュニティの方で数多くのプラグインが実装されており、ユーザーはそれをインストールすることが可能です。

https://docs.godotengine.org/ja/4.x/tutorials/plugins/editor/installing_plugins.html#finding-plugins

インストールする方法は上記の公式ドキュメントを参考にしてみてください。
今回はそのエディタプラグインを実装していきます。

プラグインを作成する

https://docs.godotengine.org/ja/4.x/tutorials/plugins/editor/making_plugins.html

こちらのリンクを参考にしつつプラグインを作成します。
必要なものは、

  • plugin.cfg
    • Unity の package.json に似たもの。プラグインの定義ファイル。
  • plugin.gd
    • プラグインアクティブ時に最初に読み込まれるファイル。プラグインの各要素の生成・削除などをする。

の2つです。

メニューバーのプロジェクトを押し、
プラグイン > 新しいプラグインを作成
を選択してください。

するとプラグイン作成のダイアログが表示されるので、必要な情報を入力してプラグインを作成してください。

独自リソースに対するインスペクタプラグインを作成する

次に、インスペクタプラグインを作成します。

https://docs.godotengine.org/ja/4.x/tutorials/plugins/editor/inspector_plugins.html

こちらを参考に作成していきます。
今回は C# で以下のように作成しました。

#if TOOLS
using System;
using System.Collections.Generic;
using Godot;

namespace PracticeCreateResourceAtGodot;

public partial class CustomResourceInspectorPlugin : EditorInspectorPlugin
{
    private CustomResourceArray target;

    public override bool _CanHandle(GodotObject @object)
    {
        // NOTE: BadCustomResource クラスの場合、Tool 属性がついてないので GetType した時に Resource 型になってる
        var type = @object.GetType();
        GD.Print($"type is: {type}");

        return @object is CustomResourceArray;
    }

    public override void _ParseBegin(GodotObject @object)
    {
        // NOTE: _CanHandle で CustomResourceArray 型のみ有効にしてるので直接キャスト
        target = (CustomResourceArray)@object;

        CreateCustomInspector();
    }

    private void CreateCustomInspector()
    {
        var randomGenerateButton = new Button();
        randomGenerateButton.Text = "Random Generate";
        randomGenerateButton.Pressed += RandomGenerate;

        AddCustomControl(randomGenerateButton);
    }

    private void RandomGenerate()
    {
        var list = new List<CustomResource>();

        for (var i = 0; i < 5; i++)
        {
            var resource = new CustomResource
            {
                IntProp = (int)GD.Randi(),
                FloatProp = GD.Randf(),
                BoolProp = GD.Randi() % 2 == 0,
                StringProp = Guid.NewGuid().ToString(),
                Vector2Prop = new Vector2((float)GD.RandRange(500.0f, 1000.0f), (float)GD.RandRange(250.0f, 500.0f)),
                ColorProp = new Color(GD.Randf(), GD.Randf(), GD.Randf()),
                CurveProp = new Curve()
            };
            
            list.Add(resource);
        }

        target.Data = list.ToArray();

        // NOTE: リソースの値が変わった変更通知を投げる必要がある
        target.EmitChanged();
    }
}
#endif
  • エディタのみ有効にしたいので #if TOOLS で囲う
  • EditorInspectorPlugin を継承して作成
  • _CanHandle でこのプラグインの有効判定を実装
  • _ParseBegin で初期化処理実装
    • 対象のオブジェクト取得
    • 初期化処理内でボタンを作成し、AddCustomControl メソッドで追加
    • ボタンには押した時に実行したい処理を登録済み

などをしています。
今回は配列の独自リソースに対してランダムに値を生成するエディタ拡張を実装してみました。

値を変更した際は、EmitChanged を呼び出して、値が変更されたことをエディタに知らせる必要があります。

作成したインスペクタプラグインを有効にする

これでインスペクタプラグインの実装ができたので、有効にしていきます。
先ほど作成した plugin.gd を書き換えます。

@tool
extends EditorPlugin

var inspector_plugin

func _enter_tree():
	inspector_plugin = preload("res://addons/my_custom_resource/src/CustomResourceInspectorPlugin.cs").new()
	add_inspector_plugin(inspector_plugin)

func _exit_tree():
	remove_inspector_plugin(inspector_plugin)
  • _enter_tree メソッドでプラグインのクラス読み込みとインスタンス生成実行
    • add_inspector_plugin メソッドで生成したプラグインを追加。これで有効に
  • _exit_tree メソッドで無効化時の実装をする
    • remove_inspector_plugin で後処理

これで実装完了です。
プロジェクト設定 > プラグイン から作成したプラグインを有効にします。

ボタンが追加されており、CustomResourceInspectorPlugin.RandomGenerate メソッドが実行されるのが確認できました。

さいごに

これで独自のリソースとそれを活用するためのプラグインの実装ができるようになりました。
この機能を活用することでゲーム制作の効率がぐっと上がるはずです。

現在これらの機能を利用して、スプレッドシートからダウンロードした値をマスターデータに変換する機能を模索中です。
実装して得られた知見は別記事にて公開を予定しています。

Discussion