Что такое стандартный игровой цикл C # / Windows Forms?


32

Как следует структурировать основной цикл игры при написании игры на C #, в которой используются обычные Windows Forms и некоторые графические оболочки API, такие как SlimDX или OpenTK ?

Каноническое приложение Windows Forms имеет точку входа, которая выглядит как

public static void Main () {
  Application.Run(new MainForm());
}

и хотя можно выполнить некоторые из необходимых действий, перехватывая различные события Formкласса , эти события не обеспечивают очевидного места для размещения фрагментов кода для выполнения постоянных периодических обновлений объектов игровой логики или для начала и окончания рендеринга. Рамка.

Какую технику должна использовать такая игра, чтобы достичь чего-то похожего на каноническое?

while(!done) {
  update();
  render();
}

игровой цикл, а что делать с минимальной производительностью и влиянием GC?

Ответы:


45

Этот Application.Runвызов приводит в действие ваш насос сообщений Windows, который, в конечном счете, обеспечивает все события, которые вы можете подключить к Formклассу (и другим). Чтобы создать игровой цикл в этой экосистеме, вы хотите прослушивать, когда насос сообщений приложения пуст и, пока он остается пустым, выполните типичные шаги «обработать состояние ввода, обновить игровую логику, визуализировать сцену» в прототипическом игровом цикле. ,

В Application.Idleсобытии срабатывает один раз каждый раз , когда очередь сообщений приложения будет очищена , и приложение переходит в исходное состояние. Вы можете перехватить событие в конструкторе вашей основной формы:

class MainForm : Form {
  public MainForm () {
    Application.Idle += HandleApplicationIdle;
  }

  void HandleApplicationIdle (object sender, EventArgs e) {
    //TODO: Implement me.
  }
}

Затем вы должны быть в состоянии определить, если приложение все еще бездействует. IdleСобытие срабатывает только один раз, когда приложение становится простаивает. Он не запускается снова, пока сообщение не попадает в очередь, а затем очередь снова очищается. Windows Forms не предоставляет метод для запроса состояния очереди сообщений, но вы можете использовать службы вызова платформы, чтобы делегировать запрос собственной функции Win32, которая может ответить на этот вопрос . Декларация import для PeekMessageи поддерживаемых типов выглядит следующим образом:

[StructLayout(LayoutKind.Sequential)]
public struct NativeMessage
{
    public IntPtr Handle;
    public uint Message;
    public IntPtr WParameter;
    public IntPtr LParameter;
    public uint Time;
    public Point Location;
}

[DllImport("user32.dll")]
public static extern int PeekMessage(out NativeMessage message, IntPtr window, uint filterMin, uint filterMax, uint remove);

PeekMessageв основном позволяет просматривать следующее сообщение в очереди; возвращает true, если он существует, в противном случае - false. Для целей этой проблемы ни один из параметров не имеет особого значения: имеет значение только возвращаемое значение. Это позволяет вам написать функцию, которая сообщает вам, если приложение все еще находится в режиме ожидания (то есть, в очереди еще нет сообщений):

bool IsApplicationIdle () {
    NativeMessage result;
    return PeekMessage(out result, IntPtr.Zero, (uint)0, (uint)0, (uint)0) == 0;
}

Теперь у вас есть все, что нужно для написания полного цикла игры:

class MainForm : Form {
  public MainForm () {
    Application.Idle += HandleApplicationIdle;
  }

  void HandleApplicationIdle (object sender, EventArgs e) {
    while(IsApplicationIdle()) {
      Update();
      Render();
    }
  }

  void Update () {
    // ...
  }

  void Render () {
    // ...
  }

  [StructLayout(LayoutKind.Sequential)]
  public struct NativeMessage
  {
      public IntPtr Handle;
      public uint Message;
      public IntPtr WParameter;
      public IntPtr LParameter;
      public uint Time;
      public Point Location;
  }

  [DllImport("user32.dll")]
  public static extern int PeekMessage(out NativeMessage message, IntPtr window, uint filterMin, uint filterMax, uint remove);
}

Кроме того, этот подход максимально приближен (с минимальной зависимостью от P / Invoke) к каноническому собственному игровому циклу Windows, который выглядит следующим образом:

while (!done) {
    if (PeekMessage(&message, window, 0, 0, PM_REMOVE)){
        TranslateMessage(&message);
        DispatchMessage(&message);
    }
    else {
        Update();
        Render();
    }
}

Зачем нужно иметь дело с такими функциями windows apis? Создание блока while, управляемого точным секундомером (для управления частотой кадров), не будет достаточно?
Эмир Лима

3
В какой-то момент вам необходимо вернуться из обработчика Application.Idle, иначе ваше приложение будет зависать (так как оно никогда не допускает дальнейших сообщений Win32). Вместо этого вы могли бы попытаться сделать цикл, основанный на сообщениях WM_TIMER, но WM_TIMER далеко не так точен, как вам бы того хотелось, и даже если бы это было так, это заставило бы все к такому низкому уровню обновления общего знаменателя. Многие игры нуждаются или хотят иметь независимые скорости рендеринга и обновления логики, некоторые из которых (например, физика) остаются фиксированными, а другие нет.
Джош

Нативные игровые циклы Windows используют ту же технику (я исправил свой ответ, включив в него простой для сравнения. Таймеры для принудительной установки фиксированной частоты обновления менее гибки, и вы всегда можете реализовать свою фиксированную частоту обновления в более широком контексте PeekMessage в стиле петли (используя таймеры с большей точностью и влиянием ГХ, чем на WM_TIMERоснове)
Джош

@JoshPetrie Для ясности, в приведенной выше проверке простоя используется функция SlimDX. Было бы идеально включить это в ответ? Или вы случайно отредактировали код для чтения «IsApplicationIdle», который является аналогом SlimDX?
Вон Хилтс

** Пожалуйста, не обращайте на меня внимания, я только что понял, что вы определили это ниже ... :)
Vaughan Hilts

2

Согласился с ответом Джоша, просто хочу добавить мои 5 центов. Цикл сообщений WinForms по умолчанию (Application.Run) можно заменить следующим (без p / invoke):

[STAThread]
static void Main()
{
    using (Form1 f = new Form1())
    {
        f.Show();
        while (true) // here should be some nice exit condition
        {
            Application.DoEvents(); // default message pump
        }
    }
}

Также, если вы хотите внедрить некоторый код в насос сообщений, используйте это:

public partial class Form1 : Form
{
    protected override void WndProc(ref Message m)
    {
        // this code is invoked inside default message pump
        base.WndProc(ref m);
    }
}

2
Однако вы должны знать о накладных расходах генерации мусора в DoEvents (), если вы выбираете этот подход.
Джош

0

Я понимаю, что это старая тема, но я хотел бы предоставить две альтернативы методикам, предложенным выше. Прежде чем я углублюсь в них, вот некоторые подводные камни с предложениями, сделанными до сих пор:

  1. PeekMessage несет значительные накладные расходы, как и методы библиотеки, которые его вызывают (SlimDX IsApplicationIdle).

  2. Если вы хотите использовать буферизованный RawInput, вам нужно будет опросить обработчик сообщений с помощью PeekMessage в другом потоке, отличном от потока пользовательского интерфейса, чтобы не вызывать его дважды.

  3. Application.DoEvents не предназначен для вызова в узком цикле, проблемы GC будут возникать быстро.

  4. При использовании Application.Idle или PeekMessage, поскольку вы выполняете работу только в режиме ожидания, ваша игра или приложение не будут работать при перемещении или изменении размера окна без запахов кода.

Чтобы обойти это (кроме 2, если вы идете по дороге RawInput), вы можете:

  1. Создайте Threading.Thread и запустите там свой игровой цикл.

  2. Создайте Threading.Tasks.Task с флагом IsLongRunning и запустите его там. В наши дни Microsoft рекомендует использовать Задачи вместо потоков, и нетрудно понять, почему.

Оба эти метода изолируют ваш графический API от потока пользовательского интерфейса и насоса сообщений, как это рекомендуется. Обработка уничтожения ресурса / состояния и воссоздания во время изменения размера окна также упрощается и эстетически намного более профессиональна, когда делается из готового (проявляя должную осторожность, чтобы избежать взаимоблокировок с насосом сообщений) извне потока пользовательского интерфейса.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.