我是的Reactive Extensions的新手。NET,在玩它的时候,我认为如果它可以用于游戏而不是传统的更新渲染模式,那将是非常棒的。与其尝试对所有游戏对象调用Update(),对象本身只需订阅他们感兴趣的属性和事件并处理任何更改,从而减少更新、提高可测试性和更简洁的查询。
但是,例如,一旦属性的值发生更改,所有订阅的查询也会希望立即更新其值。依赖关系可能非常复杂,一旦所有东西都要渲染,我不知道是否所有对象都完成了下一帧的更新。依赖关系甚至可能使得一些对象基于彼此的更改而不断更新。因此,游戏在渲染时可能处于不一致的状态。例如,移动的复杂网格,其中某些部分已更新其位置,而其他部分在渲染开始后尚未更新。这对于传统的更新渲染循环来说不会有问题,因为更新阶段将在渲染开始之前完全结束。
那么我的问题是:在渲染所有内容之前,是否有可能确保游戏处于一致状态(所有对象都完成了更新)?
简单的答案是肯定的,可以实现您所希望的游戏更新循环解耦。我使用Rx和XNA创建了一个概念验证,它使用了一个单独的渲染对象,该对象与游戏循环没有任何联系。相反,实体会启动一个事件,通知订阅者他们已经准备好进行渲染;事件数据的有效载荷包含在该时间为该对象呈现帧所需的所有信息。
渲染请求事件流与计时器事件流(仅为Observable.Interval
计时器)合并,以使渲染与帧速率同步。它似乎运行得很好,我正在考虑在稍大的范围内进行测试。我已经让它在批量渲染(一次渲染多个精灵)和单独渲染中都能很好地工作。请注意,以下代码使用的Rx版本是与WP7 ROM(Mirosoft.Phone.Requivative)一起提供的版本
假设你有一个类似的对象:
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;
}
请注意,该对象没有描述如何渲染自己,而只是充当将在渲染中使用的数据的发布者。它通过向可观察性订阅Actions来公开这一点。
考虑到这一点,我们还可以有一个独立的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
事件数据进行批处理和绘制(这更像是一条消息,但不需要太语义化!)
连接游戏类中的渲染行为只需两行代码:
entity.AddHandlerWithSubscription<FrameTicked, TexturedEntity>(
_drawTimer.Select(y => new FrameTicked(y)),
x => x.RaiseEvent);
entity.AddHandler<IEvent<Rendering>>(_renderHandler.OnNext);
在实体实际渲染之前,要做的最后一件事是挂上一个计时器,该计时器将作为游戏各个实体的同步信标。这就是我认为的Rx等效于灯塔每1/30脉冲一次(默认30Hz WP7刷新率)。
在你的游戏课上:
private readonly ISubject<GameTime> _drawTimer =
new BehaviorSubject<GameTime>(new GameTime());
// ... //
public override Draw(GameTime gameTime)
{
_drawTimer.OnNext(gameTime);
}
现在,使用Game
的Draw
方法似乎无法达到目的,所以如果你想避免这样做,你可以用Publish
代替ConnectedObservable
(热可观察),如下所示:
IConnectableObservable<FrameTick> _drawTimer = Observable
.Interval(TargetElapsedTime)
.Publish();
//...//
_drawTimer.Connect();
这种技术在Silverlight托管的XNA游戏中非常有用。在SL中,Game
对象不可用,开发者需要进行一些欺骗才能使传统的游戏循环正常工作。有了Rx和这种方法,就没有必要这么做了,有望在将游戏从纯XNA移植到XNA+SL 时提供更少的破坏性体验
这可能是一个关于在游戏循环中将渲染与更新解耦的普遍问题。这是网络游戏已经必须面对的问题;"当你还不知道发生了什么的时候,你如何渲染一些不会破坏玩家沉浸感的东西?"
一种方法是"多重缓冲"场景图或其元素,并以更高的渲染帧速率实际渲染插值版本。当特定时间步长的所有操作都完成时,您仍然需要在更新中识别一个点,但它不再与渲染绑定。相反,您可以将更新结果复制到带有时间戳的新场景图实例中,然后开始下一次更新。
这确实意味着渲染有滞后,因此可能不适合所有类型的游戏。
为什么不使用某种IScheduler来安排更改订阅。然后,你可以让你的主游戏循环步进你的调度器实现每帧16.6毫秒(假设60fps)。这个想法是,它将执行任何在该时间到期的预定操作,因此您仍然可以使用延迟或节流之类的东西。