【Godot】GDScriptで使うOOPデザインパターン(コンポジション・デコレーター・ファクトリー)

作成: 2026-02-08

Godotでよく使われる3つのデザインパターン(コンポジション、デコレーター、ファクトリー)の実装方法。継承に頼らない柔軟な設計で、再利用性と拡張性を高める実践的なGDScriptコード例を紹介します。

概要

動作確認環境: Godot 4.3+

Godotでゲームを作る際、「すべての機能を継承で実装する」と階層が深くなり、コードの再利用や拡張が難しくなります。デザインパターンを活用することで、柔軟で保守しやすい設計が実現できます。

この記事では、Godotで特に有用な3つのデザインパターンを紹介します。

3つのパターンの概要

継承ベースの設計では、機能を追加するたびにクラス階層が深くなり、「移動する敵」「飛ぶ敵」「移動して飛ぶ敵」のように組み合わせが爆発します。本記事で紹介する3つのパターンは、それぞれ異なる角度からこの問題を解決します。

パターン一言で言うとこんなときに使う
コンポジション機能を部品として組み合わせる移動・攻撃・体力などの機能を複数キャラで使い回したい
デコレーター既存オブジェクトに機能を重ねる装備・バフで能力値を動的に変えたい
ファクトリー生成処理を一元管理する敵・アイテムの種類ごとに異なる設定で生成したい

コンポジション — 「has-a」の設計

「キャラクターは移動機能を持つ」という関係で設計します。移動・攻撃・体力などの機能をそれぞれ独立したNodeやResourceとして作り、必要なキャラクターに組み合わせて使います。Godotのシーンツリーと相性が良いパターンです。

デコレーター — 「ラップして拡張」

元のオブジェクトを包み込んで機能を上乗せします。剣で攻撃力+5、さらにバフで+3というように、装飾を何層でも重ねられます。装備解除時はラップを外すだけで元に戻せるのが特徴です。

ファクトリー — 「生成の一元化」

「ゴブリンならHP50・速度100、オークならHP100・速度80」といった生成ルールを1つのクラスにまとめます。生成処理が散らばらず、新しい敵タイプの追加も設定を1箇所に書くだけで済みます。

コンポジションパターン

それでは、各パターンの詳細を見ていきましょう。最初はGodotとの相性が抜群のコンポジションパターンです。

Godotのシーンツリーは、まさに「小さな部品を組み合わせて大きなものを作る」という設計思想でできています。コンポジションパターンはこの思想にそのまま乗るため、最も自然に導入できるパターンと言えます。

問題

プレイヤーと敵に移動機能を実装する場合、継承だと以下のような階層になります。

Character (基底クラス)
├─ Player
└─ Enemy

しかし「移動速度が異なる」「入力方法が違う」といった違いを継承で表現すると、クラスが増えすぎます。

解決: コンポーネント化

移動ロジックを独立したコンポーネントとして切り出すことで、プレイヤーにも敵にも使い回せる設計になります。

ポイントは「データ(どれくらい速いか)」と「処理(どう動くか)」を分離することです。以下のコードでは、MovementStatsリソースにデータを、MovementComponentノードに処理をそれぞれ分けています。

# movement_stats.gd (Resource)
class_name MovementStats
extends Resource

@export var max_speed: float = 200.0
@export var acceleration: float = 800.0
@export var friction: float = 600.0
# movement_input.gd (Node)
class_name MovementInput
extends Node

func get_input_direction() -> Vector2:
    # 子クラスでオーバーライド
    return Vector2.ZERO
# player_input.gd
class_name PlayerInput
extends MovementInput

func get_input_direction() -> Vector2:
    return Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
# movement_component.gd
class_name MovementComponent
extends Node

@export var stats: MovementStats
@onready var input: MovementInput = $"../PlayerInput"  # シーンツリーから取得

func update_movement(actor: CharacterBody2D, delta: float):
    var direction = input.get_input_direction()

    if direction != Vector2.ZERO:
        actor.velocity = actor.velocity.move_toward(
            direction * stats.max_speed,
            stats.acceleration * delta
        )
    else:
        actor.velocity = actor.velocity.move_toward(
            Vector2.ZERO,
            stats.friction * delta
        )

    actor.move_and_slide()
# player.gd
extends CharacterBody2D

@onready var movement = $MovementComponent

func _physics_process(delta):
    movement.update_movement(self, delta)

tips: Nodeの参照を @export で設定することも可能ですが、@onready でシーンツリーから取得するほうがGodotの慣例に沿っています。Resourceの参照(MovementStats など)は @export が適切です。

メリット:

  • 移動ロジックの再利用が容易
  • MovementStats リソースで調整可能
  • input を差し替えればAI移動も実装可能

デコレーターパターン

コンポジションが「部品を組み合わせる」パターンだったのに対し、デコレーターは「既存のものに包み紙を重ねていく」イメージです。RPGで剣を装備したら攻撃力+5、さらにバフ魔法で+3――こういった処理を、クラスを増やさずに柔軟に実装できます。

問題

プレイヤーのステータスに、装備品やバフによる補正を適用したい。継承で実装すると「素のプレイヤー」「剣装備プレイヤー」「剣+盾装備プレイヤー」とクラスが爆発します。

解決: デコレーター

デコレーターパターンでは、元のオブジェクトをラップして機能を追加します。まずインターフェースとなる基底クラスを作り、その上にデコレーター層を重ねていく構成です。

# player_stats.gd (インターフェース)
class_name PlayerStats
extends RefCounted

func get_attack() -> int:
    return 0

func get_defense() -> int:
    return 0
# base_player_stats.gd
class_name BasePlayerStats
extends PlayerStats

var base_attack: int = 10
var base_defense: int = 5

func get_attack() -> int:
    return base_attack

func get_defense() -> int:
    return base_defense
# stats_decorator.gd (デコレーター基底クラス)
class_name StatsDecorator
extends PlayerStats

var wrapped_stats: PlayerStats

func _init(stats: PlayerStats):
    wrapped_stats = stats

func get_attack() -> int:
    return wrapped_stats.get_attack()

func get_defense() -> int:
    return wrapped_stats.get_defense()
# attack_boost_decorator.gd
class_name AttackBoostDecorator
extends StatsDecorator

var bonus_attack: int

func _init(stats: PlayerStats, bonus: int):
    super(stats)
    bonus_attack = bonus

func get_attack() -> int:
    return wrapped_stats.get_attack() + bonus_attack
# 使用例
var stats = BasePlayerStats.new()
print(stats.get_attack())  # 10

# 剣を装備(攻撃力+5)
stats = AttackBoostDecorator.new(stats, 5)
print(stats.get_attack())  # 15

# さらにバフ(攻撃力+3)
stats = AttackBoostDecorator.new(stats, 3)
print(stats.get_attack())  # 18

デコレーターの除去(装備解除)

追加するだけでなく、外す処理も重要です。プレイヤーが装備を外したりバフの効果時間が切れたりしたとき、対応するデコレーターを取り除く必要があります。wrapped_statsを使って1段階戻すことで実現できます。

# 装備解除: 最後に適用したデコレーターを除去
func unequip_last(current_stats: PlayerStats) -> PlayerStats:
    if current_stats is StatsDecorator:
        return current_stats.wrapped_stats
    return current_stats  # デコレーターでなければそのまま

# 使用例
stats = unequip_last(stats)
print(stats.get_attack())  # 15(バフが除去され、剣のみ)

メリット:

  • 実行時に機能を追加・削除可能
  • 装備やバフの組み合わせが柔軟
  • 基底クラスを変更せずに拡張

ファクトリーパターン

最後はファクトリーパターンです。「ゲームのあちこちで敵を生成する処理が書かれていて、新しい敵タイプを追加するたびに複数ファイルを修正しなければならない」――そんな経験はありませんか? ファクトリーパターンは生成ロジックを一箇所にまとめることで、この問題を解決します。

問題

敵の種類ごとに生成処理が異なる場合、生成コードがあちこちに散らばります。

# アンチパターン
if enemy_type == "goblin":
    var enemy = load("res://enemies/goblin.tscn").instantiate()
    enemy.health = 50
    enemy.speed = 100
elif enemy_type == "orc":
    var enemy = load("res://enemies/orc.tscn").instantiate()
    enemy.health = 100
    enemy.speed = 80

解決: ファクトリークラス

生成処理を専用のクラスにまとめましょう。シーンのパスと初期設定値を辞書で一元管理するので、新しい敵タイプの追加は辞書にエントリを足すだけで済みます。

# enemy_factory.gd
class_name EnemyFactory
extends Node

enum EnemyType { GOBLIN, ORC, DRAGON }

const ENEMY_SCENES = {
    EnemyType.GOBLIN: preload("res://enemies/goblin.tscn"),
    EnemyType.ORC: preload("res://enemies/orc.tscn"),
    EnemyType.DRAGON: preload("res://enemies/dragon.tscn"),
}

const ENEMY_CONFIGS = {
    EnemyType.GOBLIN: { "health": 50, "speed": 100 },
    EnemyType.ORC: { "health": 100, "speed": 80 },
    EnemyType.DRAGON: { "health": 300, "speed": 50 },
}

func create_enemy(type: EnemyType, position: Vector2) -> Node2D:
    var scene = ENEMY_SCENES.get(type)
    if not scene:
        push_error("Unknown enemy type: %s" % type)
        return null

    var enemy = scene.instantiate()
    var config = ENEMY_CONFIGS[type]

    enemy.global_position = position
    enemy.health = config["health"]
    enemy.speed = config["speed"]

    return enemy
# 使用例
@onready var factory = $EnemyFactory

func spawn_enemies():
    var goblin = factory.create_enemy(EnemyFactory.EnemyType.GOBLIN, Vector2(100, 100))
    add_child(goblin)

    var orc = factory.create_enemy(EnemyFactory.EnemyType.ORC, Vector2(200, 100))
    add_child(orc)

メリット:

  • 生成ロジックの一元管理
  • 新しい敵タイプの追加が容易
  • テストが書きやすい

tips: const+preload() は起動時に全シーンをメモリに読み込みます。大量のシーンを登録する場合は load() で遅延読み込みするか、ResourceLoader.load_threaded_request() で非同期読み込みを検討してください。

パターン選択ガイド

3つのパターンを見てきましたが、実際の開発ではどれを使えばいいか迷うこともあるでしょう。以下の表を判断基準にしてみてください。

パターン使用場面メリット
コンポジション機能を組み合わせたい柔軟な機能の組み合わせ、再利用性
デコレーター実行時に機能を追加したい動的な機能追加、多段階の装飾
ファクトリーオブジェクト生成が複雑生成ロジックの一元管理、拡張性

組み合わせ例:

  • ファクトリーで敵を生成 → コンポジションで行動を組み立て → デコレーターでバフ適用

Godotと親和性の高い他のパターン

Godotの設計は、標準機能としていくつかのデザインパターンを組み込んでいます。

パターンGodotでの実装典型的な用途
オブザーバーsignalが標準で提供イベント通知、UI更新
ステートmatch文 + Enum / State Machineキャラクターの状態管理
シングルトンAutoloadゲーム全体のデータ管理

Godotのシグナルシステムはオブザーバーパターンそのもので、signal の宣言と connect() で疎結合なイベント通知を実現します。ステートパターンの実装についてはステートマシンの記事も参照してください。

まとめ

  • コンポジションは継承の代わりに部品を組み合わせて機能を構築
  • デコレーターは既存オブジェクトに動的に機能を追加
  • ファクトリーはオブジェクト生成ロジックを一元管理
  • 各パターンは単独でも、組み合わせても有効
  • Godotではノードとリソースの組み合わせで柔軟に実装可能
  • 過度な設計は避け、必要になったときにリファクタリングする姿勢が重要

さらに学ぶために