R3:適用于 .NET 的新一代響應式擴展庫,事件訂閱流

R3:適用于 .NET 的新一代響應式擴展庫

R3 是 dotnet/reactive(.NET 官方響應式擴展)與 UniRx(適用于 Unity 的響應式擴展)的新一代替代方案,支持多種平臺,包括 Unity、Godot、Avalonia、WPF、WinForms、WinUI3、Stride、LogicLooper、MAUI、MonoGame、Blazor 和 Uno。

我擁有超過 10 年的 Rx 使用經驗,不僅為游戲引擎實現過自定義 Rx 運行時(UniRx),還為游戲引擎開發過異步運行時(UniTask)。基于這些經驗,我認為有必要為 .NET 實現一款全新的響應式擴展庫,它既要能體現現代 C# 的特性,又要回歸 Rx 的核心價值。

核心設計理念

R3 的設計源于對傳統 Rx 局限性的反思,核心理念如下:

  1. 在 OnError 處中斷管道是錯誤設計:傳統 Rx 中,管道遇到異常會觸發 OnError 并自動取消訂閱,這在事件處理場景中過于嚴苛,且難以通過 Retry 等操作符優雅恢復。
  2. IScheduler 是性能瓶頸的根源:傳統 Rx 的 IScheduler 抽象層導致性能損耗,且內部實現復雜(如通過 PeriodicTimer 和 IStopwatch 規避問題)。
  3. 基于幀的操作是游戲引擎的關鍵需求:傳統 Rx 缺乏幀循環適配能力,而游戲引擎(如 Unity、Godot)中,基于幀的事件處理(如每幀更新、延遲 N 幀執行)至關重要。
  4. 單一異步操作應完全交給 async/await:Rx 應專注于事件流處理,而非替代 async/await 處理單個異步任務(如網絡請求、文件讀寫)。
  5. 不實現同步 API:同步操作無需 Rx 抽象,避免冗余設計。
  6. 查詢語法(Query Syntax)僅適用于 SQL:C# 的 LINQ 查詢語法(如?from x in xs select x)在 Rx 中使用體驗不佳,應優先使用方法鏈語法。
  7. 訂閱列表是防止內存泄漏的必要手段:需提供類似 “并行調試器” 的訂閱跟蹤能力,解決 GUI 或游戲等長生命周期應用的內存泄漏問題。
  8. 背壓(Backpressure)交給 IAsyncEnumerable 和 Channels:Rx 無需重復實現背壓機制,可復用 .NET 原生組件。
  9. 分布式處理與查詢有更優方案:GraphQL、Kubernetes、Orleans、Akka.NET、gRPC、MagicOnion 等工具更適合分布式場景,Rx 應專注于內存內消息處理(LINQ to Events)

與傳統 Rx 的核心差異

為解決 dotnet/reactive 的不足,R3 對核心接口進行了重構。近年來,Kotlin Flow、Swift Combine 等面向現代語言特性的 Rx 類框架已成為標準,而 C# 也已演進至 C# 12,因此 R3 旨在打造一款與最新 C# 特性對齊的響應式擴展庫。

1. 性能優化

性能提升是 R3 重構的核心目標之一,以下是關鍵優化點:

  • 移除 IScheduler 帶來的性能提升:傳統 Rx 的 IScheduler 導致大量冗余計算,R3 改用 .NET 8 引入的 TimeProvider,性能顯著提升(如下圖,Observable.Range(1, 10000).Subscribe()?的執行效率對比)。
  • Subject 訂閱 / 取消的內存優化:傳統 Rx 的 Subject 使用 ImmutableArray 存儲訂閱者,每次添加 / 刪除訂閱都會分配新數組,導致內存頻繁波動。R3 采用自定義數據結構,避免 ImmutableArray 帶來的性能損耗(如下圖,10000 次 subject.Subscribe() + 10000 次 subscription.Dispose()?的內存分配對比)。

2. 核心接口重構

R3 的表面 API 與傳統 Rx 保持一致(便于遷移),但內部接口完全重構,不再依賴?IObservable<T>/IObserver<T>

2.1 新接口定義

傳統 Rx 的?IObservable<T>/IObserver<T>?雖在理論上優雅(與?IEnumerable<T>?對偶),但實際使用中存在諸多限制。R3 改用抽象類實現,確保行為可控:

csharp

using System;namespace R3
{/// <summary>/// 可觀察對象抽象類,替代傳統 IObservable<T>/// </summary>/// <typeparam name="T">事件流中數據的類型</typeparam>public abstract class Observable<T>{/// <summary>/// 訂閱可觀察對象,返回可取消訂閱的 IDisposable/// </summary>/// <param name="observer">觀察者對象,用于接收事件</param>/// <returns>用于取消訂閱的 IDisposable 實例</returns>public abstract IDisposable Subscribe(Observer<T> observer);}/// <summary>/// 觀察者抽象類,替代傳統 IObserver<T>,并實現 IDisposable 用于資源釋放/// </summary>/// <typeparam name="T">接收數據的類型</typeparam>public abstract class Observer<T> : IDisposable{/// <summary>/// 接收正常事件數據/// </summary>/// <param name="value">事件數據</param>public abstract void OnNext(T value);/// <summary>/// 接收異常事件(不會中斷訂閱)/// 區別于傳統 Rx 的 OnError,此處異常不會自動取消訂閱,需手動處理/// </summary>/// <param name="error">異常對象</param>public abstract void OnErrorResume(Exception error);/// <summary>/// 接收事件流完成通知(成功/失敗)/// 合并傳統 Rx 的 OnCompleted 和 OnError,用 Result 表示完成狀態/// </summary>/// <param name="result">完成結果,包含成功/失敗狀態</param>public abstract void OnCompleted(Result result);/// <summary>/// 釋放觀察者占用的資源/// </summary>public abstract void Dispose();}/// <summary>/// 事件流完成結果,區分成功/失敗狀態/// </summary>public readonly struct Result{/// <summary>/// 是否為失敗狀態/// </summary>public bool IsFailure { get; }/// <summary>/// 失敗時的異常對象(成功時為 null)/// </summary>public Exception? Exception { get; }/// <summary>/// 創建成功狀態的 Result/// </summary>/// <returns>成功狀態的 Result 實例</returns>public static Result Success() => new Result(false, null);/// <summary>/// 創建失敗狀態的 Result/// </summary>/// <param name="exception">失敗對應的異常</param>/// <returns>失敗狀態的 Result 實例</returns>public static Result Failure(Exception exception) => new Result(true, exception);private Result(bool isFailure, Exception? exception){IsFailure = isFailure;Exception = exception;}}
}
2.2 關鍵差異點
特性傳統 RxR3
異常處理OnError 觸發后自動取消訂閱OnErrorResume 觸發后不取消訂閱,需手動控制
完成通知OnCompleted(成功)+ OnError(失敗)OnCompleted (Result) 合并兩種狀態
接口類型接口(IObservable/IObserver)抽象類(Observable/Observer)
訂閱跟蹤無原生支持內置訂閱列表,支持泄漏檢測

3. 訂閱管理與內存泄漏防護

訂閱泄漏是長生命周期應用(如 GUI、游戲)的常見問題。R3 通過以下設計解決該問題:

  • 觀察者與訂閱綁定:訂閱時,Observer 會自動與目標 Observable 關聯,并作為 Subscription 實例(避免傳統 Rx 中額外的 IDisposable 分配)。
  • 全鏈路訂閱跟蹤:Observer 從上游到下游形成可靠的鏈路,確保 OnCompleted/Dispose 時能釋放所有資源。
  • ObservableTracker 工具:可啟用訂閱跟蹤功能,查看所有活躍訂閱的狀態(如創建時間、調用棧),便于定位泄漏。

csharp

using R3;
using System;// 啟用訂閱跟蹤(默認關閉)
ObservableTracker.EnableTracking = true;
// 啟用調用棧捕獲(性能損耗較高,調試時使用)
ObservableTracker.EnableStackTrace = true;// 創建一個示例訂閱
using var subscription = Observable.Interval(TimeSpan.FromSeconds(1)).Where(x => true).Take(10000).Subscribe();// 遍歷所有活躍訂閱,打印狀態
ObservableTracker.ForEachActiveTask(trackingState =>
{Console.WriteLine($"跟蹤ID: {trackingState.TrackingId}");Console.WriteLine($"類型: {trackingState.FormattedType}");Console.WriteLine($"創建時間: {trackingState.AddTime}");Console.WriteLine($"調用棧: {trackingState.StackTrace}");Console.WriteLine("------------------------");
});

核心功能特性

1. TimeProvider 替代 IScheduler

傳統 Rx 的 IScheduler 存在性能問題且設計復雜,R3 改用 .NET 8 引入的?TimeProvider?作為時間抽象,支持以下能力:

  • 異步時間操作:通過?TimeProvider.CreateTimer()TimeProvider.GetTimestamp()?實現高效的時間控制。
  • 平臺適配:默認使用?TimeProvider.System(基于線程池),同時提供平臺專用實現(如 WPF 的?DispatcherTimeProvider、Unity 的?UpdateTimeProvider)。
  • 測試友好:支持?FakeTimeProvider(來自?Microsoft.Extensions.TimeProvider.Testing),便于單元測試中模擬時間流逝。
1.1 時間相關操作符示例

csharp

using R3;
using System;// 1. 每隔 1 秒發送一個事件(使用默認 TimeProvider)
var interval = Observable.Interval(TimeSpan.FromSeconds(1));// 2. 延遲 2 秒后發送事件(指定自定義 TimeProvider,如 WPF 的 DispatcherTimeProvider)
var delay = Observable.Return(42).Delay(TimeSpan.FromSeconds(2), new DispatcherTimeProvider());// 3. 防抖操作(300ms 內無新事件則發送最后一個事件)
var debounce = Observable.FromEvent<EventArgs>(addHandler: h => button.Click += (s, e) => h(e),removeHandler: h => button.Click -= (s, e) => h(e)).Debounce(TimeSpan.FromMilliseconds(300), TimeProvider.System);

2. 基于幀的操作(FrameProvider)

游戲引擎和 GUI 應用依賴幀循環,R3 引入?FrameProvider?抽象層,提供與幀相關的操作符,支持以下場景:

  • 每幀執行:如?EveryUpdate()?每幀發送一個事件。
  • 延遲 N 幀執行:如?DelayFrame(5)?延遲 5 幀后發送事件。
  • 幀防抖 / 節流:如?DebounceFrame(10)?10 幀內無新事件則發送。
2.1 幀操作符示例

csharp

using R3;
using UnityEngine; // 以 Unity 為例,其他平臺類似// 1. 每幀發送事件(使用 Unity 的 Update 幀循環)
Observable.EveryUpdate(UnityFrameProvider.Update).Subscribe(_ =>{// 每幀更新角色位置player.transform.Translate(Vector3.forward * Time.deltaTime);});// 2. 延遲 3 幀后執行(例如:玩家死亡后 3 幀顯示游戲結束界面)
player.OnDeathAsObservable().DelayFrame(3, UnityFrameProvider.Update).Subscribe(_ =>{UIManager.ShowGameOver();});// 3. 幀防抖(避免按鈕快速點擊觸發多次事件)
button.OnClickAsObservable().DebounceFrame(2, UnityFrameProvider.Update).Subscribe(_ =>{// 處理按鈕點擊(2 幀內多次點擊僅觸發一次)SubmitRequest();});// 4. 監聽屬性變化(每幀檢查屬性值,無 INotifyPropertyChanged 也可使用)
Observable.EveryValueChanged(this, x => x.PlayerHealth, UnityFrameProvider.Update).Subscribe(health =>{// 更新生命值顯示healthText.Text = $"HP: {health}";});

3. Subject 與 ReactiveProperty

R3 提供 5 種 Subject 類型,滿足不同事件分發場景,且默認在 Dispose 時觸發 OnCompleted,確保訂閱自動釋放。

Subject 類型用途說明
Subject<T>基礎事件分發器,無狀態,僅分發訂閱后的事件。
BehaviorSubject<T>持有最新值,新訂閱者會立即收到當前值。
ReactiveProperty<T>繼承 BehaviorSubject,支持去重(相同值不觸發通知),適合數據綁定。
ReplaySubject<T>緩存指定數量 / 時間范圍內的事件,新訂閱者會收到歷史事件。
ReplayFrameSubject<T>基于幀緩存事件(如緩存最近 10 幀的事件),適合游戲幀同步場景。
3.1 ReactiveProperty 示例(數據綁定與驗證)

csharp

using R3;
using System;
using System.ComponentModel.DataAnnotations;// 1. 響應式模型定義(以游戲角色為例)
public class Enemy
{// 生命值(ReactiveProperty 自動去重,值變化時觸發通知)public ReactiveProperty<long> CurrentHp { get; }// 是否死亡(由 CurrentHp 推導,自動同步狀態)public ReadOnlyReactiveProperty<bool> IsDead { get; }public Enemy(int initialHp){CurrentHp = new ReactiveProperty<long>(initialHp);// 從 CurrentHp 派生 IsDead(當生命值 ≤ 0 時為 true)IsDead = CurrentHp.Select(hp => hp <= 0).ToReadOnlyReactiveProperty();}
}// 2. 帶驗證的 ReactiveProperty(如限制數值范圍)
public sealed class ClampedReactiveProperty<T> : ReactiveProperty<T> where T : IComparable<T>
{private readonly T _min;private readonly T _max;private static readonly IComparer<T> _comparer = Comparer<T>.Default;// 構造函數:初始化時限制值在 [min, max] 范圍內public ClampedReactiveProperty(T initialValue, T min, T max): base(initialValue, EqualityComparer<T>.Default, callOnValueChangeInBaseConstructor: false){_min = min;_max = max;// 手動修正初始值(確保符合范圍)OnValueChanging(ref GetValueRef());}// 重寫值變更前的邏輯,強制限制范圍protected override void OnValueChanging(ref T value){if (_comparer.Compare(value, _min) < 0){value = _min; // 小于最小值時強制設為最小值}else if (_comparer.Compare(value, _max) > 0){value = _max; // 大于最大值時強制設為最大值}}
}// 3. XAML 數據綁定(以 WPF 為例)
public class PlayerViewModel
{// 可寫屬性(用于綁定輸入控件,如 Slider)public ReactiveProperty<int> Level { get; } = new ReactiveProperty<int>(1);// 只讀屬性(用于綁定顯示控件,如 TextBlock)public ReadOnlyReactiveProperty<string> LevelText { get; }public PlayerViewModel(){// 派生 LevelText(格式化為 "等級:X")LevelText = Level.Select(level => $"等級:{level}").ToReadOnlyReactiveProperty();}
}
3.2 Subject 自動釋放示例

R3 的 Subject 在 Dispose 時會自動觸發 OnCompleted,確保所有訂閱者收到完成通知并釋放資源:

csharp

using R3;
using System;var subject = new Subject<string>();// 訂閱 Subject
var subscription = subject.Subscribe(onNext: msg => Console.WriteLine($"收到消息:{msg}"),onCompleted: result => Console.WriteLine(result.IsFailure ? $"失敗:{result.Exception.Message}" : "成功完成")
);// 發送消息
subject.OnNext("Hello R3");// 釋放 Subject(會觸發 OnCompleted)
subject.Dispose();// 此時訂閱已自動取消,后續發送消息不會被接收
subject.OnNext("This message is ignored");

4. 靈活的 Disposable 管理

R3 提供多種 Disposable 組合方式,滿足不同場景的性能與靈活性需求,性能從高到低排序如下:

  1. `Disposable.Combine(d1, d2,..., d8):適用于已知數量(≤8個)的訂閱,內部使用字段存儲,性能最優。 2.?Disposable.CreateBuilder():適用于動態數量但構建時已知的訂閱,Builder 為值類型,無內存分配。 3.?Disposable.Combine(params IDisposable[]):適用于動態數量的訂閱,內部使用數組存儲。 4.?DisposableBag:適用于動態添加的訂閱,輕量級值類型,非線程安全。 5.?CompositeDisposable`:支持動態添加 / 移除,線程安全,但性能最低。
4.1 各 Disposable 用法示例

csharp

using R3;
using System;
using System.Reactive.Disposables;// 1. Disposable.Combine(已知3個訂閱,性能最優)
public class Example1
{private IDisposable _disposables;public void Initialize(){var d1 = Observable.EveryUpdate().Subscribe(_ => Console.WriteLine("Update 1"));var d2 = Observable.EveryUpdate().Subscribe(_ => Console.WriteLine("Update 2"));var d3 = Observable.EveryUpdate().Subscribe(_ => Console.WriteLine("Update 3"));// 組合3個訂閱,Dispose時會釋放所有_disposables = Disposable.Combine(d1, d2, d3);}public void Cleanup(){_disposables?.Dispose();}
}// 2. Disposable.CreateBuilder(動態添加但構建時確定數量)
public class Example2
{private IDisposable _disposables;public void Initialize(){var builder = Disposable.CreateBuilder();// 動態添加訂閱(數量在構建時確定)Observable.EveryUpdate().Subscribe(_ => Console.WriteLine("Builder 1")).AddTo(ref builder);Observable.EveryUpdate().Subscribe(_ => Console.WriteLine("Builder 2")).AddTo(ref builder);Observable.EveryUpdate().Subscribe(_ => Console.WriteLine("Builder 3")).AddTo(ref builder);// 構建組合訂閱_disposables = builder.Build();}public void Cleanup(){_disposables?.Dispose();}
}// 3. DisposableBag(動態添加,非線程安全)
public class Example3
{// DisposableBag 是值類型,無需 new,也不要拷貝private DisposableBag _disposableBag;public void Initialize(){// 初始添加訂閱Observable.EveryUpdate().Subscribe(_ => Console.WriteLine("Bag 1")).AddTo(ref _disposableBag);Observable.EveryUpdate().Subscribe(_ => Console.WriteLine("Bag 2")).AddTo(ref _disposableBag);}// 動態添加(如按鈕點擊時)public void OnButtonClick(){Observable.EveryUpdate().Subscribe(_ => Console.WriteLine("Dynamic Bag")).AddTo(ref _disposableBag);}public void Cleanup(){_disposableBag.Dispose();}
}// 4. CompositeDisposable(支持移除,線程安全)
public class Example4
{private CompositeDisposable _compositeDisposable = new CompositeDisposable();private IDisposable _dynamicSubscription;public void Initialize(){// 添加固定訂閱_compositeDisposable.Add(Observable.EveryUpdate().Subscribe(_ => Console.WriteLine("Composite 1")));_compositeDisposable.Add(Observable.EveryUpdate().Subscribe(_ => Console.WriteLine("Composite 2")));// 保存動態訂閱,后續可移除_dynamicSubscription = Observable.EveryUpdate().Subscribe(_ => Console.WriteLine("Dynamic Composite"));_compositeDisposable.Add(_dynamicSubscription);}// 移除動態訂閱public void RemoveDynamicSubscription(){_compositeDisposable.Remove(_dynamicSubscription);}public void Cleanup(){_compositeDisposable.Dispose();}
}

5. 與 async/await 深度集成

R3 針對 async/await 做了專門優化,支持異步操作符(如?SelectAwaitWhereAwait),并提供靈活的異步執行策略(如串行、并行、丟棄、切換)。

5.1 異步操作符示例(避免按鈕重復點擊)

csharp

using R3;
using System;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;// 按鈕點擊后發起網絡請求,使用 AwaitOperation.Drop 避免重復請求
public class AsyncExample : MonoBehaviour
{public Button RequestButton;public Text ResultText;private void Start(){RequestButton.OnClickAsObservable()// 異步轉換:發起網絡請求.SelectAwait(async (_, ct) =>{// 使用 UnityWebRequest 發起請求,并支持取消using var request = UnityWebRequest.Get("https://api.example.com/data");var operation = request.SendWebRequest();// 綁定取消令牌(當訂閱取消或完成時取消請求)using (ct.Register(() => operation.Abort())){await operation;}if (request.result != UnityWebRequest.Result.Success){throw new Exception($"請求失敗:{request.error}");}return request.downloadHandler.text;}, // 執行策略:當異步操作未完成時,丟棄新的點擊事件awaitOperation: AwaitOperation.Drop,// 訂閱完成時取消正在進行的異步操作cancelOnCompleted: true)// 訂閱結果并更新UI.Subscribe(result => ResultText.text = $"結果:{result}",error => ResultText.text = $"錯誤:{error.Message}")// 將訂閱綁定到當前 MonoBehaviour,銷毀時自動釋放.AddTo(this);}
}
5.2 異步執行策略(AwaitOperation)
策略行為說明
Sequential所有事件排隊,下一個事件等待前一個異步操作完成。
Drop異步操作執行期間,丟棄新的事件(如避免重復點擊)。
Switch新事件到來時,取消前一個未完成的異步操作,立即執行新操作(如搜索聯想)。
Parallel所有事件立即執行異步操作,不限制并發數。
SequentialParallel異步操作并行執行,但結果按事件順序傳遞給下一個操作符。
ThrottleFirstLast異步操作執行期間,僅保留第一個和最后一個事件(如批量處理)。

6. 單元測試支持

R3 提供完善的單元測試工具,支持時間 / 幀模擬,結合?LiveList?可輕松驗證事件流結果。

6.1 時間模擬測試(使用 FakeTimeProvider)

csharp

using R3;
using Microsoft.Extensions.TimeProvider.Testing;
using Xunit;
using Shouldly;public class TimeProviderTests
{[Fact]public void Timer_Should_Trigger_After_Delay(){// 1. 創建虛假時間提供器var fakeTime = new FakeTimeProvider();// 2. 創建事件流:5秒后發送一個事件var timer = Observable.Timer(TimeSpan.FromSeconds(5), fakeTime);// 3. 將事件流轉換為 LiveList,便于斷言var results = timer.ToLiveList();// 斷言:提前4秒,事件未觸發fakeTime.Advance(TimeSpan.FromSeconds(4));results.AssertIsNotCompleted();results.AssertEmpty();// 斷言:再提前1秒(總計5秒),事件觸發fakeTime.Advance(TimeSpan.FromSeconds(1));results.AssertIsCompleted();results.AssertEqual([Unit.Default]);}
}
6.2 幀模擬測試(使用 FakeFrameProvider)

csharp

using R3;
using System;
using Xunit;
using Shouldly;public class FrameProviderTests
{[Fact]public void EveryUpdate_Should_Trigger_Per_Frame(){// 1. 創建虛假幀提供器var fakeFrameProvider = new FakeFrameProvider();var cts = new CancellationTokenSource();// 2. 創建事件流:每幀發送當前幀計數var everyUpdate = Observable.EveryUpdate(fakeFrameProvider, cts.Token).Select(_ => fakeFrameProvider.GetFrameCount());var results = everyUpdate.ToLiveList();// 斷言:初始狀態無事件results.AssertEmpty();// 斷言:推進1幀,收到幀計數0fakeFrameProvider.Advance();results.AssertEqual([0]);// 斷言:推進3幀,收到幀計數1、2、3fakeFrameProvider.Advance(3);results.AssertEqual([0, 1, 2, 3]);// 斷言:取消令牌,事件流完成cts.Cancel();results.AssertIsCompleted();// 斷言:繼續推進幀,無新事件fakeFrameProvider.Advance();results.AssertEqual([0, 1, 2, 3]);}
}// 測試輔助擴展方法
public static class LiveListExtensions
{public static void AssertEqual<T>(this LiveList<T> list, params T[] expected){list.ShouldBe(expected);}public static void AssertEmpty<T>(this LiveList<T> list){list.Count.ShouldBe(0);}public static void AssertIsCompleted<T>(this LiveList<T> list){list.IsCompleted.ShouldBeTrue();}public static void AssertIsNotCompleted<T>(this LiveList<T> list){list.IsCompleted.ShouldBeFalse();}
}

平臺支持

R3 支持多種 .NET 平臺,核心庫無需額外配置即可使用,平臺專用擴展需安裝對應 NuGet 包,并替換?TimeProvider/FrameProvider?以適配平臺特性。

1. 平臺支持列表

平臺NuGet 包名稱核心適配點
WPFR3Extensions.WPF提供?WpfDispatcherTimeProvider(UI 線程時間)、WpfRenderingFrameProvider(渲染幀)。
AvaloniaR3Extensions.Avalonia提供?AvaloniaDispatcherTimeProviderAvaloniaRenderingFrameProvider(基于渲染事件)。
UnoR3Extensions.Uno提供?UnoDispatcherTimeProviderUnoRenderingFrameProvider(跨平臺 UI 適配)。
MAUIR3Extensions.Maui提供?MauiDispatcherTimeProviderMauiTickerFrameProvider(基于 Ticker 幀循環)。
WinFormsR3Extensions.WinForms提供?WinFormsTimeProviderWinFormsFrameProvider(基于消息循環)。
WinUI3R3Extensions.WinUI3提供?WinUI3DispatcherTimeProviderWinUI3RenderingFrameProvider
UnityR3 + R3.Unity提供?UnityTimeProvider(支持 Update/FixedUpdate 等生命周期)、UnityFrameProvider
GodotR3 + R3.Godot提供?GodotTimeProvider(Process/PhysicsProcess)、GodotFrameProvider
StrideR3Extensions.Stride提供?StrideTimeProviderStrideFrameProvider(游戲引擎幀循環)。
MonoGameR3Extensions.MonoGame提供?MonoGameTimeProviderMonoGameFrameProvider(基于 Game.Update)。
LogicLooperR3Extensions.LogicLooper提供?LogicLooperTimeProviderLogicLooperFrameProvider(循環邏輯適配)。
BlazorR3Extensions.Blazor適配 Blazor 同步上下文,避免 UI 線程阻塞。

2. 平臺初始化示例(以 Unity 為例)

Unity 平臺需安裝兩個包:核心?R3?和 Unity 專用擴展?R3.Unity,步驟如下:

  1. 通過 NuGetForUnity 安裝?R3(搜索 “R3” 并安裝)。
  2. 引用 Git 地址安裝?R3.Unityhttps://github.com/Cysharp/R3.git?path=src/R3.Unity/Assets/R3.Unity
  3. (可選)指定版本:https://github.com/Cysharp/R3.git?path=src/R3.Unity/Assets/R3.Unity#1.0.0
2.1 Unity 平臺核心用法

csharp

using R3;
using R3.Unity;
using UnityEngine;
using UnityEngine.UI;public class UnityExample : MonoBehaviour
{public Button AttackButton;public Text HpText;public Slider HpSlider;// 響應式屬性:角色生命值(初始1000)private ReactiveProperty<long> _currentHp = new ReactiveProperty<long>(1000);// 派生屬性:是否死亡(生命值 ≤ 0)private ReadOnlyReactiveProperty<bool> _isDead;private void Awake(){// 初始化派生屬性_isDead = _currentHp.Select(hp => hp <= 0).ToReadOnlyReactiveProperty();}private void Start(){// 1. 綁定生命值到 UI(自動同步更新)_currentHp.Subscribe(hp =>{HpText.text = $"HP: {hp}";HpSlider.value = hp;}).AddTo(this);// 2. 綁定死亡狀態到按鈕(死亡后禁用攻擊按鈕)_isDead.Subscribe(isDead =>{AttackButton.interactable = !isDead;if (isDead){HpText.text = "已死亡";}}).AddTo(this);// 3. 綁定按鈕點擊事件(每點擊一次減少100生命值)AttackButton.OnClickAsObservable().Subscribe(_ => _currentHp.Value -= 100).AddTo(this);// 4. 每幀檢測生命值變化(無 INotifyPropertyChanged 也可使用)Observable.EveryValueChanged(this, x => x.transform.position, UnityFrameProvider.Update).Subscribe(pos => Debug.Log($"當前位置: {pos}")).AddTo(this);// 5. 固定幀執行(如物理邏輯)Observable.EveryUpdate(UnityFrameProvider.FixedUpdate).Subscribe(_ =>{// 物理相關邏輯(如碰撞檢測)}).AddTo(this);}
}

與傳統 Rx 的兼容性

R3 提供與?IObservable<T>(傳統 Rx)的雙向轉換,便于逐步遷移現有項目。

1. 轉換方法

轉換方向方法說明
IObservable<T> → R3.Observable<T>ToObservable()(擴展方法)將傳統 Rx 的可觀察對象轉換為 R3 的 Observable<T>。
R3.Observable<T> → IObservable<T>AsSystemObservable()(擴展方法)將 R3 的 Observable<T> 轉換為傳統 Rx 的 IObservable<T>。
1.1 轉換示例

csharp

using R3;
using System.Reactive.Linq;
using System.Reactive.Subjects;public class CompatibilityExample
{public void ConvertFromSystemRx(){// 1. 創建傳統 Rx 的 Subjectvar systemSubject = new Subject<int>();// 2. 轉換為 R3 的 Observable<int>var r3Observable = systemSubject.ToObservable();// 3. 使用 R3 的操作符處理r3Observable.Where(x => x % 2 == 0).Delay(TimeSpan.FromSeconds(1)).Subscribe(x => Console.WriteLine($"R3 處理結果:{x}"));// 4. 發送事件(傳統 Rx 側發送,R3 側接收)systemSubject.OnNext(1); // 被過濾(奇數)systemSubject.OnNext(2); // 被 R3 接收并延遲1秒輸出}public void ConvertToSystemRx(){// 1. 創建 R3 的 Observablevar r3Observable = Observable.Interval(TimeSpan.FromSeconds(1));// 2. 轉換為傳統 Rx 的 IObservable<Unit>var systemObservable = r3Observable.AsSystemObservable();// 3. 使用傳統 Rx 的操作符處理systemObservable.Take(5).Subscribe(_ => Console.WriteLine("傳統 Rx 接收事件"),() => Console.WriteLine("傳統 Rx 事件流完成"));}
}

許可證

R3 基于?MIT 許可證

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/95075.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/95075.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/95075.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Android Framework打電話禁止播放運營商視頻彩鈴

文章目錄定位Android電話的源碼及UI禁止打電話時播放運營商廣告視頻彩鈴運營商視頻彩鈴framework禁止播放視頻彩鈴需求&#xff1a;打電話時&#xff0c;對方未接聽&#xff0c;這個時候可能會播放運營商的視頻彩鈴&#xff0c;需求是屏蔽彩鈴播放。測試平臺&#xff1a;展銳。…

WebIDEPLOY 賦能數字校園建設:智慧管理系統的效能升級與實踐路徑 —— 以校園資源協同優化構建高效教育生態的探索

一、教育數字化轉型中的現實困境&#xff1a;從 "管理孤島" 到 "效率瓶頸"教育數字化轉型的加速推進&#xff0c;讓智慧校園建設成為高校提升核心競爭力的關鍵抓手。但當前校園物聯網應用中&#xff0c;一系列痛點逐漸凸顯&#xff1a;設備管理呈現 "…

開源AI大模型AI智能名片S2B2C商城小程序賦能下的“信息找人“:人工智能驅動的線下零售精準化革命

摘要&#xff1a;在人工智能技術深度滲透零售行業的背景下&#xff0c;線下零售場景正經歷從"人找信息"到"信息找人"的范式轉變。本文聚焦開源AI大模型、AI智能名片與S2B2C商城小程序的技術融合&#xff0c;系統分析其在客戶定位、行為分析、精準營銷等環節…

【第三方網站運行環境測試:服務器配置(如Nginx/Apache)的WEB安全測試重點】

服務器配置安全測試是WEB安全評估的關鍵&#xff0c;一般關注信息泄露、傳輸安全、訪問控制及資源防護等方面。信息泄露控制 檢查服務器響應頭是否暴露敏感信息。Server頭應去除Nginx/Apache詳細版本號&#xff0c;防止攻擊者針對特定版本漏洞進行利用。錯誤頁面需自定義&#…

【Hot100】15.三數之和

解法&#xff1a;排序 雙指針首先對數組排序&#xff0c;便于后面處理重復元素。第一層循環遍歷數組中的每一個元素&#xff0c;作為三元組中的第一個元素 nums[i] &#xff0c;并跳過重復的元素。對于每個 i &#xff0c;使用雙指針 l &#xff08;初始為 i1&#xff09;和 r…

Flutter 本地持久化存儲:Hive 與 SharedPreferences 實戰對比

在移動應用開發中&#xff0c;本地持久化存儲是必不可少的功能。無論是保存用戶登錄狀態、應用配置&#xff0c;還是緩存數據&#xff0c;合理選擇存儲方案都能提高應用的性能與用戶體驗。在 Flutter 中&#xff0c;常用的本地存儲方式主要有兩種&#xff1a;SharedPreferences…

Lombok 實用注解深度解析!

目錄一、AllArgsConstructor&#xff1a;全參數構造函數生成器1. 基本概念2. 使用示例3. 高級特性4. 注意事項二、RequiredArgsConstructor&#xff1a;必需參數構造函數生成器1. 基本概念2. 使用示例3. 高級特性4. 注意事項三、SneakyThrows&#xff1a;異常處理"偷懶&qu…

Go+Gdal 完成高性能GIS數據空間分析

概要 環境準備 技術流程 一、在golang中如何調用gdal 二、讀取數據 三、執行空間分析 四、性能提升 小結 概要 Gdal庫可以說是所有gis軟件的基礎&#xff0c;基本上現在所有的工業gis軟件都是基于gdal開發的&#xff0c;其主要包括了柵格處理、矢量處理、坐標系處理所涉及的各類…

【python】python進階——Lambda 函數

目錄 引言 一、簡介 1.1 基本語法 1.2 優勢 1.3 局限性 二、基本用法 2.1 無參數lambda 函數 2.2 多參數 lambda 函數 三、常見使用場景 3.1 與高階函數配合使用 3.2 作為排序鍵 3.3 在 GUI 編程中作為回調函數 3.4 在 Pandas 中的應用 四、高級技巧 4.1 條件表…

基于單片機電動車充電樁/充電車棚環境監測設計

傳送門 &#x1f449;&#x1f449;&#x1f449;&#x1f449;其他作品題目速選一覽表 &#x1f449;&#x1f449;&#x1f449;&#x1f449;其他作品題目功能速覽 概述 隨著電動車普及&#xff0c;充電樁的環境安全監測成為重要課題。基于單片機的電動車充電樁環境檢…

Linux初始——編譯器gcc

編譯器gcc編譯器編譯器自舉動靜態庫動靜態庫的差異gcc編譯器 眾所周知&#xff0c;代碼運行的前提是經過四個步驟的 預處理&#xff0c;其進行宏替換&#xff0c;去注釋&#xff0c;條件編譯&#xff0c;頭文件展開的工作&#xff0c;在gcc的選項中對應gcc -E&#xff0c;其就…

Three.js + AI預測:在數字孿生中實現數據可視化智能決策

某智慧工廠的數字孿生系統曾陷入尷尬&#xff1a;3D 模型里的生產線數據實時跳動&#xff0c;卻沒人能預判 “2 小時后哪臺機器會停機”。這就像有了高清監控&#xff0c;卻不會分析監控畫面 ——Three.js 做出的可視化是 “眼睛”&#xff0c;AI 預測才是 “大腦”。不少團隊用…

刀客doc:亞馬遜持續猛攻程序化廣告

文/刀客doc(頭條深一度精選作者)一7月的尾聲和8月的開端&#xff0c;廣告市場見證了兩場截然不同的場面。7月31日&#xff0c;亞馬遜公布了截至6月30日的2025年第二季度財報。廣告業務表現尤為亮眼&#xff1a;單季收入達到157億美元&#xff0c;同比增長約22%&#xff0c;成為…

政府網站IPv6檢測怎么做?檢測指標有哪些?

隨著信息技術的飛速發展&#xff0c;IPv6作為下一代互聯網的核心協議&#xff0c;已成為全球互聯網發展的必然趨勢。我國政府高度重視IPv6的規模部署和應用推廣&#xff0c;明確要求各級政府網站必須完成IPv6改造&#xff0c;以提升網絡基礎設施的現代化水平&#xff0c;增強網…

有N個控制點的三次B樣條曲線轉化為多段三階Bezier曲線的方法

將具有N 個控制點的三次B樣條曲線轉換為多段三階Bezier曲線&#xff0c;是計算機圖形學和CAD系統中常見的操作。這種轉換基于B樣條曲線的局部性質以及其與Bezier曲線之間的關系。基本原理三次B樣條曲線由一組控制點 P?, P?, ..., P??? 和一個節點向量 U {u?, u?, ..., …

chrome好用的瀏覽器插件

https://ad.infread.com/?utm_sourcebaidu_sem&utm_mediumweb_pc&utm_campaignkeywords_website_translate&bd_vid2831968530895394443 目前我自己覺得比較用的谷歌瀏覽器翻譯插件->沉浸式翻譯 個人覺得無論時速度還是準確度都是比較好的

k8s---prometheus 監控

目錄 環境準備 下載 kube-prometheus 軟件包 下載prometheus 鏡像 master節點 master節點導入prometheus軟件包 解壓 node節點 node節點導入鏡像 解壓 從tar包中加載鏡像 部署 prometheus 修改映射端口 提交 查看pod pod和svc正常啟動 deployment daemonset se…

華大時空組學空轉圖像處理

華大時空組學空轉圖像處理 library(png) library(tiff) st <- readRDS(01.Stereo-seq/output_all/Demo_Mouse_Kidney/outs/feature_expression/seurat_out.rds) dim(stassays$Spatialcounts) stassays$Spatialcounts[1:4,1:4] coord.df <- data.frame(imagerow st$x, im…

如何在SptingBoot項目中引入swagger生成API文檔

目錄 背景介紹&#xff0c;swagger的必要性 swagger的引入&#xff1a; 1.首先我們需要在 pom.xml文件中導入jar包 2.給swagger創建一個配置類&#xff1a; 3.為實體類添加注解 4.為controller添加注解 背景介紹&#xff0c;swagger的必要性 自從在2005年前端工程師誕生之…

GD32入門到實戰21--輸入捕獲

我們新建capture_drv.c#include <stdint.h> #include <stdio.h> #include "gd32f30x.h" #include "delay.h"static void GpioInit(void) {rcu_periph_clock_enable(RCU_GPIOA);gpio_init(GPIOA,GPIO_MODE_IN_FLOATING,GPIO_OSPEED_10MHZ,GPIO…