次の方法で共有


ユニバーサル Windows プラットフォーム (UWP) DirectX ゲームの入力待機時間を最適化する

入力待ち時間はゲームのエクスペリエンスに大きな影響を与える可能性があり、最適化することでゲームがより洗練された感じになる可能性があります。 さらに、適切な入力イベントの最適化により、バッテリ寿命を向上させることができます。 適切な CoreDispatcher 入力イベント処理オプションを選択して、ゲームができるだけスムーズに入力を処理できるようにする方法について説明します。

入力の待機時間

入力待機時間は、システムがユーザー入力に応答するのにかかる時間です。 応答は、多くの場合、画面に表示される内容や、オーディオ フィードバックを通じて聞かれる内容の変化です。

すべての入力イベントは、タッチ ポインター、マウス ポインター、キーボードのいずれからのものであっても、イベント ハンドラーによって処理されるメッセージを生成します。 最新のタッチ デジタイザーとゲーム周辺機器は、ポインターあたり少なくとも 100 Hz の入力イベントを報告します。つまり、アプリはポインターあたり 1 秒あたり 100 イベント以上 (またはキーストローク) を受け取ることができます。 この更新速度は、複数のポインターが同時に発生している場合や、より高精度の入力デバイス (ゲーム マウスなど) が使用されている場合に増幅されます。 イベント メッセージ キューは、非常に迅速にいっぱいになる可能性があります。

シナリオに最適な方法でイベントが処理されるように、ゲームの入力待機時間の要求を理解することが重要です。 すべてのゲームに対して 1 つのソリューションはありません。

電力効率

入力待機時間のコンテキストでは、"電力効率" とは、ゲームが GPU を使用する量を指します。 GPU リソースの使用が少ないゲームは、電力効率が高く、バッテリ寿命が長くなります。 これは CPU にも当てはまります。

ユーザーのエクスペリエンスを低下させることなく、1 秒あたり 60 フレーム未満 (現在、ほとんどのディスプレイで最大レンダリング速度) で画面全体を描画できる場合、描画頻度が低くなることで電力効率が向上します。 一部のゲームでは、ユーザー入力に応じて画面が更新されるため、1 秒あたり 60 フレームで同じコンテンツを繰り返し描画しないようにする必要があります。

最適化対象の選択

DirectX アプリを設計するときは、いくつかの選択を行う必要があります。 アプリはスムーズなアニメーションを表示するために 1 秒あたり 60 フレームをレンダリングする必要がありますか、それとも入力に応答してレンダリングするだけで済みますか? 可能な限り短い入力待ち時間が必要ですか。それとも、少しの遅延を許容できますか? ユーザーは、アプリのバッテリー使用量に慎重に取り組む必要がありますか?

これらの質問に対する回答は、アプリを次のいずれかのシナリオに合わせる可能性があります。

  1. オンデマンドでレンダリングします。 このカテゴリのゲームは、特定の種類の入力に応じて画面を更新するだけで済みます。 アプリでは同じフレームが繰り返しレンダリングされず、入力待ち時間の大半がアプリによって入力の待機に費やされるため、電力効率は優れています。 ボード ゲームとニュース リーダーは、このカテゴリに分類される可能性があるアプリの例です。
  2. 一時的なアニメーションを使用してオンデマンドでレンダリングします。 このシナリオは最初のシナリオに似ていますが、特定の種類の入力では、ユーザーからの後続の入力に依存しないアニメーションが開始される点が異なります。 ゲームでは同じフレームが繰り返しレンダリングされず、ゲームがアニメーション化されていない間は入力の待機時間が短いため、電力効率は良好です。 それぞれの動きをアニメーション化するインタラクティブな子供のゲームやボードゲームは、このカテゴリに分類される可能性のあるアプリの例です。
  3. 1 秒あたり 60 フレームをレンダリングします。 このシナリオでは、ゲームは常に画面を更新しています。 ディスプレイに表示できるフレームの最大数がレンダリングされるため、電力効率が低下します。 コンテンツの表示中に DirectX がスレッドをブロックするため、入力待機時間が長くなります。 これにより、スレッドがユーザーに表示できるフレーム数よりも多くのフレームをディスプレイに送信できなくなります。 一人称シューティング ゲーム、リアルタイム戦略ゲーム、物理ベースのゲームは、このカテゴリに分類される可能性のあるアプリの例です。
  4. 1 秒あたり 60 フレームをレンダリングし、可能な限り低い入力待機時間を実現します。 シナリオ 3 と同様に、アプリは常に画面を更新しているため、電力効率が低下します。 違いは、ゲームが別のスレッドの入力に応答するため、ディスプレイにグラフィックスを表示して入力処理がブロックされないようにすることです。 オンラインマルチプレイヤーゲーム、ファイティングゲーム、またはリズム/タイミングゲームは、非常にタイトなイベントウィンドウ内での移動入力をサポートしているため、このカテゴリに分類される可能性があります。

実装

ほとんどの DirectX ゲームは、ゲーム ループと呼ばれるものによって駆動されます。 基本的なアルゴリズムは、ユーザーがゲームまたはアプリを終了するまで、次の手順を実行することです。

  1. プロセス入力
  2. ゲームの状態を更新する
  3. ゲーム コンテンツを描画する

DirectX ゲームのコンテンツがレンダリングされ、画面に表示される準備ができたら、ゲーム ループは、GPU が新しいフレームを受け取る準備ができるまで待機してから、再び入力を処理します。

単純なジグソーパズル ゲームを繰り返して、前述した各シナリオのゲーム ループの実装を示します。 各実装で説明されている決定ポイント、利点、トレードオフは、低待機時間の入力と電力効率のためにアプリを最適化するのに役立つガイドとして役立ちます。

シナリオ 1: オンデマンドでレンダリングする

ジグソーパズル ゲームの最初のイテレーションでは、ユーザーがパズルピースを移動したときにのみ画面が更新されます。 ユーザーは、パズルピースを所定の位置にドラッグするか、それを選択して正しい目的地に触れることで、所定の位置にスナップすることができます。 2番目のケースでは、パズルピースはアニメーションや効果なしで目的地にジャンプします。

このコードには、CoreProcessEventsOption::P rocessOneAndAllPending を使用する IFrameworkView::Run メソッド内にシングル スレッド のゲーム ループがあります。 このオプションを使用すると、キューで現在使用可能なすべてのイベントがディスパッチされます。 保留中のイベントがない場合、ゲーム ループはイベントが表示されるまで待機します。

void App::Run()
{
    
    while (!m_windowClosed)
    {
        // Wait for system events or input from the user.
        // ProcessOneAndAllPending will block the thread until events appear and are processed.
        CoreWindow::GetForCurrentThread()->Dispatcher->ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);

        // If any of the events processed resulted in a need to redraw the window contents, then we will re-render the
        // scene and present it to the display.
        if (m_updateWindow || m_state->StateChanged())
        {
            m_main->Render();
            m_deviceResources->Present();

            m_updateWindow = false;
            m_state->Validate();
        }
    }
}

シナリオ 2: 一時的なアニメーションを使用してオンデマンドでレンダリングする

2 番目のイテレーションでは、ユーザーがパズルピースを選択し、そのピースの正しい目的地に触れると、その目的に達するまで画面全体でアニメーション化されるように、ゲームが変更されます。

前と同様に、コードには、 ProcessOneAndAllPending を使用してキュー内の入力イベントをディスパッチするシングル スレッド ゲーム ループがあります。 現在の違いは、アニメーション中に、新しい入力イベントを待機しないように、 CoreProcessEventsOption::P rocessAllIfPresent を使用するようにループが変更されるということです。 保留中のイベントがない場合、 ProcessEvents はすぐに返され、アプリはアニメーション内の次のフレームを表示できます。 アニメーションが完了すると、ループは ProcessOneAndAllPending に戻り、画面の更新を制限します。

void App::Run()
{

    while (!m_windowClosed)
    {
        // 2. Switch to a continuous rendering loop during the animation.
        if (m_state->Animating())
        {
            // Process any system events or input from the user that is currently queued.
            // ProcessAllIfPresent will not block the thread to wait for events. This is the desired behavior when
            // you are trying to present a smooth animation to the user.
            CoreWindow::GetForCurrentThread()->Dispatcher->ProcessEvents(CoreProcessEventsOption::ProcessAllIfPresent);

            m_state->Update();
            m_main->Render();
            m_deviceResources->Present();
        }
        else
        {
            // Wait for system events or input from the user.
            // ProcessOneAndAllPending will block the thread until events appear and are processed.
            CoreWindow::GetForCurrentThread()->Dispatcher->ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);

            // If any of the events processed resulted in a need to redraw the window contents, then we will re-render the
            // scene and present it to the display.
            if (m_updateWindow || m_state->StateChanged())
            {
                m_main->Render();
                m_deviceResources->Present();

                m_updateWindow = false;
                m_state->Validate();
            }
        }
    }
}

ProcessOneAndAllPendingProcessAllIfPresent の間の遷移をサポートするには、アプリがアニメーション化されているかどうかを知るために状態を追跡する必要があります。 ジグソーパズル アプリでは、これを行うには、GameState クラスのゲーム ループ中に呼び出すことができる新しいメソッドを追加します。 ゲーム ループのアニメーション ブランチは、GameState の新しい Update メソッドを呼び出すことによって、アニメーションの状態の更新を駆動します。

シナリオ 3: 1 秒あたり 60 フレームをレンダリングする

3 番目のイテレーションでは、ユーザーがパズルに取り組んでいる時間を示すタイマーがアプリに表示されます。 経過時間はミリ秒まで表示されるため、ディスプレイを最新の状態に保つために、1 秒あたり 60 フレームをレンダリングする必要があります。

シナリオ 1 と 2 と同様に、アプリにはシングル スレッドのゲーム ループがあります。 このシナリオの違いは、常にレンダリングされるため、最初の 2 つのシナリオで行われたように、ゲームの状態の変更を追跡する必要がなくなったということです。 その結果、イベントの処理には既定で ProcessAllIfPresent を使用できます。 保留中のイベントがない場合、 ProcessEvents はすぐに返され、次のフレームのレンダリングに進みます。

void App::Run()
{

    while (!m_windowClosed)
    {
        if (m_windowVisible)
        {
            // 3. Continuously render frames and process system events and input as they appear in the queue.
            // ProcessAllIfPresent will not block the thread to wait for events. This is the desired behavior when
            // trying to present smooth animations to the user.
            CoreWindow::GetForCurrentThread()->Dispatcher->ProcessEvents(CoreProcessEventsOption::ProcessAllIfPresent);

            m_state->Update();
            m_main->Render();
            m_deviceResources->Present();
        }
        else
        {
            // 3. If the window isn't visible, there is no need to continuously render.
            // Process events as they appear until the window becomes visible again.
            CoreWindow::GetForCurrentThread()->Dispatcher->ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);
        }
    }
}

この方法は、レンダリングするタイミングを決定するために追加の状態を追跡する必要がないため、ゲームを記述する最も簡単な方法です。 タイマー間隔での妥当な入力応答性と共に、可能な限り最速のレンダリングを実現します。

ただし、この開発の容易さには価格が伴います。 60 フレーム/秒でのレンダリングでは、オンデマンドレンダリングよりも多くの電力が使用されます。 ProcessAllIfPresentゲームがフレームごとに表示される内容を変更する場合に使用することをお勧めします。 また、アプリが ProcessEvents ではなくディスプレイの同期間隔でゲーム ループをブロックしているため、入力待ち時間も 16.7 ミリ秒も長くなります。 キューがフレームごとに 1 回だけ処理されるため、一部の入力イベントが削除されることがあります (60 Hz)。

シナリオ 4: 1 秒あたり 60 フレームをレンダリングし、入力待機時間を最小限に抑える

一部のゲームでは、シナリオ 3 で見られる入力待機時間の増加を無視または補正できる場合があります。 ただし、ゲームのエクスペリエンスとプレイヤーフィードバックの感覚にとって低い入力待ち時間が重要な場合は、1 秒あたり 60 フレームをレンダリングするゲームは、別のスレッドで入力を処理する必要があります。

ジグソーパズル ゲームの 4 番目のイテレーションは、入力処理とグラフィックス レンダリングをゲーム ループから別のスレッドに分割することで、シナリオ 3 に基づいています。 各スレッドを個別に設定すると、グラフィックス出力によって入力が遅延されることはありません。ただし、その結果、コードはより複雑になります。 シナリオ 4 では、入力スレッドは ProcessEvents CoreProcessEventsOption::P rocessUntilQuit を呼び出します。これは、新しいイベントを待機し、使用可能なすべてのイベントをディスパッチします。 ウィンドウが閉じられるか、ゲームが CoreWindow::Close を呼び出すまで、この動作を続行します。

void App::Run()
{
    // 4. Start a thread dedicated to rendering and dedicate the UI thread to input processing.
    m_main->StartRenderThread();

    // ProcessUntilQuit will block the thread and process events as they appear until the App terminates.
    CoreWindow::GetForCurrentThread()->Dispatcher->ProcessEvents(CoreProcessEventsOption::ProcessUntilQuit);
}

void JigsawPuzzleMain::StartRenderThread()
{
    // If the render thread is already running, then do not start another one.
    if (IsRendering())
    {
        return;
    }

    // Create a task that will be run on a background thread.
    auto workItemHandler = ref new WorkItemHandler([this](IAsyncAction^ action)
    {
        // Notify the swap chain that this app intends to render each frame faster
        // than the display's vertical refresh rate (typically 60 Hz). Apps that cannot
        // deliver frames this quickly should set this to 2.
        m_deviceResources->SetMaximumFrameLatency(1);

        // Calculate the updated frame and render once per vertical blanking interval.
        while (action->Status == AsyncStatus::Started)
        {
            // Execute any work items that have been queued by the input thread.
            ProcessPendingWork();

            // Take a snapshot of the current game state. This allows the renderers to work with a
            // set of values that won't be changed while the input thread continues to process events.
            m_state->SnapState();

            m_sceneRenderer->Render();
            m_deviceResources->Present();
        }

        // Ensure that all pending work items have been processed before terminating the thread.
        ProcessPendingWork();
    });

    // Run the task on a dedicated high priority background thread.
    m_renderLoopWorker = ThreadPool::RunAsync(workItemHandler, WorkItemPriority::High, WorkItemOptions::TimeSliced);
}

Microsoft Visual Studio 2015 の DirectX 11 と XAML アプリ (ユニバーサル Windows) テンプレートは、同様の方法でゲーム ループを複数のスレッドに分割します。 Windows::UI::Core::CoreIndependentInputSource オブジェクトを使用して、入力の処理専用のスレッドを開始し、XAML UI スレッドに依存しないレンダリング スレッドも作成します。 これらのテンプレートの詳細については、「テンプレートから ユニバーサル Windows プラットフォーム および DirectX ゲーム プロジェクトを作成するを参照してください。

入力待ち時間を短縮するその他の方法

待機可能なスワップ チェーンを使用する

DirectX ゲームは、ユーザーが画面上に表示する内容を更新することで、ユーザー入力に応答します。 60 Hz ディスプレイでは、画面は 16.7 ミリ秒 (1 秒/60 フレーム) ごとに更新されます。 図 1 は、1 秒あたり 60 フレームをレンダリングするアプリの 16.7 ミリ秒更新信号 (VBlank) に対する入力イベントに対するおおよそのライフ サイクルと応答を示しています。

図 1

図 1 directx の入力待機時間

Windows 8.1 では、DXGI にスワップ チェーンの DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT フラグが導入されました。このフラグを使うと、アプリは現在のキューを空の状態に維持するためにヒューリスティックを実装しなくても、この待ち時間を簡単に減らすことができます。 このフラグを使用して作成されたスワップ チェーンは、待機可能なスワップ チェーンと呼ばれます。 図 2 は、待機可能なスワップ チェーンを使用する場合の入力イベントに対するおおよそのライフ サイクルと応答を示しています。

図 2

図 2 directx 待機中の入力待機時間

これらの図からわかるように、ゲームは、ディスプレイのリフレッシュ レートによって定義された 16.7 ミリ秒の予算内で各フレームをレンダリングして表示できる場合、2 つの完全なフレームによって入力待ち時間を短縮できる可能性があります。 ジグソーパズルのサンプルでは、待機可能なスワップ チェーンを使用し、次の呼び出しによって Present キューの制限を制御します。 m_deviceResources->SetMaximumFrameLatency(1);