LOADING

加载过慢请开启缓存 浏览器默认开启

Unity 常用面向对象设计模式系列(9):策略模式(Strategy)


一、策略模式核心动机

  1. 算法或行为切换
    客户端无需知道具体实现,只需持有策略接口并在运行时赋予不同策略实例。

  2. 消除条件分支
    将复杂的 if/elseswitch 逻辑移入各个策略类,使主流程代码更清晰。

  3. 开闭原则
    新增策略只需添加新的策略类,而不修改现有代码。

  4. 可测试性
    各策略在独立环境下可单元测试,客户端只需 Mock 策略接口。


二、模式定义与 UML 类图

StrategyPattern

  • IAttackStrategy:策略接口,定义统一方法 Execute
  • ConcreteStrategyMeleeStrategyRangedStrategyMagicStrategy 等,实现不同攻击算法。
  • Context(Player):持有策略引用,在 Attack 时委托给当前策略执行。

三、Unity 实战示例:角色攻击策略

3.1 定义策略接口与实现

public interface IAttackStrategy {
    /// <summary>
    /// 执行一次攻击,caster 是攻击者 Transform,target 是被攻击对象
    /// </summary>
    void Execute(Transform caster, GameObject target);
}

public class MeleeStrategy : IAttackStrategy {
    public float damage = 10f;
    public float range  = 2f;

    public void Execute(Transform caster, GameObject target) {
        if (Vector3.Distance(caster.position, target.transform.position) <= range) {
            var health = target.GetComponent<Health>();
            health?.TakeDamage(damage);
        }
        // 可播放近战动画、音效等
    }
}

public class RangedStrategy : IAttackStrategy {
    public GameObject projectilePrefab;
    public float     speed = 20f;

    public void Execute(Transform caster, GameObject target) {
        var go = Object.Instantiate(projectilePrefab, caster.position, Quaternion.identity);
        var dir = (target.transform.position - caster.position).normalized;
        go.GetComponent<Rigidbody>().velocity = dir * speed;
    }
}

public class MagicStrategy : IAttackStrategy {
    public GameObject effectPrefab;
    public float      areaRadius = 3f;
    public float      damage     = 8f;

    public void Execute(Transform caster, GameObject target) {
        Object.Instantiate(effectPrefab, target.transform.position, Quaternion.identity);
        var colliders = Physics.OverlapSphere(target.transform.position, areaRadius);
        foreach (var col in colliders) {
            col.GetComponent<Health>()?.TakeDamage(damage);
        }
    }
}

3.2 Context:Player 类

public class Player : MonoBehaviour {
    private IAttackStrategy _strategy;

    void Start() {
        // 默认近战策略
        _strategy = new MeleeStrategy();
    }

    public void SetStrategy(IAttackStrategy strategy) {
        _strategy = strategy;
    }

    public void Attack(GameObject target) {
        _strategy.Execute(transform, target);
    }

    void Update() {
        if (Input.GetKeyDown(KeyCode.Alpha1)) {
            SetStrategy(new MeleeStrategy());
        } else if (Input.GetKeyDown(KeyCode.Alpha2)) {
            SetStrategy(new RangedStrategy());
        } else if (Input.GetKeyDown(KeyCode.Alpha3)) {
            SetStrategy(new MagicStrategy());
        }

        if (Input.GetMouseButtonDown(0)) {
            RaycastHit hit;
            if (Physics.Raycast(Camera.main.ScreenPointToRay(Input.mousePosition), out hit)) {
                Attack(hit.collider.gameObject);
            }
        }
    }
}
  • 按数字键动态切换策略;
  • Attack 方法中,完全委托给当前策略执行。

四、用法

4.1 ScriptableObject 驱动策略

将策略实现为可编辑资产,便于策划调参与热更新:

public abstract class AttackStrategySO : ScriptableObject, IAttackStrategy {
    public abstract void Execute(Transform caster, GameObject target);
}

[CreateAssetMenu("Strategy/Melee")]
public class MeleeStrategySO : AttackStrategySO {
    public float damage = 10f;
    public float range  = 2f;
    public override void Execute(Transform caster, GameObject target) {
        // 同上
    }
}
class Player : MonoBehaviour {
    public AttackStrategySO initialStrategy;
    private IAttackStrategy _strategy;

    void Start() {
        _strategy = initialStrategy;
    }

    public void SetStrategy(AttackStrategySO strategySO) {
        _strategy = strategySO;
    }
    // Attack 与 Update 同上...
}

4.2 策略工厂 & DI

结合抽象工厂与 Zenject,将策略注册到容器,按需注入:

public class GameInstaller : MonoInstaller {
    public AttackStrategySO meleeSO;
    public AttackStrategySO rangedSO;
    public override void InstallBindings() {
        Container.Bind<IAttackStrategy>().WithId("Melee").FromInstance(meleeSO).AsTransient();
        Container.Bind<IAttackStrategy>().WithId("Ranged").FromInstance(rangedSO).AsTransient();
    }
}

public class Player : MonoBehaviour {
    [Inject(Id = "Melee")] IAttackStrategy _defaultStrategy;
    private IAttackStrategy _strategy;
    void Start() {
        _strategy = _defaultStrategy;
    }
    // ...
}

4.3 策略组合(Decorator)

可在现有策略上叠加额外功能,如暴击、伤害加成:

public class CriticalDecorator : IAttackStrategy {
    private IAttackStrategy _inner;
    private float _critChance;
    public CriticalDecorator(IAttackStrategy inner, float critChance) {
        _inner = inner; _critChance = critChance;
    }
    public void Execute(Transform caster, GameObject target) {
        if (Random.value < _critChance) {
            // 增强逻辑
        }
        _inner.Execute(caster, target);
    }
}

五、性能与调试

  1. GC 控制

    • 避免在 Update() 中频繁 new 策略实例,使用 ScriptableObject 或单例策略;
  2. Profiler

    • 确认策略切换、执行不带来帧率突降;
  3. 可视化

    • 在 Inspector 中显示当前策略名称,便于调试:
    [ReadOnly] public string currentStrategy;
    void Update() {
        currentStrategy = _strategy.GetType().Name;
    }
    

六、注意事项

场景 陷阱 建议
频繁实例化策略 每帧创建新实例造成 GC 压力 对无状态策略使用单例或 ScriptableObject
策略依赖外部资源 策略内部直接 InstantiateFind,导致难以测试 将依赖注入到策略构造或 SO 资产,通过工厂模式注入
过度抽象 每个微小差异都写策略,策略类爆炸 把真正可变部分提炼到策略,公共逻辑可放在组合/基类中
策略切换时机不明确 在渲染或物理回调中切换,可能导致逻辑不一致 在统一 Update 或事件回调时切换,保证执行顺序可控

七、小结

  1. 职责分离:将可变算法或行为封装到 IAttackStrategyIMoveStrategy 等策略中;
  2. 接口依赖:客户端只依赖策略接口,易 Mock、易扩展;
  3. 资源驱动:建议使用 ScriptableObject 策略,便于无代码热更新与调参;
  4. 策略复用:对无内部状态策略使用单例,避免过度分配;
  5. 组合增强:结合装饰器模式,为基础策略动态添加功能(Buff、Debuff、日志等)。