Search code examples
c#system.reactivegame-loop

Game update-render loop in Rx: how to ensure consistent state?


I'm new to Reactive Extensions for .NET and while playing with it I thought that it would be awesome if it could be used for games instead of the traditional update-render paradigm. Rather than trying to call Update() on all game objects, the objects themselves would just subscribe to the properties and events they are interested in and handle any changes, resulting in fewer updates, better testability and more concise queries.

But as soon as, for example, a property's value changes, all subscribed queries will also want to update their values immediately. The dependencies may be very complex, and once everything is going to be rendered I don't know whether all objects have finished updating themselves for the next frame. The dependencies may even be such that some objects are continuously updating based on each other's changes. Therefore the game might be in an inconsistent state on rendering. For example a complex mesh that moves, where some parts have updated their positions and other have not yet once rendering starts. This would not have been a problem with the traditional update-render loop, as the update phase will finish completely before rendering starts.

So then my question is: is it possible to ensure that the game is in a consistent state (all objects finished their updates) just before rendering everything?


Solution

  • The short answer is yes, it is possible to accomplish what you're looking for regarding game update loop decoupling. I created a proof-of-concept using Rx and XNA which used a single rendering object which was not tied in any way to the game loop. Instead, entities would fire an event off to inform subscribers they were ready for render; the payload of the event data contained all the info needed to render a frame at that time for that object.

    The render request event stream is merged with a timer event stream (just an Observable.Interval timer) to synchronize renders with the frame rate. It seems to work pretty well, and I'm considering testing it out on slightly larger scales. I've gotten it to work seemingly well both for batched rendering (many sprites at once) and with individual renders. Note that the version of Rx the code below uses is the one that ships with the WP7 ROM (Mirosoft.Phone.Reactive).

    Assume you have an object similar to this:

    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;
        }
    

    Notice that this object doesn't describe anything how to render itself, but only acts as a publisher of data that will be used in rendering. It exposes this by subscribing Actions to observables.

    Given that, we could also have an independent 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);
        }
    

    Note the overloaded OnRender methods which do the batching and drawing of the Rendering event data (it's more of a message, but no need to get too semantic!)

    Hooking up the render behavior in the game class is simply two lines of code:

    entity.AddHandlerWithSubscription<FrameTicked, TexturedEntity>(
                                          _drawTimer.Select(y => new FrameTicked(y)), 
                                          x => x.RaiseEvent);
    entity.AddHandler<IEvent<Rendering>>(_renderHandler.OnNext);
    

    One last thing to do before the entity will actually render is to hook up a timer that will serve as a synchronization beacon for the game's various entities. This is what I think of as the Rx equivalent of a lighthouse pulsing every 1/30s (for default 30Hz WP7 refresh rate).

    In your game class:

    private readonly ISubject<GameTime> _drawTimer = 
                                             new BehaviorSubject<GameTime>(new GameTime());
    
    // ... //
    
    public override Draw(GameTime gameTime)
    {
        _drawTimer.OnNext(gameTime);
    }
    

    Now, using the Game's Draw method may seemingly defeat the purpose, so if you would rather avoid doing that, you could instead Publish a ConnectedObservable (Hot observable) like this:

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

    Where this technique can be incredibly useful is in Silverlight-hosted XNA games. In SL, the Game object is unavailable, and a developer needs to do some finagling in order to get the traditional game loop working correctly. With Rx and this approach, there is no need to do that, promising a much less disruptive experience in porting games from pure XNA to XNA+SL