概要
動作確認環境: Godot 4.3+
Godotでプロジェクトが大きくなると、「この組み合わせのノードを何度も作っている」「インスペクタでの設定項目を整理したい」といった課題が出てきます。class_nameによるカスタムノード型の定義と@exportによるプロパティ公開を組み合わせると、エディタ上で再利用しやすい部品を作れます。
この記事では、カスタムノードの基本から、@exportの整理テクニック、カスタムResourceの作成、インスペクタの拡張まで解説します。
class_nameでカスタムノード型を定義
まずはカスタムノードの出発点、class_nameの宣言から始めましょう。class_nameをスクリプトに宣言すると、そのクラスがGodotエディタのグローバル型として登録されます。これにより、ノード追加ダイ アログで検索できたり、型アノテーションで参照できたりと、コードの再利用性と型安全性が向上します。
たとえばアクションゲームなら、敵・プレイヤー・破壊可能オブジェクトに共通する「HP管理」をHealthComponentとして切り出せます。以下のコードでその基本形を実装してみましょう。
# health_component.gd
class_name HealthComponent
extends Node
signal died
signal health_changed(new_health: int)
@export var max_health: int = 100
var current_health: int
func _ready():
current_health = max_health
func take_damage(amount: int):
current_health = max(current_health - amount, 0)
health_changed.emit(current_health)
if current_health <= 0:
died.emit()
func heal(amount: int):
current_health = min(current_health + amount, max_health)
health_changed.emit(current_health)
登録後にできること:
- 「ノードを追加」ダイアログで
HealthComponentを検索・追加できる - 型アノテーションで
var health: HealthComponentと書ける is演算子でif node is HealthComponent:と判定できる
tips:
class_nameはグローバル名前空間に登録されるため、プロジェクト内で同じ名前を使うとエラーになります。大規模プロジェクトやアドオン開発ではプレフィックスを付けるなどの対策を検討してください。
@iconでカスタムアイコンを設定
class_nameでノード型を定義したら、次はアイコンで見た目を整えましょう。@iconでエディタ上のアイコンをカスタマイズできます。シーンツリーにカスタムノードが増えてくると、デフォルトアイコンのままでは区 別がつきにくくなります。専用アイコンを設定すると、ノードの役割を一目で把握しやすくなります。
@icon("res://icons/health_heart.svg")
class_name HealthComponent
extends Node
- シーンツリーとノード追加ダイアログの両方に反映される
- SVG形式を推奨(スケーリングで劣化しない)
- アイコンサイズは16x16pxで表示される
@icon("res://icons/hitbox.svg")
class_name HitboxComponent
extends Area2D
@export var damage: int = 10
@exportでインスペクタにプロパティを公開
カスタムノードの見た目が整ったところで、次はインスペクタから設定できるプロパティを用意してみましょう。@exportを付けた変数はインスペクタに表示され、エディタ上で編集可能になります。敵の移動速度やHPの最大値など、ゲームバランスに関わるパラメータをスクリプトを開かずに調整できるので、デザイナーとの協業もスムーズになります。
基本的な@exportアノテーション
class_name EnemyConfig
extends CharacterBody2D
# 基本型
@export var speed: float = 100.0
@export var enemy_name: String = "Goblin"
@export var is_boss: bool = false
# 範囲付き
@export_range(0, 100, 1) var health: int = 50
@export_range(0.0, 10.0, 0.1) var attack_interval: float = 2.0
# リソース参照
@export var sprite_texture: Texture2D
@export var death_effect: PackedScene
# 列挙型
@export_enum("Patrol", "Chase", "Guard") var ai_type: String = "Patrol"
# 色
@export var tint_color: Color = Color.WHITE
# ファイルパス
@export_file("*.tscn") var next_scene: String
配列とDictionary
巡回ルートのポイントリストやドロップアイテムのリストなど、配列型のプロパティもエクスポートできます。
# 型付き配列
@export var patrol_points: Array[Vector2] = []
@export var drop_items: Array[PackedScene] = []
# フラグ列挙
@export_flags("Fire", "Water", "Earth", "Wind") var elements: int = 0
@export_flagsの結果はビットフラグのint値です。フラグのチェックにはビット演算を使います。
# フラグのチェック方法
const FIRE = 1
const WATER = 2
const EARTH = 4
const WIND = 8
func has_element(flag: int) -> bool:
return elements & flag != 0
# 使用例
if has_element(FIRE):
print("炎属性あり")
@export_group / @export_subgroupで整理
@exportの種類がわかったところで、次は整理術です。プロパティが10個、20個と増えてくると、インスペクタが長大なリストになって目的の項目を探すのが大変になります。グループで分類して見やすくしましょう。
以下のように、プレイヤーキャラクターの移動・戦闘・ビジュアル関連のプロパティをグループ化します。
class_name PlayerCharacter
extends CharacterBody2D
@export_group("Movement")
@export var move_speed: float = 200.0
@export var jump_force: float = 400.0
@export var gravity_scale: float = 1.0
@export_group("Combat")
@export var attack_power: int = 10
@export var defense: int = 5
@export_subgroup("Weapon")
@export var weapon_range: float = 50.0
@export var weapon_cooldown: float = 0.5
@export_subgroup("Special")
@export var special_attack_cost: int = 20
@export var special_damage_multiplier: float = 2.5
@export_group("Visual")
@export var trail_color: Color = Color.CYAN
@export var particle_effect: PackedScene
インスペクタでの表示:
@export_groupは折りたたみ可能なセクションヘッダになる@export_subgroupはグループ内のサブセクションとして表示される- 見た目が整理され、設定ミスを防げる
カスタムResourceの作成
ここまではノードのプロパティを公開する方法を見てきましたが、データそのものをスクリプトから分離したい場面もあります。たとえばRPGの武器データは、名前・攻撃力・射程・アイコンなどの情報をまとめて持ちます。これをResourceとして定義すると、.tresファイルとして保存でき、複数のノードで共有したり、エディタからドラッグ&ドロップで割り当てたりできます。
# weapon_data.gd
class_name WeaponData
extends Resource
@export var weapon_name: String = ""
@export var damage: int = 10
@export var attack_speed: float = 1.0
@export var range: float = 50.0
@export var icon: Texture2D
@export_multiline var description: String = ""
リソースファイルの作成手順:
- FileSystemパネルで右クリック → 「新規リソース」
WeaponDataを検索して選 択- インスペクタで値を設定して
.tresファイルとして保存
# 使用例: weapon_holder.gd
class_name WeaponHolder
extends Node
@export var equipped_weapon: WeaponData
func get_damage() -> int:
if equipped_weapon:
return equipped_weapon.damage
return 0
メリット:
- データとロジックの分離
- エディタ上でドラッグ&ドロップで割り当て可能
- 複数のノードで同じリソースを共有できる
共有リソースの注意点: 複数のノードで同じ.tresファイルを参照している場合、一方でプロパティを変更すると全参照先に影響します。ノードごとに個別の値を持たせたい場合はduplicate()で複製するか、インスペクタの「Resource」→「Local to Scene」を有効にしてください。
func _ready():
# 共有リソースを個別に変更したい場合は複製する
equipped_weapon = equipped_weapon.duplicate()
equipped_weapon.damage = 20 # このノードだけ変更される
EditorInspectorPluginでプロパティエディタを拡張
ここからは少し上級編です。特定のカスタムノードが選択されたとき、インスペクタにカスタムUIを追加できます。「このWeaponHolderのダメージ計算結果をプレビューしたい」といった要望に応えられる、エディタの使い勝手を大きく向上させる機能です。
以下のコードでは、WeaponHolderを選択するとインスペクタ上部に装備情報とダメージ計算ボタンが表示されるようにします。
# addons/my_tools/custom_inspector.gd
@tool
class_name WeaponDataInspector
extends EditorInspectorPlugin
func _can_handle(object: Object) -> bool:
return object is WeaponHolder
func _parse_begin(object: Object):
var holder = object as WeaponHolder
if holder.equipped_weapon:
var info = Label.new()
info.text = "装備中: %s (ATK: %d)" % [
holder.equipped_weapon.weapon_name,
holder.equipped_weapon.damage
]
add_custom_control(info)
var preview_button = Button.new()
preview_button.text = "ダメージ計算プレビュー"
preview_button.pressed.connect(func():
print("予想ダメージ: %d" % holder.get_damage())
)
add_custom_control(preview_button)
# addons/my_tools/plugin.gd
@tool
extends EditorPlugin
var inspector_plugin: WeaponDataInspector
func _enter_tree():
inspector_plugin = WeaponDataInspector.new()
add_inspector_plugin(inspector_plugin)
func _exit_tree():
if inspector_plugin:
remove_inspector_plugin(inspector_plugin)
プラグインの有効化手順: EditorInspectorPluginを動作させるには、plugin.cfgファイルの作成とプラグインの有効化が必要です。
; addons/my_tools/plugin.cfg
[plugin]
name="My Tools"
description="Custom inspector for WeaponHolder"
author="Your Name"
version="1.0"
script="plugin.gd"
addons/my_tools/フォルダに上記のplugin.cfgとplugin.gd、custom_inspector.gdを配置- 「プロジェクト」→「プロジェクト設定」→「プラグイン」タブで有効化
カスタムノード vs コンポジション
「カスタムノードを作るべきか、それとも既存ノードの組み合わせ(コンポジション)で済ませるべきか?」と迷ったことはありませんか。両者にはそれぞれ適した用途があり、プロジェクトの規模や再利用性の必要度によって使い分けることが重要です。
| 観点 | カスタムノード (class_name) | コンポジション (子ノード構成) |
|---|---|---|
| 再利用性 | プロジェクト全体で使い回しやすい | 特定シーンに限定されがち |
| 発見性 | ノード追加ダイアログに表示される | シーンファイルを探す必要がある |
| 型安全 | is演算子や型アノテーションで判定可能 | スクリプトの存在確認が必要 |
| 複雑さ | 単一機能に向いている | 複数ノードの組み合わせに向いている |
| 設定 | @exportでインスペクタから直接設定 | 子ノードごとに個別設定 |
使い分けの指針:
# カスタムノード向き: 単一機能を再利用
@icon("res://icons/health.svg")
class_name HealthComponent
extends Node
# → 敵、プレイヤー、破壊可能オブジェクトなどで共通利用
# コンポジション向き: 複数ノードの構成
# player.tscn
# ├── CharacterBody2D
# │ ├── HealthComponent
# │ ├── MovementComponent
# │ ├── Sprite2D
# │ └── CollisionShape2D
# → シーンとして保存し、必要に応じてインスタンス化
まとめ
- class_nameでカスタムノード型を定義すると、ノード追加ダイアログや型アノテーションで利用可能
- @iconでエディタ上のアイコンをカスタマイズし、視認性を向上
- @exportでインスペクタにプロパティを公開し、コード変更なしで調整可能に
- @export_group / @export_subgroupで多数のプロパティを整理
- カスタムResourceでデータとロジックを分離し、再利用性を高める
- EditorInspectorPluginで特定ノード選択時にカスタムUIを表示
- 単一機能の再利用にはカスタムノード、複合構成にはコンポジションを選択