响应式编程入门教程第一节:揭秘 UniRx 核心 - ReactiveProperty - 让你的数据动起来!-CSDN博客
响应式编程入门教程第二节:构建 ObservableProperty<T> — 封装 ReactiveProperty 的高级用法-CSDN博客
在上一篇中,我们详细探讨了 UniRx 的核心组件 ReactiveProperty<T>
,了解了它如何让数据变化自动通知订阅者,从而简化了数据绑定和状态管理。ReactiveProperty<T>
能够告诉我们“值变了,新值是什么”,这在很多场景下都非常有用。
然而,在实际的游戏开发中,我们经常会遇到这样的需求:不仅想知道“值变了”,还想知道“值是从什么变成了什么”——也就是说,我们希望在收到通知时,能够同时获取到旧值 (Old Value) 和新值 (New Value)。
例如,当玩家血量从 50 变为 30 时,我们可能需要播放一个“受伤”的音效;当从 10 变为 0 时,则需要触发“死亡”动画。如果只有新值,我们通常需要额外记录旧值,并进行比较。这虽然不难,但如果每个需要新旧值的地方都重复这些逻辑,代码就会变得冗余。
这就是我的框架中 ObservableProperty<T>
类诞生的原因。它正是为了优雅地解决这个痛点,在 ReactiveProperty<T>
的基础上,提供一个功能更强大的“可观察属性”。那么现在我们来聊聊ObservableProperty<T>类是如何设计的、以及如何使用的。
ObservableProperty
的设计思想
ObservableProperty<T>
的核心目标是:在 ReactiveProperty<T>
强大的数据通知能力之上,额外提供对旧值的追踪,并将旧值和新值同时传递给订阅者。
为了实现这一点,它的设计思想可以概括为以下几点:
内部封装:
ObservableProperty<T>
会在内部私有地持有一个ReactiveProperty<T>
实例。所有的值设置和变化通知,仍然由这个内部的ReactiveProperty<T>
来驱动。旧值记录: 引入一个私有变量
_oldValue
,专门用于保存属性上一次的值。定制订阅行为: 在提供给外部的
Subscribe
方法中,我们会对内部ReactiveProperty<T>
的通知流进行一些巧妙的操作:过滤初始值: 使用 UniRx 的操作符
Skip(1)
,跳过ReactiveProperty<T>
在订阅时立即发射的那个初始值通知,确保我们只关心真正的“变化”。注入旧值: 当
ReactiveProperty<T>
实际发生变化时,将我们记录的_oldValue
和当前变化的newVal
一同传递给订阅者。
自动更新旧值: 在每次值变化并成功通知订阅者后,立即将
_oldValue
更新为当前的newVal
,为下一次变化做好准备。
ObservableProperty<T>
的实现与解析
现在,让我们来详细剖析这个类的代码实现,理解它是如何将上述设计思想变为现实的:
using UniRx; // 引入 UniRx 库,提供 ReactiveProperty 和各种操作符
using System; // 引入 System 命名空间,提供 Action 和 IDisposable
/// <summary>
/// 一个包装 ReactiveProperty 的类,支持旧值记录与变化事件
/// 使得订阅时能同时获取到属性的旧值和新值。
/// </summary>
public class ObservableProperty<T>
{
private ReactiveProperty<T> _rp; // 内部封装的 ReactiveProperty 实例,负责底层的通知机制
private T _oldValue; // 用于记录属性上一次的值
/// <summary>
/// 构造函数,初始化 ObservableProperty。
/// 旧值和当前值都将首先被设置为 initialValue。
/// </summary>
/// <param name="initialValue">属性的初始值。</param>
public ObservableProperty(T initialValue)
{
_oldValue = initialValue; // 初始时,旧值就等于我们传入的初始值
_rp = new ReactiveProperty<T>(initialValue); // 内部的 ReactiveProperty 也用初始值进行初始化
}
/// <summary>
/// 属性的当前值。读写此属性将间接操作内部的 ReactiveProperty。
/// </summary>
public T Value
{
get => _rp.Value; // 获取当前值,等同于获取内部 ReactiveProperty 的值
set => _rp.Value = value; // 设置值时,会自动触发内部 _rp 的通知机制
}
/// <summary>
/// 订阅属性的变化事件。回调函数将同时接收到旧值和新值。
/// </summary>
/// <param name="onChanged">当属性值变化时触发的回调函数,第一个参数是旧值,第二个参数是新值。</param>
/// <returns>一个 IDisposable 对象,用于取消订阅,防止内存泄漏。</returns>
public IDisposable Subscribe(Action<T, T> onChanged)
{
return _rp
.Skip(1) // UniRx 操作符:跳过 ReactiveProperty 在订阅时立即发射的第一个(初始)值。
// 我们只关心真正的“变化”发生时的通知。
.Subscribe(newVal => // 订阅 _rp 后续的值变化
{
// 调用传入的回调函数,同时提供我们记录的 _oldValue 和当前最新的 newVal
onChanged?.Invoke(_oldValue, newVal);
// 核心逻辑:通知完成后,更新 _oldValue 为当前的新值,为下一次变化做准备
_oldValue = newVal;
});
}
}
关键点解析:
private T _oldValue;
: 这个私有变量是整个ObservableProperty
实现旧值记录的关键。它就像一个记忆装置,总能记住上一次的值。构造函数
ObservableProperty(T initialValue)
: 在这里,_oldValue
和内部的_rp
都被赋予了相同的initialValue
。这确保了在属性的生命周期开始时,所有状态都是同步的。Value
属性: 这是一个简单的封装层。我们通过它来访问和修改内部_rp.Value
。当你set
新值时,_rp
会自动触发它的通知机制。public IDisposable Subscribe(Action<T, T> onChanged)
: 这是这个类的核心对外接口。Action<T, T> onChanged
: 注意这个委托的签名。它明确地表示你的回调函数需要接收两个T
类型的参数,这正是我们期望的旧值和新值。.Skip(1)
: 这是 UniRx 中一个非常常用的操作符。它的作用是“跳过序列中的第一个元素”。为什么需要跳过呢?因为ReactiveProperty
在被订阅时,会“礼貌性地”立即发送一次它当前的值。但在ObservableProperty
的上下文中,我们通常只关心“值从 A 变为 B”这种变化,而不是初始状态的通知。Skip(1)
确保了onChanged
回调只在值真正改变之后才被调用。onChanged?.Invoke(_oldValue, newVal);
: 当内部_rp
的值发生变化时,这个 Lambda 表达式会被执行。我们在这里调用了用户传入的onChanged
回调函数,并巧妙地将_oldValue
(我们之前记录的上一次的值) 和newVal
(当前最新的值) 一同传递了过去。?.Invoke
是 C# 6.0 引入的空条件运算符,它确保只有当onChanged
不为null
时才调用Invoke
,防止潜在的空引用异常。_oldValue = newVal;
: 这一步是整个旧值追踪逻辑的关键! 在通知了所有订阅者之后,我们将_oldValue
更新为当前的newVal
。这样,当属性在未来再次发生变化时,_oldValue
就能准确地代表它前一个值,从而确保了整个机制的正确性。
ObservableProperty<T>
的使用示例
现在,让我们通过一个具体的 Unity 游戏场景来展示 ObservableProperty<T>
的实际用法,看看它如何让你的代码更清晰、更强大:
using UnityEngine;
using UniRx; // 别忘了在你的脚本顶部加上这个
using System; // 也需要这个,因为 Action 和 IDisposable 在这里
public class PlayerStatusManager : MonoBehaviour
{
// 定义一个 ObservableProperty<int> 来追踪玩家的当前血量,初始值为100
public ObservableProperty<int> CurrentHealth = new ObservableProperty<int>(100);
// 定义一个 ObservableProperty<bool> 来追踪玩家是否处于中毒状态
public ObservableProperty<bool> IsPoisoned = new ObservableProperty<bool>(false);
void Start()
{
Debug.Log("--- 游戏开始,初始化玩家状态 ---");
// 1. 订阅玩家血量变化
// 注意,我们的回调函数 now 可以同时接收 oldHealth (旧血量) 和 newHealth (新血量)
CurrentHealth.Subscribe((oldHealth, newHealth) =>
{
Debug.Log($"玩家血量变化:从 {oldHealth} 变为 {newHealth}");
// 根据新旧值,进行更精细的逻辑判断和表现
if (newHealth <= 0 && oldHealth > 0) // 从活着到死亡
{
Debug.Log("<color=red>玩家死亡!触发游戏结束逻辑。</color>");
// 比如:播放死亡动画,显示游戏结束界面
}
else if (newHealth < oldHealth) // 血量减少(受到伤害)
{
Debug.Log("玩家受到了伤害,播放受击音效或显示伤害数字。");
// 比如:GetComponent<AudioSource>().PlayOneShot(damageSound);
// 或:显示血条减少动画
}
else if (newHealth > oldHealth) // 血量增加(受到治疗)
{
Debug.Log("玩家获得治疗,播放治疗特效或显示治疗数字。");
// 比如:Instantiate(healEffectPrefab, transform.position, Quaternion.identity);
}
}).AddTo(this); // 使用 AddTo(this) 管理订阅生命周期,避免内存泄漏
// 2. 订阅中毒状态变化
IsPoisoned.Subscribe((oldState, newState) =>
{
Debug.Log($"玩家中毒状态变化:从 {oldState} 变为 {newState}");
if (newState && !oldState) // 从未中毒变为中毒
{
Debug.Log("<color=green>玩家中毒了!开始持续掉血。</color>");
// 可以在这里启动一个中毒DOT (Damage Over Time) 协程
}
else if (!newState && oldState) // 从中毒变为解毒
{
Debug.Log("<color=cyan>玩家解毒了!停止持续掉血。</color>");
// 可以在这里停止中毒DOT协程
}
}).AddTo(this);
// 模拟游戏中的事件
Invoke("TakeDamage", 2f); // 2秒后受到伤害
Invoke("ApplyPoison", 4f); // 4秒后中毒
Invoke("Heal", 6f); // 6秒后回血
Invoke("TakeFatalDamage", 8f); // 8秒后受到致命伤害
}
void TakeDamage()
{
Debug.Log("\n--- 模拟:玩家受到攻击 ---");
CurrentHealth.Value -= 30; // 改变血量,会自动触发订阅
}
void ApplyPoison()
{
Debug.Log("\n--- 模拟:玩家中毒 ---");
IsPoisoned.Value = true; // 改变中毒状态,会自动触发订阅
}
void Heal()
{
Debug.Log("\n--- 模拟:玩家获得治疗 ---");
CurrentHealth.Value += 20; // 改变血量
}
void TakeFatalDamage()
{
Debug.Log("\n--- 模拟:玩家受到致命攻击 ---");
CurrentHealth.Value = 0; // 改变血量,触发死亡
}
// 当这个 MonoBehaviour 被销毁时,所有通过 AddTo(this) 注册的订阅都会自动被 Dispose。
}
为什么选择 ObservableProperty<T>
?
通过 ObservableProperty<T>
,我们获得了以下显著优势:
更丰富的变化信息: 同时获得旧值和新值,让你的逻辑判断更加精准和灵活,尤其适用于需要根据变化方向或幅度执行不同行为的场景。
更简洁的订阅代码: 无需在每次订阅时手动处理
Skip(1)
和旧值记录的逻辑,ObservableProperty<T>
已经为你将这些细节封装起来。统一的接口: 无论什么类型的数据(
int
,string
,bool
,enum
甚至自定义类),你都可以用统一的Subscribe(Action<T, T>)
接口来监听其变化,提高代码的可读性和一致性。数据驱动逻辑: 鼓励你使用数据变化来驱动游戏逻辑,而不是依赖传统的
Update()
或复杂的事件链,从而构建更清晰、更模块化、更具响应性的架构。UniRx 生态集成: 作为 UniRx 家族的一员,
ObservableProperty<T>
能够无缝地与其他 UniRx 操作符(如Where
,Select
,Throttle
,Debounce
等)结合使用,进一步增强其处理复杂数据流的能力。
总结
ReactiveProperty<T>
是 UniRx 库中一个非常强大和实用的概念,是构建响应式系统的基石。而 ObservableProperty<T>
则是对其能力的进一步拓展和封装,它完美解决了在数据变化时同时获取旧值和新值的需求,使得基于数据变化的逻辑处理更加优雅和高效。
希望这篇笔记能帮助你深入理解 ObservableProperty<T>
的设计思想、实现原理和实际应用。将其融入你的 Unity 框架中,你将能够构建更加健壮、可维护且响应迅速的游戏系统。
如果你对 UniRx 的其他高级操作符感兴趣,或者想了解如何将 ObservableProperty<T>
应用到更复杂的场景中,比如结合 MVVM 模式进行 UI 绑定,欢迎在评论区留言讨论!
响应式编程入门教程第一节:揭秘 UniRx 核心 - ReactiveProperty - 让你的数据动起来!-CSDN博客
响应式编程入门教程第二节:构建 ObservableProperty<T> — 封装 ReactiveProperty 的高级用法-CSDN博客