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

Posted

技术标签:

【中文标题】Rx 中的游戏更新渲染循环:如何确保一致的状态?【英文标题】:Game update-render loop in Rx: how to ensure consistent state? 【发布时间】:2012-02-22 09:27:16 【问题描述】:

我是 .NET 的 Reactive Extensions 的新手,在使用它时,我认为如果它可以用于游戏而不是传统的更新渲染范例,那就太棒了。与其尝试对所有游戏对象调用 Update(),对象本身只需订阅他们感兴趣的属性和事件并处理任何更改,从而减少更新、更好的可测试性和更简洁的查询。

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

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

【问题讨论】:

【参考方案1】:

简短的回答是肯定的,可以完成您正在寻找的关于游戏更新循环解耦的内容。我使用 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;
    

请注意,此对象不描述任何如何呈现自身,而只是充当将用于呈现的数据的发布者。它通过将 Actions 订阅到 observables 来公开这一点。

鉴于此,我们还可以有一个独立的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);

现在,使用GameDraw 方法可能看起来无法达到目的,所以如果您不想这样做,您可以改为Publish a ConnectedObservable(可观察到的),如下所示:

IConnectableObservable<FrameTick> _drawTimer = Observable
                                                .Interval(TargetElapsedTime)
                                                .Publish();
//...//

_drawTimer.Connect();

这种技术在 Silverlight 托管的 XNA 游戏中非常有用。在 SL 中,Game 对象不可用,开发人员需要做一些调整才能使传统的游戏循环正常工作。使用 Rx 和这种方法,没有必要这样做,承诺将游戏从纯 XNA 移植到 XNA+SL 的破坏性体验要小得多

【讨论】:

【参考方案2】:

这可能是一个关于在游戏循环中将渲染与更新解耦的非常普遍的问题。这已经是网络游戏必须应对的事情了; “当您实际上还不知道发生了什么时,如何渲染不会破坏玩家沉浸感的东西?”

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

这确实意味着您的渲染存在延迟,因此可能并不适合所有类型的游戏。

【讨论】:

我的问题要简单得多。我确实知道发生了什么,但只有在一个或多个完整更新阶段完成之后。问题是:我如何知道使用 Rx 时更新阶段已经完成?或者也许有一个概念不需要更新阶段并且仍然允许游戏在渲染时保持一致。渲染一个滞后的游戏只是为了能够使用 Rx 意味着 Rx 不适合我的需求,但我认为这不是真的。 假设我们谈论的是一个相当传统的模拟类型游戏,那么我认为你必须知道在给定时间点要渲染的所有内容的状态。否则,例如,您可能会看到应该对齐、连接在一起或同步移动的对象(例如汽车和它的司机)之间的抖动。我猜这就是你所说的一致性? 没错。一些游戏对象,例如汽车零件,需要相互对齐。传统的游戏循环完成了更新阶段,因此永远不会出现问题。【参考方案3】:

您为什么不使用某种 IScheduler 来安排您的更改订阅。然后你可以让你的主游戏循环每帧执行 16.6 毫秒(假设 60fps)。这个想法是它会在那个时间执行任何计划的操作,所以你仍然可以使用延迟或限制之类的东西。

【讨论】:

我应该补充一点,我认为 Rx 不适合更新您的主要游戏状态。不过,我确实认为它可能非常适合人工智能行为。

以上是关于Rx 中的游戏更新渲染循环:如何确保一致的状态?的主要内容,如果未能解决你的问题,请参考以下文章

如何将 UI 与 C# 中的渲染分开?

游戏开发——更新方法和状态模式

为啥游戏循环渲染的次数多于更新的次数

Android - 游戏循环中的主动渲染?

小松教你手游开发游戏渲染游戏开发中基于图像的渲染技术总结

如何使用 for 循环访问渲染部分中的状态?