Domanda

Sono nuovo alle estensioni reattive per. NET e mentre giocavo con esso ho pensato che sarebbe stato fantastico se potesse essere usato per i giochi invece del tradizionale paradigma di aggiornamento-rendering.Piuttosto che provare a chiamare Update() su tutti gli oggetti di gioco, gli oggetti stessi si iscriverebbero alle proprietà e agli eventi a cui sono interessati e gestirebbero eventuali modifiche, con conseguente minor numero di aggiornamenti, migliore testabilità e query più concise.

Ma non appena, ad esempio, il valore di una proprietà cambia, anche tutte le query sottoscritte vorranno aggiornare immediatamente i loro valori.Le dipendenze possono essere molto complesse, e una volta che tutto sarà reso non so se tutti gli oggetti hanno finito di aggiornarsi per il fotogramma successivo.Le dipendenze possono anche essere tali che alcuni oggetti si aggiornano continuamente in base alle reciproche modifiche.Pertanto il gioco potrebbe essere in uno stato incoerente sul rendering.Ad esempio, una mesh complessa che si muove, in cui alcune parti hanno aggiornato le loro posizioni e altre non hanno ancora iniziato il rendering.Questo non sarebbe stato un problema con il tradizionale ciclo di aggiornamento-rendering, poiché la fase di aggiornamento terminerà completamente prima dell'inizio del rendering.

Quindi la mia domanda è:è possibile assicurarsi che il gioco sia in uno stato coerente (tutti gli oggetti hanno terminato i loro aggiornamenti) poco prima di eseguire il rendering di tutto?

È stato utile?

Soluzione

La risposta breve è sì, è possibile realizzare ciò che stai cercando per quanto riguarda il disaccoppiamento del ciclo di aggiornamento del gioco.Ho creato un proof-of-concept usando Rx e XNA che usavano un singolo oggetto di rendering che non era legato in alcun modo al ciclo di gioco.Invece, le entità attivavano un evento per informare gli abbonati che erano pronti per il rendering;il payload dei dati dell'evento conteneva tutte le informazioni necessarie per eseguire il rendering di un frame in quel momento per quell'oggetto.

Il flusso di eventi di richiesta di rendering viene unito a un flusso di eventi timer (solo un Observable.Interval timer) per sincronizzare i rendering con il frame rate.Sembra funzionare abbastanza bene, e sto pensando di testarlo su scale leggermente più grandi.L'ho fatto funzionare apparentemente bene sia per il rendering in batch (molti sprite contemporaneamente) che con i singoli rendering.Si noti che la versione di Rx il codice qui sotto utilizza è quella che viene fornita con la ROM WP7 (Mirosoft.Telefono.Reattivo).

Supponiamo di avere un oggetto simile a questo:

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

Si noti che questo oggetto non descrive nulla come per il rendering stesso, ma agisce solo come editore di dati che verranno utilizzati nel rendering.Espone questo sottoscrivendo Azioni a osservabili.

Dato che, potremmo anche avere un indipendente 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);
    }

Notare i metodi OnRender sovraccaricati che eseguono il batch e il disegno del Rendering dati degli eventi (è più un messaggio, ma non c'è bisogno di diventare troppo semantici!)

Collegare il comportamento di rendering nella classe di gioco è semplicemente due righe di codice:

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

Un'ultima cosa da fare prima che l'entità sia effettivamente renderizzata è collegare un timer che fungerà da faro di sincronizzazione per le varie entità del gioco.Questo è quello che penso come l'equivalente Rx di un faro che pulsa ogni 1/30s (per la frequenza di aggiornamento predefinita 30Hz WP7).

Nella tua classe di gioco:

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

// ... //

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

Ora, usando il Gamedi Draw il metodo può apparentemente sconfiggere lo scopo, quindi se preferisci evitare di farlo, potresti invece Publish a ConnectedObservable (Caldo osservabile) come questo:

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

_drawTimer.Connect();

Dove questa tecnica può essere incredibilmente utile è nei giochi XNA ospitati da Silverlight.In SL, il Game l'oggetto non è disponibile e uno sviluppatore deve fare un po ' di finagling per far funzionare correttamente il ciclo di gioco tradizionale.Con Rx e questo approccio, non c'è bisogno di farlo, promettendo un'esperienza molto meno dirompente nel porting di giochi da XNA puro a XNA + SL

Altri suggerimenti

Questa è potenzialmente una domanda generale sul disaccoppiamento del rendering dall'aggiornamento in un ciclo di gioco.Questo è qualcosa che i giochi in rete devono affrontare già;"Come rendi qualcosa che non rompe l'immersione del giocatore quando non sai davvero cosa è successo?"

Un approccio a questo è quello di "multi buffer" il grafico della scena o gli elementi di esso, e rendono effettivamente una versione interpolata a una frequenza fotogrammi più elevata.Devi ancora identificare un punto nel tuo aggiornamento quando tutto è finito per un particolare passo temporale, ma non è più legato al rendering.Invece copia i risultati di aggiornamento in una nuova istanza del grafico di scena con un timestamp e iniziare il prossimo aggiornamento.

Significa che stai rendendo con un ritardo, quindi potrebbe non essere adatto a tutti i tipi di gioco.

Perché non utilizzi una sorta di IScheduler per pianificare le tue sottoscrizioni di modifica.Quindi potresti fare in modo che il tuo ciclo di gioco principale passi all'implementazione dello scheduler di 16,6 ms ogni frame (assumendo 60 fps).L'idea sarebbe che eseguisse tutte le azioni pianificate dovute in quel momento, quindi potresti ancora usare cose come ritardo o accelerazione.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top