EXTREMELY สับสนมากกับ "เกมความเร็วคงที่สูงสุด FPS" ลูปเกม


12

ฉันเพิ่งอ่านบทความนี้เกี่ยวกับ Game Loops: http://www.koonsolo.com/news/dewitters-gameloop/

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

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

ฉันคิดว่าคุณไม่สามารถใช้:

  • รับอินพุต 25 ขีด
  • เกมแสดงผลสำหรับ 975 เห็บ

วิธีการตั้งแต่คุณจะได้รับการป้อนข้อมูลสำหรับส่วนแรกของสองและสิ่งนี้จะรู้สึกแปลก? หรือเป็นสิ่งที่เกิดขึ้นในบทความ?


เป็นหลัก:

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

มันจะใช้ได้ยังไง?

สมมติว่าคุณค่าของเขา

MAX_FRAMESKIP = 5

สมมุติว่า next_game_tick ซึ่งกำหนดช่วงเวลาหลังจากเริ่มต้นก่อนที่ลูปเกมหลักจะพูดว่า ... 500

ในที่สุดเมื่อฉันใช้ SDL และ OpenGL สำหรับเกมของฉันด้วย OpenGL ที่ใช้สำหรับการเรนเดอร์เท่านั้นสมมติว่าGetTickCount()คืนค่าเวลาตั้งแต่ SDL_Init ถูกเรียกซึ่งมันทำ

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

ที่มา: http://www.libsdl.org/docs/html/sdlgetticks.html

ผู้เขียนสมมติว่า:

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

ถ้าเราขยายwhileคำสั่งที่เราได้รับ:

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

750 เพราะเวลาผ่านไปนับตั้งแต่next_game_tickได้รับมอบหมาย loopsเป็นศูนย์ตามที่คุณเห็นในบทความ

ดังนั้นเราจึงได้ป้อนขณะที่วงขอทำตรรกะบางอย่างและยอมรับการป้อนข้อมูล

Yadayadayada

ในตอนท้ายของ while while ซึ่งผมขอเตือนคุณว่าอยู่ใน loop ของเกมหลักของเราคือ:

next_game_tick += SKIP_TICKS;
loops++;

มาอัปเดตว่าการทำซ้ำครั้งต่อไปของรหัส while เป็นอย่างไร

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

1,000เนื่องจากเวลาผ่านไปและการทำสิ่งต่างๆก่อนที่เราจะไปถึงการเปิดตัวลูปครั้งต่อไปซึ่ง GetTickCount () ถูกเรียกคืน

540เพราะในรหัส 1000/25 = 40 ดังนั้น 500 + 40 = 540

1เพราะลูปของเราวนซ้ำหนึ่งครั้ง

5คุณรู้ไหมว่าทำไม


ดังนั้นตั้งแต่นี้ในขณะที่วนรอบจะขึ้นอยู่กับชัดเจนMAX_FRAMESKIPและไม่ได้ตั้งใจTICKS_PER_SECOND = 25;ว่าเกมจะทำงานได้อย่างถูกต้องอย่างไร

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

ฉันวางfprintf( stderr, "Test\n" );ลูปในขณะที่ไม่พิมพ์จนกว่าเกมจะจบ

ลูปของเกมนี้ทำงานอย่างไร 25 เท่าต่อวินาทีรับประกันในขณะที่เรนเดอร์เร็วเท่าที่จะทำได้?

สำหรับฉันถ้าฉันไม่มีอะไรใหญ่มันก็ดูเหมือนว่า ... ไม่มีอะไรเลย

และนี่ไม่ใช่โครงสร้างของสิ่งนี้ในขณะที่วนซ้ำวิ่งประมาณ 25 ครั้งต่อวินาทีจากนั้นอัปเดตเกมให้ตรงกับที่ฉันพูดไว้ตอนเริ่มต้นของบทความ

ถ้าเป็นเช่นนั้นทำไมเราไม่ทำอะไรที่ง่าย ๆ

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

    loops++;
}

drawGame();

และนับการแก้ไขด้วยวิธีอื่น

ยกโทษให้กับคำถามที่ยาวมากของฉัน แต่บทความนี้ทำอันตรายมากกว่าดีกับฉัน ตอนนี้ฉันสับสนอย่างรุนแรง - และไม่รู้ว่าจะใช้ลูปเกมที่เหมาะสมได้อย่างไรเนื่องจากคำถามเหล่านี้เกิดขึ้นทั้งหมด


1
Rants เป็นผู้กำกับบทความที่ดีกว่า ซึ่งเป็นส่วนหนึ่งของคำถามวัตถุประสงค์ ?
Anko

3
การวนซ้ำของเกมนี้ถูกต้องหรือไม่มีคนอธิบาย จากการทดสอบของฉันมันไม่มีโครงสร้างที่ถูกต้องในการรัน 25 ครั้งต่อวินาที อธิบายให้ฉันฟังหน่อยว่าเพราะเหตุใด นอกจากนี้ยังไม่ได้คุยโวนี่เป็นชุดคำถาม ฉันต้องใช้อีโมติคอนฉันดูโกรธไหม?
tsujp

2
เมื่อคำถามของคุณเดือดร้อนถึง "ฉันไม่เข้าใจอะไรเกี่ยวกับการวนซ้ำของเกมนี้" และคุณมีคำที่เป็นตัวอักษรจำนวนมากมันออกมาอย่างน้อยก็โกรธ
Kirbinator

@ Kirbinator ฉันรู้สึกขอบคุณ แต่ฉันพยายามหาเหตุผลทั้งหมดที่ฉันพบว่าผิดปกติในบทความนี้ดังนั้นจึงไม่ใช่คำถามตื้น ๆ ฉันไม่คิดว่ามันเป็นแก่นสารที่อย่างไรก็ตามฉันต้องการคิดว่าฉันมีบางจุดที่ถูกต้อง - หลังจากทั้งหมดที่เป็นบทความพยายามสอน แต่ไม่ทำงานดี
tsujp

7
อย่าเข้าใจฉันผิดมันเป็นคำถามที่ดีมันอาจจะสั้นกว่า 80%
Kirbinator

คำตอบ:


8

ฉันคิดว่าผู้เขียนทำผิดพลาดเล็กน้อย:

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

ควรจะเป็น

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

นั่นคือ: ตราบใดที่ยังไม่ถึงเวลาที่จะวาดเฟรมถัดไปของเราและในขณะที่เราไม่ข้ามเฟรมมากเท่ากับ MAX_FRAMESKIP เราควรรอ

ฉันยังไม่เข้าใจว่าทำไมเขาอัพเดทnext_game_tickในลูปและฉันคิดว่ามันเป็นข้อผิดพลาดอื่น ตั้งแต่เริ่มเฟรมคุณสามารถกำหนดได้ว่าควรจะให้เฟรมถัดไปเมื่อใดเมื่อใช้อัตราเฟรมคงที่ สิ่งnext game tickนี้ไม่ได้ขึ้นอยู่กับเวลาที่เหลืออยู่หลังจากการอัปเดตและการแสดงผล

ผู้เขียนทำให้เกิดข้อผิดพลาดทั่วไปอีกประการหนึ่ง

ด้วยสิ่งที่เหลือทำให้เกมหลายครั้งเท่าที่เป็นไปได้

ซึ่งหมายความว่าการแสดงผลเฟรมเดียวกันหลายครั้ง ผู้เขียนยังตระหนักถึงสิ่งต่อไปนี้:

เกมดังกล่าวจะได้รับการอัปเดตอย่างสม่ำเสมอ 50 ครั้งต่อวินาทีและการเรนเดอร์จะเสร็จเร็วที่สุด โปรดทราบว่าเมื่อการเรนเดอร์เสร็จมากกว่า 50 ครั้งต่อวินาทีเฟรมที่ตามมาบางเฟรมจะเหมือนกันดังนั้นเฟรมภาพที่แท้จริงจะแสดงสูงสุด 50 เฟรมต่อวินาที

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


+ โหวต mmmm นี่ทำให้ฉันงุนงงกับสิ่งที่ต้องทำ ฉันขอขอบคุณสำหรับความเข้าใจของคุณ ฉันอาจจะต้องเล่นรอบ ๆ ปัญหาคือความรู้เกี่ยวกับการ จำกัด FPS หรือตั้ง FPS แบบไดนามิกและวิธีการทำ ในใจฉันจำเป็นต้องจัดการอินพุตให้คงที่เพื่อให้เกมทำงานในอัตราที่เท่ากันสำหรับทุกคน นี่เป็นเพียง 2D Platformer MMO ง่าย ๆ (ในระยะยาว)
tsujp

ในขณะที่ลูปดูเหมือนถูกต้องและเพิ่มขึ้น next_game_tick อยู่ที่นั่น มีการจำลองการทำงานที่ความเร็วคงที่บนฮาร์ดแวร์ที่ช้าลงและเร็วขึ้น การเรียกใช้รหัสการเรนเดอร์ "เร็วที่สุด" (หรือเร็วกว่าฟิสิกส์อยู่ดี) เหมาะสมเมื่อมีการแก้ไขในการเรนเดอร์โค้ดเพื่อให้มันราบรื่นกว่าสำหรับวัตถุที่รวดเร็ว ฯลฯ (ตอนท้ายของบทความ) แต่นั่นเป็นการสิ้นเปลืองพลังงานหาก มันแสดงผลได้มากกว่าอุปกรณ์แสดงผลใด ๆ (หน้าจอ) ที่สามารถแสดงได้
Dotti

ดังนั้นรหัสของเขาคือการใช้งานที่ถูกต้องตอนนี้ก็คือ และเราต้องอยู่กับขยะนี้หรือไม่? หรือมีวิธีฉันสามารถค้นหานี้
tsujp

การยอมแพ้กับระบบปฏิบัติการและการรอคอยเป็นเพียงความคิดที่ดีถ้าคุณมีความคิดที่ดีว่าเมื่อใดที่ระบบปฏิบัติการจะกลับมาควบคุมคุณ - ซึ่งอาจไม่เร็วเท่าที่คุณต้องการ
Kylotan

ฉันไม่สามารถโหวตคำตอบนี้ได้ เขาขาดสิ่งที่แก้ไขอย่างสมบูรณ์ซึ่งทำให้ครึ่งหลังของคำตอบไม่ถูกต้อง
AlbeyAmakiir

4

อาจจะดีที่สุดถ้าฉันทำให้มันง่ายขึ้นเล็กน้อย:

while( game_is_running ) {

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

        next_game_tick += SKIP_TICKS;
    }
    display_game();
}

whileวนรอบภายใน mainloop ถูกใช้เพื่อเรียกใช้ขั้นตอนการจำลองจากที่เคยเป็นไปยังที่ที่ควรจะเป็นตอนนี้ update_game()ฟังก์ชั่นควรสันนิษฐานว่าSKIP_TICKSจำนวนเวลาเท่านั้นได้ผ่านไปนับตั้งแต่การโทรครั้งล่าสุด สิ่งนี้จะทำให้ฟิสิกส์ของเกมทำงานที่ความเร็วคงที่บนฮาร์ดแวร์ที่ช้าและเร็ว

เพิ่มขึ้นnext_game_tickตามปริมาณการSKIP_TICKSเคลื่อนไหวใกล้เคียงกับเวลาปัจจุบัน เมื่อสิ่งนี้มีขนาดใหญ่กว่าเวลาปัจจุบันมันจะแบ่ง ( current > next_game_tick) และ mainloop ยังคงแสดงเฟรมปัจจุบันต่อไป

หลังจากที่การแสดงผลโทรไปเพื่อGetTickCount()จะกลับมาเวลาปัจจุบันใหม่ หากเวลานี้สูงกว่าnext_game_tickหมายความว่าเราอยู่หลังขั้นตอน 1-N แล้วในการจำลองสถานการณ์และเราควรติดตามการทำงานทุกขั้นตอนในการจำลองด้วยความเร็วคงที่เดียวกัน ในกรณีนี้ถ้ามันต่ำกว่ามันก็จะแสดงเฟรมเดียวกันอีกครั้ง (เว้นแต่จะมีการแก้ไข)

รหัสดั้งเดิม จำกัด จำนวนลูปถ้าเราอยู่ไกลเกินไป ( MAX_FRAMESKIP) สิ่งนี้ทำให้มันแสดงบางสิ่งบางอย่างเท่านั้นและไม่ดูเหมือนถูกล็อกหากตัวอย่างเช่นการกลับมาทำงานต่อจากการหยุดชั่วคราวหรือเกมที่หยุดชั่วคราวในดีบักเกอร์เป็นเวลานาน (สมมติว่าGetTickCount()ไม่หยุดในช่วงเวลานั้น) จนกว่าจะทันเวลา

เพื่อกำจัดการแสดงผลไร้ประโยชน์เฟรมเดียวกันถ้าคุณไม่ได้ใช้ภายในการแก้ไขdisplay_game()คุณสามารถห่อมันไว้ภายในถ้ามีคำสั่งที่ชอบ:

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

นี่เป็นบทความที่ดีเกี่ยวกับเรื่องนี้: http://gafferongames.com/game-physics/fix-your-timestep/

นอกจากนี้อาจเป็นเหตุผลว่าทำไมfprintfผลลัพธ์ของคุณเมื่อเกมจบอาจเป็นเพราะมันไม่ได้ถูกฟลัช

ขออภัยเกี่ยวกับภาษาอังกฤษของฉัน


4

รหัสของเขาดูเหมือนถูกต้องทั้งหมด

พิจารณาการwhileวนซ้ำของชุดสุดท้าย:

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

ฉันมีระบบในสถานที่ที่ระบุว่า "ในขณะที่เกมกำลังทำงานตรวจสอบเวลาปัจจุบัน - ถ้ามันยิ่งใหญ่กว่าจำนวนเฟรมที่ใช้งานอยู่ของเราและเราข้ามการวาดน้อยกว่า 5 เฟรมแล้วข้ามการวาดและเพียงอัปเดตอินพุตและ ฟิสิกส์: อื่นวาดฉากและเริ่มการทำซ้ำการปรับปรุงต่อไป "

เมื่อการอัปเดตแต่ละครั้งเกิดขึ้นคุณจะเพิ่มเวลา "next_frame" ตามอัตราเฟรมที่คุณกำหนด จากนั้นคุณตรวจสอบเวลาของคุณอีกครั้ง หากเวลาปัจจุบันของคุณน้อยกว่าที่ควรอัปเดตเฟรมถัดไปคุณจะข้ามการอัปเดตและวาดสิ่งที่คุณมี

หาก current_time ของคุณดีขึ้น (ลองนึกภาพว่ากระบวนการวาดครั้งสุดท้ายใช้เวลานานมากเพราะมีอาการสะอึกอยู่ที่ไหนสักแห่งหรือมีการรวบรวมขยะในภาษาที่ได้รับการจัดการหรือการใช้งานหน่วยความจำที่มีการจัดการใน C ++ หรืออะไรก็ตาม) การดึงได้รับการข้ามไปและการnext_frameอัปเดตด้วยอีกหนึ่งเฟรมที่มีค่าของเวลาจนกว่าการอัปเดตจะทันเวลาที่เราควรจะอยู่บนนาฬิกาหรือเราข้ามการวาดเฟรมมากพอที่เราจะต้องวาดอย่างใดอย่างหนึ่ง พวกเขากำลังทำอะไรอยู่

หากเครื่องของคุณเร็วมากหรือเกมของคุณนั้นง่ายมากcurrent_timeอาจจะน้อยกว่าnext_frameบ่อยครั้งซึ่งหมายความว่าคุณไม่ได้อัปเดตระหว่างจุดเหล่านั้น

นั่นคือสิ่งที่การแก้ไขเข้ามาในทำนองเดียวกันคุณอาจมีบูลแยกต่างหากประกาศนอกลูปเพื่อประกาศพื้นที่ "สกปรก"

ภายในห่วงการอัปเดตคุณจะตั้งค่าdirty = trueแสดงว่าคุณได้ทำการอัปเดตจริงๆแล้ว

จากนั้นแทนที่จะโทรdraw()มาคุณจะพูดว่า:

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

จากนั้นคุณพลาดการแก้ไขใด ๆ เพื่อให้ภาพเคลื่อนไหวราบรื่น แต่คุณมั่นใจได้ว่าคุณกำลังอัปเดตเมื่อคุณมีการอัปเดตที่เกิดขึ้นจริงเท่านั้น (แทนที่จะแก้ไขระหว่างรัฐ)

หากคุณกล้าหาญมีบทความชื่อ "แก้ไขเวลาของคุณ!" โดย GafferOnGames
มันเข้าใกล้ปัญหาแตกต่างกันเล็กน้อย แต่ฉันคิดว่ามันเป็นวิธีแก้ปัญหาที่สวยกว่าซึ่งส่วนใหญ่จะเป็นสิ่งเดียวกัน (ขึ้นอยู่กับคุณสมบัติของภาษาของคุณและคุณสนใจการคำนวณทางฟิสิกส์ของเกมของคุณมากแค่ไหน)

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