敵を増やすたびに FlyingEnemy、FastEnemy、ArmoredEnemy、FlyingFastEnemy のようなクラスが増えていく。装備やバフを足すたびに攻撃力の計算場所が分からなくなる。敵を生成する処理がステージ、ウェーブ、イベント会話の中に散らばってしまう。
こうした問題は、GDScriptの書き方だけではなく 設計の分け方 の問題です。デザインパターン は難しい名前がついていますが、ゲーム制作では「増えたときに壊れにくくする整理術」と考えると使いやすくなります。

この記事でわかること
- 継承だけで設計すると、どこがつらくなるのか
- Godotで自然に使いやすい コンポジション の考え方
- 装備やバフを重ねる デコレーター の考え方
- 敵やアイテム生成を集約する ファクトリー の作り方
- 3つのパターンを、実際のゲーム機能へどうつなげるか
デザインパターンは「増えたとき」のための道具
最初に大事なのは、デザインパターンを「かっこいい設計名」として覚えないことです。ゲームを作っていると、最初は動いていたコードが次のように苦しくなります。
| 起きる問題 | 具体例 | 困ること |
|---|---|---|
| クラスが増えすぎる | 飛ぶ敵、速い敵、硬い敵、その組み合わせ | 似たコードを何度も書く |
| 値の計算場所が散る | 装備、バフ、スキル、難易度補正 | 最終的な攻撃力の理由が追えない |
| 生成処理が散る | ステージ開始、イベント、ウェーブ管理で敵を作る | 新しい敵を追加すると修正箇所が多い |
この記事で扱う3つのパターンは、それぞれこの痛みに対応します。
| パターン | 一言で言うと | ゲームでの使いどころ |
|---|---|---|
| コンポジション | 機能を部品として組み合わせる | プレイヤーと敵で移動・体力・攻撃処理を使い回す |
| デコレーター | 元の値や処理に効果を重ねる | 装備、バフ、デバフでステータスを変える |
| ファクトリー | 生成方法を専用クラスに集める | 敵、弾、アイテム、NPCを決まったルールで作る |
小さいプロトタイプなら、これらを無理に入れる必要はありません。1ファイルで済む処理にパターンを入れると、逆に読むファイルが増えて分かりにくくなります。目安は「同じ種類の処理を3回書き始めた」「変更のたびに複数ファイルを直している」と感じたタイミングです。
まず継承とコンポジションを分けて考える
オブジェクト指向では 継承 がよく出てきます。Godotでも、スクリプトの先頭に extends CharacterBody2D や extends Node と書きます。これは「このスクリプトはCharacterBody2Dとして振る舞う」という土台を決めるものです。
# player.gd
extends CharacterBody2D
このような継承は自然です。プレイヤーは2D物理キャラクターとして移動したいので、CharacterBody2D を継承します。
問題は、ゲームごとの細かい違いまで継承で表そうとする場合です。
Enemy
├─ FlyingEnemy
├─ FastEnemy
├─ ArmoredEnemy
├─ FlyingFastEnemy
├─ FlyingArmoredEnemy
└─ FastArmoredEnemy
敵の特徴が「飛ぶ」「速い」「硬い」だけならまだ読めます。しかしここに「毒攻撃」「遠距離攻撃」「夜だけ強い」「分裂する」が増えると、組み合わせ用のクラスが一気に増えます。
そこで使うのが コンポジション です。「敵は移動機能を持つ」「敵は攻撃機能を持つ」「敵は体力機能を持つ」のように、変わりやすい機能を部品として持たせます。
| 考え方 | 使う場面 | 例 |
|---|---|---|
| 継承 | 土台の種類を決める | Player extends CharacterBody2D |
| コンポジション | 機能を組み合わせる | Player が MovementComponent を持つ |
判断のコツは「これは種類か、部品か」です。Player は CharacterBody2D の一種です。一方、移動処理や攻撃処理はプレイヤーの一種ではなく、プレイヤーが持っている機能です。
コンポジション: 機能を部品として持たせる
Godotのシーンツリーは、もともとコンポジションと相性が良い作りです。Player シーンの下に Sprite2D、CollisionShape2D、AnimationPlayer を置くのも、複数の部品で1つのキャラクターを作っているからです。
スクリプトでも同じように考えます。ここではトップダウンアクションを例に、移動処理を部品化します。

例: プレイヤーと敵で移動処理を使い回す
プレイヤーと敵はどちらも動きますが、入力の取り方が違います。
- プレイヤー: キーボードやゲームパッドから方向を取る
- 敵: AIやターゲット位置から方向を決める
このとき、速度計算や move_and_slide() の呼び出しは共通化できます。違うのは「どちらへ動きたいか」を決める部分です。
まず、移動パラメータを Resource にします。Resource にしておくと、エディタ上で「速い敵用」「重い敵用」のデータを作りやすくなります。
# movement_stats.gd
class_name MovementStats
extends Resource
@export var max_speed: float = 200.0
@export var acceleration: float = 800.0
@export var friction: float = 600.0
次に、入力方向を返す基底クラスを作ります。プレイヤー入力でも敵AIでも、最終的に Vector2 の方向を返せば、移動コンポーネント側は同じように扱えます。
# movement_input.gd
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")
移動コンポーネントは、入力方向と移動データを受け取って、実際に CharacterBody2D を動かします。
# movement_component.gd
class_name MovementComponent
extends Node
@export var stats: MovementStats
@export var input_path: NodePath
@onready var input: MovementInput = get_node(input_path) as MovementInput
func update_movement(actor: CharacterBody2D, delta: float) -> void:
if stats == null or input == null:
return
# 入力の種類を知らずに、方向だけを受け取る
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
)
# 実際に動かす責務はMovementComponentへ集約する
actor.move_and_slide()
最後に、プレイヤー本体から移動コンポーネントを呼びます。
# player.gd
extends CharacterBody2D
@onready var movement: MovementComponent = $MovementComponent
func _physics_process(delta: float) -> void:
# プレイヤー本体は「移動部品を使う」だけにする
movement.update_movement(self, delta)
シーン構成は次のようになります。
Player (CharacterBody2D)
├─ CollisionShape2D
├─ Sprite2D
├─ PlayerInput
└─ MovementComponent
MovementComponent の input_path に ../PlayerInput を設定しておけば、移動コンポーネントはプレイヤー入力を使います。敵用に EnemyInput を作って差し替えれば、同じ MovementComponent を敵にも使えます。
実装レシピ: アクションゲームのプレイヤーと敵
アクションゲームで「プレイヤーも敵も同じ加速感で動かしたい」ときは、次の順で作ると整理しやすいです。
MovementStatsで速度、加速度、摩擦をデータ化するMovementInputを基底にして、PlayerInputとEnemyChaseInputを分けるMovementComponentは入力方向を受け取り、速度計算だけを担当する- プレイヤーと敵のシーンに同じ
MovementComponentを置く - キャラクターごとの違いは
MovementStatsとMovementInputの差し替えで表現する
この形にすると、「敵だけ滑りやすくしたい」「プレイヤーだけ最高速度を上げたい」といった調整が、移動処理の書き換えではなくデータ差し替えで済みます。バグが出たときも、速度計算の問題なのか、入力方向の問題なのかを切り分けやすくなります。
デコレーター: 装備やバフを重ねて計算する
次は デコレーター です。デコレーターは、元の値や処理を包んで、後から効果を足す考え方です。
RPGで考えると分かりやすいです。
- 素の攻撃力: 10
- 剣を装備: +5
- 攻撃力アップのバフ: +3
- 最終攻撃力: 18
このとき、「剣装備プレイヤー」「剣装備かつバフ中プレイヤー」のようなクラスを作るとすぐ破綻します。装備、バフ、デバフ、料理効果、難易度補正が増えるたびに組み合わせが増えるからです。

例: ステータス計算を重ねる
まず、ステータスを取得する共通の型を用意します。ここではシーン上に置くNodeではなく、軽い計算用オブジェクトとして RefCounted を使います。
# 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
次に、別の PlayerStats を包む基底デコレーターを作ります。
# stats_decorator.gd
class_name StatsDecorator
extends PlayerStats
var wrapped_stats: PlayerStats
func _init(stats: PlayerStats) -> void:
# 元のステータス計算を内側に保持する
wrapped_stats = stats
func get_attack() -> int:
return wrapped_stats.get_attack()
func get_defense() -> int:
return wrapped_stats.get_defense()
攻撃力を上げるデコレーターを作ります。
# attack_boost_stats.gd
class_name AttackBoostStats
extends StatsDecorator
var bonus_attack: int
func _init(stats: PlayerStats, bonus: int) -> void:
super(stats)
bonus_attack = bonus
func get_attack() -> int:
# 内側の計算結果に、今回の補正を上乗せする
return wrapped_stats.get_attack() + bonus_attack
使う側では、元のステータスに効果を重ねていきます。
var stats: PlayerStats = BasePlayerStats.new()
print(stats.get_attack()) # 10
# 剣を装備して +5
stats = AttackBoostStats.new(stats, 5)
print(stats.get_attack()) # 15
# 攻撃力アップのバフでさらに +3
stats = AttackBoostStats.new(stats, 3)
print(stats.get_attack()) # 18
ここで大事なのは、プレイヤー本体が「攻撃力はどう計算されているか」を細かく知らなくてよいことです。プレイヤーは stats.get_attack() を呼ぶだけで、装備やバフを含んだ最終値を受け取れます。
実装レシピ: RPGの装備と一時バフ
RPGで装備とバフを扱うなら、次のように考えます。
- プレイヤーの素の値を
BasePlayerStatsに置く - 剣、指輪、料理効果、スキル効果などを「ステータスを変える効果」として分ける
- 攻撃時やUI表示時に、現在有効な効果を反映した
stats.get_attack()を読む - バ フの効果時間が切れたら、その効果を外して再計算する
ただし、デコレーターは「最後に足した効果から外す」だけなら簡単ですが、「真ん中の効果だけ外す」「同じ種類のバフを更新する」ようになると扱いが難しくなります。
その場合は、厳密なデコレーターにこだわらず、StatModifier の配列を持って合計する設計の方が実装しやすいこともあります。
# 装備やバフが多いゲームでは、配列方式の方が扱いやすい場合もある
var attack_modifiers: Array[int] = [5, 3, -2]
func get_total_attack(base_attack: int) -> int:
var total := base_attack
for modifier in attack_modifiers:
# 有効な補正を順番に合計する
total += modifier
return total
この記事で覚えてほしいのは「デコレーターパターンの厳密な形」よりも、素の値に効果を重ねて最終値を作る という考え方です。装備、バフ、難易度補正、ステージ効果などが増えるゲームでは、この考え方を持っているだけで設計がかなり整理されます。
ファクトリー: 生成ルールを1箇所に集める
最後は ファクトリー です。ファクトリーは、敵やアイテムなどの生成方法を専用の場所に集めるパターンです。
たとえば、ステージ開始時、宝箱を開けたとき、タワーディフェンスのウェーブ開始時に、それぞれ次のようなコードを書いているとします。
# あちこちに散らばりやすい生成コード
var enemy = preload("res://enemies/goblin.tscn").instantiate()
enemy.global_position = spawn_position
enemy.health = 50
enemy.speed = 100
add_child(enemy)
最初は問題ありません。しかしゴブリンのHPを変えたい、新しい敵を追加したい、生成時にドロップ率も設定したい、となると修正箇所が増えていきます。
生成処理を使う側は「ゴブリンをこの位置に出したい」だけを伝え、細かいシーンや初期値はファクトリーに任せます。

例: 敵生成をEnemyFactoryへ集める
# 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": 120, "speed": 75 },
EnemyType.DRAGON: { "health": 500, "speed": 45 },
}
func create_enemy(type: EnemyType, position: Vector2) -> Node2D:
if not ENEMY_SCENES.has(type):
push_error("Unknown enemy type: %s" % type)
return null
# 生成するシーンと初期値をファクトリー側で決める
var enemy: Node2D = ENEMY_SCENES[type].instantiate()
var config: Dictionary = ENEMY_CONFIGS[type]
enemy.global_position = position
enemy.health = config["health"]
enemy.speed = config["speed"]
return enemy
使う側はかなり短くなります。
# wave_spawner.gd
extends Node2D
@onready var factory: EnemyFactory = $EnemyFactory
func spawn_wave() -> void:
var spawn_points := [
Vector2(100, 120),
Vector2(180, 120),
Vector2(260, 120),
]
for point in spawn_points:
# どの敵をどこに出すかだけを書く
var enemy := factory.create_enemy(EnemyFactory.EnemyType.GOBLIN, point)
if enemy != null:
add_child(enemy)
実装レシピ: タワーディフェンスのウェーブ
タワーディフェンスでは、敵生成のルールが増えやすいです。ウェーブ番号、敵の種類、出現間隔、出現位置、難易度補正などが絡むからです。
この場合、役割を次のように分けると読みやすくなります。
| 役割 | 持たせる内容 |
|---|---|
WaveSpawner | いつ、何体、どの順番で出すか |
EnemyFactory | 敵タイプからシーンと初期値を決める |
Enemy シーン | 実際の移動、攻撃、死亡処理 |
WaveSpawner がすべてを知っていると、ウェーブ演出を直したいだけなのに敵ステータスのコードまで読む必要が出ます。EnemyFactory に生成ルールを集めておけば、「出す順番」と「敵の作り方」を分けて考えられます。
ファクトリーは、RPGの村人生成、ローグライクのアイテム生成、シューティングの弾生成にも使えます。ただし弾を大量に出すゲームでは、生成コストを避けるために オブジェクトプール を使うことも多いです。その話は別テーマなので、ここでは「生成ルールを1箇所に集める」考え方 だけ押さえれば十分です。
3つを組み合わせるとどうなるか
実際のゲームでは、3つのパターンを単独で使うより、役割ごとに組み合わせることが多いです。

たとえばアクションRPGの敵なら、次のように分けられます。
| 処理 | 使う考え方 | 具体例 |
|---|---|---|
| 敵を出す | ファクトリー | EnemyFactory.create_enemy(EnemyType.ORC, position) |
| 敵の機能 | コンポジション | MovementComponent、HealthComponent、AttackComponent |
| 一時強化 | デコレーター | 怒り状態で攻撃力+20%、毒で防御力低下 |
この分け方のメリットは、変更理由が分かれることです。
- 新しい敵タイプを追加するなら
EnemyFactory - 移動の手触りを変えるなら
MovementComponentやMovementStats - 装備やバフの計算を変えるならステータス計算側
変更理由ごとにファイルが分かれると、「どこを直せばいいか」が見つけやすくなります。これは初心者のうちは地味に見えますが、ゲームが大きくなるほど効いてきます。
よくあるつまずき
パターンを先に決めすぎる
「最初から完璧な設計にしよう」とすると、まだ必要ないクラスが増えます。プロトタイプでは、まず素直に作って動かして構いません。
ただし、次の兆候が出たら整理を考えるタイミングです。
- 似たコードを3箇所以上にコピーしている
if enemy_type == ...が複数ファイルに出てくる- クラス名が組み合わせ名だらけになる
- 攻撃力やHPの最終値が、どこで決まったのか追えない
コンポーネント同士を強く結びすぎる
コンポジションにしても、各コンポーネントが get_node("../../SomeNode") のように遠いノードを直接探し始めると、結局壊れやすくなります。
まずは親ノードから必要な情報を渡す、@export で参照を設定する、シグナルで通知する、といった形を検討してください。部品化の目的はファイルを増やすことではなく、依存関係を見えやすくすることです。
デコレーターで解除処理が複雑になる
デコレーターは効果を重ねるには分かりやすいですが、任意の効果を途中から外す処理は複雑になりがちです。
たとえば「剣 + バフ + 毒」の順で重ねたあと、真ん中のバフだけ消したい場合、単純に一番外側を外すだけでは対応できません。バフやデバフの数が多いゲームでは、StatModifier を配列で管理し、有効期限や優先度を持たせる設計の方が向いてい ます。
ファクトリーが何でも屋になる
ファクトリーは生成を担当しますが、敵AIの判断、ウェーブ進行、スコア加算、演出再生まで入れる場所ではありません。
ファクトリーの仕事は「何を作り、初期値をどう入れるか」までに留めると扱いやすいです。作った後にどう動くかは敵自身やコンポーネント、いつ出すかはスポナーに任せます。
おまけ: 先に知っておくと良いこと
GodotではResourceをうまく使うと設計が軽くなる
コンポジションというとNodeを増やすイメージがありますが、すべてをNodeにする必要はありません。
- 速度、HP、攻撃力のような調整データ:
Resource - 毎フレーム処理したい機能:
Node - シーン上に存在するキャラクター:
CharacterBody2DやNode2D
このように分けると、シーンツリーが必要以上に深くなりません。特に敵やアイテムのデータは、後続のカスタムResource記事と相性が良いテーマです。
継承を完全に避ける必要はない
この記事では継承のつらさを強調しましたが、継承が悪いわけではありません。
CharacterBody2D、Control、Resource、RefCounted のようなGodotの土台を継承するのは自然です。また、敵全体に共通する最低限の処理を BaseEnemy にまとめる程度なら読みやすいことも多いです。
避けたいのは、「飛ぶ」「速い」「硬い」「毒を持つ」のような組み合わせを、全部クラス階層で表現しようとすることです。
迷ったら「変更理由」で分ける
設計で迷ったときは、次の問いが役立ちます。
このコードは、何が変わったときに修正されるのか?
敵の種類が増えたらファクトリー、移動の手触りが変わったら移動コンポーネント、装備効果が増えたらステータス計算。変更理由が違うものを同じファイルに詰め込むほど、後から読みづらくなります。
まとめ
- デザインパターン は、増えたときに壊れにくくするための整理術
- 継承 は土台を決めるときに使い、変わりやすい機能は コンポジション で部品化する
- デコレーター は、装備やバフのように効果を重ねて最終値を作る考え方
- ファクトリー は、敵やアイテムの生成ルールを1箇所に集める考え方
- 小さいうちは素直に作り、コピペ・組み合わせ爆発・生成処理の散らばりが見えてから導入する
デザインパターンを覚える目的は、名前を暗記することではありません。「この処理はどこに置くと、後から変更しやすいか」を考えられるようになることです。