Vòng lặp trò chơi C # / Windows Forms tiêu chuẩn là gì?


32

Khi viết một trò chơi bằng C # sử dụng Windows Forms cũ và một số trình bao bọc API đồ họa như SlimDX hoặc OpenTK , vòng lặp trò chơi chính phải được cấu trúc như thế nào?

Một ứng dụng Windows Forms chuẩn có một điểm vào giống như

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

và trong khi người ta có thể thực hiện một số điều cần thiết bằng cách nối các sự kiện khác nhau của Formlớp , thì những sự kiện đó không cung cấp một vị trí rõ ràng để đặt các đoạn mã để thực hiện cập nhật định kỳ liên tục cho các đối tượng logic trò chơi hoặc bắt đầu và kết thúc kết xuất khung.

Một trò chơi như vậy nên sử dụng kỹ thuật gì để đạt được điều gì đó giống với kinh điển

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

vòng lặp trò chơi, và để làm gì với hiệu suất tối thiểu và tác động của GC?

Câu trả lời:


45

Cuộc Application.Rungọi điều khiển máy bơm thông báo Windows của bạn, cuối cùng là thứ cung cấp năng lượng cho tất cả các sự kiện bạn có thể kết nối trên Formlớp (và các sự kiện khác). Để tạo một vòng lặp trò chơi trong hệ sinh thái này, bạn muốn lắng nghe khi bơm thông báo của ứng dụng trống và trong khi nó vẫn trống, hãy thực hiện "các trạng thái nhập quy trình, cập nhật logic trò chơi, kết xuất các cảnh" của vòng lặp trò chơi nguyên mẫu .

Sự Application.Idlekiện này kích hoạt một lần mỗi khi hàng đợi tin nhắn của ứng dụng bị xóa và ứng dụng đang chuyển sang trạng thái không hoạt động. Bạn có thể nối sự kiện trong hàm tạo của biểu mẫu chính của bạn:

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

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

Tiếp theo, bạn cần có khả năng xác định xem ứng dụng có còn không. Các Idlesự kiện chỉ bắn một lần, khi ứng dụng trở nên nhàn rỗi. Nó không bị bắn lần nữa cho đến khi một tin nhắn được đưa vào hàng đợi và sau đó hàng đợi lại xuất hiện. Windows Forms không đưa ra một phương thức để truy vấn trạng thái của hàng đợi tin nhắn, nhưng bạn có thể sử dụng các dịch vụ gọi nền tảng để ủy quyền truy vấn cho hàm Win32 gốc có thể trả lời câu hỏi đó . Khai báo nhập khẩu PeekMessagevà các loại hỗ trợ của nó trông giống như:

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

PeekMessagevề cơ bản cho phép bạn xem tin nhắn tiếp theo trong hàng đợi; nó trả về true nếu tồn tại, sai khác. Đối với mục đích của vấn đề này, không có tham số nào đặc biệt phù hợp: chỉ có giá trị trả về mới là vấn đề. Điều này cho phép bạn viết một hàm cho bạn biết nếu ứng dụng vẫn ở chế độ chờ (nghĩa là vẫn không có tin nhắn nào trong hàng đợi):

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

Bây giờ bạn có mọi thứ bạn cần để viết vòng lặp trò chơi hoàn chỉnh của mình:

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

Hơn nữa, cách tiếp cận này khớp càng gần càng tốt (với sự phụ thuộc tối thiểu vào P / Gọi) với vòng lặp trò chơi Windows gốc chính tắc , trông giống như:

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

Những gì cần thiết để đối phó với các tính năng apis windows như vậy? Tạo khối trong một thời gian được cai trị bởi đồng hồ bấm giờ chính xác (để kiểm soát khung hình / giây), sẽ không đủ?
Tiểu vương quốc Lima

3
Bạn cần phải quay lại từ trình xử lý Application.Idle tại một số điểm, nếu không ứng dụng của bạn sẽ bị đóng băng (vì nó không bao giờ cho phép các tin nhắn Win32 tiếp tục xảy ra). Thay vào đó, bạn có thể thử tạo một vòng lặp dựa trên các thông điệp WM_TIMER, nhưng WM_TIMER không chính xác như bạn thực sự muốn, và ngay cả khi nó sẽ buộc mọi thứ phải đạt tốc độ cập nhật mẫu số chung thấp nhất. Nhiều trò chơi cần hoặc muốn có tốc độ cập nhật logic và kết xuất độc lập, một số trò chơi (như vật lý) vẫn cố định trong khi các trò chơi khác thì không.
Josh

Các vòng lặp trò chơi Windows gốc sử dụng cùng một kỹ thuật (Tôi đã sửa đổi câu trả lời của mình để đưa ra một câu hỏi đơn giản để so sánh. Bộ hẹn giờ để buộc tốc độ cập nhật cố định kém linh hoạt hơn và bạn luôn có thể thực hiện tốc độ cập nhật cố định của mình trong bối cảnh rộng hơn của PeekMessage vòng lặp kiểu (sử dụng bộ hẹn giờ với độ chính xác và tác động GC tốt hơn so với vòng lặp WM_TIMERdựa trên cơ sở).
Josh

@JoshPetrie Để rõ ràng, việc kiểm tra nhàn rỗi ở trên sử dụng một chức năng cho SlimDX. Nó sẽ là lý tưởng để bao gồm điều này trong câu trả lời? Hay chỉ là tình cờ bạn chỉnh sửa mã để đọc 'IsApplicationIdle', bản sao của SlimDX?
Vaughan Hilts

** Xin hãy phớt lờ tôi, tôi mới nhận ra bạn xác định rõ hơn ... :)
Vaughan Hilts

2

Đồng ý với câu trả lời từ Josh, chỉ muốn thêm 5 xu của tôi. Vòng lặp thông báo mặc định của WinForms (Application.Run) có thể được thay thế bằng dòng sau (không có 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
        }
    }
}

Ngoài ra nếu bạn muốn tiêm một số mã vào bơm thông báo, thì hãy sử dụng:

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
Tuy nhiên, bạn cần lưu ý về chi phí tạo rác DoEvents () nếu bạn chọn phương pháp này.
Josh

0

Tôi hiểu rằng đây là một chủ đề cũ, nhưng tôi muốn cung cấp hai lựa chọn thay thế cho các kỹ thuật được đề xuất ở trên. Trước khi tôi đi sâu vào chúng, đây là một số cạm bẫy với các đề xuất được đưa ra cho đến nay:

  1. PeekMessage mang chi phí đáng kể, cũng như các phương thức thư viện gọi nó (SlimDX IsApplicationIdle).

  2. Nếu bạn muốn sử dụng bộ đệm RawInput được đệm, bạn sẽ cần thăm dò bơm thông báo bằng PeekMessage trên một luồng khác ngoài luồng UI, vì vậy bạn không muốn gọi nó hai lần.

  3. Application.DoEvents không được thiết kế để được gọi trong một vòng lặp chặt chẽ, các vấn đề về GC sẽ nhanh chóng xuất hiện.

  4. Khi sử dụng Application.Idle hoặc PeekMessage, vì bạn chỉ làm việc khi ở chế độ Chờ, trò chơi hoặc ứng dụng của bạn sẽ không chạy khi di chuyển hoặc thay đổi kích thước cửa sổ của bạn, không có mùi mã.

Để khắc phục những điều này (ngoại trừ 2 nếu bạn đang đi trên đường RawInput), bạn có thể:

  1. Tạo một Threading.Thread và chạy vòng lặp trò chơi của bạn ở đó.

  2. Tạo một Threading.T task.Task với cờ IsLongRasty và chạy nó ở đó. Microsoft khuyến nghị rằng Nhiệm vụ được sử dụng thay cho Chủ đề ngày nay và không khó để hiểu tại sao.

Cả hai kỹ thuật này đều tách biệt API đồ họa của bạn khỏi luồng UI và bơm thông báo, như cách tiếp cận được đề xuất. Việc xử lý phá hủy tài nguyên / trạng thái và giải trí trong quá trình thay đổi kích thước Window cũng được đơn giản hóa và chuyên nghiệp hơn rất nhiều khi thực hiện (thực hiện thận trọng để tránh bế tắc khi bơm thông điệp) từ bên ngoài luồng UI.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.