2

私は Reactive Extensions for .NET を初めて使用しましたが、これを使って遊んでいるうちに、従来の update-render パラダイムの代わりにゲームに使用できれば素晴らしいと思いました。すべてのゲーム オブジェクトで Update() を呼び出そうとするのではなく、オブジェクト自体が関心のあるプロパティとイベントをサブスクライブして変更を処理するだけなので、更新が少なくなり、テストが容易になり、クエリがより簡潔になります。

ただし、たとえば、プロパティの値が変更されるとすぐに、サブスクライブされたすべてのクエリもすぐに値を更新する必要があります。依存関係は非常に複雑な場合があり、すべてがレンダリングされると、すべてのオブジェクトが次のフレームのために更新を完了したかどうかわかりません。依存関係は、一部のオブジェクトが互いの変更に基づいて継続的に更新されるようなものでさえあるかもしれません。したがって、ゲームはレンダリング時に一貫性のない状態になる可能性があります。たとえば、移動する複雑なメッシュで、一部のパーツが位置を更新し、他のパーツがまだレンダリングを開始していない場合などです。これは、レンダリングが開始される前に更新フェーズが完全に終了するため、従来の update-render ループでは問題になりませんでした。

それでは、私の質問は次のとおりです。すべてをレンダリングする直前に、ゲームが一貫した状態 (すべてのオブジェクトが更新を完了) であることを確認することは可能ですか?

4

3 に答える 3

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(これはメッセージのようなものですが、セマンティックになりすぎる必要はありません!)

ゲーム クラスでレンダリング動作をフックするのは、次の 2 行のコードだけです。

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

エンティティが実際にレンダリングされる前に行う最後の作業は、ゲームのさまざまなエンティティの同期ビーコンとして機能するタイマーを接続することです。これは、1/30 秒ごとにパルスする灯台に相当する Rx と私が考えるものです (デフォルトの 30Hz WP7 リフレッシュ レートの場合)。

あなたのゲームクラスで:

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

// ... //

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

ここで、GameDrawメソッドを使用すると、一見目的に反する可能性があるため、それを避けたい場合は、代わりに次のようPublishConnectedObservable(Hot observable)を使用できます。

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

_drawTimer.Connect();

この手法が非常に役立つのは、Silverlight でホストされる XNA ゲームです。SL では、Gameオブジェクトは使用できず、開発者は従来のゲーム ループを正しく機能させるためにいくつかの仕上げを行う必要があります。Rx とこのアプローチを使用すると、それを行う必要がなくなり、ゲームを純粋な XNA から XNA+SL に移植する際の混乱の少ないエクスペリエンスが約束されます。

于 2012-02-29T14:04:47.137 に答える
0

変更サブスクリプションをスケジュールするために、ある種のISchedulerを使用してみませんか。次に、メインのゲームループでスケジューラーの実装をフレームごとに16.6ミリ秒(60 fpsと想定)ステップすることができます。その時間内にスケジュールされたアクションを実行するという考えであるため、遅延やスロットルなどを引き続き使用できます。

于 2012-02-23T08:02:01.053 に答える
0

これは、ゲーム ループ内の更新からレンダリングを分離することに関して、かなり一般的な質問になる可能性があります。これは、ネットワーク化されたゲームがすでに対処しなければならないことです。「実際に何が起こったのかまだわからないときに、プレイヤーの没入感を損なわないようにレンダリングするにはどうすればよいでしょうか?」

これに対する 1 つのアプローチは、シーン グラフまたはその要素を「マルチ バッファリング」し、補間されたバージョンをより高いレンダリング フレーム レートで実際にレンダリングすることです。特定のタイム ステップですべてが終了したときに更新のポイントを特定する必要がありますが、それはもはやレンダリングに関連付けられていません。代わりに、更新結果をタイム スタンプ付きの新しいシーン グラフ インスタンスにコピーし、次の更新を開始します。

これは、レンダリングに遅延があることを意味するため、すべてのタイプのゲームに適しているとは限りません。

于 2012-02-22T10:59:07.127 に答える