Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Задержка ввода может значительно повлиять на опыт игры, и оптимизация может сделать игру более полированной. Кроме того, правильная оптимизация событий ввода может улучшить время работы батареи. Узнайте, как выбрать правильные параметры обработки событий ввода CoreDispatcher, чтобы убедиться, что игра обрабатывает входные данные максимально плавно.
Задержка ввода
Задержка ввода — это время, необходимое системе для реагирования на входные данные пользователя. Ответ часто приводит к изменению отображения на экране или того, что слышится через аудиосигнал.
Каждое событие ввода, исходя из сенсорного указателя, указателя мыши или клавиатуры, создает сообщение для обработки обработчиком событий. Современные сенсорные устройства ввода и игровые периферийные устройства фиксируют события ввода с частотой не менее 100 Гц на каждый указатель, что означает, что приложения могут получать 100 событий или больше в секунду на каждый указатель (или нажатие клавиш). Эта скорость обновлений усиливается, если одновременно происходит несколько указателей или используется устройство ввода с более высокой точностью (например, игровой мышью). Очередь сообщений о событии может быстро заполниться.
Важно понимать требования к задержке входных данных в игре, чтобы события обрабатывались таким образом, что лучше всего подходит для сценария. Нет ни одного решения для всех игр.
Эффективность питания
В контексте задержки ввода "эффективность питания" относится к тому, сколько игра использует GPU. Игра, использующая меньше ресурсов GPU, более эффективна и обеспечивает большую продолжительное время работы батареи. Это также относится к ЦП.
Если игра может нарисовать весь экран менее чем 60 кадров в секунду (в настоящее время максимальная скорость отрисовки на большинстве дисплеев), не ухудшая опыт пользователя, это будет более эффективно благодаря более редкой отрисовке. Некоторые игры обновляют экран только в ответ на входные данные пользователя, поэтому эти игры не должны рисовать одно и то же содержимое несколько раз в 60 кадров в секунду.
Выбор критериев для оптимизации
При разработке приложения DirectX необходимо выбрать некоторые варианты. Требуется ли приложению отрисовка 60 кадров в секунду для отображения гладкой анимации или требуется ли отрисовка только в ответ на входные данные? Должна ли она иметь наименьшую возможную задержку ввода или может ли она терпеть немного задержки? Будут ли мои пользователи ожидать, что мое приложение будет рассудительным в отношении использования батареи?
Ответы на эти вопросы, скорее всего, свяжут ваше приложение с одним из следующих сценариев:
- Отрисовка по запросу. Игры в этой категории должны обновлять экран только в ответ на определенные типы входных данных. Эффективность питания отлична, так как приложение не отображает идентичные кадры многократно, и задержка ввода низка, так как приложение тратит большую часть времени ожидания входных данных. Настольные игры и читатели новостей — это примеры приложений, которые могут попасть в эту категорию.
- Отрисовка по запросу с кратковременными анимациями. Этот сценарий аналогичен первому сценарию, за исключением того, что некоторые типы входных данных запускают анимацию, которая не зависит от последующих входных данных от пользователя. Эффективность питания хороша, так как игра не отображает идентичные кадры многократно, и задержка ввода низка, пока игра не анимирует. Интерактивные детские игры и настольные игры, которые анимируют каждый шаг, являются примерами приложений, которые могут попасть в эту категорию.
- Отрисовка 60 кадров в секунду. В этом сценарии игра постоянно обновляет экран. Энергоэффективность низкая, так как выводится максимальное количество кадров, которое дисплей может воспроизвести. Задержка ввода высока, так как DirectX блокирует поток во время представления содержимого. Таким образом предотвращается отправка потоком большего количества кадров на дисплей, чем он может отображать пользователю. Шутеры от первого лица, стратегии в реальном времени и физические игры являются примерами приложений, которые могут попасть в эту категорию.
- Обеспечить отображение 60 кадров в секунду и минимальную возможную задержку ввода. Аналогично сценарию 3, приложение постоянно обновляет экран, поэтому эффективность питания будет плохой. Разница заключается в том, что игра реагирует на входные данные в отдельном потоке, поэтому обработка входных данных не блокируется путем представления графики на дисплее. Онлайн-многопользовательские игры, боевые игры или ритм-игры могут относиться к этой категории, потому что они поддерживают ввод движений в условиях чрезвычайно ограниченных временных окон.
Внедрение
Большинство игр DirectX управляются тем, что называется циклом игры. Базовый алгоритм состоит в том, чтобы выполнить следующие действия, пока пользователь не выйдет из игры или приложения:
- Входные данные процесса
- Обновление состояния игры
- Рисование содержимого игры
Когда содержимое игры DirectX отрисовывается и готово к отображению на экране, цикл игры ожидает, пока GPU будет готов к получению нового кадра, прежде чем снова просыпаться, чтобы обработать входные данные.
Мы покажем реализацию цикла игры для каждого из упомянутых ранее сценариев с помощью итерации на простой головоломке. Точки принятия решений, преимущества и компромиссы, обсуждаемые с каждой реализацией, могут служить руководством для оптимизации приложений для низкой задержки ввода и эффективности питания.
Сценарий 1. Отрисовка по запросу
Первая итерация игры-головоломки обновляет экран только тогда, когда пользователь перемещает кусок пазла. Пользователь может либо перетащить кусок головоломки на место, либо подогнать его на место, выбрав его и затем коснувшись правильного места. Во втором случае головоломка будет переходить к месту назначения без анимации или эффектов.
Код имеет игровой цикл, работающий в одном потоке, в методе IFrameworkView::Run, использующий CoreProcessEventsOption::ProcessOneAndAllPending. При выборе этой опции все текущие доступные события в очереди отправляются. Если нет ожидаемых событий, цикл игры ожидает, пока какое-либо не появится.
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. Отрисовка по запросу с временными анимациями
Во второй итерации игра изменена таким образом, что когда пользователь выбирает кусок головоломки и касается правильного места для этого фрагмента, он перемещается по экрану, пока не достигнет своего места.
Как и раньше, код содержит однопоточный игровой цикл, использующий ProcessOneAndAllPending для обработки входных событий в очереди. Разница заключается в том, что во время анимации цикл изменяет использование CoreProcessEventsOption::ProcessAllIfPresent, чтобы не ожидать новых входных событий. Если нет ожидающих событий, 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();
}
}
}
}
Чтобы обеспечить переход между ProcessOneAndAllPending и ProcessAllIfPresent, приложение должно отслеживать состояние, чтобы определить, находится ли оно в процессе анимации. В приложении пазлов вы можете добавить новый метод, который можно вызвать на классе GameState во время цикла игры. Ветвь анимации цикла игры обновляет состояние анимации путем вызова нового метода GameState Update.
Сценарий 3: Отрисовка 60 кадров в секунду
В третьей итерации приложение отображает таймер, показывающий пользователю, сколько времени они работали над головоломкой. Так как оно отображает истекшее время до миллисекунда, оно должно отображать 60 кадров в секунду, чтобы обеспечить актуальность отображения.
Как и в сценариях 1 и 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, когда игра изменяет отображаемое на каждом кадре. Это также увеличивает задержку ввода до 16,7 мс, так как приложение теперь блокирует игровой цикл на интервале синхронизации дисплея вместо ProcessEvents. Некоторые события ввода могут быть пропущены, так как очередь обрабатывается только один раз за кадр (60 Гц).
Сценарий 4. Отрисовка 60 кадров в секунду и достижение минимальной возможной задержки ввода
Некоторые игры могут игнорировать или компенсировать увеличение задержки ввода, наблюдаемой в сценарии 3. Однако если низкая задержка ввода критически важна для опыта игры и чувства обратной связи игрока, игры, которые отображают 60 кадров в секунду, необходимо обрабатывать входные данные в отдельном потоке.
Четвертая итерация игры головоломки основывается на сценарии 3, разделяя обработку ввода и отрисовку графики вне игрового цикла на отдельные потоки. Наличие отдельных потоков для каждого гарантирует, что входные данные никогда не задерживаются графическими выходными данными; однако код становится более сложным в результате. В сценарии 4 входной поток вызывает ProcessEvents с CoreProcessEventsOption::ProcessUntilQuit, ожидая новых событий и отправляя все доступные. Это поведение продолжается до закрытия окна или пока игра не вызовет 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);
}
Шаблон приложения DirectX 11 и XAML (Универсальное приложение для Windows) в Microsoft Visual Studio 2015 аналогичным образом разделяет игровой цикл на несколько потоков. Он использует объект Windows::UI::Core::CoreIndependentInputSource для запуска потока, выделенного для обработки входных данных, а также создает поток отрисовки независимо от потока пользовательского интерфейса XAML. Дополнительные сведения об этих шаблонах см. в статье "Создание проекта универсальной платформы Windows и Игры DirectX" на основе шаблона.
Дополнительные способы уменьшения задержки ввода
Используйте ожидаемые цепочки обмена
Игры DirectX реагируют на входные данные пользователя, обновляя то, что пользователь видит на экране. На дисплее 60 Гц экран обновляется каждые 16,7 мс (1 секунда/60 кадров). На рисунке 1 показан приблизительный жизненный цикл и ответ на входное событие относительно сигнала обновления 16,7 мс (VBlank) для приложения, которое отрисовывает 60 кадров в секунду:
Рисунок 1
В Windows 8.1 DXGI представила флаг DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT для цепочки обмена, который позволяет приложениям легко уменьшить эту задержку, не требуя от них реализации эвристических алгоритмов для поддержания пустоты очереди Present. Цепочки буферов, созданные с помощью этого флага, называются ожидаемыми цепочками буферов. На рисунке 2 показан приблизительный жизненный цикл и ответ на входное событие при использовании ожидаемых цепочек обмена.
Рисунок 2
задержка ввода directx
На этих схемах мы видим, что игры могут снизить задержку ввода на два полных кадра, если они способны отрисовывать и представлять каждый кадр в пределах 16,7 мс, определённых скоростью обновления дисплея. В примере головоломки используются цепочки обмена с возможностью ожидания и управляется ограничение очереди Present, вызывая: m_deviceResources->SetMaximumFrameLatency(1);