अत्यधिक "लगातार खेल की गति अधिकतम एफपीएस" गेम लूप पर भ्रमित


12

मैंने हाल ही में गेम लूप्स पर यह लेख पढ़ा: 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।

लूप के अंत में, जो मुझे याद दिलाता है कि आप हमारे मुख्य गेम लूप के अंदर हैं:

next_game_tick += SKIP_TICKS;
loops++;

चलो अपडेट करते हैं कि कोड का अगला पुनरावृत्ति कैसा दिखता है

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

१००० क्योंकि समय बीतने से पहले ही इनपुट मिलता है और सामान करते हुए हम लूप के अगले प्रवेश पर पहुँच जाते हैं, जहाँ 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
लेख के लेखक की ओर रुझान बेहतर है। आपका उद्देश्य कौन सा भाग है ?
एको

3
क्या यह गेम लूप भी मान्य है, कोई समझाता है। मेरे परीक्षणों से यह 25 बार एक दूसरे को चलाने के लिए सही संरचना नहीं है। मुझे समझाएं कि यह क्यों होता है। इसके अलावा यह एक शेख़ी नहीं है, यह सवालों की एक श्रृंखला है। क्या मुझे इमोटिकॉन्स का उपयोग करना चाहिए, क्या मुझे गुस्सा आता है?
tsujp

2
चूँकि आपका प्रश्न "इस गेम लूप के बारे में मेरी समझ में नहीं आ रहा है" पर उबलता है, और आपके पास बहुत सारे बोल्ड-फ़ॉन्ट शब्द हैं, यह कम से कम अतिरंजित के रूप में आता है।
किरिबनेटर

@Kirbinator मैं उसकी सराहना कर सकता हूं, लेकिन मैं इस लेख में सभी असामान्य होने की कोशिश कर रहा था, इसलिए यह उथला और खाली सवाल नहीं है। मुझे नहीं लगता कि यह उचित है कि वैसे भी, मुझे लगता है कि मेरे पास कुछ मान्य बिंदु हैं - आखिरकार यह एक लेख है जो सिखाने की कोशिश कर रहा है, लेकिन ऐसा अच्छा काम नहीं कर रहा है।
tsujp

7
मुझे गलत मत समझो, यह एक अच्छा सवाल है, यह सिर्फ 80% कम हो सकता है।
किर्बिनेटर

जवाबों:


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 को अनावश्यक काम करने का कारण बनता है और अगर प्रतिपादन अपेक्षा से अधिक समय लेता है तो इससे आप अपने अगले फ्रेम पर बाद में काम करना शुरू कर सकते हैं, ताकि यह बेहतर हो कि OS पर चले और प्रतीक्षा करें।


+ वोट दें। Mmmm। यह वास्तव में मुझे क्या करना है पर हैरान छोड़ देता है। मैं आपकी जानकारी के लिए धन्यवाद। मेरे पास संभवतः एक नाटक होगा, समस्या वास्तव में एफपीएस को सीमित करने या एफपीएस को गतिशील रूप से सेट करने और ऐसा करने का ज्ञान है। मेरे दिमाग में फिक्स्ड इनपुट हैंडलिंग की आवश्यकता है इसलिए खेल सभी के लिए समान गति से चलता है। यह केवल एक सरल 2D Platformer MMO है (बहुत लंबे समय में)
tsujp

जबकि लूप सही लगता है और इसके लिए incrementing next_game_tick है। यह धीमी और तेजी से हार्डवेयर पर निरंतर गति से सिमुलेशन चलाने के लिए है। रेंडरिंग कोड को "जितनी जल्दी हो सके" (या वैसे भी भौतिकी से अधिक तेज़) चलाना केवल तभी समझ में आता है जब रेंडरिंग कोड में प्रक्षेप होता है ताकि तेज़ वस्तुओं आदि के लिए इसे और अधिक सुचारू बनाया जा सके (लेख का अंत), लेकिन यह सिर्फ बेकार ऊर्जा है यह जो भी आउटपुट डिवाइस (स्क्रीन) दिखाने में सक्षम है, उससे अधिक प्रदान कर रहा है।
डॉटी

तो उसका कोड एक मान्य कार्यान्वयन है, जो अब है। और हमें बस इस बर्बादी के साथ जीना है? या वहाँ तरीके मैं इस के लिए देख सकते हैं।
tsujp

OS से बचना और प्रतीक्षा करना केवल एक अच्छा विचार है यदि आपके पास एक अच्छा विचार है कि OS कब आपका नियंत्रण लौटाएगा - जो जितनी जल्दी हो सके उतना अच्छा नहीं होगा।
काइलोटन

मैं इस जवाब को वोट नहीं दे सकता। वह प्रक्षेप सामग्री पूरी तरह से गायब है, जो उत्तर की पूरी दूसरी छमाही को अमान्य बनाता है।
एल्बेअमकीर

4

शायद सबसे अच्छा अगर मैं इसे थोड़ा सरल करूं:

while( game_is_running ) {

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

        next_game_tick += SKIP_TICKS;
    }
    display_game();
}

whileमेनलूप के अंदर लूप का उपयोग सिमुलेशन चरणों को चलाने के लिए किया जाता है जहां से यह कभी भी था, जहां यह होना चाहिए। update_game()फ़ंक्शन को हमेशा यह मानना ​​चाहिए कि SKIP_TICKSअंतिम कॉल के बाद से केवल समय की राशि ही बीतती है। यह खेल भौतिकी को धीमी और तेज हार्डवेयर पर निरंतर गति से चलता रहेगा।

चाल की next_game_tickमात्रा में वृद्धि SKIP_TICKSइसे वर्तमान समय के करीब ले जाती है। जब यह वर्तमान समय से बड़ा हो जाता है, तो यह टूट जाता है ( current > next_game_tick) और मेनलूप वर्तमान फ्रेम को प्रस्तुत करना जारी रखता है।

रेंडर करने के बाद, अगली कॉल GetTickCount()नए समय की वापसी करेगी। यदि यह समय इससे अधिक है next_game_tickतो इसका मतलब है कि हम पहले से ही सिमुलेशन में 1-एन चरणों से पीछे हैं और हमें उसी स्थिर गति से सिमुलेशन में हर कदम पर चलना चाहिए। इस मामले में यदि यह कम है, तो यह एक ही फ्रेम को फिर से प्रस्तुत करेगा (जब तक कि प्रक्षेप न हो)।

मूल कोड ने छोरों की संख्या को सीमित कर दिया था यदि हम बहुत दूर छोड़ दिए जाते हैं ( 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-timestle/

इसके अलावा, शायद इसका कारण यह है कि 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 को अपडेट किया जाना चाहिए, तो आप अपडेट को छोड़ दें और जो आपको मिला है उसे ड्रा करें।

यदि आपका करंट_टाइम अधिक है (कल्पना करें कि अंतिम ड्रॉ प्रक्रिया में वास्तव में लंबा समय लगा, क्योंकि कहीं न कहीं कुछ हिचकी थी, या किसी प्रबंधित भाषा में कचरा-संग्रह का एक गुच्छा, या C ++ या किसी भी तरह से एक प्रबंधित-मेमोरी कार्यान्वयन), तब ड्रा समाप्त हो जाता है और समय के लायक एक और अतिरिक्त फ्रेम के next_frameसाथ अपडेट हो जाता है , जब तक कि या तो अपडेट हमें घड़ी में कहां होना चाहिए, या हमने पर्याप्त फ्रेम खींचना छोड़ दिया है, जिसे हम बिल्कुल आकर्षित करते हैं, इसलिए खिलाड़ी देख सकते हैं वे क्या कर रहे हैं।

यदि आपकी मशीन सुपर-फास्ट है या आपका गेम सुपर-सरल है, तो अक्सर current_timeकम हो सकता है next_frame, जिसका अर्थ है कि आप उन बिंदुओं के दौरान अपडेट नहीं कर रहे हैं।

यही वह जगह है जहाँ प्रक्षेप आता है। इसी तरह, आप "गंदे" स्थान की घोषणा करने के लिए, छोरों के बाहर घोषित एक अलग बूल रख सकते हैं।

अपडेट लूप के अंदर, आप यह निर्धारित dirty = trueकरेंगे कि आपने वास्तव में अपडेट किया है।

फिर, केवल कॉल करने के बजाय draw(), आप कहेंगे:

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

तब आप सुचारू गति के लिए किसी भी प्रक्षेप से चूक रहे हैं, लेकिन आप यह सुनिश्चित कर रहे हैं कि आप केवल तभी अपडेट कर रहे हैं जब आपको वास्तव में अपडेट मिल रहा हो (बजाय राज्यों के बीच प्रक्षेप के)।

यदि आप बहादुर हैं, तो "फिक्स योर टाइमस्टेप" नामक एक लेख है! GafferOnGames द्वारा।
यह समस्या को थोड़ा अलग तरीके से पेश करता है, लेकिन मैं इसे एक पूर्ववर्ती समाधान मानता हूं जो ज्यादातर एक ही काम करता है (आपकी भाषा की विशेषताओं पर निर्भर करता है, और आप अपने खेल की भौतिकी गणना के बारे में कितना ध्यान रखते हैं)।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.