一、组合模式概述
意图:将对象组合成树形结构以表示“整体/部分”层次,客户端对单个对象和组合对象使用相同的接口。
1.1 核心角色
Component(组件接口)
定义叶子和组合对象的公共方法,如Operation()、Add()、Remove()、GetChild()。Leaf(叶子节点)
实现 Component 接口,没有子节点,直接执行自身业务。Composite(组合节点)
持有子 Component 列表,实现Add/Remove并在Operation中递归调用子节点。
1.2 UML 类图

二、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<Button>();
}
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<IUIComponent>();
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 >= 0 && index < _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 列表 |
六、小结
- 统一接口:客户端只依赖
IComponent,无需区分叶子或组合; - 职责清晰:叶子只聚焦自身行为,组合只管理子节点;
- 可视化配置:结合 ScriptableObject 或自定义 Inspector 构建树形结构;
- 性能权衡:对深层树操作分帧或批量,提高运行时流畅度;
- 模式组合:与策略、观察者等模式结合,实现更复杂的系统架构。