TUYỆT VỜI Bối rối trong quá khứ Trò chơi liên tục Tốc độ FPS tối đa Vòng lặp trò chơi FPS


12

Gần đây tôi đã đọc bài viết này về Vòng lặp trò chơi: http://www.koonsolo.com/news/dewitters-gameloop/

Và đề nghị thực hiện cuối cùng là làm tôi bối rối. Tôi không hiểu làm thế nào nó hoạt động, và nó trông giống như một mớ hỗn độn.

Tôi hiểu nguyên tắc: Cập nhật trò chơi với tốc độ không đổi, với bất cứ điều gì còn lại sẽ khiến trò chơi diễn ra nhiều lần nhất có thể.

Tôi lấy nó bạn không thể sử dụng một:

  • Nhận đầu vào cho 25 tick
  • Kết xuất trò chơi cho 975 tick

Cách tiếp cận vì bạn sẽ nhận được đầu vào cho phần đầu tiên của phần thứ hai và điều này sẽ cảm thấy kỳ lạ? Hay đó là những gì đang diễn ra trong bài viết?


Bản chất:

while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP)

Làm thế nào là thậm chí hợp lệ?

Hãy giả sử giá trị của anh ấy.

MAX_FRAMESKIP = 5

Giả sử next_game_tick, được chỉ định các khoảnh khắc sau khi khởi tạo, trước khi vòng lặp trò chơi chính có nghĩa là ... 500.

Cuối cùng, khi tôi đang sử dụng SDL và OpenGL cho trò chơi của mình, với OpenGL chỉ được sử dụng để kết xuất, hãy giả sử rằng GetTickCount()sẽ trả về thời gian kể từ khi SDL_Init được gọi, như vậy.

SDL_GetTicks -- Get the number of milliseconds since the SDL library initialization.

Nguồn: http://www.libsdl.org/docs/html/sdlgetticks.html

Tác giả cũng giả định điều này:

DWORD next_game_tick = GetTickCount();
// GetTickCount() returns the current number of milliseconds
// that have elapsed since the system was started

Nếu chúng tôi mở rộng whiletuyên bố, chúng tôi nhận được:

while( ( 750 > 500 ) && ( 0 < 5 ) )

750 vì thời gian đã trôi qua kể từ khi next_game_tickđược chỉ định. loopsbằng không như bạn có thể thấy trong bài viết.

Vì vậy, chúng tôi đã bước vào vòng lặp while, hãy thực hiện một số logic và chấp nhận một số đầu vào.

Yada yada yada.

Ở cuối vòng lặp while, tôi nhắc bạn ở trong vòng lặp trò chơi chính của chúng tôi là:

next_game_tick += SKIP_TICKS;
loops++;

Chúng ta hãy cập nhật lần lặp tiếp theo của mã while như thế nào

while( ( 1000 > 540 ) && ( 1 < 5 ) )

1000 bởi vì thời gian đã trôi qua khi nhận đầu vào và thực hiện công cụ trước khi chúng tôi đạt được thông số tiếp theo của vòng lặp, trong đó GetTickCount () được gọi lại.

540 bởi vì, trong mã 1000/25 = 40, do đó, 500 + 40 = 540

1 vì vòng lặp của chúng tôi đã lặp lại một lần

5 , bạn biết tại sao.


Vì vậy, vì vòng lặp While này không phụ thuộc vào RAR RÀNG MAX_FRAMESKIPvà không có ý định TICKS_PER_SECOND = 25;làm thế nào trò chơi được cho là chạy chính xác?

Không có gì ngạc nhiên với tôi khi tôi triển khai mã này vào mã của mình, tôi có thể thêm chính xác khi tôi đổi tên các chức năng của mình để xử lý đầu vào của người dùng và vẽ trò chơi theo những gì tác giả của bài viết có trong mã ví dụ của mình, trò chơi không làm gì cả .

Tôi đã đặt một fprintf( stderr, "Test\n" );vòng lặp bên trong mà không được in cho đến khi trò chơi kết thúc.

Làm thế nào để vòng lặp trò chơi này chạy 25 lần một giây, được đảm bảo, trong khi kết xuất nhanh nhất có thể?

Đối với tôi, trừ khi tôi thiếu một cái gì đó LỚN, có vẻ như ... không có gì.

Và không phải cấu trúc này, của vòng lặp while này, được cho là chạy 25 lần một giây và sau đó cập nhật trò chơi chính xác những gì tôi đã đề cập trước khi bắt đầu bài viết?

Nếu đó là trường hợp tại sao chúng ta không thể làm một cái gì đó đơn giản như:

while( loops < 25 )
{
    getInput();
    performLogic();

    loops++;
}

drawGame();

Và tính cho nội suy một số cách khác.

Tha thứ cho câu hỏi cực kỳ dài của tôi, nhưng bài viết này đã gây hại nhiều hơn là tốt cho tôi. Bây giờ tôi rất bối rối - và không biết làm thế nào để thực hiện một vòng lặp trò chơi thích hợp vì tất cả những câu hỏi phát sinh này.


1
Rants được hướng tốt hơn về phía tác giả của bài viết. Phần nào là câu hỏi khách quan của bạn ?
Anko

3
Là vòng lặp trò chơi này thậm chí hợp lệ, một người giải thích. Từ các thử nghiệm của tôi, nó không có cấu trúc chính xác để chạy 25 lần một giây. Giải thích cho tôi tại sao nó làm. Ngoài ra đây không phải là một câu nói hay, đây là một loạt các câu hỏi. Tôi phải sử dụng biểu tượng cảm xúc, tôi có vẻ tức giận?
tsujp

2
Vì câu hỏi của bạn tập trung vào "Tôi không hiểu gì về vòng lặp trò chơi này" và bạn có rất nhiều từ in đậm, nên nó ít nhất là bực tức.
Kirbinator

@Kirbinator Tôi có thể đánh giá cao điều đó, nhưng tôi đã cố gắng đưa ra tất cả những gì tôi thấy bất thường trong bài viết này để nó không phải là một câu hỏi nông cạn và trống rỗng. Tôi không nghĩ rằng dù sao đi nữa, tôi muốn nghĩ rằng tôi có một số điểm hợp lệ - sau tất cả đó là một bài viết cố gắng dạy, nhưng không làm một công việc tốt như vậy.
tsujp

7
Đừng hiểu sai ý tôi, đó là một câu hỏi hay, nó chỉ có thể ngắn hơn 80%.
Bộ kết hợp

Câu trả lời:


8

Tôi nghĩ rằng tác giả đã mắc một lỗi nhỏ:

while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP)

nên là

while( GetTickCount() < next_game_tick && loops < MAX_FRAMESKIP)

Đó là: miễn là chưa đến lúc vẽ khung hình tiếp theo của chúng tôi và trong khi chúng tôi chưa bỏ qua nhiều khung hình như MAX_FRAMESKIP, chúng tôi nên chờ.

Tôi cũng không hiểu tại sao anh ta cập nhật next_game_ticktrong vòng lặp và tôi cho rằng đó là một lỗi khác. Vì khi bắt đầu một khung hình, bạn có thể xác định khi nào khung hình tiếp theo (khi sử dụng tốc độ khung hình cố định). Điều next game ticknày không phụ thuộc vào thời gian chúng tôi còn lại sau khi cập nhật và kết xuất.

Tác giả cũng mắc một lỗi phổ biến khác

với bất cứ điều gì còn lại làm cho trò chơi nhiều lần nhất có thể.

Điều này có nghĩa là kết xuất cùng một khung nhiều lần. Tác giả thậm chí còn nhận thức được rằng:

Trò chơi sẽ được cập nhật với tốc độ ổn định 50 lần mỗi giây và kết xuất được thực hiện nhanh nhất có thể. Lưu ý rằng khi kết xuất được thực hiện hơn 50 lần mỗi giây, một số khung hình tiếp theo sẽ giống nhau, vì vậy các khung hình trực quan thực tế sẽ được hiển thị ở mức tối đa 50 khung hình mỗi giây.

Điều này chỉ khiến GPU thực hiện các công việc không cần thiết và nếu kết xuất mất nhiều thời gian hơn dự kiến, nó có thể khiến bạn bắt đầu làm việc trên khung tiếp theo muộn hơn dự định, vì vậy tốt hơn là nên nhường hệ điều hành và chờ đợi.


+ Bình chọn. Ừm. Điều này thực sự khiến tôi hoang mang không biết phải làm gì sau đó. Tôi cảm ơn bạn đã hiểu biết của bạn. Tôi có lẽ sẽ chơi xung quanh, vấn đề thực sự là kiến ​​thức về việc giới hạn FPS hoặc cài đặt FPS một cách linh hoạt, và làm thế nào để làm điều đó. Trong tâm trí tôi, việc xử lý đầu vào cố định là bắt buộc để trò chơi chạy cùng tốc độ cho mọi người. Đây chỉ là một MMO Platformer 2D đơn giản (về lâu dài)
tsujp

Trong khi vòng lặp có vẻ đúng và tăng next_game_tick là có cho điều đó. Nó ở đó để giữ cho mô phỏng chạy ở tốc độ không đổi trên phần cứng chậm hơn và nhanh hơn. Chạy mã kết xuất "nhanh nhất có thể" (hoặc nhanh hơn vật lý) chỉ có ý nghĩa khi có phép nội suy trong mã kết xuất để làm cho nó trơn tru hơn cho các đối tượng nhanh, v.v. (cuối bài viết), nhưng điều đó chỉ lãng phí năng lượng nếu nó hiển thị nhiều hơn bất cứ thứ gì mà thiết bị đầu ra (màn hình) có thể hiển thị.
Dotti

Vì vậy, mã của anh ấy là một triển khai hợp lệ, bây giờ là vậy. Và chúng ta chỉ cần sống với chất thải này? Hoặc có những phương pháp tôi có thể tìm kiếm này.
tsujp

Mang lại cho HĐH và chờ đợi chỉ là một ý tưởng tốt nếu bạn có ý tưởng tốt khi nào HĐH sẽ trả lại quyền kiểm soát cho bạn - điều này có thể không sớm như bạn muốn.
Kylotan

Tôi không thể bình chọn câu trả lời này lên. Anh ta hoàn toàn thiếu các công cụ nội suy, điều này làm cho toàn bộ nửa sau của câu trả lời không hợp lệ.
AlbeyAmakiir

4

Có lẽ tốt nhất nếu tôi đơn giản hóa nó một chút:

while( game_is_running ) {

    current = GetTickCount();
    while(current > next_game_tick) {
        update_game();

        next_game_tick += SKIP_TICKS;
    }
    display_game();
}

while vòng lặp bên trong mainloop được sử dụng để chạy các bước mô phỏng từ bao giờ, đến nơi cần đến bây giờ. update_game()Hàm phải luôn luôn giả định rằng chỉ có SKIP_TICKSlượng thời gian đã trôi qua kể từ cuộc gọi cuối cùng. Điều này sẽ giữ cho vật lý trò chơi chạy ở tốc độ không đổi trên phần cứng chậm và nhanh.

Tăng next_game_ticktheo số lượng SKIP_TICKSdi chuyển nó gần hơn với thời gian hiện tại. Khi điều này trở nên lớn hơn thời điểm hiện tại, nó sẽ bị hỏng (current > next_game_tick ) và mainloop tiếp tục hiển thị khung hình hiện tại.

Sau khi kết xuất, cuộc gọi tiếp theo GetTickCount()sẽ trả về thời gian hiện tại mới. Nếu thời gian này cao hơnnext_game_tick có nghĩa là chúng ta đã ở sau các bước 1-N trong mô phỏng và chúng ta nên bắt kịp, chạy mọi bước trong mô phỏng với cùng tốc độ không đổi. Trong trường hợp này nếu nó thấp hơn, nó sẽ chỉ hiển thị lại cùng một khung (trừ khi có phép nội suy).

Mã gốc đã giới hạn số vòng lặp nếu chúng ta ở quá xa ( MAX_FRAMESKIP). Điều này chỉ làm cho nó thực sự hiển thị một cái gì đó và trông không giống như bị khóa nếu ví dụ như tiếp tục tạm dừng hoặc trò chơi bị tạm dừng trong trình gỡ lỗi trong thời gian dài (giả sửGetTickCount() không dừng lại trong thời gian đó) cho đến khi nó bắt kịp thời gian.

Để loại bỏ kết xuất khung hình vô dụng nếu bạn không sử dụng phép nội suy bên trong display_game(), bạn có thể bọc nó bên trong nếu câu lệnh như:

while (game_is_running) {
    current = GetTickCount();
    if (current > next_game_tick) {
        while(current > next_game_tick) {
            update_game();

            next_game_tick += SKIP_TICKS;
        }
    display_game();
    }
    else {
    // could even sleep here
    }
}

Đây cũng là một bài viết hay về điều này: http://gafferongames.com/game-physics/fix-your-timestep/

Ngoài ra, có thể lý do tại sao fprintfkết quả đầu ra của bạn khi trò chơi của bạn kết thúc có thể chỉ là nó không bị xóa.

Xin lỗi về tiếng Anh của tôi.


4

Mã của anh ta trông hoàn toàn hợp lệ.

Hãy xem xét whilevòng lặp của tập cuối:

// JS / pseudocode
var current_time = function () { return Date.now(); }, // in ms
    framerate = 1000/30, // 30fps
    next_frame = current_time(),

    max_updates_per_draw = 5,

    iterations;

while (game_running) {

    iterations = 0;

    while (current_time() > next_frame && iterations < max_updates_per_draw) {
        update_game(); // input, physics, audio, etc

        next_frame += framerate;
        iterations += 1;
    }

    draw();
}

Tôi đã có một hệ thống tại chỗ cho biết "trong khi trò chơi đang chạy, hãy kiểm tra thời gian hiện tại - nếu nó lớn hơn tốc độ khung hình đang chạy của chúng tôi và chúng tôi đã bỏ qua việc vẽ ít hơn 5 khung hình, sau đó bỏ qua bản vẽ và chỉ cập nhật đầu vào và vật lý: khác vẽ cảnh và bắt đầu lặp lại cập nhật tiếp theo "

Khi mỗi bản cập nhật xảy ra, bạn tăng thời gian "next_frame" theo tốc độ khung hình lý tưởng của mình. Sau đó, bạn kiểm tra lại thời gian của bạn. Nếu thời gian hiện tại của bạn bây giờ ít hơn khi cập nhật next_frame, thì bạn bỏ qua bản cập nhật và rút ra những gì bạn có.

Nếu current_time của bạn lớn hơn (hãy tưởng tượng rằng quá trình rút thăm cuối cùng mất một thời gian rất dài, bởi vì có một số trục trặc ở đâu đó hoặc một nhóm thu gom rác trong ngôn ngữ được quản lý hoặc triển khai bộ nhớ được quản lý trong C ++ hoặc bất cứ điều gì), sau đó vẽ bị bỏ qua và next_frameđược cập nhật với một khung thời gian bổ sung khác , cho đến khi các bản cập nhật bắt kịp với vị trí của chúng ta trên đồng hồ, hoặc chúng ta đã bỏ qua việc vẽ đủ các khung mà chúng ta PHẢI vẽ một, để người chơi có thể thấy họ đang làm gì vậy

Nếu máy của bạn siêu nhanh hoặc trò chơi của bạn siêu đơn giản, thì current_timecó thể ít hơn next_framethường xuyên, điều đó có nghĩa là bạn không cập nhật trong những thời điểm đó.

Đó là nơi nội suy xuất hiện. Tương tự như vậy, bạn có thể có một bool riêng, được khai báo bên ngoài các vòng lặp, để khai báo không gian "bẩn".

Bên trong vòng cập nhật, bạn đã đặt dirty = true, biểu thị rằng bạn thực sự đã thực hiện cập nhật.

Sau đó, thay vì chỉ gọi draw(), bạn sẽ nói:

if (is_dirty) {
    draw(); 
    is_dirty = false;
}

Sau đó, bạn đang bỏ lỡ bất kỳ phép nội suy nào để chuyển động mượt mà, nhưng bạn đảm bảo rằng bạn chỉ cập nhật khi bạn có các cập nhật thực sự xảy ra (thay vì nội suy giữa các trạng thái).

Nếu bạn dũng cảm, có một bài báo tên là "Khắc phục dấu thời gian của bạn!" bởi GafferOnGames.
Nó tiếp cận vấn đề hơi khác một chút, nhưng tôi coi đó là một giải pháp đẹp hơn, chủ yếu giống như vậy (tùy thuộc vào tính năng ngôn ngữ của bạn và mức độ bạn quan tâm đến các tính toán vật lý của trò chơ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.