在计时器上生成事件时在值之间切换



我正在XNA概念验证中使用Rx,在编写一些查询时遇到了一些障碍,我希望你们能帮助我理解其中一些运算符是如何工作的。

在我的POC中,我希望玩家的分数只在没有主动拖动操作的情况下增加。此外,还有一个"抓取量表",只要有持续的拖动,我就想耗尽它,如果没有,我就填充它。最后,如果拖动操作正在进行,抓取量表降至0以下,我想取消拖动操作。

我的分数递增工作得很好:

 IObservable<bool> PrincessGrabbed; // e.g., OnGrabbedBegin
 _playerScoreChanged = IObservable<Unit>
// ... //
// In the initialization method
_playerScoreChanged = from startTrigger in PrincessGrabbed.StartWith(false)
                                                          .Where(x => !x)
                      from i in Observable.Interval(TargetElapsedTime)
                                          .TakeUntil(PrincessGrabbed
                                                       .Where(x => x)
                      select new Unit(); 
_playerScoreChanged.Subscribe(unit => PlayerScore += 1);

分数将在应该增加的时候增加,并在角色被拾取时停止。然而,让仪表行为正常工作一直很麻烦。我已经尝试了使用WindowGenerate等的大量变体……但最终似乎发生的是,要么它根本不起作用,要么增量/减量量表操作最终相互冲突,要么它看起来都能正常工作,但继续在后台减去或添加点/量表。以下是仪表的实现(性能极差,大约10-15秒后崩溃,无法正常工作):

var a = from startTrigger in PrincessGrabbed.StartWith(false).Where(x => x)
                from i in Observable.Interval(TargetElapsedTime)
                    .Where(x => GrabGaugeFillAmount > 0)
                    .TakeUntil(PrincessGrabbed.Where(x => !x))
                select new Unit();
a.TimeInterval().Subscribe(unit => 
            GrabGaugeFillAmount -= (float)unit.Interval.TotalSeconds * 
                GrabGaugeDepletionPerSecond);

我毫不怀疑,我对Rx的理解不足在某种程度上、形状或形式上是错误的,但我已经达到了尝试不同运算符/查询的极限。有什么见解吗?

<小时>

旁白:Gideon Engelberth的回答符合我的需求——我希望我能投10倍的赞成票!以下是他的答案的快速C#表示(在IDisposable.Dispose()上不是100%,但应该很接近):

public class AlternatingSubject : IDisposable
{
    private readonly object _lockObj = new object();
    private int _firstTriggered;
    private readonly ISubject<Unit> _first = new Subject<Unit>();
    public ISubject<Unit> First { get { return _first; }}
    private readonly ISubject<Unit> _second = new Subject<Unit>();
    public ISubject<Unit> Second { get { return _second; }}
    public void TriggerFirst()
    {
        if (System.Threading.Interlocked.Exchange(ref _firstTriggered, 1) == 1)
            return;
        First.OnNext(Unit.Default);
    }
    public void TriggerSecond()
    {
        if (System.Threading.Interlocked.Exchange(ref _firstTriggered, 0) == 0)
            return;
        Second.OnNext(Unit.Default);
    }
    #region Implementation of IDisposable
    public void Dispose()
    {
        lock (_lockObj)
        {
            First.OnCompleted();
            Second.OnCompleted();
        }
    }
    #endregion
}

以及连接游戏类中事件的逻辑(存在一些重构机会)。总结:作品魅力十足!谢谢

public class PrincessCatcherGame : Game
{
    // ... //
    public IObservable<bool> PrincessGrabbed // external source fires these events
    { 
        get 
        { 
            return princessGrabbed.AsObservable();
        }
    } 
    // ... //
    private readonly ISubject<bool> _princessGrabbed = new Subject<bool>();
    private readonly ISubject<Unit> _grabGaugeEmptied = new Subject<Unit>();
    private readonly ISubject<Unit> _grabGaugeFull = new Subject<Unit>();
    private readonly AlternatingSubject _alternatingSubject = new AlternatingSubject();
    private ISubject<Unit> _grabs;
    private ISubject<Unit> _releases;
    // ... //
    private void SubscribeToGrabbedEvents()
    {
        var decrements = from g in _grabs
                         from tick in Observable.Interval(TargetElapsedTime).TakeUntil(_releases)
                         select Unit.Default;
        decrements.Subscribe(x =>
                                 {
                                     Debug.Assert(GrabGaugeFillAmount >= 0);
                                     GrabGaugeFillAmount -= (GrabGaugeDepletionPerSecond/30f);
                                     if (GrabGaugeFillAmount <= 1)
                                     {
                                         GrabGaugeFillAmount = 0;
                                         _alternatingSubject.TriggerSecond();
                                         _grabGaugeEmptied.OnNext(Unit.Default);
                                     }
                                 });
        decrements.Subscribe(x => PlayerScore += 1);
        var increments = from r in _releases
                         from tick in Observable.Interval(TargetElapsedTime).TakeUntil(_grabs.Merge(_grabGaugeFull))
                         select Unit.Default;
        increments.Subscribe(x =>
                                 {
                                     Debug.Assert(GrabGaugeFillAmount <= 100);
                                     GrabGaugeFillAmount += (GrabGaugeFillPerSecond/30f);
                                     if (GrabGaugeFillAmount >= 100)
                                     {
                                         GrabGaugeFillAmount = 100;
                                         _grabGaugeFull.OnNext(Unit.Default);
                                     }
                                 });
    }

你肯定走在了正确的轨道上。我会从制作它们自己的抓取和释放可观察性开始,然后基于这两个可观察器制作PrincessGrabbed。对于这样的情况,我使用一个称为AlternatingSubject的类。

Public NotInheritable Class AlternatingSubject
    Implements IDisposable
    'IDisposable implementation left out for sample
    Private _firstTriggered As Integer
    Private ReadOnly _first As New Subject(Of Unit)()
    Public ReadOnly Property First As IObservable(Of Unit)
        Get
            Return _first
        End Get
    End Property
    Private ReadOnly _second As New Subject(Of Unit)()
    Public ReadOnly Property Second As IObservable(Of Unit)
        Get
            Return _second
        End Get
    End Property
    Public Sub TriggerFirst()
        If System.Threading.Interlocked.Exchange(_firstTriggered, 1) = 1 Then Exit Sub
        _first.OnNext(Unit.Default)
    End Sub
    Public Sub TriggerSecond()
        If System.Threading.Interlocked.Exchange(_firstTriggered, 0) = 0 Then Exit Sub
        _second.OnNext(Unit.Default)
    End Sub
End Class

除此之外,您可能还想添加一个可以从递增方法触发的"gague full"可观察对象。"gague empty"将触发AlternatingSubject的释放部分。

Sub Main()
    Dim alt As New AlternatingSubject
    Dim grabs = alt.First
    Dim releases = alt.Second
    Dim isGrabbed As New Subject(Of Boolean)()
    'I assume you have these in your real app, 
    'simulate them with key presses here
    Dim mouseDowns As New Subject(Of Unit)
    Dim mouseUps As New Subject(Of Unit)
    Dim gagueFulls As New Subject(Of Unit)()
    'the TakeUntils ensure that the timers stop ticking appropriately
    Dim decrements = From g In grabs
                     From tick In Observable.Interval(TargetElapsedTime) _
                                    .TakeUntil(releases)
                     Select Unit.Default
    'this TakeUnitl watches for either a grab or a gague full
    Dim increments = From r In releases
                     From tick In Observable.Interval(TargetElapsedTime) _
                                    .TakeUntil(grabs.Merge(gagueFulls))
                     Select Unit.Default
    'simulated values for testing, you may just have
    'these be properties on an INotifyPropertyChanged object
    'rather than having a PlayerScoreChanged observable.
    Const GagueMax As Integer = 20
    Const GagueMin As Integer = 0
    Const GagueStep As Integer = 1
    Dim gagueValue As Integer = GagueMax
    Dim playerScore As Integer
    Dim disp As New CompositeDisposable()
    'hook up IsGrabbed to the grabs and releases
    disp.Add(grabs.Subscribe(Sub(v) isGrabbed.OnNext(True)))
    disp.Add(releases.Subscribe(Sub(v) isGrabbed.OnNext(False)))
    'output grabbed state to the console for testing
    disp.Add(isGrabbed.Subscribe(Sub(v) Console.WriteLine("Grabbed: " & v)))
    disp.Add(gagueFulls.Subscribe(Sub(v) Console.WriteLine("Gague full")))

    disp.Add(decrements.Subscribe(Sub(v)
                                      'testing use only
                                      If gagueValue <= GagueMin Then
                                          Console.WriteLine("Should not get here, decrement below min!!!")
                                      End If
                                      'do the decrement
                                      gagueValue -= GagueStep
                                      Console.WriteLine("Gague value: " & gagueValue.ToString())
                                      If gagueValue <= GagueMin Then
                                          gagueValue = GagueMin
                                          Console.WriteLine("New gague value: " & gagueValue)
                                          alt.TriggerSecond() 'trigger a release when the gague empties
                                      End If
                                  End Sub))
    disp.Add(decrements.Subscribe(Sub(v)
                                      'based on your example, it seems you score just for grabbing
                                      playerScore += 1
                                      Console.WriteLine("Player Score: " & playerScore)
                                  End Sub))
    disp.Add(increments.Subscribe(Sub(v)
                                      'testing use only
                                      If gagueValue >= GagueMax Then
                                          Console.WriteLine("Should not get here, increment above max!!!")
                                      End If
                                      'do the increment
                                      gagueValue += GagueStep
                                      Console.WriteLine("Gague value: " & gagueValue.ToString())
                                      If gagueValue >= GagueMax Then
                                          gagueValue = GagueMax
                                          Console.WriteLine("New gague value: " & gagueValue)
                                          gagueFulls.OnNext(Unit.Default) 'trigger a full
                                      End If
                                  End Sub))
    'hook the "mouse" to the grab/release subject
    disp.Add(mouseDowns.Subscribe(Sub(v) alt.TriggerFirst()))
    disp.Add(mouseUps.Subscribe(Sub(v) alt.TriggerSecond()))
    'mouse simulator
    Dim done As Boolean
    Do
        done = False
        Dim key = Console.ReadKey()
        If key.Key = ConsoleKey.G Then
            mouseDowns.OnNext(Unit.Default)
        ElseIf key.Key = ConsoleKey.R Then
            mouseUps.OnNext(Unit.Default)
        Else
            done = True
        End If
    Loop While Not done
    'shutdown
    disp.Dispose()
    Console.ReadKey()
End Sub

为了测试应用程序,一切都在一个函数中。在你的真实应用程序中,你当然应该考虑暴露什么以及如何暴露。

相关内容

  • 没有找到相关文章

最新更新