デザインパターンとは?
デザインパターンとは、プログラミングでよく出てくる問題に対する「定番の解決方法」のことです。
料理のレシピのように、「こういう問題にはこの方法で解決しよう!」という先人の知恵をまとめたものです。
Strategyパターンは、処理のアルゴリズムを切り替えたい場合に使えるデザインパターンです。
Strategy パターンとは?
Strategy(ストラテジー)パターンは、アルゴリズムや振る舞いをカプセル化し、実行時に切り替え可能にするデザインパターンです。GoFの23パターンの中で「振る舞いに関するパターン(Behavioral Patterns)」に分類されます。
解決する問題
ゲーム開発では、以下のような「同じ処理だが、やり方が複数ある」場面に遭遇します。
- 武器の攻撃方法 - 剣で斬る、弓で射る、魔法で攻撃する
- 敵AIの行動 - 攻撃的、防御的、逃走など
- 移動方法 - 歩く、走る、飛ぶ、泳ぐ
- ソート処理 - クイックソート、マージソート、バブルソート
これらをswitch文やif-elseで分岐すると、新しいパターンを追加するたびにコードを修正する必要があり、保守性が低下します。
Strategy パターンの構造
Strategyパターンは以下の要素で構成されます。
Strategy(戦略インターフェイス) - アルゴリズムの共通インターフェイスConcreteStrategy(具体的な戦略) - 各アルゴリズムの実装クラスContext(コンテキスト) - 戦略を使用するクラス
Strategyパターンを使わない場合の問題点
まず、Strategyパターンを使わずに武器の攻撃処理を実装した例を見てみましょう。
using UnityEngine;
public class Player : MonoBehaviour
{
[SerializeField] private WeaponType _weaponType;
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
Attack();
}
}
private void Attack()
{
// switch文で武器ごとの処理を分岐
switch (_weaponType)
{
case WeaponType.Sword:
Debug.Log("剣で斬りつける!ダメージ: 10");
// 近接攻撃の処理
break;
case WeaponType.Bow:
Debug.Log("矢を射る!ダメージ: 8");
// 遠距離攻撃の処理
break;
case WeaponType.Magic:
Debug.Log("魔法を唱える!ダメージ: 15");
// 魔法攻撃の処理
break;
}
}
}
public enum WeaponType
{
Sword,
Bow,
Magic
}
この実装の問題点
- 新しい武器を追加するたびに
Attackメソッドを修正する必要がある switch文が肥大化し、可読性が低下する- テストがしにくい - 各武器の処理を個別にテストできない
これらの問題をStrategyパターンで解決していきます。
UnityにおけるStrategy パターンの実装
1. 戦略インターフェイスの定義
まず、すべての攻撃戦略が実装すべきインターフェイスを定義します。
using UnityEngine;
// 攻撃戦略のインターフェイス
public interface IAttackStrategy
{
void Execute(GameObject attacker);
}
2. 具体的な戦略クラスの実装
各武器の攻撃処理を、個別のクラスとして実装します。
using UnityEngine;
// 剣による攻撃戦略
public class SwordAttackStrategy : IAttackStrategy
{
public void Execute(GameObject attacker)
{
Debug.Log($"{attacker.name}が剣で斬りつける!ダメージ: 10");
// 近接範囲の敵を検索
Collider[] hitColliders = Physics.OverlapSphere(attacker.transform.position, 2f);
foreach (var hitCollider in hitColliders)
{
if (hitCollider.CompareTag("Enemy"))
{
// ダメージ処理
Debug.Log($"{hitCollider.name}にヒット!");
}
}
}
}
// 弓による攻撃戦略
public class BowAttackStrategy : IAttackStrategy
{
public void Execute(GameObject attacker)
{
Debug.Log($"{attacker.name}が矢を射る!ダメージ: 8");
// 矢のプレハブを生成して発射
// 実際の実装では矢のGameObjectを生成し、Rigidbodyで飛ばす
}
}
// 魔法による攻撃戦略
public class MagicAttackStrategy : IAttackStrategy
{
public void Execute(GameObject attacker)
{
Debug.Log($"{attacker.name}が魔法を唱える!ダメージ: 15");
// 範囲魔法のエフェクトを表示
// 広範囲の敵にダメージを与える
}
}
3. コンテキスト(戦略を使用するクラス)の実装
プレイヤークラスで戦略を保持し、実行時に切り替えられるようにします。
using UnityEngine;
public class Player : MonoBehaviour
{
private IAttackStrategy _attackStrategy;
private void Start()
{
// 初期装備は剣
SetAttackStrategy(new SwordAttackStrategy());
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
Attack();
}
// 武器の切り替え
if (Input.GetKeyDown(KeyCode.Alpha1))
{
SetAttackStrategy(new SwordAttackStrategy());
Debug.Log("武器を剣に変更");
}
else if (Input.GetKeyDown(KeyCode.Alpha2))
{
SetAttackStrategy(new BowAttackStrategy());
Debug.Log("武器を弓に変更");
}
else if (Input.GetKeyDown(KeyCode.Alpha3))
{
SetAttackStrategy(new MagicAttackStrategy());
Debug.Log("武器を魔法に変更");
}
}
// 攻撃戦略を設定
public void SetAttackStrategy(IAttackStrategy strategy)
{
_attackStrategy = strategy;
}
// 攻撃を実行
private void Attack()
{
if (_attackStrategy != null)
{
_attackStrategy.Execute(gameObject);
}
else
{
Debug.LogWarning("攻撃戦略が設定されていません");
}
}
}
これで、新しい武器を追加する場合はIAttackStrategyを実装した新しいクラスを作成するだけで済みます。
既存のPlayerクラスを修正する必要はありません。
別の実装例:AIの行動パターン
Strategyパターンは、敵AIの行動切り替えにも効果的です。
1. 行動戦略インターフェイス
using UnityEngine;
// 敵の行動戦略インターフェイス
public interface IEnemyBehavior
{
void Execute(GameObject enemy, GameObject target);
}
2. 具体的な行動戦略
using UnityEngine;
// 攻撃的な行動
public class AggressiveBehavior : IEnemyBehavior
{
public void Execute(GameObject enemy, GameObject target)
{
// プレイヤーに向かって突進
Vector3 direction = (target.transform.position - enemy.transform.position).normalized;
enemy.transform.position += direction * 5f * Time.deltaTime;
Debug.Log($"{enemy.name}が攻撃的に接近中!");
}
}
// 防御的な行動
public class DefensiveBehavior : IEnemyBehavior
{
public void Execute(GameObject enemy, GameObject target)
{
// 一定の距離を保つ
float distance = Vector3.Distance(enemy.transform.position, target.transform.position);
if (distance < 5f)
{
// 距離が近すぎたら後退
Vector3 direction = (enemy.transform.position - target.transform.position).normalized;
enemy.transform.position += direction * 3f * Time.deltaTime;
Debug.Log($"{enemy.name}が距離を取っている");
}
}
}
// 逃走行動
public class FleeBehavior : IEnemyBehavior
{
public void Execute(GameObject enemy, GameObject target)
{
// プレイヤーから逃げる
Vector3 direction = (enemy.transform.position - target.transform.position).normalized;
enemy.transform.position += direction * 7f * Time.deltaTime;
Debug.Log($"{enemy.name}が逃走中!");
}
}
3. 敵AIクラス
using UnityEngine;
public class Enemy : MonoBehaviour
{
[SerializeField] private float _maxHp = 100f;
private float _currentHp;
private IEnemyBehavior _behavior;
private GameObject _player;
private void Start()
{
_currentHp = _maxHp;
_player = GameObject.FindGameObjectWithTag("Player");
// 初期状態は攻撃的
SetBehavior(new AggressiveBehavior());
}
private void Update()
{
if (_player != null && _behavior != null)
{
// 現在の行動戦略を実行
_behavior.Execute(gameObject, _player);
}
// HPに応じて行動を変更
UpdateBehaviorByHp();
}
// HPに応じて行動を動的に変更
private void UpdateBehaviorByHp()
{
float hpPercentage = _currentHp / _maxHp;
if (hpPercentage > 0.7f)
{
// HP70%以上:攻撃的
if (!(_behavior is AggressiveBehavior))
{
SetBehavior(new AggressiveBehavior());
}
}
else if (hpPercentage > 0.3f)
{
// HP30-70%:防御的
if (!(_behavior is DefensiveBehavior))
{
SetBehavior(new DefensiveBehavior());
}
}
else
{
// HP30%以下:逃走
if (!(_behavior is FleeBehavior))
{
SetBehavior(new FleeBehavior());
}
}
}
public void SetBehavior(IEnemyBehavior behavior)
{
_behavior = behavior;
}
public void TakeDamage(float damage)
{
_currentHp -= damage;
_currentHp = Mathf.Max(_currentHp, 0);
}
}
この実装により、敵のHPに応じて行動が自動的に切り替わります。新しい行動パターン(例:「仲間を呼ぶ」「罠を仕掛ける」など)を追加する場合も、IEnemyBehaviorを実装した新しいクラスを作成するだけです。
Strategyパターンの利点
1. 新しい戦略の追加が容易
新しいアルゴリズムを追加する際、既存のコードを変更する必要がありません。IAttackStrategyやIEnemyBehaviorを実装した新しいクラスを作成するだけです。
// 新しい武器を追加する場合
public class SpearAttackStrategy : IAttackStrategy
{
public void Execute(GameObject attacker)
{
Debug.Log("槍で突く!ダメージ: 12");
// 槍特有の攻撃処理
}
}
2. テストのしやすさ
各戦略クラスを独立してテストできます。
using NUnit.Framework;
using UnityEngine;
public class AttackStrategyTests
{
[Test]
public void SwordAttack_ShouldExecuteCorrectly()
{
// Arrange
var strategy = new SwordAttackStrategy();
var attacker = new GameObject("TestPlayer");
// Act
strategy.Execute(attacker);
// Assert
// 期待される動作を検証
}
}
3. コードの可読性向上
巨大なswitch文やif-elseの連鎖がなくなり、各戦略の処理が独立したクラスに分離されるため、コードが読みやすくなります。
注意点
1. 戦略クラスが増えすぎる場合
戦略の数が非常に多くなる場合、クラス数が増えすぎて管理が大変になることがあります。この場合の対処法:
- ファイル構成を工夫 - フォルダで整理(例:
Strategies/Attack/,Strategies/Behavior/) ScriptableObjectとの組み合わせ -ScriptableObjectにすることでInspector上からパラメータを調整でき、プログラマー以外のチームメンバー(プランナーなど)も武器のバランス調整が可能になります
using UnityEngine;
[CreateAssetMenu(fileName = "NewAttackStrategy", menuName = "Strategy/Attack")]
public class AttackStrategyData : ScriptableObject
{
public string attackName;
public int damage;
public float range;
public AttackType attackType;
public IAttackStrategy CreateStrategy()
{
return attackType switch
{
AttackType.Melee => new SwordAttackStrategy(),
AttackType.Ranged => new BowAttackStrategy(),
AttackType.Magic => new MagicAttackStrategy(),
_ => null
};
}
}
public enum AttackType
{
Melee,
Ranged,
Magic
}
2. パフォーマンスへの影響
毎フレーム新しい戦略インスタンスを生成すると、GC(ガベージコレクション)が発生します。GCが発生するとフレーム単位の処理落ち(スパイク)を引き起こし、特にモバイル環境ではカクつきの原因になります。
対処法:
事前にインスタンスを用意しておき再利用できるようにします。
public class Player : MonoBehaviour
{
// 戦略インスタンスを事前に作成して再利用
private readonly SwordAttackStrategy _swordStrategy = new SwordAttackStrategy();
private readonly BowAttackStrategy _bowStrategy = new BowAttackStrategy();
private readonly MagicAttackStrategy _magicStrategy = new MagicAttackStrategy();
private IAttackStrategy _attackStrategy;
private void Start()
{
_attackStrategy = _swordStrategy; // インスタンスを再利用
}
public void SwitchToSword()
{
_attackStrategy = _swordStrategy; // 既存インスタンスを使用
}
}
3. Stateパターンとの使い分け
Strategy パターンとState パターンは構造が似ていますが、目的が異なります。
| パターン | 目的 | 切り替えのタイミング |
|---|---|---|
Strategy | アルゴリズムの切り替え | 外部から任意のタイミングで |
State | 状態遷移の管理 | 内部のルールに基づいて自動的に |
使い分けの例:
- 武器の切り替え →
Strategy(プレイヤーが任意に選択) - キャラクターの状態(
Idle/Walk/Run) →State(条件に応じて自動遷移)
迷ったときは、「切り替えの主導権がどこにあるか」で判断するとわかりやすいです。
外部(呼び出し側)が任意に切り替えるならStrategy、オブジェクト内部のルールで自動的に遷移するならStateと考えましょう。
両者を組み合わせることも可能です。例えば、各State内で異なるStrategyを使用する、といった設計もあります。
まとめ
Strategy パターンは、アルゴリズムや振る舞いをカプセル化し、実行時に切り替え可能にするデザインパターンです。
重要なポイント
- インターフェイスで共通の処理を定義する
- 具体的な戦略クラスで各アルゴリズムを実装する
- コンテキストクラスで戦略を保持・実行する
switch文の連鎖を避け、拡張性の高い設計にできる
使用を検討すべき場面
以下のような場面でStrategyパターンの使用を検討してください:
- 同じ処理に複数のアルゴリズムが存在する
- 実行時にアルゴリズムを切り替える必要がある
switch文やif-elseが複雑になっている- 新しい機能を頻繁に追加する可能性がある
📣おしらせ!
Unity Asset Storeで
世界を揺るがすフラッシュセール
が開催中です。
最高のワールド制作に必要なすべてが、24時間限定で70% OFF!