LOADING

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

Unity 常用面向对象设计模式系列(10):组合模式(Composite)


一、组合模式概述

意图:将对象组合成树形结构以表示“整体/部分”层次,客户端对单个对象和组合对象使用相同的接口。

1.1 核心角色

  • Component(组件接口)
    定义叶子和组合对象的公共方法,如 Operation()Add()Remove()GetChild()

  • Leaf(叶子节点)
    实现 Component 接口,没有子节点,直接执行自身业务。

  • Composite(组合节点)
    持有子 Component 列表,实现 Add/Remove 并在 Operation 中递归调用子节点。

1.2 UML 类图

CompositePattern

二、Unity 实战示例:UI 树操作

假设我们要实现一个“批量隐藏/显示 UI 元素”的功能,无论是单个按钮(叶子)还是面板(可能包含多个子元素),都希望调用相同接口。

2.1 定义 Component 接口

public interface IUIComponent {
    /// <summary>
    ///执行一次 UI 操作,例如 Show/Hide、SetInteractable 等。
    ///</summary>
    void Operation();
    void Add(IUIComponent child);
    void Remove(IUIComponent child);
    IUIComponent GetChild(int index);
}

2.2 叶子节点:ButtonComponent

public class ButtonComponent : MonoBehaviour, IUIComponent {
    private Button _button;

void Awake() {
    _button = GetComponent&lt;Button&gt;();
}

public void Operation() {
    // 统一操作:隐藏该按钮
    gameObject.SetActive(false);
}
public void Add(IUIComponent child)    { /* 不支持添加 */ }
public void Remove(IUIComponent child) { /* 不支持移除 */ }
public IUIComponent GetChild(int index){ return null; }

}

2.3 组合节点:PanelComponent

public class PanelComponent : MonoBehaviour, IUIComponent {
    private readonly List<IUIComponent> _children = new();

void Awake() {
    // 自动收集所有直接子 IUIComponent
    foreach (Transform t in transform) {
        var comp = t.GetComponent&lt;IUIComponent&gt;();
        if (comp != null) _children.Add(comp);
    }
}

public void Operation() {
    // 隐藏自己
    gameObject.SetActive(false);
    // 递归对子节点调用
    foreach (var child in _children) {
        child.Operation();
    }
}
public void Add(IUIComponent child) {
    if (!_children.Contains(child)) _children.Add(child);
}
public void Remove(IUIComponent child) {
    _children.Remove(child);
}
public IUIComponent GetChild(int index) {
    return (index &gt;= 0 &amp;&amp; index &lt; _children.Count) ? _children[index] : null;
}

}

2.4 使用示例

public class UIController : MonoBehaviour {
    [SerializeField] private IUIComponent rootPanel;

void Update() {
    if (Input.GetKeyDown(KeyCode.H)) {
        // 隐藏整个 UI 树
        rootPanel.Operation();
    }
}

}

客户端无需关心 rootPanel 是 Leaf 还是 Composite,Operation() 会递归隐藏所有子层级。


三、示例:技能树节点

3.1 SkillNode 抽象

public interface ISkillNode {
    void Activate();
    void Add(ISkillNode child);
    void Remove(ISkillNode child);
    ISkillNode GetChild(int idx);
}

3.2 基本技能叶子

[CreateAssetMenu("Skill/BasicSkill")]
public class BasicSkill : ScriptableObject, ISkillNode {
    public string skillName;
    public void Activate() {
        Debug.Log($"Activate skill: {skillName}");
    }
    public void Add(ISkillNode child)    { }
    public void Remove(ISkillNode child) { }
    public ISkillNode GetChild(int idx)  => null;
}

3.3 组合技能节点

[CreateAssetMenu("Skill/CompositeSkill")]
public class CompositeSkill : ScriptableObject, ISkillNode {
    public List<ISkillNode> children = new();
    public void Activate() {
        foreach (var child in children) {
            child.Activate();
        }
    }
    public void Add(ISkillNode child) {
        if (!children.Contains(child)) children.Add(child);
    }
    public void Remove(ISkillNode child) {
        children.Remove(child);
    }
    public ISkillNode GetChild(int idx) {
        return idx >= 0 && idx < children.Count ? children[idx] : null;
    }
}
  • 用途:定义“组合技”,激活时依次发动子技能;
  • 优势:技能树结构可在 Inspector 可视化配置,新增子技能无需改代码。

四、优化策略

4.1 动态加载与延迟绑定

  • 对于经常变化的层级,可延迟加载子节点:
public async Task InitializeAsync() {
    // 异步加载子 Prefab 并 Add
    var go = await Addressables.LoadAssetAsync<GameObject>(assetKey).Task;
    var comp = go.GetComponent<IUIComponent>();
    Add(comp);
}

4.2 通用 Composite 实现

public interface IComponent<T> {
    void Operation();
    void Add(IComponent<T> child);
    void Remove(IComponent<T> child);
    IComponent<T> GetChild(int idx);
}

public class Composite<T> : IComponent<T> {
private readonly List<IComponent<T>> _children = new();
public void Operation() {
foreach (var child in _children) {
child.Operation();
}
}
public void Add(IComponent<T> child) {
if (!_children.Contains(child)) _children.Add(child);
}
public void Remove(IComponent<T> child) {
_children.Remove(child);
}
public IComponent<T> GetChild(int idx) {
return idx >= 0 && idx < _children.Count ? _children[idx] : null;
}
}

4.3 性能优化

  • 缓存子节点:避免每次 Operation 中反射或 GetComponent
  • 批量操作:对深层树可一次性合并状态变更,减少多次 SetActive 代价;
  • 异步分帧:对于节点数庞大的操作,按帧分批执行,避免卡顿。

五、注意事项

场景 陷阱 建议
过度使用递归 深度层级过深导致栈溢出 适当限制树深度,或改用显式栈/队列遍历
叶子与组合混淆 叶子节点误实现 Add/Remove 导致异常 对叶子节点抛出 NotSupportedException 或空实现
根节点管理不当 根节点生命周期结束,但子节点未清理,导致内存泄漏 在根节点销毁时递归调用子节点清理或 Dispose
单个节点逻辑过重 叶子节点承担太多行为代码,违背 SRP 采用组合 + 其他模式(如策略)拆分叶子逻辑
编辑时断开引用 Inspector 手动拖拽子节点时,运行时未 Add 到父列表 自动在 Awake/Start 中同步 transform 子对象与 Composite 列表

六、小结

  1. 统一接口:客户端只依赖 IComponent,无需区分叶子或组合;
  2. 职责清晰:叶子只聚焦自身行为,组合只管理子节点;
  3. 可视化配置:结合 ScriptableObject 或自定义 Inspector 构建树形结构;
  4. 性能权衡:对深层树操作分帧或批量,提高运行时流畅度;
  5. 模式组合:与策略、观察者等模式结合,实现更复杂的系统架构。