响应式编程入门教程第一节:揭秘 UniRx 核心 - ReactiveProperty - 让你的数据动起来!
响应式编程入门教程第二节:构建 ObservableProperty<T> — 封装 ReactiveProperty 的高级用法
响应式编程入门教程第三节:ReactiveCommand 与 UI 交互
响应式编程入门教程第四节:响应式集合与数据绑定
响应式编程入门教程第五节:Unity 生命周期与资源管理中的响应式编程
在上一篇教程中,我们学习了 ReactiveCommand 如何优雅地处理 UI 交互和命令执行。现在,我们将把目光转向另一个在数据驱动型 UI 开发中同样至关重要的概念:响应式集合 (Reactive Collection)。
在 Unity 开发中,我们经常需要展示和管理动态列表数据,比如背包物品、任务列表、排行榜、聊天记录等。当这些数据发生变化时,我们期望 UI 能够自动更新。传统的做法是手动遍历数据、手动创建/销毁 UI 元素、手动更新显示。这不仅繁琐,而且容易出错,尤其是在数据量大或更新频繁的场景下。
响应式集合正是为了解决这些痛点而生。它们能够将集合的变化(添加、删除、修改、排序等)转化为可观察的事件流,从而让 UI 能够对这些变化做出自动响应。
考虑一个简单的背包系统:
如果我们使用传统的 List
或 Array
,每当数据源发生变化,我们都需要手动:
这种“推倒重建”的方式在数据量大时会带来显著的性能开销,并且代码会变得非常冗长和耦合。
响应式集合 提供了更高效、更声明式的解决方案。它继承了响应式编程的精髓,将集合的变化转换为事件流,让订阅者(例如 UI 列表)能够精准地响应特定的变化,而不是每次都全量刷新。
ReactiveCollection
UniRx 库提供了 ReactiveCollection
,它是 System.Collections.ObjectModel.ObservableCollection
的 UniRx 版本增强。它不仅继承了 ObservableCollection
的 CollectionChanged
事件,更将其包装成了 IObservable
、IObservable
等更细粒度的事件流。
这意味着你可以订阅:
ObserveAdd()
ObserveRemove()
ObserveReplace()
ObserveReset()
ObserveMove()
ObserveEveryValueChanged()
或直接订阅 CollectionChangedAsObservable()
基本用法:
using UniRx;
using UnityEngine;
using System.Collections.Generic; // 为了使用 List
public class Item
{
public string Name;
public ReactiveProperty<int> Count = new ReactiveProperty<int>(1);
public Item(string name, int count = 1)
{
Name = name;
Count.Value = count;
}
}
public class InventorySystem : MonoBehaviour
{
// 使用 ReactiveCollection 来管理物品列表
public ReactiveCollection<Item> Inventory = new ReactiveCollection<Item>();
private void Awake()
{
// 订阅添加物品事件
Inventory.ObserveAdd()
.Subscribe(addEvent =>
{
Debug.Log($"添加物品: {addEvent.Value.Name}, 索引: {addEvent.Index}");
// 还可以订阅新添加物品的 Count 变化
addEvent.Value.Count.Subscribe(count =>
{
Debug.Log($"物品 {addEvent.Value.Name} 的数量变为: {count}");
}).AddTo(this); // 注意生命周期管理
})
.AddTo(this);
// 订阅删除物品事件
Inventory.ObserveRemove()
.Subscribe(removeEvent =>
{
Debug.Log($"移除物品: {removeEvent.Value.Name}, 索引: {removeEvent.Index}");
})
.AddTo(this);
// 订阅清空集合事件
Inventory.ObserveReset()
.Subscribe(_ =>
{
Debug.Log("清空背包");
})
.AddTo(this);
// 测试操作
AddItem("剑");
AddItem("盾牌");
Inventory[0].Count.Value++; // 修改第一个物品的数量
RemoveItem("剑");
ClearInventory();
}
public void AddItem(string name, int count = 1)
{
Inventory.Add(new Item(name, count));
}
public void RemoveItem(string name)
{
Item itemToRemove = Inventory.FirstOrDefault(item => item.Name == name);
if (itemToRemove != null)
{
Inventory.Remove(itemToRemove);
}
}
public void ClearInventory()
{
Inventory.Clear();
}
}
在上面的例子中,我们创建了一个 ReactiveCollection
。每当 Inventory
集合发生添加、删除或清空操作时,相应的 ObserveAdd()
、ObserveRemove()
、ObserveReset()
订阅者就会收到通知。这使得我们可以精确地响应集合的变化,而不需要重新构建整个 UI。
将响应式集合与 UI 列表结合,最常用的模式是 Item Template(物品模板)模式。基本思路是:
GameObject
包含 Text
和 Image
)。ReactiveCollection
发生变化时,根据模板动态创建或销毁 UI 列表项。为了简化这种绑定过程,UniRx 提供了 RectTransform
的扩展方法 BindTo
和 ObserveEveryAdd
结合 Transform
相关的操作。
实战案例:简单的任务列表
假设我们有一个任务系统,需要动态显示任务列表。
步骤:
TaskItem
类,包含 Name
(字符串) 和 IsCompleted
(ReactiveProperty)。GameObject
,包含 Text
(用于显示任务名称) 和 Toggle
(用于显示完成状态)。给这个预制体添加一个脚本,用于处理单个任务项的绑定逻辑。ReactiveCollection
存储任务,并通过代码将集合变化绑定到 UI 列表的父节点。TaskItem.cs
:
using UniRx;
using System;
[Serializable] // 允许在 Inspector 中显示
public class TaskItem
{
public string Name;
public ReactiveProperty<bool> IsCompleted = new ReactiveProperty<bool>(false);
public TaskItem(string name, bool isCompleted = false)
{
Name = name;
IsCompleted.Value = isCompleted;
}
}
TaskListItemUI.cs
(挂载在任务列表项预制体上):
using UnityEngine;
using UnityEngine.UI;
using UniRx;
public class TaskListItemUI : MonoBehaviour
{
public Text taskNameText;
public Toggle completeToggle;
private CompositeDisposable _disposables = new CompositeDisposable(); // 用于管理内部订阅
// 外部调用此方法来绑定数据
public void SetData(TaskItem task)
{
// 清理旧的订阅,防止复用时订阅重复
_disposables.Clear();
// 绑定任务名称
taskNameText.text = task.Name;
// 绑定 Toggle 的值到 IsCompleted
// SubscribeToToggle: 自动将 ReactiveProperty 的值绑定到 Toggle 的 isOn,
// 并在 Toggle 值改变时更新 ReactiveProperty。
task.IsCompleted.SubscribeToToggle(completeToggle).AddTo(_disposables);
// 监听 IsCompleted 变化来更新文本样式(例如,完成时加删除线)
task.IsCompleted.Subscribe(isCompleted =>
{
taskNameText.fontStyle = isCompleted ? FontStyle.Italic : FontStyle.Normal;
taskNameText.color = isCompleted ? Color.gray : Color.black;
}).AddTo(_disposables);
}
private void OnDestroy()
{
_disposables.Dispose(); // 确保在销毁时清理所有订阅
}
}
TaskListManager.cs
(挂载在场景中的某个 GameObject 上,管理整个任务列表):
using UnityEngine;
using UniRx;
using System.Linq; // 用于 FirstOrDefault
public class TaskListManager : MonoBehaviour
{
public ReactiveCollection<TaskItem> Tasks = new ReactiveCollection<TaskItem>();
public RectTransform contentParent; // UI 列表中所有任务项的父节点
public GameObject taskItemPrefab; // 任务列表项的 UI 预制体
public UnityEngine.UI.Button addTaskButton;
public UnityEngine.UI.Button removeTaskButton;
private int _taskCounter = 0; // 用于生成不同的任务名称
private void Awake()
{
// 核心绑定逻辑:ReactiveCollection 的变化自动驱动 UI 列表的创建/销毁
Tasks.ObserveAdd()
.Subscribe(addEvent =>
{
// 创建新的 UI 列表项
GameObject newItem = Instantiate(taskItemPrefab, contentParent);
TaskListItemUI uiComponent = newItem.GetComponent<TaskListItemUI>();
if (uiComponent != null)
{
uiComponent.SetData(addEvent.Value); // 绑定数据到 UI
}
newItem.name = $"TaskItem_{addEvent.Value.Name}"; // 方便在 Hierarchy 中查看
})
.AddTo(this);
Tasks.ObserveRemove()
.Subscribe(removeEvent =>
{
// 查找并销毁对应的 UI 列表项
// 注意:这里需要根据某种标识符来查找,例如原始数据对象或唯一ID
// 更健壮的做法是维护一个数据-UI映射,或者使用 UniRx 的 BindToCollection
// 这里为了演示简单,我们假设可以根据名字查找
GameObject itemToRemove = contentParent.Cast<Transform>()
.FirstOrDefault(child => child.name == $"TaskItem_{removeEvent.Value.Name}")?.gameObject;
if (itemToRemove != null)
{
Destroy(itemToRemove);
}
})
.AddTo(this);
Tasks.ObserveReset()
.Subscribe(_ =>
{
// 清空所有子对象
foreach (Transform child in contentParent)
{
Destroy(child.gameObject);
}
})
.AddTo(this);
// 按钮事件绑定
addTaskButton.OnClickAsObservable()
.Subscribe(_ => AddNewTask())
.AddTo(this);
removeTaskButton.OnClickAsObservable()
.Subscribe(_ => RemoveLastTask())
.AddTo(this);
// 初始化一些任务
Tasks.Add(new TaskItem("学习响应式编程"));
Tasks.Add(new TaskItem("完成游戏原型", true));
}
private void AddNewTask()
{
_taskCounter++;
Tasks.Add(new TaskItem($"新任务 {_taskCounter}"));
}
private void RemoveLastTask()
{
if (Tasks.Any())
{
Tasks.RemoveAt(Tasks.Count - 1);
}
}
}
设置 Unity UI:
contentParent
。taskItemPrefab
的基础,并添加 TaskListItemUI
脚本,将其中的 Text
和 Toggle
关联到脚本的公共变量。确保这个 Panel 是一个预制体。TaskListManager
脚本中,将 contentParent
和 taskItemPrefab
拖拽到 Inspector 中。TaskListManager
的相应公共变量中。通过这种方式,Tasks
集合的任何增删操作,都会自动反映到 UI 列表中。单个任务项的完成状态(IsCompleted
)变化也会自动更新其 Toggle
状态,反之亦然。这大大减少了手动同步数据和 UI 的工作量。
BindToCollection
(扩展库支持)虽然上面的手动订阅方法有效,但 UniRx 提供了一个更高级的绑定方式,可以进一步简化集合与 UI 的同步:BindToCollection
。这个功能通常需要 UniRx.UI
或类似专注于 UI 绑定的扩展库支持。
BindToCollection
允许你指定一个模板,然后 UniRx 会自动管理列表项的创建、更新和销毁。
// 假设你引入了 UniRx.UI 扩展
// 这部分代码只是示意,需要确保你安装了相应的 UniRx UI 绑定扩展包
/*
using UnityEngine;
using UniRx;
using UniRx.UI; // 假定 UniRx.UI 提供了 BindToCollection
public class AdvancedTaskListManager : MonoBehaviour
{
public ReactiveCollection Tasks = new ReactiveCollection();
public RectTransform contentParent;
public GameObject taskItemPrefab;
void Awake()
{
// 这种方式会将 ReactiveCollection 的数据绑定到 UI 列表
// 当集合变化时,会自动创建、更新、销毁对应的 UI 项
Tasks.BindToCollection(contentParent, taskItemPrefab, (item, view) =>
{
// 当一个 TaskItem 绑定到一个 TaskListItemUI 实例时,执行此回调
// item 是 TaskItem 数据,view 是 TaskListItemUI 的 GetComponent 结果
view.GetComponent().SetData(item);
}).AddTo(this);
// ... 其他添加/移除任务的逻辑 ...
}
}
*/
BindToCollection
内部处理了更复杂的优化,例如对象池(Pooling)以减少 Instantiate
和 Destroy
的开销,从而在列表频繁变动时提供更好的性能。对于大规模动态列表,使用 BindToCollection
或自行实现对象池是强烈推荐的做法。
尽管响应式集合极大地简化了开发,但在处理大量数据时,仍需考虑性能:
过度创建/销毁 UI 元素: 如果你的列表有成千上万个项目,并且这些项目会频繁增删,那么每次都 Instantiate
和 Destroy
对应的 UI 元素会带来显著的性能开销。
BindToCollection
内部通常会处理这一优化,或者你可以自行实现。频繁触发的订阅: 如果集合中每个 Item
内部都有 ReactiveProperty
,并且这些 ReactiveProperty
频繁更新,会导致大量订阅事件被触发。
Throttle
、Debounce
等操作符来控制事件触发频率,或者在设计数据结构时,只将真正需要响应式更新的字段设为 ReactiveProperty
。数据结构设计: 对于复杂的数据结构,考虑使用 ReactiveDictionary
。它提供与 ReactiveCollection
类似的事件,但基于键值对操作,适用于需要通过键快速查找和更新数据的场景。
响应式集合 是构建动态、数据驱动型 Unity UI 的核心工具。它将集合的变化抽象为可观察的事件流,让 UI 能够以声明式、高效的方式响应这些变化。通过结合 Item Template 模式和 UniRx 提供的绑定工具,我们可以大大简化列表型 UI 的开发和维护工作。
掌握了 ReactiveProperty
、ReactiveCommand
和 ReactiveCollection
,你已经掌握了 UniRx 在 Unity UI 开发中的三大核心武器。在接下来的教程中,我们将探讨响应式编程如何在 Unity 生命周期管理和资源加载 中发挥作用,帮助你构建更健壮、更不容易泄漏内存的应用程序。
响应式编程入门教程第一节:揭秘 UniRx 核心 - ReactiveProperty - 让你的数据动起来!
响应式编程入门教程第二节:构建 ObservableProperty<T> — 封装 ReactiveProperty 的高级用法
响应式编程入门教程第三节:ReactiveCommand 与 UI 交互
响应式编程入门教程第四节:响应式集合与数据绑定
响应式编程入门教程第五节:Unity 生命周期与资源管理中的响应式编程