Một cách tốt để xây dựng một vòng lặp trò chơi trong OpenGL


31

Tôi hiện đang bắt đầu học OpenGL ở trường và tôi đã bắt đầu làm một trò chơi đơn giản vào ngày khác (một mình, không phải cho trường học). Tôi đang sử dụng freeglut và đang xây dựng nó trong C, vì vậy đối với vòng lặp trò chơi của tôi, tôi thực sự đã sử dụng một chức năng mà tôi đã thực hiện glutIdleFuncđể cập nhật tất cả các bản vẽ và vật lý trong một lần. Điều này tốt cho các hình ảnh động đơn giản mà tôi không quan tâm quá nhiều về tốc độ khung hình, nhưng vì trò chơi chủ yếu dựa trên vật lý, tôi thực sự muốn (cần) trói buộc nó cập nhật nhanh như thế nào.

Vì vậy, nỗ lực đầu tiên của tôi là có chức năng của mình, tôi chuyển đến glutIdleFunc( myIdle()) để theo dõi thời gian đã trôi qua kể từ cuộc gọi trước đó và cập nhật vật lý (và hiện tại là đồ họa) cứ sau nhiều mili giây. Tôi đã từng timeGetTime()làm điều này (bằng cách sử dụng <windows.h>). Và điều này khiến tôi phải suy nghĩ, liệu sử dụng chức năng nhàn rỗi có thực sự là một cách hay để đi vòng lặp trò chơi?

Câu hỏi của tôi là, cách tốt hơn để thực hiện vòng lặp trò chơi trong OpenGL là gì? Tôi có nên tránh sử dụng chức năng nhàn rỗi?



Tôi cảm thấy câu hỏi này cụ thể hơn đối với OpenGL và liệu có nên sử dụng GLUT cho vòng lặp hay không
Jeff

1
Ở đây , đừng sử dụng GLUT cho các dự án lớn hơn . Nó là tốt cho những người nhỏ hơn, mặc dù.
bobobobo

Câu trả lời:


29

Câu trả lời đơn giản là không, bạn không muốn sử dụng hàm gọi lại glutIdleFunc trong một trò chơi có một số loại mô phỏng. Lý do cho điều này là chức năng này phân tách hoạt hình và rút mã từ xử lý sự kiện cửa sổ nhưng không đồng bộ. Nói cách khác, nhận và xử lý các sự kiện cửa sổ ngăn chặn mã (hoặc bất cứ điều gì bạn đặt trong cuộc gọi lại này), điều này hoàn toàn tốt cho một ứng dụng tương tác (tương tác, sau đó phản hồi), nhưng không phải cho một trò chơi mà trạng thái vật lý hoặc trò chơi phải tiến triển độc lập với thời gian tương tác hoặc kết xuất.

Bạn muốn tách rời hoàn toàn việc xử lý đầu vào, trạng thái trò chơi và vẽ mã. Có một giải pháp dễ dàng và rõ ràng cho vấn đề này không liên quan trực tiếp đến thư viện đồ họa (nghĩa là nó dễ mang theo và dễ hình dung); bạn muốn toàn bộ vòng lặp trò chơi tạo ra thời gian và mô phỏng sẽ tiêu tốn thời gian sản xuất (tính theo khối). Tuy nhiên, điều quan trọng là sau đó tích hợp lượng thời gian mô phỏng của bạn tiêu tốn vào hoạt hình của bạn.

Lời giải thích và hướng dẫn tốt nhất mà tôi đã tìm thấy ở đây là Fix Your Timestep của Glenn Fiedler

Hướng dẫn này có cách xử lý đầy đủ, tuy nhiên nếu bạn không có mô phỏng vật lý thực tế , bạn có thể bỏ qua tích hợp thực sự nhưng vòng lặp cơ bản vẫn sôi sục (trong mã giả mã dài dòng):

// The amount of time we want to simulate each step, in milliseconds
// (written as implicit frame-rate)
timeDelta = 1000/30
timeAccumulator = 0
while ( game should run )
{
  timeSimulatedThisIteration = 0
  startTime = currentTime()

  while ( timeAccumulator >= timeDelta )
  {
    stepGameState( timeDelta )
    timeAccumulator -= timeDelta
    timeSimulatedThisIteration += timeDelta
  }

  stepAnimation( timeSimulatedThisIteration )

  renderFrame() // OpenGL frame drawing code goes here

  handleUserInput()

  timeAccumulator += currentTime() - startTime 
}

Bằng cách thực hiện theo cách này, ngăn chặn mã kết xuất, xử lý đầu vào hoặc hệ điều hành của bạn không khiến trạng thái trò chơi của bạn bị tụt lại phía sau. Phương pháp này cũng là thư viện di động và đồ họa độc lập.

GLUT là một thư viện tốt, tuy nhiên nó hoàn toàn theo hướng sự kiện. Bạn đăng ký gọi lại và bắn ra vòng lặp chính. Bạn luôn bàn giao quyền kiểm soát vòng lặp chính của mình bằng GLUT. Có nhiều hack để khắc phục nó , bạn cũng có thể giả mạo một vòng lặp bên ngoài bằng cách sử dụng bộ tính giờ và như vậy, nhưng một thư viện khác có lẽ là một cách tốt hơn (dễ dàng hơn) để đi. Có nhiều lựa chọn thay thế, đây là một vài lựa chọn (những tài liệu tốt và hướng dẫn nhanh):

  • GLFW cung cấp cho bạn khả năng nhận các sự kiện đầu vào nội tuyến (trong vòng lặp chính của riêng bạn).
  • SDL , tuy nhiên, điểm nhấn của nó không phải là OpenGL.

Bài báo hay. Vì vậy, để làm điều này trong OpenGL, tôi sẽ không sử dụng glutMainLoop, mà thay vào đó là của riêng tôi? Nếu tôi đã làm điều đó, tôi có thể sử dụng glutMouseFunc và các chức năng khác không? Tôi hy vọng điều đó có ý nghĩa, tôi vẫn còn khá mới đối với tất cả những điều này
Jeff

Không may măn. Tất cả các cuộc gọi lại đã đăng ký trong GLUT sẽ bị loại bỏ từ bên trong glutMainLoop khi hàng đợi sự kiện bị rút cạn và nó lặp lại. Tôi đã thêm một số liên kết đến các lựa chọn thay thế trong phần thân câu trả lời.
charstar

1
+1 để bỏ GLUT cho bất cứ điều gì khác. Theo như tôi biết, GLUT không bao giờ có ý nghĩa cho bất cứ điều gì ngoài các ứng dụng thử nghiệm.
Jari Komppa

Bạn vẫn phải xử lý các sự kiện hệ thống, phải không? Tôi hiểu rằng bằng cách sử dụng glutIdleFunc, nó chỉ xử lý vòng lặp GetMessage-TranslateMessage-DispatchMessage (gọi hàm của bạn bất cứ khi nào không có tin nhắn nào nhận được. Dù sao bạn cũng sẽ tự làm điều đó, nếu không sẽ phải chịu sự thiếu kiên nhẫn của HĐH trong việc gắn cờ ứng dụng của bạn là không phản hồi, phải không?
Ricket

@Ricket Về mặt kỹ thuật, glutMainLoopEvent () xử lý các sự kiện đến bằng cách gọi các cuộc gọi lại đã đăng ký. glutMainLoop () có hiệu quả {glutMainLoopEvent (); IdleCallback (); } Một xem xét quan trọng ở đây là, vẽ lại cũng là một cuộc gọi lại được xử lý từ trong vòng lặp sự kiện. Bạn có thể làm cho một thư viện hướng sự kiện hoạt động giống như một thư viện theo thời gian, nhưng Maslow's Hammer và tất cả những thứ đó ...
charstar

4

Tôi nghĩ glutIdleFunclà tốt về cơ bản, đó là những gì bạn sẽ làm bằng tay, nếu bạn không sử dụng nó. Bất kể điều gì bạn sẽ có một vòng lặp chặt chẽ không được gọi trong một mẫu cố định và bạn cần đưa ra lựa chọn cho dù bạn muốn chuyển đổi nó thành một vòng lặp thời gian cố định hay giữ cho nó một vòng lặp thời gian thay đổi và thực hiện chắc chắn tất cả các tài khoản toán học của bạn cho thời gian nội suy. Hoặc thậm chí là một kết hợp của những điều này, bằng cách nào đó.

Bạn chắc chắn có thể có một myIdlechức năng mà bạn chuyển đến glutIdleFuncvà trong đó, đo thời gian đã trôi qua, chia nó theo dấu thời gian cố định của bạn và gọi một chức năng khác nhiều lần.

Đây là những gì không nên làm:

void myFunc() {
    // (calculate timeDifference here)
    int numOfTimes = timeDifference / 10;
    for(int i=0; i<numOfTimes; i++) {
        fixedTimestep();
    }
}

fixedTimestep sẽ không được gọi sau mỗi 10 mili giây. Trong thực tế, nó sẽ chạy chậm hơn so với thời gian thực và bạn có thể sẽ kết thúc những con số mờ nhạt để bù đắp khi thực sự gốc rễ của vấn đề nằm ở việc mất phần còn lại khi bạn chia timeDifferencecho 10.

Thay vào đó, hãy làm một cái gì đó như thế này:

long queuedMilliseconds; // static or whatever, this must persist outside of your loop

void myFunc() {
    // (calculate timeDifference here)
    queuedMilliseconds += timeDifference;
    while(queuedMilliseconds >= 10) {
        fixedTimestep();
        queuedMilliseconds -= 10;
    }
}

Bây giờ cấu trúc vòng lặp này đảm bảo rằng fixedTimestepchức năng của bạn sẽ được gọi một lần trong 10 mili giây, mà không mất bất kỳ mili giây nào như phần còn lại hoặc bất cứ thứ gì tương tự.

Do tính chất đa nhiệm của các hệ điều hành, bạn không thể dựa vào một chức năng được gọi chính xác cứ sau 10 mili giây; nhưng vòng lặp trên sẽ diễn ra đủ nhanh để hy vọng nó đủ gần và bạn có thể giả sử rằng mỗi cuộc gọi fixedTimestepsẽ tăng giá trị mô phỏng 10 mili giây của bạn.

Xin vui lòng đọc câu trả lời này và đặc biệt là các liên kết được cung cấp trong câu trả lời.

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.