Rx中的游戏更新渲染循环:如何确保一致的状态?

时间:2012-02-22 09:27:16

标签: c# system.reactive game-loop

我是.NET的Reactive Extensions的新手,在玩它时我认为如果它可以用于游戏而不是传统的更新渲染范例,那将会很棒。这些对象本身只会订阅他们感兴趣的属性和事件并处理任何更改,而不是尝试在所有游戏对象上调用Update(),从而减少更新,提高可测试性并提供更简洁的查询。

但是,例如,一旦属性的值发生变化,所有订阅的查询也会立即更新其值。依赖关系可能非常复杂,一旦要渲染所有内容,我不知道所有对象是否已经为下一帧完成了更新。依赖性甚至可以使得一些对象基于彼此的改变而不断地更新。因此,游戏可能在渲染时处于不一致状态。例如,移动的复杂网格,其中某些部分已更新其位置,而其他部分尚未开始渲染。这对于传统的更新渲染循环来说不是问题,因为更新阶段将在渲染开始之前完成。

那么我的问题是:在呈现所有内容之前,是否可以确保游戏处于一致状态(所有对象都完成了更新)?

3 个答案:

答案 0 :(得分:3)

简短的回答是肯定的,有可能完成你正在寻找的有关游戏更新循环解耦的内容。我使用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事件数据进行批处理和绘制(它更像是一条消息,但不需要过于语义化!)

在游戏类中连接渲染行为只需两行代码:

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的{​​{1}}方法似乎可能会失败,所以如果您不想这样做,可以改为Draw Publish( Hot observable)像这样:

ConnectedObservable

这项技术在Silverlight托管的XNA游戏中非常有用。在SL中,IConnectableObservable<FrameTick> _drawTimer = Observable .Interval(TargetElapsedTime) .Publish(); //...// _drawTimer.Connect(); 对象不可用,开发人员需要进行一些操作才能使传统游戏循环正常工作。使用Rx和这种方法,没有必要这样做,承诺在将游戏从纯XNA移植到XNA + SL方面具有更少破坏性的体验

答案 1 :(得分:0)

这可能是一个非常普遍的问题,即在游戏循环中将渲染与更新分离。这是网络游戏必须应对的事情; “如果你实际上不知道发生了什么,你怎么渲染不会破坏玩家沉浸感的东西?”

一种方法是对场景图或其元素进行“多缓冲”,并实际以更高的渲染帧速率渲染插值版本。当特定时间步的所有内容都完成时,您仍然需要在更新中识别一个点,但它不再与渲染相关联。而是将更新结果复制到带有时间戳的新场景图实例,并开始下一次更新。

这确实意味着您正在使用滞后渲染,因此可能不适合所有类型的游戏。

答案 2 :(得分:0)

为什么不使用某种IScheduler来安排更改订阅。然后你可以让你的主游戏循环步骤你的调度程序实现每帧16.6毫秒(假设60fps)。我们的想法是,它会在那段时间内执行任何预定的操作,因此您仍然可以使用延迟或限制等操作。