133 lines
4.9 KiB
Markdown
133 lines
4.9 KiB
Markdown
# 响应式编程模块
|
||
|
||
**脚本入口:** `eve.Rx()`
|
||
|
||
参考 [UniRx](https://github.com/neuecc/UniRx) 提供基于推送的响应式流:`Subject` 家族、
|
||
LINQ 风格操作符、`ReactiveProperty`,以及把现有事件队列桥接为 Observable 流的能力。
|
||
|
||
## 基本用法
|
||
|
||
```squirrel
|
||
local rx = eve.Rx();
|
||
local clicks = rx.newSubject();
|
||
|
||
// 订阅(返回 Subscription,可取消)
|
||
local sub = clicks.subscribe(function(v) {
|
||
print("click at " + v);
|
||
});
|
||
|
||
// 推送
|
||
clicks.onNext("(100, 200)");
|
||
clicks.onNext("(300, 50)");
|
||
|
||
// 不再需要时取消订阅
|
||
sub.dispose();
|
||
```
|
||
|
||
## 对象关系与调用时机
|
||
|
||
`Rx` 是工厂模块:`newSubject()` / `newBehaviorSubject(v)` / `newReplaySubject(n)` /
|
||
`newProperty(v)` 创建流对象。流对象本身不绑定到帧循环——游戏代码负责在合适的时机
|
||
调用 `onNext` / `setValue`,订阅回调在调用线程同步执行。`fromEvent` + `pump` 提供与
|
||
`event` 模块的桥接:`pump(ev)` 排空事件队列并把匹配名称的消息推给对应流。
|
||
|
||
## 目标导向指南
|
||
|
||
### 解耦生产者与消费者
|
||
|
||
生产者持有 `Subject` 并调用 `onNext`;消费者持有同一个对象并 `subscribe`。双方无需
|
||
互相引用,取消订阅即解除关联。适合按键、点击、资源加载完成等事件广播。
|
||
|
||
### 用操作符链做数据变换
|
||
|
||
```squirrel
|
||
local numbers = rx.newSubject();
|
||
numbers
|
||
.filter(function(v) { return v % 2 == 0; })
|
||
.map(function(v) { return v * 10; })
|
||
.subscribe(function(v) { print("result " + v); });
|
||
numbers.onNext(1); // 不输出
|
||
numbers.onNext(2); // result 20
|
||
```
|
||
|
||
### 观察可变状态
|
||
|
||
`ReactiveProperty` 既是读取入口也是可订阅流:状态变化自动通知订阅者,适合 UI 绑定、
|
||
模型字段监听等场景。
|
||
|
||
```squirrel
|
||
local hp = rx.newProperty(100);
|
||
hp.subscribe(function(v) { print("hp -> " + v); });
|
||
hp.set(80); // 打印 hp -> 80
|
||
print(hp.get()); // 80
|
||
```
|
||
|
||
### 桥接事件队列
|
||
|
||
```squirrel
|
||
local ev = eve.PlatformEvent();
|
||
local rx = eve.Rx();
|
||
local quest = rx.fromEvent("quest-complete");
|
||
quest.subscribe(function(data) { print("quest done: " + data); });
|
||
|
||
// 帧循环或任何时机排空事件队列,匹配的消息进入对应流
|
||
rx.pump(ev);
|
||
```
|
||
|
||
## 常见问题
|
||
|
||
- 忘记 `dispose()`:订阅者会一直留在流里接收通知。持订阅对象并在不再需要时 dispose。
|
||
- 在 worker 线程推送:回调在 `onNext` 的调用线程同步执行;脚本闭包只能在主线程被调用,
|
||
worker 请通过 `thread.postMain` 回主线程后再推送。
|
||
- 把 `ReactiveProperty` 当普通变量:写 `set()` 会触发通知,若只是临时状态请用局部变量。
|
||
- 混淆 `subscribe` 与 `subscribe3`:前者只有 onNext 回调;后者是
|
||
`subscribe3(onNext, onError, onCompleted)`,全部必填(不需要就传 `null`)。
|
||
|
||
## API 快查
|
||
|
||
下列方法名来自当前 Squirrel 绑定;同一模块创建的辅助对象的方法也列在这里。
|
||
|
||
**模块 `Rx`:**
|
||
|
||
- `newSubject()` → `Subject`
|
||
- `newBehaviorSubject(v)` → `BehaviorSubject`
|
||
- `newReplaySubject(capacity)` → `ReplaySubject`
|
||
- `newProperty(v)` → `ReactiveProperty`
|
||
- `fromEvent(name)` → `Observable`
|
||
- `pump(event)`:排空事件队列,把匹配 `fromEvent` 的消息推给对应流
|
||
|
||
**`Observable` / `Subject` / `BehaviorSubject` / `ReplaySubject`:**
|
||
|
||
- `subscribe(onNext)` → `Subscription`
|
||
- `subscribe3(onNext, onError, onCompleted)` → `Subscription`
|
||
- `map(fn)` → `Observable`
|
||
- `filter(pred)` → `Observable`
|
||
- `take(n)` → `Observable`
|
||
- `skip(n)` → `Observable`
|
||
- `first()` → `Observable`
|
||
- `distinctUntilChanged()` → `Observable`
|
||
- `Subject` 额外:`onNext(v)` / `onError(msg)` / `onCompleted()` / `hasObservers()`
|
||
- `BehaviorSubject` 额外:`getValue()` / `setValue(v)`
|
||
- `ReactiveProperty`:`get()` / `set(v)` / `subscribe` / `subscribe3`
|
||
|
||
**`Subscription`:**
|
||
|
||
- `dispose()` / `isDisposed()`
|
||
|
||
## 使用要点
|
||
|
||
- 流对象和订阅对象应保存在全局或实体状态中,不要在每帧重复创建。
|
||
- 默认回调在当前线程同步执行;跨线程需求先回到主线程再推送。
|
||
- 参数约束、默认值和返回类型以对应模块头文件及 `addFunc` 绑定为准;本文 API 快查与当前源码同步生成。
|
||
|
||
## 引擎内的 Editor 观察会话
|
||
|
||
Editor automation 直接复用本模块的 C++ `Subject` 和 `distinctUntilChanged` 管线,实现 MCP
|
||
`eve_editor_observe_start/poll/close`。Editor 持有会话、流和订阅,是生命周期唯一所有者;MCP 的
|
||
poll 只在主线程采样 live runtime 并推送事件,不复制场景或材质权威状态。连续相同 JSON 事件不会
|
||
重复交给 Agent,provider 丢失或 stale identity 则作为可观察的状态变化返回。使用方必须显式 close;
|
||
target 或引擎对象仍按原所有权销毁,关闭观察会话不会销毁被观察对象。
|
||
|
||
**源码:** [`src/modules/rx/`](../../../src/modules/rx/)
|
||
**相关测试:** [`test/rx.cpp`](../../../test/rx.cpp)
|