Игровой цикл обновления-рендеринга в Rx: как обеспечить согласованное состояние? - PullRequest
2 голосов
/ 22 февраля 2012

Я новичок в Reactive Extensions для .NET и, играя с ним, я подумал, что было бы здорово, если бы его можно было использовать для игр вместо традиционной парадигмы рендеринга обновлений. Вместо того чтобы пытаться вызывать Update () для всех игровых объектов, сами объекты просто подписываются на интересующие их свойства и события и обрабатывают любые изменения, что приводит к меньшему количеству обновлений, лучшей тестируемости и более лаконичным запросам.

Но как только, например, изменяется значение свойства, все подписанные запросы также захотят немедленно обновить свои значения. Зависимости могут быть очень сложными, и как только все будет отображено, я не знаю, закончили ли все объекты обновление для следующего кадра. Зависимости могут даже быть такими, что некоторые объекты постоянно обновляются на основе изменений друг друга. Поэтому игра может быть в несовместимом состоянии при рендеринге. Например, сложная сетка, которая перемещается, когда некоторые части обновили свои позиции, а другие еще не начали рендеринг. Это не было бы проблемой для традиционного цикла обновления-рендеринга, поскольку фаза обновления полностью завершится до начала рендеринга.

Итак, мой вопрос: возможно ли гарантировать, что игра находится в согласованном состоянии (все объекты закончили свои обновления) непосредственно перед рендерингом всего?

Ответы [ 3 ]

3 голосов
/ 29 февраля 2012

Короткий ответ - да, возможно выполнить то, что вы ищете в отношении развязки цикла обновления игры.Я создал концептуальное подтверждение с использованием Rx и XNA, в котором использовался один объект рендеринга, который никак не был привязан к игровому циклу.Вместо этого сущности запускают событие, чтобы сообщить подписчикам, что они готовы к визуализации;полезная нагрузка данных события содержала всю информацию, необходимую для визуализации кадра в это время для этого объекта.

Поток событий запроса рендеринга объединяется с потоком событий таймера (просто таймер Observable.Interval) для синхронизации рендеринга с частотой кадров.Кажется, он работает довольно хорошо, и я рассматриваю возможность его тестирования в несколько больших масштабах.Я понял, что он хорошо работает как для пакетного рендеринга (много спрайтов одновременно), так и для отдельных рендеров.Обратите внимание, что версия Rx, которую использует приведенный ниже код, является той, которая поставляется с ПЗУ WP7 (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 с (для частоты обновления WP7 по умолчанию 30 Гц).

В вашем игровом классе:

private readonly ISubject<GameTime> _drawTimer = 
                                         new BehaviorSubject<GameTime>(new GameTime());

// ... //

public override Draw(GameTime gameTime)
{
    _drawTimer.OnNext(gameTime);
}

Теперь, используяDraw метод *1029* может, казалось бы, победить цель, поэтому, если вы предпочитаете избегать этого, вы можете вместо этого Publish a ConnectedObservable (горячая наблюдаемая) вот так:

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

_drawTimer.Connect();

Где эта техника может быть невероятно полезной - в играх XNA от Silverlight.В SL объект Game недоступен, и разработчик должен сделать некоторую путаницу для правильной работы традиционного игрового цикла.С Rx и этим подходом нет необходимости делать это, обещая гораздо менее разрушительный опыт переноса игр с чистого XNA на XNA + SL

0 голосов
/ 23 февраля 2012

Почему бы вам не использовать IScheduler для планирования ваших изменений подписок. Тогда вы можете сделать так, чтобы ваш основной игровой цикл увеличивал реализацию планировщика на 16,6 мс каждый кадр (при условии 60 кадров в секунду). Идея заключалась бы в том, что он будет выполнять любые запланированные действия, которые должны быть выполнены в это время, поэтому вы все равно можете использовать такие вещи, как задержка или газ.

0 голосов
/ 22 февраля 2012

Это потенциально довольно общий вопрос об отделении рендеринга от обновления в игровом цикле.Это то, с чем сетевые игры уже должны справиться;«Как сделать что-то, что не нарушает погружение игрока, когда вы на самом деле не знаете, что еще произошло?»

Один из подходов к этому - «мультибуферизовать» граф сцены или его элементы.и фактически рендерит интерполированную версию с более высокой частотой кадров рендеринга.Вам все еще нужно определить точку в вашем обновлении, когда все будет завершено за определенный временной шаг, но он больше не привязан к визуализации.Вместо этого вы копируете результаты своего обновления в новый экземпляр графа сцены с отметкой времени и приступаете к следующему обновлению.

Это означает, что вы выполняете рендеринг с задержкой, поэтому может не подходить для всех типовигры.

...