响应式编程入门教程第一节:揭秘 UniRx 核心 - ReactiveProperty - 让你的数据动起来!

响应式编程入门教程第二节:构建 ObservableProperty<T> — 封装 ReactiveProperty 的高级用法

响应式编程入门教程第三节:ReactiveCommand 与 UI 交互

响应式编程入门教程第四节:响应式集合与数据绑定

在前面的教程中,我们了解了 ReactiveProperty 如何帮助我们管理和响应数据的变化。现在,我们将步入响应式编程在 Unity UI 交互中的另一个核心角色:ReactiveCommand。它不仅仅是一个简单的命令模式实现,更是将命令的执行与 UI 状态、异步操作以及数据流紧密结合的强大工具。


1. 传统命令模式的局限性与响应式需求

在传统的 Unity UI 开发中,我们经常使用按钮的 onClick.AddListener() 来触发某个操作。当操作逻辑变得复杂,比如需要判断前置条件(玩家是否有足够的金币施放技能)、操作是异步的(网络请求),或者需要根据操作状态更新 UI(按钮禁用、加载动画),我们往往需要写大量的条件判断和状态管理代码。

考虑一个简单的例子:一个技能按钮。

  • 点击后施放技能。
  • 施放技能需要消耗能量。
  • 能量不足时,按钮应该禁用。
  • 技能施放过程中,按钮也应该禁用,并显示冷却时间。

传统实现中,这些逻辑会散布在按钮的点击回调、Update 函数、以及各种状态变量中,导致代码耦合、难以维护和测试。

响应式编程的目标就是解决这类问题:将状态的变化、事件的发生都视为数据流,然后通过操作符对其进行转换、组合和响应。ReactiveCommand 正是这种理念在命令执行上的体现。


2. ReactiveCommand 核心概念

ReactiveCommand 本质上是一个 ICommand 的响应式实现。它包含两个核心功能:

  • 执行命令: 当命令被触发时,执行预设的逻辑。
  • 判断能否执行: 提供一个 IObservable<bool> 流,该流的值决定了命令当前是否可执行。当此流的值变为 false 时,任何绑定到该命令的 UI 元素(如按钮)会自动禁用;变为 true 时则会启用。

让我们看看它的基本构造:

public class SkillSystem : MonoBehaviour
{// ReactiveProperty 用于管理玩家能量public ReactiveProperty<int> PlayerEnergy = new ReactiveProperty<int>(100);// ReactiveCommand 用于施放技能public ReactiveCommand ReleaseSkillCommand { get; private set; }private void Awake(){// 1. 创建 ReactiveCommand// 参数是一个 IObservable<bool>,用于决定命令是否可执行// 这里表示当 PlayerEnergy.Value >= 10 时,命令可执行ReleaseSkillCommand = PlayerEnergy.Select(energy => energy >= 10) // 根据能量值判断是否可施放.ToReactiveCommand(); // 将 IObservable<bool> 转换为 ReactiveCommand// 2. 订阅命令的执行// 当命令被执行时,扣除能量并打印日志ReleaseSkillCommand.Subscribe(_ =>{PlayerEnergy.Value -= 10;Debug.Log("技能施放成功!当前能量:" + PlayerEnergy.Value);}).AddTo(this); // 生命周期管理:当 GameObject 销毁时,自动取消订阅// 3. 订阅命令的可执行状态变化// 这可以用于在控制台观察按钮的禁用启用状态,实际UI绑定后会自动处理ReleaseSkillCommand.CanExecute.Subscribe(canExecute => Debug.Log("技能按钮是否可用: " + canExecute)).AddTo(this);}// 假设这是 UI 按钮的点击事件处理器// 我们会直接将按钮绑定到 ReleaseSkillCommand,所以这个方法通常不需要手动调用public void OnSkillButtonClick(){// 手动执行命令 (通常通过 UI 绑定自动触发)ReleaseSkillCommand.Execute();}
}

在上面的例子中,ReleaseSkillCommand 的可执行性完全由 PlayerEnergy 的值决定。当 PlayerEnergy.Value 低于 10 时,ReleaseSkillCommand.CanExecute 流会发出 false,此时任何绑定到此命令的 UI 元素将自动禁用。


3. UI 按钮绑定与数据驱动

这是 ReactiveCommand 最直观的应用场景。UniRx 提供了方便的扩展方法,可以直接将 ButtonToggle 等 UI 元素绑定到 ReactiveCommand

步骤:

  1. 在 Unity Inspector 中创建一个 UI Button
  2. 确保你的脚本 (SkillSystem) 挂载在一个 GameObject 上。
  3. Button 拖拽到脚本中需要绑定的地方(如果使用 Inspector 绑定)。

代码实现:

using UnityEngine;
using UnityEngine.UI;
using UniRx;
using UniRx.Triggers; // 用于将 UI 事件转换为 Observablepublic class SkillSystemWithUI : MonoBehaviour
{public ReactiveProperty<int> PlayerEnergy = new ReactiveProperty<int>(100);public ReactiveCommand ReleaseSkillCommand { get; private set; }public Button skillButton; // 在 Inspector 中拖拽赋值public Text energyText; // 用于显示能量值的Textprivate void Awake(){// 创建 ReactiveCommandReleaseSkillCommand = PlayerEnergy.Select(energy => energy >= 10).ToReactiveCommand();// 订阅命令的执行逻辑ReleaseSkillCommand.Subscribe(_ =>{PlayerEnergy.Value -= 10;Debug.Log("技能施放成功!当前能量:" + PlayerEnergy.Value);}).AddTo(this);// UI 绑定:将按钮的点击事件与 ReleaseSkillCommand 关联// BindTo 扩展方法会自动处理按钮的禁用启用状态skillButton.onClick.AsObservable() // 将 onClick 事件转换为 Observable.SubscribeWithState(ReleaseSkillCommand, (x, command) => command.Execute()) // 当点击时执行命令.AddTo(this);// 或者更简洁的方式,直接使用 BindTo:// releaseSkillCommand.BindTo(skillButton).AddTo(this);// 注意:BindTo(Button) 会将 Command 的可执行性绑定到 Button 的 Interactable 属性,// 并且 Button 点击时会自动执行 Command。ReleaseSkillCommand.BindTo(skillButton).AddTo(this);// 绑定能量值到 TextPlayerEnergy.SubscribeToText(energyText, energy => $"能量: {energy}").AddTo(this);}
}

skillButton.BindTo(ReleaseSkillCommand).AddTo(this); 这一行中,UniRx 帮我们做了两件事:

  1. ReleaseSkillCommand.CanExecute 的值变化时,自动更新 skillButton.interactable 属性。
  2. skillButton 被点击时,自动调用 ReleaseSkillCommand.Execute()

这样一来,按钮的禁用启用状态完全由 PlayerEnergy 的值驱动,我们无需手动在 Update 或其他地方去修改按钮的 interactable 属性。这就是数据驱动 UI 的魅力!


4. 异步命令与命令的禁用

很多时候,我们的命令执行会涉及到异步操作,比如网络请求、加载资源、播放动画等。ReactiveCommand 能够很好地处理这些异步场景,并且在异步操作进行时,自动将命令标记为不可执行。

核心机制: ToReactiveCommand 的重载方法可以接受一个 IObservable<bool> 作为 canExecute 源,而当命令被执行时,它会内部管理一个 IsExecutingReactiveProperty。当 Execute 被调用时,IsExecuting 变为 true,直到内部订阅的 IObservable 完成(Next 或 Error 或 Complete),IsExecuting 才变为 false。我们可以将这个 IsExecuting 结合到 canExecute 的逻辑中。

using UnityEngine;
using UnityEngine.UI;
using UniRx;
using System;
using System.Threading.Tasks; // 为了使用 Taskpublic class AsyncSkillSystem : MonoBehaviour
{public ReactiveProperty<int> PlayerEnergy = new ReactiveProperty<int>(100);public ReactiveCommand AsyncSkillCommand { get; private set; }public Button asyncSkillButton;public Text energyText;public Text statusText; // 用于显示异步操作状态private void Awake(){// 组合条件:能量充足 并且 当前命令没有在执行var canExecuteSource = PlayerEnergy.Select(energy => energy >= 10);// 创建异步 ReactiveCommand// 注意这里 ToReactiveCommand() 的重载,它会自动跟踪内部异步操作的执行状态AsyncSkillCommand = canExecuteSource.ToReactiveCommand(); // 不传入参数,内部会自动处理 IsExecuting// 订阅命令的执行逻辑 (异步操作)// 注意:这里我们使用 SelectMany 来处理异步操作AsyncSkillCommand.SelectMany(_ => SimulateAsyncTask()) // 当命令执行时,触发异步任务.Subscribe(_ => { /* 异步任务完成 */ },ex => Debug.LogError("异步技能施放失败: " + ex.Message) // 错误处理).AddTo(this);// 监听命令的执行状态 (用于显示加载动画或禁用其他UI)AsyncSkillCommand.IsExecuting.Subscribe(isExecuting =>{statusText.text = isExecuting ? "技能冷却中..." : "准备就绪";asyncSkillButton.interactable = !isExecuting && AsyncSkillCommand.CanExecute.Value; // 确保在异步执行时不禁用按钮}).AddTo(this);// 将 AsyncSkillCommand 绑定到按钮// BindTo 会自动处理 CanExecute 和 IsExecuting 的组合逻辑,// 使得按钮在能量不足或异步操作进行中时自动禁用AsyncSkillCommand.BindTo(asyncSkillButton).AddTo(this);// 绑定能量值到 TextPlayerEnergy.SubscribeToText(energyText, energy => $"能量: {energy}").AddTo(this);}// 模拟一个异步任务,例如网络请求或耗时计算private async UniTask<Unit> SimulateAsyncTask(){PlayerEnergy.Value -= 10;Debug.Log("开始施放异步技能...");await UniTask.Delay(TimeSpan.FromSeconds(2)); // 模拟2秒的延迟Debug.Log("异步技能施放完成!当前能量:" + PlayerEnergy.Value);return Unit.Default; // Unit.Default 表示一个空值,类似于 void}
}

在这个例子中:

  1. AsyncSkillCommand 的可执行性不仅取决于 PlayerEnergy,还隐式地取决于它内部的 IsExecuting 状态。
  2. 当点击按钮触发 AsyncSkillCommand.Execute() 时,SimulateAsyncTask() 会被调用。
  3. SimulateAsyncTask() 执行期间(2秒),AsyncSkillCommand.IsExecuting 会为 true,导致 asyncSkillButton 自动禁用,并且 statusText 显示“技能冷却中…”。
  4. SimulateAsyncTask() 完成后,AsyncSkillCommand.IsExecuting 变回 false,按钮和状态文本恢复正常。

这种处理异步命令的方式极大地简化了状态管理代码,让开发者可以专注于业务逻辑本身。


5. ReactiveCommand 的高级用法与注意事项
  • 组合多个 CanExecute 源: 你可以通过 CombineLatestZip 等操作符,组合多个 IObservable<bool> 来决定一个 ReactiveCommand 的可执行性。例如,一个按钮可能需要同时满足“玩家在线”和“有足够的金币”两个条件才能点击。

    public ReactiveProperty<bool> IsOnline = new ReactiveProperty<bool>(true);
    public ReactiveProperty<int> Gold = new ReactiveProperty<int>(50);private void CreateCombinedCommand()
    {var canExecuteSource = IsOnline.CombineLatest(Gold, (online, gold) => online && gold >= 20);var purchaseCommand = canExecuteSource.ToReactiveCommand();purchaseCommand.Subscribe(_ =>{Gold.Value -= 20;Debug.Log("购买成功!");}).AddTo(this);purchaseCommand.BindTo(GetComponent<Button>()).AddTo(this);
    }
    
  • 指定执行参数: ReactiveCommand<TParam> 允许你在执行命令时传入参数。

    public ReactiveCommand<int> SpendGoldCommand { get; private set; }private void CreateSpendGoldCommand()
    {SpendGoldCommand = PlayerEnergy.Select(energy => energy > 0) // 假设只要有能量就能执行此命令.ToReactiveCommand<int>(); // 指定参数类型为 intSpendGoldCommand.Subscribe(amount =>{PlayerEnergy.Value -= amount;Debug.Log($"花费了 {amount} 能量。当前能量:{PlayerEnergy.Value}");}).AddTo(this);// 可以在 UI 元素的回调中调用:// SpendGoldCommand.Execute(10); // 花费10能量
    }
    
  • 错误处理: 如果命令的订阅链中发生错误,错误会传播并可能导致订阅终止。你可以使用 CatchOnErrorResumeNext 等操作符来处理这些错误,保持命令的健壮性。

  • 生命周期管理: 再次强调,务必使用 AddTo(this)CompositeDisposable 来管理订阅的生命周期,避免内存泄漏。当 GameObject 被销毁时,所有通过 AddTo(this) 添加的订阅都会自动取消。


6. 总结与展望

ReactiveCommand 极大地提升了 Unity UI 交互开发的效率和代码质量。它提供了一种声明式的方式来管理命令的可执行性,优雅地处理了异步操作,并与 UI 元素进行了无缝集成。通过将命令视为数据流的一部分,我们能够构建更加响应式、可维护和可测试的 Unity 应用程序。

在下一篇教程中,我们将探讨 响应式集合(ReactiveCollection/ReactiveDictionary),以及它们如何与 UI 列表(如 ScrollView)结合,实现动态数据的自动绑定和刷新,进一步解锁数据驱动 UI 的潜力。

响应式编程入门教程第一节:揭秘 UniRx 核心 - ReactiveProperty - 让你的数据动起来!

响应式编程入门教程第二节:构建 ObservableProperty<T> — 封装 ReactiveProperty 的高级用法

响应式编程入门教程第三节:ReactiveCommand 与 UI 交互

响应式编程入门教程第四节:响应式集合与数据绑定

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/91304.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/91304.shtml
英文地址,请注明出处:http://en.pswp.cn/diannao/91304.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

500+技术栈覆盖:Web测试平台TestComplete的对象识别技术解析

在用户界面&#xff08;UI&#xff09;测试领域&#xff0c;传统的测试工具往往依赖于XPath或CSS选择器来定位页面元素。然而&#xff0c;在面对动态变化的界面、多语言支持或是跨越多种技术框架的应用时&#xff0c;这些传统方法常导致脚本失效&#xff0c;增加了维护成本。 …

研究人员利用提示注入漏洞绕过Meta的Llama防火墙防护

Trendyol应用安全团队发现了一系列绕过技术&#xff0c;使得Meta的Llama防火墙在面对复杂的提示注入攻击时防护失效。这一发现引发了人们对现有大语言模型&#xff08;LLM&#xff09;安全措施准备情况的担忧&#xff0c;并凸显出在企业日益将大语言模型嵌入工作流程时&#xf…

Shell 脚本系统学习 · 第5篇:多命令顺序执行的三种方式详解(`;`、``、`||`)

在日常的 Linux 运维与脚本编写中&#xff0c;我们经常需要依次执行多条命令。本篇将带你彻底搞懂三种命令顺序执行方式&#xff1a;;、&& 和 ||&#xff0c;并通过实用示例掌握它们的区别与应用场景。一、为什么要了解多命令执行方式&#xff1f; 在实际运维或脚本编写…

K8s存储系统(通俗易懂版)

Kubernetes中存储中有四个重要的概念&#xff1a;Volume、PersistentVolume PV、PersistentVolumeClaim PVC、StorageClass一、存储系统核心概念Volume&#xff08;卷&#xff09;定义&#xff1a;Kubernetes 中最基础的存储单元&#xff0c;用于将外部存储挂载到 Pod 中的容器…

小白学Python,标准库篇——随机库、正则表达式库

一、随机库1.随机生成数值在random库中可以随机生成数值的方法有uniform()、random()、randint()、randrange()等。&#xff08;1&#xff09;uniform()方法uniform(参数1, 参数2)方法用于生成参数1到参数2之间的随机小数&#xff0c;其中参数的类型都为数值类型。示例代码&…

Qt窗口:菜单栏

目录 一、窗口预览 二、菜单栏 快捷键 子菜单 分割线 图标 内存泄露 一、窗口预览 在前面几篇文章中&#xff0c;或者说&#xff0c;Qt初学阶段&#xff0c;接触到的都是QWidget&#xff0c;QWidget指控件&#xff0c;往往作为一个窗口的一部分出现。所谓的窗口&#x…

STM32裸机开发(中断,轮询,状态机)与freeRTOS

裸机&#xff1a;没有操作系统&#xff0c;程序是单流程的&#xff08;比如一个大循环里依次执行各个功能&#xff0c;或者用中断嵌套处理事件&#xff09;。优点是资源占用极少&#xff08;几乎不占 RAM/Flash&#xff09;、执行流程直观&#xff1b;但复杂项目里&#xff0c;…

电脑上如何查看WiFi密码

打开控制面板>点击网络和Internet在查看网络和共享中心找到网络状态和任务点击进去点击连接的WLAN在WLAN状态中点击无线属性在无线网络属性中点击安全&#xff0c;点击显示字符&#xff08;H&#xff09;就可以显示密码了

文心一言4.5企业级部署实战:多模态能力与Docker容器化测评

随着大语言模型在企业服务中的应用日益广泛&#xff0c;如何选择一款既能满足多模态创作需求&#xff0c;又具备良好企业级适配性的AI模型成为了关键问题。文心一言4.5作为百度最新开源的大模型&#xff0c;不仅在传统的文本处理上表现出色&#xff0c;更是在多模态理解和企业级…

VUE Promise基础语法

目录 异步和同步 异步的问题 new Promise语法 promise的状态 promise.then() Promise.resolve() Promise.reject() Promise.all() Promise.race() Promise.catch() Promise.finally() 异步和同步 同步模式下&#xff0c;代码按顺序执行&#xff0c;前一条执行完毕后…

用TensorFlow进行逻辑回归(六)

import tensorflow as tfimport numpy as npfrom tensorflow.keras.datasets import mnistimport time# MNIST数据集参数num_classes 10 # 数字0到9, 10类num_features 784 # 28*28# 训练参数learning_rate 0.01training_steps 1000batch_size 256display_step 50# 预处…

【HTTP版本演变】

在浏览器中输入URL并按回车之后会发生什么1. 输入URL并解析输入URL后&#xff0c;浏览器会解析出协议、主机、端口、路径等信息&#xff0c;并构造一个HTTP请求&#xff08;浏览器会根据请求头判断是否又HTTP缓存&#xff0c;并根据是否有缓存决定从服务器获取资源还是使用缓存…

Android 16系统源码_窗口动画(一)窗口过渡动画层级图分析

一 窗口过渡动画 1.1 案例效果图1.2 案例源码 1.2.1 添加权限 (AndroidManifest.xml) <!-- 系统悬浮窗权限&#xff08;Android 6.0需动态请求&#xff09; --> <uses-permission android:name"android.permission.SYSTEM_ALERT_WINDOW" />1.2.2 窗口显示…

腾讯云WAF域名分级防护实战笔记

基于业务风险等级、合规要求及腾讯云最佳实践&#xff0c;提供可直接落地的配置方案&#xff0c;供学习借鉴&#xff1a;一、域名分级与防护原则1. ​域名分级清单&#xff08;核心资产&#xff09;​​​主域名​​业务类型​​风险等级​​合规要求​​防护等级​example.com…

1. 请说出你知道的水平垂直居中的方法

总结 容器 flex 布局&#xff0c;jsutify-content: center; align-items: center;容器 flex 布局&#xff0c;子项 margin: auto;容器 relative 布局&#xff0c;子项 absolute 布局&#xff0c;left: 50%; top: 50%; transform: translate(-50%, -50%);子项 absolute 布局&…

VS Code `launch.json` 完整配置指南:参数详解 + 配置实例

文章目录&#x1f4e6; 一、基本结构&#x1f50d; 二、单个配置项详解示例配置&#xff1a;&#x1f9e9; 三、字段说明与可选值&#x1f4c1; 四、常用变量&#xff08;宏替换&#xff09;&#x1f6e0;️ 五、常见配置实例1️⃣ 调试当前打开的 .py 文件2️⃣ 调试 Jupyter …

使用浏览器inspect调试wx小程序

edge://inspect/#devices调试wx小程序 背景&#xff1a; 在开发混合项目的过程中&#xff0c;常常需要在app环境排查问题&#xff0c;接口可以使用fiddler等工具来抓包&#xff0c;但是js错误就不好抓包了&#xff0c;这里介绍一种调试工具-浏览器。 调试过程 首先电脑打开edg…

【论文阅读】-《Simple Black-box Adversarial Attacks》

简单黑盒对抗攻击 Chuan Guo Jacob R. Gardner Yurong You Andrew Gordon Wilson Kilian Q. Weinberger 摘要 我们提出了一种在黑盒&#xff08;black-box&#xff09;场景下构建对抗样本&#xff08;adversarial images&#xff09;的极其简单的方法。与白盒&#xff08;…

基于ASP.NET+SQL Server实现(Web)企业进销存管理系统

企业进销存管理系统的设计和实现一、摘要进销存管理是现代企业生产经营中的重要环节&#xff0c;是完成企业资源配置的重要管理工作&#xff0c;对企业生产经营效率的最大化发挥着重要作用。本文以我国中小企业的进销存管理为研究对象&#xff0c;描述了企业进销存管理系统从需…

(LeetCode 面试经典 150 题 ) 15. 三数之和 (排序+双指针)

题目&#xff1a;15. 三数之和 思路&#xff1a;排序双指针&#xff0c;时间复杂度0(n^2nlogn)。 先将数组nums升序排序&#xff0c;方便去重和使用双指针。第一层for循环来枚举第一位数&#xff0c;后面使用双指针来找到第二个、第三个数即可&#xff0c;细节看注释。 C版本…