短い答えはイエスです。ゲーム更新ループの分離に関して、あなたが探しているものを達成することは可能です。ゲーム ループに結び付けられていない単一のレンダリング オブジェクトを使用する Rx と XNA を使用して、概念実証を作成しました。代わりに、エンティティはイベントを発生させて、レンダリングの準備が整ったことをサブスクライバーに通知します。イベント データのペイロードには、その時点でそのオブジェクトのフレームをレンダリングするために必要なすべての情報が含まれていました。
レンダリング要求イベント ストリームは、タイマー イベント ストリーム (単なるObservable.Interval
タイマー) とマージされ、レンダリングをフレーム レートと同期させます。かなりうまく機能しているようで、もう少し大きなスケールでテストすることを検討しています。バッチ レンダリング (一度に多くのスプライト) と個々のレンダリングの両方で一見うまく機能するようになりました。以下のコードが使用する Rx のバージョンは、WP7 ROM (Mirosoft.Phone.Reactive) に同梱されているものであることに注意してください。
次のようなオブジェクトがあるとします。
public abstract class SomeEntity
{
/* members omitted for brevity */
IList _eventHandlers = new List<object>();
public void AddHandlerWithSubscription<T, TType>(IObservable<T> observable,
Func<TType, Action<T>> handlerSelector)
where TType: SomeEntity
{
var handler = handlerSelector((TType)this);
observable.Subscribe(observable, eventHandler);
}
public void AddHandler<T>(Action<T> eventHandler) where T : class
{
var subj = Observer.Create(eventHandler);
AddHandler(subj);
}
protected void AddHandler<T>(IObserver<T> handler) where T : class
{
if (handler == null)
return;
_eventHandlers.Add(handler);
}
/// <summary>
/// Changes internal rendering state for the object, then raises the Render event
/// informing subscribers that this object needs rendering)
/// </summary>
/// <param name="rendering">Rendering parameters</param>
protected virtual void OnRender(PreRendering rendering)
{
var renderArgs = new Rendering
{
SpriteEffects = this.SpriteEffects = rendering.SpriteEffects,
Rotation = this.Rotation = rendering.Rotation.GetValueOrDefault(this.Rotation),
RenderTransform = this.Transform = rendering.RenderTransform.GetValueOrDefault(this.Transform),
Depth = this.DrawOrder = rendering.Depth,
RenderColor = this.Color = rendering.RenderColor,
Position = this.Position,
Texture = this.Texture,
Scale = this.Scale,
Size = this.DrawSize,
Origin = this.TextureCenter,
When = rendering.When
};
RaiseEvent(Event.Create(this, renderArgs));
}
/// <summary>
/// Extracts a render data object from the internal state of the object
/// </summary>
/// <returns>Parameter object representing current internal state pertaining to rendering</returns>
private PreRendering GetRenderData()
{
var args = new PreRendering
{
Origin = this.TextureCenter,
Rotation = this.Rotation,
RenderTransform = this.Transform,
SpriteEffects = this.SpriteEffects,
RenderColor = Color.White,
Depth = this.DrawOrder,
Size = this.DrawSize,
Scale = this.Scale
};
return args;
}
このオブジェクトは、それ自体をレンダリングする方法を記述していませんが、レンダリングで使用されるデータのパブリッシャーとしてのみ機能することに注意してください。これは、アクションをオブザーバブルにサブスクライブすることで公開されます。
それを考えると、独立した も持つことができますRenderHandler
:
public class RenderHandler : IObserver<IEvent<Rendering>>
{
private readonly SpriteBatch _spriteBatch;
private readonly IList<IEvent<Rendering>> _renderBuffer = new List<IEvent<Rendering>>();
private Game _game;
public RenderHandler(Game game)
{
_game = game;
this._spriteBatch = new SpriteBatch(game.GraphicsDevice);
}
public void OnNext(IEvent<Rendering> value)
{
_renderBuffer.Add(value);
if ((value.EventArgs.When.ElapsedGameTime >= _game.TargetElapsedTime))
{
OnRender(_renderBuffer);
_renderBuffer.Clear();
}
}
private void OnRender(IEnumerable<IEvent<Rendering>> obj)
{
var renderBatches = obj.GroupBy(x => x.EventArgs.Depth)
.OrderBy(x => x.Key).ToList(); // TODO: profile if.ToList() is needed
foreach (var renderBatch in renderBatches)
{
_spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend);
foreach (var @event in renderBatch)
{
OnRender(@event.EventArgs);
}
_spriteBatch.End();
}
}
private void OnRender(Rendering draw)
{
_spriteBatch.Draw(
draw.Texture,
draw.Position,
null,
draw.RenderColor,
draw.Rotation ?? 0f,
draw.Origin ?? Vector2.Zero,
draw.Scale,
draw.SpriteEffects,
0);
}
イベント データのバッチ処理と描画を行うオーバーロードされた OnRender メソッドに注意してくださいRendering
(これはメッセージのようなものですが、セマンティックになりすぎる必要はありません!)
ゲーム クラスでレンダリング動作をフックするのは、次の 2 行のコードだけです。
entity.AddHandlerWithSubscription<FrameTicked, TexturedEntity>(
_drawTimer.Select(y => new FrameTicked(y)),
x => x.RaiseEvent);
entity.AddHandler<IEvent<Rendering>>(_renderHandler.OnNext);
エンティティが実際にレンダリングされる前に行う最後の作業は、ゲームのさまざまなエンティティの同期ビーコンとして機能するタイマーを接続することです。これは、1/30 秒ごとにパルスする灯台に相当する Rx と私が考えるものです (デフォルトの 30Hz WP7 リフレッシュ レートの場合)。
あなたのゲームクラスで:
private readonly ISubject<GameTime> _drawTimer =
new BehaviorSubject<GameTime>(new GameTime());
// ... //
public override Draw(GameTime gameTime)
{
_drawTimer.OnNext(gameTime);
}
ここで、Game
のDraw
メソッドを使用すると、一見目的に反する可能性があるため、それを避けたい場合は、代わりに次のようPublish
なConnectedObservable
(Hot observable)を使用できます。
IConnectableObservable<FrameTick> _drawTimer = Observable
.Interval(TargetElapsedTime)
.Publish();
//...//
_drawTimer.Connect();
この手法が非常に役立つのは、Silverlight でホストされる XNA ゲームです。SL では、Game
オブジェクトは使用できず、開発者は従来のゲーム ループを正しく機能させるためにいくつかの仕上げを行う必要があります。Rx とこのアプローチを使用すると、それを行う必要がなくなり、ゲームを純粋な XNA から XNA+SL に移植する際の混乱の少ないエクスペリエンスが約束されます。