เกมวน C # / Windows Forms มาตรฐานคืออะไร?


32

เมื่อเขียนเกมใน C # ที่ใช้ Windows Forms แบบเก่าและกราฟิก API wrapper บางอย่างเช่นSlimDXหรือOpenTKเกมหลักจะต้องมีโครงสร้างอย่างไร

แอปพลิเคชันแบบฟอร์มมาตรฐานของ Windows มีจุดเข้าใช้ที่มีลักษณะดังนี้

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

และในขณะที่บางคนสามารถบรรลุสิ่งที่จำเป็นโดยการเชื่อมโยงเหตุการณ์ต่าง ๆ ของFormชั้นเรียนเหตุการณ์เหล่านั้นไม่ได้ให้สถานที่ที่ชัดเจนในการวางบิตของรหัสเพื่อดำเนินการปรับปรุงเป็นระยะเพื่อวัตถุตรรกะของเกมหรือเริ่มและสิ้นสุดการเรนเดอร์ กรอบ

เทคนิคใดที่เกมดังกล่าวควรใช้เพื่อให้ได้สิ่งที่คล้ายกับบัญญัติ

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

ลูปเกมและจะทำอย่างไรกับประสิทธิภาพที่น้อยที่สุดและผลกระทบ GC?

คำตอบ:


45

การApplication.Runโทรจะขับเคลื่อนปั๊มข้อความ Windows ของคุณซึ่งท้ายที่สุดแล้วจะช่วยเพิ่มพูนกิจกรรมทั้งหมดที่คุณสามารถขอได้ในFormชั้นเรียน (และอื่น ๆ ) ในการสร้างลูปเกมในระบบนิเวศนี้คุณต้องการฟังเมื่อปั๊มข้อความของแอปพลิเคชันว่างเปล่าและในขณะที่ยังว่างอยู่ให้ทำ "สถานะอินพุตกระบวนการ, อัปเดตลอจิกเกม, แสดงฉาก" ของลูปเกมต้นแบบ .

Application.Idleเหตุการณ์ fires ครั้งทุกเวลาคิวข้อความของโปรแกรมประยุกต์ที่มีการอบและการประยุกต์ใช้จะถูกเปลี่ยนไปเป็นรัฐที่ไม่ได้ใช้งาน คุณสามารถขอเหตุการณ์ในตัวสร้างของฟอร์มหลักของคุณ:

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โดยทั่วไปช่วยให้คุณดูข้อความถัดไปในคิว มันจะคืนค่าจริงหากมีอยู่จริงเป็นอย่างอื่น สำหรับวัตถุประสงค์ของปัญหานี้ไม่มีพารามิเตอร์ที่เกี่ยวข้องโดยเฉพาะ: เป็นเพียงค่าส่งคืนที่มีความสำคัญ วิธีนี้ช่วยให้คุณสามารถเขียนฟังก์ชั่นที่จะบอกคุณว่าแอปพลิเคชันยังคงว่างอยู่ (นั่นคือยังไม่มีข้อความในคิว):

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

ความต้องการในการจัดการกับคุณลักษณะของหน้าต่าง apis คืออะไร การบล็อกชั่วขณะถูกควบคุมโดยนาฬิกาจับเวลาที่แม่นยำ (สำหรับการควบคุม fps) จะไม่เพียงพอหรือไม่
Emir Lima

3
คุณจำเป็นต้องกลับมาจากตัวจัดการ Application.Idle ในบางจุดมิฉะนั้นแอปพลิเคชันของคุณจะหยุดนิ่ง คุณสามารถลองวนซ้ำตามข้อความ WM_TIMER ได้ แต่ WM_TIMER นั้นแทบจะไม่แม่นยำเท่ากับที่คุณต้องการจริงๆและแม้ว่ามันจะบังคับให้ทุกอย่างเป็นอัตราการอัปเดตต่ำสุดสามัญทั่วไป เกมจำนวนมากต้องการหรือต้องการให้มีเรนเดอร์อิสระและอัตราการอัปเดตเชิงตรรกะซึ่งบางเกม (เช่นฟิสิกส์) ยังคงได้รับการแก้ไขในขณะที่เกมอื่น ๆ
จอช

Native Game ลูปของ Windows ใช้เทคนิคเดียวกัน (ฉันได้แก้ไขคำตอบของฉันเพื่อให้ง่ายสำหรับการเปรียบเทียบตัวจับเวลาเพื่อบังคับให้อัตราการอัปเดตคงที่มีความยืดหยุ่นน้อยกว่าและคุณสามารถใช้อัตราการอัปเดตคงที่ของคุณ -style loop (ใช้ตัวจับเวลาที่มีความแม่นยำและผลกระทบ GC ดีกว่าWM_TIMER-based)
Josh

@JoshPetrie เพื่อให้ชัดเจนการตรวจสอบข้างบนสำหรับการไม่ใช้งานใช้ฟังก์ชั่นสำหรับ SlimDX มันจะเหมาะที่จะรวมไว้ในคำตอบหรือไม่ หรือมันเป็นโอกาสที่คุณจะแก้ไขโค้ดเพื่ออ่าน 'IsApplicationIdle' ซึ่งเป็นรุ่น SlimDX
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 () ค่าโสหุ้ยการสร้างขยะ หากคุณเลือกวิธีการนี้
จอช

0

ฉันเข้าใจว่านี่เป็นหัวข้อเก่า แต่ฉันต้องการให้ทางเลือกสองทางกับเทคนิคที่แนะนำข้างต้น ก่อนที่ฉันจะเข้าไปพวกเขานี่คือข้อผิดพลาดบางอย่างที่มีข้อเสนอที่ได้ทำไปแล้ว:

  1. PeekMessage มีค่าใช้จ่ายจำนวนมากเช่นเดียวกับวิธีการห้องสมุดที่เรียกมันว่า (SlimDX IsApplicationIdle)

  2. หากคุณต้องการจ้าง RawInput ที่บัฟเฟอร์คุณจะต้องสำรวจความคิดเห็นของปั๊มข้อความด้วย PeekMessage บนเธรดอื่นนอกเหนือจากเธรด UI ดังนั้นคุณจึงไม่ต้องการเรียกมันซ้ำสองครั้ง

  3. Application.DoEvents ไม่ได้ถูกออกแบบมาให้เรียกใช้ในการวนรอบอย่างแน่นหนาปัญหา GC จะเกิดขึ้นอย่างรวดเร็ว

  4. เมื่อใช้ Application.Idle หรือ PeekMessage เนื่องจากคุณทำงานเฉพาะเมื่อไม่ได้ใช้งานเกมหรือแอปพลิเคชันของคุณจะไม่ทำงานเมื่อย้ายหรือปรับขนาดหน้าต่างของคุณโดยไม่มีกลิ่นรหัส

ในการหลีกเลี่ยงสิ่งเหล่านี้ (ยกเว้น 2 หากคุณกำลังเดินไปตามถนน RawInput) คุณสามารถ:

  1. สร้าง Threading.Thread และรันลูปเกมของคุณที่นั่น

  2. สร้าง Threading.Tasks.Task ด้วยการตั้งค่าสถานะ IsLongRunning และเรียกใช้ที่นั่น Microsoft แนะนำให้ใช้ Tasks แทนเธรดในวันนี้และไม่ยากที่จะดูว่าทำไม

เทคนิคทั้งสองนี้แยก API กราฟิกของคุณออกจากเธรด UI และปั๊มข้อความเช่นเดียวกับวิธีที่แนะนำ การจัดการการทำลายทรัพยากร / รัฐและการพักผ่อนหย่อนใจในระหว่างการปรับขนาดหน้าต่างก็ง่ายขึ้นและมีความเป็นมืออาชีพมากขึ้นเมื่อดำเนินการเสร็จแล้ว

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.