问题
I'm playing with using the Rx in an XNA proof-of-concept, and I've run into a bit of an obstacle composing some queries that I'm hoping you folks can assist me with understanding how some of these operators work.
In my POC, I would like the player's score to increment only while there is not an active drag operation occurring. In addition, there is a 'grab gauge' that I would like to deplete whenever there is an ongoing drag, and fill whenever there isn't. Finally, if a drag operation is under way and the grab gauge drops below 0, I want to cancel the drag operation.
I've got the score incrementing working just fine with this:
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);
The score will increment when it is supposed to, and stop when the character is picked up. Getting the gauge behavior to work correctly has been troublesome however. I've tried a ton of variations using Window
, Generate
, etc... but what seems to end up happening is that either it doesn't work at all, or the increment/decrement gauge operations end up fighting each other, or it will all seem to work properly, but continue to subtract or add points/gauge in the background. Here's the gauge implementation (extremely poor performance, crashes after about 10-15s, doesn't work properly):
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);
I have no doubts that my lack of understanding with Rx is at fault in some way, shape, or form, but I've reached the limit of experimenting with different operators/queries. Any insights?
EPILOGUE: Gideon Engelberth's answer fit my needs spot-on - I wish I could upvote it 10x! Here's the quick C# representation of his answer (not 100% on the IDisposable.Dispose(), but should be close):
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
}
And the logic to hook up the events in the game class (there are some refactoring opportunities). Summary: works like a charm! Thanks!
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);
}
});
}
回答1:
You are definitely on the right track. I would start by making grabs and releases observables of their own and then making PrincessGrabbed
based on those two observables. For a case like this, I use a class I call 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
Along with that, you will probably want to add a "gague full" observable you can trigger from the incrementing method. The "gague empty" will trigger the release portion of the 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
For the sake of the test app, everything is in one function. In your real app, you should of course consider what to expose and how.
来源:https://stackoverflow.com/questions/9418385/toggling-between-values-while-producing-events-on-timer