デザインパターンとは?

デザインパターンとは、プログラミングでよく出てくる問題に対する「定番の解決方法」のことです。

料理のレシピのように、「こういう問題にはこの方法で解決しよう!」という先人の知恵をまとめたものです。

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
}

この実装の問題点

  1. 新しい武器を追加するたびにAttackメソッドを修正する必要がある
  2. switch文が肥大化し、可読性が低下する
  3. テストがしにくい - 各武器の処理を個別にテストできない

これらの問題を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. 新しい戦略の追加が容易

新しいアルゴリズムを追加する際、既存のコードを変更する必要がありません。IAttackStrategyIEnemyBehaviorを実装した新しいクラスを作成するだけです。

// 新しい武器を追加する場合
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!

フラッシュセール

50%オフ

🔗関連ページ