표준 C # / Windows Forms 게임 루프 란 무엇입니까?


32

일반 Windows Forms와 SlimDX 또는 OpenTK 같은 일부 그래픽 API 래퍼를 사용하는 C #으로 게임을 작성할 때 주 게임 루프는 어떻게 구성해야합니까?

표준 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 함수에 쿼리를 위임 할 수 있습니다 . 에 대한 가져 오기 선언 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);
}

또한이 접근 방식은 표준 네이티브 Windows 게임 루프 와 최대한 비슷하게 (P / Invoke에 의존 함) 다음과 같습니다.

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

이러한 Windows API 기능을 처리해야합니까? 정확한 스톱워치 (fps 제어용)로 제어되는 while 블록을 만드는 것만으로는 충분하지 않습니까?
Emir Lima

3
어떤 시점에서 Application.Idle 처리기에서 반환해야합니다. 그렇지 않으면 응용 프로그램이 중지됩니다 (추가 Win32 메시지가 발생하지 않기 때문에). 대신 WM_TIMER 메시지를 기반으로 루프를 만들려고 시도 할 수 있지만 WM_TIMER은 실제로 원하는만큼 정확하지 않으며 모든 경우에 가장 낮은 공통 분모 업데이트 속도로 모든 것을 강제합니다. 많은 게임들이 독립적 인 렌더 및 로직 업데이트 속도를 필요로하거나 원하는데, 일부는 (물리와 같은) 고정되어 있지만 다른 것들은 그렇지 않습니다.
Josh

기본 Windows 게임 루프는 동일한 기술을 사용합니다 (비교를 위해 간단한 답변을 포함하도록 답변을 수정했습니다. 고정 업데이트 속도를 강제로 적용하는 타이머는 유연성이 떨어지며 PeekMessage의 더 넓은 컨텍스트 내에서 항상 고정 업데이트 속도를 구현할 수 있습니다 스타일 루프 ( WM_TIMER기반 타이머보다 정밀도 및 GC 영향이 더 우수한 타이머 사용 )
Josh

@JoshPetrie 위의 유휴 상태 확인은 SlimDX 기능을 사용합니다. 이것을 답변에 포함시키는 것이 이상적입니까? 아니면 우연히 SlimDX에 해당하는 'IsApplicationIdle'을 읽도록 코드를 편집 했습니까?
Vaughan Hilts

** 저를 무시하십시오, 나는 당신이 그것을 더 아래로 정의한다는 것을 깨달았습니다 ... :)
Vaughan Hilts

2

Josh의 대답에 동의하면 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 () 가비지 생성 오버 헤드를 알고 있어야 합니다.
Josh

0

나는 이것이 오래된 스레드라는 것을 이해하지만 위에서 제안 된 기술에 대한 두 가지 대안을 제공하고 싶습니다. 내가 그들에게 들어가기 전에, 지금까지 제안한 함정 중 일부는 다음과 같습니다.

  1. PeekMessage는이를 호출하는 라이브러리 메소드 (SlimDX IsApplicationIdle)와 마찬가지로 상당한 오버 헤드를 전달합니다.

  2. 버퍼 된 RawInput을 사용하려면 UI 스레드 이외의 다른 스레드에서 PeekMessage를 사용하여 메시지 펌프를 폴링해야하므로 두 번 호출하고 싶지 않습니다.

  3. Application.DoEvents는 타이트한 루프에서 호출되도록 설계되지 않았으므로 GC 문제가 빠르게 발생합니다.

  4. Application.Idle 또는 PeekMessage를 사용하는 경우 유휴 상태 일 때만 작업하기 때문에 코드 냄새없이 창을 이동하거나 크기를 조정할 때 게임 또는 응용 프로그램이 실행되지 않습니다.

이 문제를 해결하려면 (RawInput 도로를 내려가는 경우 2 제외) 다음 중 하나를 수행 할 수 있습니다.

  1. Threading.Thread를 만들고 게임 루프를 실행하십시오.

  2. IsLongRunning 플래그를 사용하여 Threading.Tasks.Task를 작성하여 실행하십시오. 요즘 스레드 대신 작업을 사용하는 것이 좋으며 그 이유를 알기가 어렵지 않습니다.

이 두 가지 방법 모두 권장 방식과 마찬가지로 그래픽스 API를 UI 스레드 및 메시지 펌프에서 분리합니다. 창 크기 조정 중 리소스 / 상태 파괴 및 재생 처리가 단순화되어 UI 스레드 외부에서 완료된 경우 (메시지 펌프로 교착 상태를 피하기 위해주의를 기울임) 미학적으로 훨씬 전문적입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.