Standart C # / Windows Forms oyun döngüsü nedir?


32

Eski Windows Formlarını ve SlimDX veya OpenTK gibi bazı grafik API sarıcılarını kullanan C # dilinde bir oyun yazarken , ana oyun döngüsü nasıl yapılandırılmalıdır?

Bir kurallı Windows Forms uygulamasının, benzeyen bir giriş noktası vardır.

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

ve sınıfın çeşitli olaylarını çengellemek gerekli olan bazı şeyleri başarabilirken , bu olaylar oyun mantığı nesnelerine sürekli periyodik güncellemeler yapmak veya bir oyuna başlamak ve bitirmek için kod parçalarını koymak için açık bir yer sağlamaz çerçevesi.Form

Böyle bir oyun kanonik bir şeye ulaşmak için hangi tekniği kullanmalıdır?

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

oyun döngüsü ve minimum performans ve GC etkisi ile yapmak için?

Yanıtlar:


45

Bu Application.Runçağrı, Windows mesaj pompanızı yönlendirir; bu, sonuçta Formsınıfa (ve diğerlerine) bağlayabileceğiniz tüm olaylara güç sağlayan şeydir . Bu ekosistemde bir oyun döngüsü oluşturmak için, uygulamanın mesaj pompasının ne zaman boş olduğunu ve boş kalırken, prototipik oyun döngüsünün tipik "işlem giriş durumunu, oyun mantığını güncelle, sahneyi oluştur" adımlarını dinlemek istersiniz. .

Application.IdleOlay yangınları her zaman bir kez uygulamanın ileti sırası boşaltılır ve uygulama boş durumuna geçiyor. Olayı ana formunuzun yapıcısına bağlayabilirsiniz:

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

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

Daha sonra, uygulamanın hala boşta olup olmadığını belirleyebilmeniz gerekir . IdleUygulama sırasında olay sadece bir kez patlar hale boşta. Bir mesaj sıraya girinceye kadar tekrar kovulmaz ve sıra tekrar boşaltılır. Windows Forms, mesaj kuyruğunun durumunu sorgulamak için bir yöntem göstermez, ancak sorguyu bu soruyu yanıtlayabilecek yerel bir Win32 işlevine devretmek için platform başlatma hizmetlerini kullanabilirsiniz . İçin ithalat beyanı ve destekleyici türleri şöyle görünür: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);

PeekMessagetemel olarak sıradaki bir sonraki mesaja bakmanıza izin verir; eğer varsa, doğru, aksi takdirde yanlış olur. Bu problemin amaçları için, parametrelerden hiçbiri özellikle ilgili değildir: sadece önemli olan geri dönüş değeridir. Bu, uygulamanın hala boşta olup olmadığını söyleyen bir işlev yazmanıza olanak tanır (yani, sırada hala mesaj yok):

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

Artık oyun döngüsünün tamamını yazmak için gereken her şeye sahipsin:

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);
}

Dahası, bu yaklaşım , aşağıdaki gibi görünen kanonik yerel Windows oyun döngüsüne mümkün olduğunca yakındır (P / Invoke'a en az güvenerek) :

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

Bu tür pencereler apis özellikleri ile başa çıkmak için ihtiyaç nedir? Kesin bir kronometre (fps kontrolü için) tarafından yönetilen bir süre blok yapmak yeterli olmaz mıydı?
Emir Lima

3
Bir noktada Application.Idle işleyicisinden geri dönmeniz gerekir, aksi halde uygulamanız donacaktır (çünkü Win32 iletilerinin gerçekleşmesine asla izin vermez). Bunun yerine, WM_TIMER mesajlarına dayanarak bir döngü oluşturmayı deneyebilirsiniz, ancak WM_TIMER neredeyse istediğiniz kadar kesin değildir ve her ne kadar olsa bile her şeyi o en düşük ortak payda güncelleme hızına zorlar. Birçok oyunun bazıları (fizik gibi) diğerleri değilken sabit kaldığı bağımsız renderleme ve mantıksal güncelleme oranlarına ihtiyaç duyar veya ister.
Josh

Yerel Windows oyun döngüleri aynı tekniği kullanıyor (karşılaştırma için basit bir cevap içeren cevabımı değiştirdim. Sabit bir güncelleme oranını zorlamak için zamanlayıcılar daha az esnektir ve sabit güncelleme hızınızı PeekMessage'ın daha geniş bağlamında her zaman uygulayabilirsiniz) tarzı döngü (zamanlayıcıları WM_TIMERtemelli olanlardan daha iyi hassasiyet ve GC etkisi ile kullanma )
Josh

@JoshPetrie Net olmak gerekirse, yukarıdaki boşta kontrol, SlimDX için bir işlev kullanır. Bunu cevaba dahil etmek ideal olur mu? Yoksa SlimDX meslektaşı olan 'IsApplicationIdle' kodunu okumak için kodunuzu değiştirmiş olmanız şans eseri mi?
Vaughan Hilts

** Lütfen beni görmezden
gelin

2

Josh'un cevabını kabul etti, sadece 5 kuruş eklemek istiyorum. WinForms varsayılan mesaj döngüsü (Application.Run), aşağıdakilerle değiştirilebilir (p / invoke olmadan):

[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
        }
    }
}

Ayrıca, mesaj pompasına bir miktar kod enjekte etmek istiyorsanız, bunu kullanın:

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
Bununla birlikte, bu yaklaşımı seçerseniz , DoEvents () çöp üretim yükünün farkında olmanız gerekir .
Josh

0

Bunun eski bir konu olduğunu biliyorum, ancak yukarıda önerilen tekniklere iki alternatif sunmak istiyorum. Onlara gitmeden önce, şimdiye kadar yapılan önerileri ile bazı tuzaklar:

  1. PeekMessage, buna ek olarak kütüphane yöntemlerini (SlimDX IsApplicationIdle) yaptığı gibi, oldukça fazla yükü taşır.

  2. Tamponlu RawInput kullanmak istiyorsanız, mesaj pompasını PeekMessage ile UI iş parçacığı dışındaki başka bir iş parçacığına çağırmanız gerekir, bu nedenle iki kez çağırmak istemezsiniz.

  3. Application.DoEvents sıkı bir döngü içinde çağrılacak şekilde tasarlanmamıştır, GC problemleri hızla ortaya çıkacaktır.

  4. Application.Idle veya PeekMessage kullanırken, yalnızca Boşta çalışırken iş yaptığınız için, oyununuz veya uygulamanız, kod kokusu olmadan pencerenizi taşırken veya yeniden boyutlandırırken çalışmaz.

Bunları çözmek için (RawInput yolundan inerseniz 2 hariç) aşağıdakilerden birini yapabilirsiniz:

  1. Bir Threading oluşturun. Thread ve oyun döngüsünü orada çalıştırın.

  2. IsLongRunning bayrağıyla bir Threading.Tasks.Task oluşturun ve orada çalıştırın. Microsoft, Görevlerin bugünlerde Threads yerine kullanılmasını önerir ve nedenini görmek zor değildir.

Bu tekniklerin her ikisi de grafik API'nizi, önerilen yaklaşım gibi UI iş parçacığından ve mesaj pompasından izole eder. Pencere yeniden boyutlandırma sırasında kaynak / durum tahribatı ve yeniden yaratmanın ele alınması işlemi de basitleştirilmiştir ve yapıldığında estetik açıdan çok daha profesyoneldir (mesaj pompasında kilitlenmeleri önlemek için dikkatli olunması) UI dişinin dışından.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.