गेम आर्किटेक्चर / डिज़ाइन प्रश्न - वैश्विक उदाहरणों से बचने के लिए एक कुशल इंजन का निर्माण (C ++ गेम)


28

मेरे पास गेम आर्किटेक्चर के बारे में एक सवाल था: विभिन्न घटकों के एक दूसरे के साथ संवाद करने का सबसे अच्छा तरीका क्या है?

अगर यह सवाल पहले ही एक लाख बार पूछा जा चुका है, तो मैं वास्तव में माफी मांगता हूं, लेकिन मैं उस तरह की जानकारी के साथ कुछ भी नहीं पा सकता हूं जिसकी मुझे तलाश है।

मैं स्क्रैच से एक गेम बनाने की कोशिश कर रहा हूं (C ++ अगर यह मायने रखता है) और प्रेरणा के लिए कुछ ओपन सोर्स गेम सॉफ्टवेयर का अवलोकन किया है (सुपर मैरीओ इतिहास, ओपनटीडीटी और अन्य)। मुझे लगता है कि इनमें से बहुत सारे गेम डिज़ाइन वैश्विक उदाहरणों और / या सिंगलटन का उपयोग पूरे स्थान पर करते हैं (रेंडर कतार, इकाई प्रबंधक, वीडियो प्रबंधक और इसके आगे की चीज़ों के लिए)। मैं वैश्विक उदाहरणों और एकलताओं से बचने की कोशिश कर रहा हूं और एक इंजन का निर्माण कर रहा हूं, जो संभवत: शिथिल रूप से युग्मित है, लेकिन मैं कुछ बाधाओं को मार रहा हूं जो प्रभावी डिजाइन में मेरी अनुभवहीनता के कारण हैं। (इस परियोजना के लिए प्रेरणा का एक हिस्सा यह पता करने के लिए है :))

मैंने एक डिज़ाइन बनाया है जहाँ मेरे पास एक मुख्य GameCoreवस्तु है जिसमें सदस्य हैं जो वैश्विक उदाहरणों के अनुरूप हैं जिन्हें मैं अन्य परियोजनाओं में देखता हूं (यानी, इसमें एक इनपुट प्रबंधक, एक वीडियो प्रबंधक, एक GameStageऑब्जेक्ट है जो सभी संस्थाओं और गेम प्ले को नियंत्रित करता है। वर्तमान में जो भी चरण लोड किया गया है, उसके लिए)। समस्या यह है कि चूंकि सब कुछ GameCoreऑब्जेक्ट में केंद्रीकृत है , इसलिए मेरे पास विभिन्न घटकों के लिए एक दूसरे के साथ संवाद करने का आसान तरीका नहीं है।

उदाहरण के लिए, सुपर मैरीओ इतिहास को देखते हुए, जब भी खेल के एक घटक को दूसरे घटक के साथ संवाद करने की आवश्यकता होती है (यानी, एक दुश्मन वस्तु रेंडर चरण में खींची जाने वाली कतार में खुद को जोड़ना चाहती है), यह सिर्फ बातचीत करता है वैश्विक उदाहरण।

मेरे लिए, मुझे अपने खेल ऑब्जेक्ट्स को संबंधित जानकारी वापस GameCoreऑब्जेक्ट पर भेजनी होगी, ताकि GameCoreऑब्जेक्ट उस जानकारी को सिस्टम के अन्य घटक (ओं) तक पहुंचा सके, जिन्हें इसकी आवश्यकता है (यानी: ऊपर की स्थिति के लिए, प्रत्येक दुश्मन ऑब्जेक्ट अपनी रेंडर जानकारी को GameStageऑब्जेक्ट पर वापस भेज देंगे, जो इसे इकट्ठा करेगा और इसे वापस पास GameCoreकरेगा, जो इसे रेंडर करने के लिए वीडियो मैनेजर को पास करेगा)। यह वास्तव में एक भयानक डिजाइन की तरह लगता है, और मैं इस के लिए एक संकल्प के बारे में सोचने की कोशिश कर रहा था। संभव डिजाइनों पर मेरे विचार:

  1. वैश्विक उदाहरण (सुपर मेरीयो क्रॉनिकल्स, ओपनटीडी आदि का डिजाइन)
  2. बीत रहा है GameCoreएक बिचौलिया जिसके माध्यम से सभी वस्तुओं संवाद के रूप में वस्तु अधिनियम (वर्तमान डिजाइन ऊपर वर्णित है)
  3. अन्य सभी घटकों को कंपोनेंट पॉइंटर्स दें, जिनसे उन्हें बात करने की आवश्यकता होगी (यानी, ऊपर मेरीरो उदाहरण में, दुश्मन वर्ग के पास उस वीडियो ऑब्जेक्ट के लिए एक पॉइंटर होगा जिसमें उसे बात करने की आवश्यकता है)
  4. गेम को सबसिस्टम में तोड़ें - उदाहरण के लिए, ऑब्जेक्ट में मैनेजर ऑब्जेक्ट्स हैं GameCoreजो अपने सबसिस्टम में ऑब्जेक्ट्स के बीच संचार को संभालते हैं
  5. (अन्य विकल्प? ....)

मैं सबसे अच्छा समाधान होने के लिए विकल्प 4 से ऊपर की कल्पना करता हूं, लेकिन मुझे इसे डिजाइन करने में थोड़ी परेशानी हो रही है ... शायद इसलिए कि मैं उन डिजाइनों के बारे में सोच रहा हूं जिन्हें मैंने देखा है कि ग्लोबल्स का उपयोग करें। ऐसा लगता है कि मैं उसी समस्या को ले रहा हूं जो मेरे वर्तमान डिजाइन में मौजूद है और प्रत्येक उपतंत्र में इसे छोटे स्तर पर दोहरा रहा है। उदाहरण के लिए, GameStageऊपर वर्णित वस्तु कुछ हद तक इस पर एक प्रयास है, लेकिन GameCoreवस्तु अभी भी प्रक्रिया में शामिल है।

किसी को भी किसी भी डिजाइन सलाह यहाँ की पेशकश कर सकते हैं?

धन्यवाद!


1
मैं आपकी वृत्ति को समझता हूं कि सिंगलटन महान डिजाइन नहीं हैं। मेरे अनुभव में, वे सिस्टम भर में संचार का प्रबंधन करने का सबसे सरल तरीका है
एम्मेट बटलर

4
एक टिप्पणी के रूप में जोड़ना क्योंकि मुझे नहीं पता कि यह सबसे अच्छा अभ्यास है। मेरे पास एक केंद्रीय GameManager है जो सबसिस्टम से बना है जैसे कि InputSystem, GraphicsSystem, आदि। प्रत्येक सबसिस्टम GameManager को कंस्ट्रक्टर में एक पैरामीटर के रूप में लेता है और एक क्लास प्राइवेट मेंबर को रेफरेंस स्टोर करता है। उस समय, मैं GameManager संदर्भ के माध्यम से इसे एक्सेस करके किसी अन्य प्रणाली का उल्लेख कर सकता हूं।
इनिशीर

मैंने टैग बदल दिए क्योंकि यह प्रश्न कोड के बारे में है, न कि गेम डिज़ाइन के बारे में।
१४:११ पर Kl

यह धागा थोड़ा पुराना है, लेकिन मुझे बिल्कुल वही समस्या है। मैं OGRE का उपयोग करता हूं और मैं सबसे अच्छा तरीका इस्तेमाल करने की कोशिश करता हूं, मेरे विचार में विकल्प # 4 सबसे अच्छा तरीका है। मैंने उन्नत ओग्रे फ्रेमवर्क जैसा कुछ बनाया है, लेकिन यह बहुत मॉड्यूलर नहीं है। मुझे लगता है कि मुझे एक सबसिस्टम इनपुट हैंडलिंग की आवश्यकता है जो केवल कीबोर्ड हिट और माउस आंदोलनों को प्राप्त करते हैं। जो मुझे पता नहीं है, मैं सबसिस्टम के बीच इस तरह का "संचार" प्रबंधक कैसे बना सकता हूं?
डोमिनिको २२

1
Hi @ Dominik2000, यह एक Q & A साइट है, न कि एक फोरम। यदि आपके पास एक प्रश्न है तो आपको एक वास्तविक प्रश्न पोस्ट करना चाहिए, और किसी मौजूदा का जवाब नहीं। देखें पूछे जाने वाले प्रश्न अधिक जानकारी के लिए।
जोश

जवाबों:


19

हमारे वैश्विक डेटा को व्यवस्थित करने के लिए हम अपने गेम में जो कुछ भी उपयोग करते हैं, वह है ServiceLocator डिज़ाइन पैटर्न। सिंगलटन पैटर्न की तुलना में इस पैटर्न का लाभ यह है कि एप्लिकेशन के रनटाइम के दौरान आपके वैश्विक डेटा का कार्यान्वयन बदल सकता है। साथ ही, रनटाइम के दौरान आपकी वैश्विक वस्तुओं को भी बदला जा सकता है। एक और लाभ यह है कि अपनी वैश्विक वस्तुओं के प्रारंभिककरण के क्रम को प्रबंधित करना आसान है, जो विशेष रूप से C ++ में बहुत महत्वपूर्ण है।

उदाहरण के लिए (C # कोड जिसे आसानी से C ++ या Java में अनुवादित किया जा सकता है)

कहते हैं कि आपके पास एक रेंडर बैकएंड इंटरफ़ेस है जिसमें सामान प्रदान करने के लिए कुछ सामान्य ऑपरेशन हैं।

public interface IRenderBackend
{
    void Draw();
}

और आपके पास डिफ़ॉल्ट रेंडर बैकएंड कार्यान्वयन है

public class DefaultRenderBackend : IRenderBackend
{
    public void Draw()
    {
        //do default rendering stuff.
    }
}

कुछ डिजाइनों में यह विश्व स्तर पर रेंडर बैकएंड का उपयोग करने में सक्षम होने के लिए वैध लगता है। में सिंगलटन पैटर्न है कि इसका मतलब है कि प्रत्येक IRenderBackend कार्यान्वयन अद्वितीय वैश्विक उदाहरण के रूप में लागू किया जाना चाहिए। लेकिन ServiceLocator पैटर्न का उपयोग करने के लिए इसकी आवश्यकता नहीं होती है।

ऐसे:

public class ServiceLocator<T>
{
    private static T currGlobalInstance;

    public static T Service
    {
        get { return currGlobalInstance; }
        set { currGlobalInstance = value; }
    }
}

अपनी वैश्विक वस्तु तक पहुँचने में सक्षम होने के लिए आपको पहले इसे पहचानना होगा।

//somewhere during program initialization
ServiceLocator<IRenderBackend>.Service = new DefaultRenderBackend();

//somewhere else in the code
IRenderBackend currentRenderBackend = ServiceLocator<IRenderBackend>.Service;

बस यह प्रदर्शित करने के लिए कि रनटाइम के दौरान कार्यान्वयन कैसे भिन्न हो सकते हैं, मान लें कि आपके गेम में एक मिनीगेम है जहां रेंडरिंग आइसोमेट्रिक है और आप एक IsometricRenderBackend को लागू करते हैं ।

public class IsometricRenderBackend : IRenderBackend
{
    void draw()
    {
        //do rendering using an isometric view
    }
}

जब आप वर्तमान स्थिति से मिनीगेम राज्य में संक्रमण करते हैं, तो आपको सेवा लोकेटर द्वारा प्रदान किए गए वैश्विक रेंडर बैकेंड को बदलने की आवश्यकता होती है।

ServiceLocator<IRenderBackend>.Service = new IsometricRenderBackend();

एक और लाभ यह है कि आप अशक्त सेवाओं का भी उपयोग कर सकते हैं। उदाहरण के लिए, यदि हमारे पास ISoundManager सेवा थी और उपयोगकर्ता ध्वनि बंद करना चाहता था, तो हम बस एक NullSoundManager को लागू कर सकते हैं, जब इसके तरीकों को कॉल करने पर कुछ नहीं करता है, इसलिए NullSoundManager ऑब्जेक्ट को ServiceLocator की सेवा ऑब्जेक्ट सेट करके हम प्राप्त कर सकते हैं इस परिणाम के साथ शायद ही किसी भी काम की राशि।

संक्षेप में, कभी-कभी वैश्विक डेटा को खत्म करना असंभव हो सकता है लेकिन इसका मतलब यह नहीं है कि आप उन्हें ठीक से और ऑब्जेक्ट ओरिएंटेड तरीके से व्यवस्थित नहीं कर सकते हैं।


मैंने पहले इस पर ध्यान दिया है लेकिन वास्तव में इसे अपने किसी भी डिजाइन में लागू नहीं किया है। इस बार, मैंने योजना बनाई है। धन्यवाद :)
Awesomania

3
@Erevis तो, मूल रूप से, आप बहुरूपी वस्तु के वैश्विक संदर्भ का वर्णन कर रहे हैं। बदले में, यह सिर्फ दोहरे अप्रत्यक्ष (सूचक -> इंटरफ़ेस -> कार्यान्वयन) है। C ++ में इसे आसानी से लागू किया जा सकता है std::unique_ptr<ISomeService>
छाया में बारिश

1
आप "पहले पहुंच पर आरंभ" के लिए आरंभीकरण रणनीति को बदल सकते हैं और लोकेटर के लिए कुछ बाहरी कोड अनुक्रम आवंटित करने और सेवाओं को धक्का देने की आवश्यकता से बच सकते हैं। आप सेवाओं में एक "निर्भर करता है" सूची को जोड़ सकते हैं ताकि जब एक को प्रारंभिक किया जाए तो यह स्वचालित रूप से अन्य सेवाओं को स्थापित करेगा जो कि इसकी आवश्यकता है और प्रार्थना न करें कि किसी को याद है कि main.cpp में ऐसा करें। भविष्य के ट्वीकिंग के लिए लचीलेपन के साथ एक अच्छा जवाब।
पैट्रिक ह्यूजेस

4

गेम इंजन को डिजाइन करने के कई तरीके हैं और यह वास्तव में वरीयता के लिए उबलता है।

मूल बातें से बाहर निकलने के लिए, कुछ डेवलपर्स इसे पिरामिड की तरह डिजाइन करना पसंद करते हैं, जहां कुछ शीर्ष कोर वर्ग होते हैं, जिन्हें अक्सर कर्नेल, कोर या फ्रेमवर्क क्लास कहा जाता है, जो उप-श्रेणियों की एक श्रृंखला बनाता है, उनका मालिक होता है और आरंभ करता है। ऑडियो, ग्राफिक्स, नेटवर्क, भौतिकी, एआई, और टास्क, इकाई, और संसाधन प्रबंधन के रूप में। आमतौर पर, ये सबसिस्टम इस फ्रेमवर्क क्लास द्वारा आपके सामने आते हैं और आमतौर पर आप इस फ्रेमवर्क क्लास को कंस्ट्रक्टर के तर्क के रूप में उपयुक्त रूप से अपनी कक्षाओं में पास कर देते हैं।

मेरा मानना ​​है कि आप विकल्प # 4 की अपनी सोच के साथ सही रास्ते पर हैं।

यह ध्यान में रखें कि जब यह संचार की बात आती है, तो इसका मतलब यह नहीं है कि सीधे एक सीधा फ़ंक्शन कॉल करें। कई अप्रत्यक्ष तरीके संचार हो सकते हैं, चाहे वह किसी अप्रत्यक्ष विधि के माध्यम से हो Signal and Slotsया उपयोग हो Messages

कभी-कभी खेलों में, हमारे गेम लूप को जितना जल्दी संभव हो सके रखने के लिए, अतुल्यकालिक रूप से क्रियाओं को करने की अनुमति देना महत्वपूर्ण है ताकि फ्रेम दर नग्न आंखों पर तरल हो। खिलाड़ी धीमे और तड़क-भड़क वाले दृश्यों को पसंद नहीं करते हैं और इसलिए हमें उनके लिए चीजों को बहते रहने के तरीके खोजने पड़ते हैं लेकिन तर्क को बहते रहना चाहिए, लेकिन जांच और आदेश भी देना चाहिए। जबकि अतुल्यकालिक संचालन में अपना स्थान होता है, वे हर ऑपरेशन के लिए उत्तर नहीं होते हैं।

बस यह जान लें कि आपके पास सिंक्रोनस और एसिंक्रोनस दोनों संचारों का मिश्रण होगा। जो उपयुक्त है उसे उठाओ, लेकिन पता है कि आपको अपने उप-प्रणालियों के बीच दोनों शैलियों का समर्थन करने की आवश्यकता होगी। दोनों के लिए डिजाइनिंग समर्थन आपको भविष्य में अच्छी तरह से काम देगा।


1

आपको बस यह सुनिश्चित करना है कि कोई उल्टा या चक्रीय निर्भरता न हो। उदाहरण के लिए, यदि आपके पास एक वर्ग है Core, और यह Coreएक है Level, और Levelउसकी एक सूची है Entity, तो निर्भरता के पेड़ को इस तरह दिखना चाहिए:

Core --> Level --> Entity

तो, इस प्रारंभिक निर्भरता पेड़ को देखते हुए, आपको कभी भी Entityनिर्भर नहीं होना चाहिए , Levelया कभी भी निर्भर नहीं होना चाहिए । यदि दोनों में से या जरूरत डेटा निर्भरता वृक्ष में उच्च ऊपर है कि करने के लिए उपयोग किया है, यह संदर्भ द्वारा एक पैरामीटर के रूप में पारित किया जाना चाहिए।CoreLevelCoreLevelEntity

निम्नलिखित कोड पर विचार करें (C ++):

class Core;
class Entity;
class Level;

class Level
{
    public:
        Level(Core& coreIn) : core(coreIn) {}

        Core& core;
}

class Entity
{
    public:
        Entity(Level& levelIn) : level(levelIn) {}

        Level& level;
}

इस तकनीक का उपयोग करके, आप देख सकते हैं कि प्रत्येक Entityकी पहुंच है Level, और उस Levelतक पहुंच है Core। ध्यान दें कि प्रत्येक मेमोरी को बर्बाद Entityकरते हुए उसी के संदर्भ में स्टोर करता है Level। इस पर ध्यान देने पर, आपको सवाल करना चाहिए कि क्या प्रत्येक को Entityवास्तव में एक्सेस की आवश्यकता है या नहीं Level

मेरे अनुभव में, या तो ए) रिवर्स निर्भरता से बचने के लिए वास्तव में एक स्पष्ट समाधान है, या बी) वैश्विक उदाहरणों और एकलताओं से बचने का कोई संभव तरीका नहीं है।


क्या मैं कुछ भूल रहा हूँ? आप उल्लेख करते हैं कि 'आपके पास एंटिटी को लेवल पर कभी निर्भर नहीं होना चाहिए' लेकिन फिर आप इसका वर्णन 'एंटिटी (लेवल एंड लेवल इन)' के रूप में करते हैं। मैं समझता हूं कि निर्भरता को रेफ द्वारा पारित किया जाता है लेकिन यह अभी भी एक निर्भरता है।
एडम नाइलर

@AdamNaylor बिंदु यह है कि कभी-कभी आपको वास्तव में रिवर्स निर्भरता की आवश्यकता होती है, और आप संदर्भों को पारित करके ग्लोबल्स से बच सकते हैं। सामान्य तौर पर, हालांकि, इन निर्भरताओं से पूरी तरह से बचना सबसे अच्छा है, और यह हमेशा स्पष्ट नहीं है कि यह कैसे करना है।
सेब

0

तो, मूल रूप से, आप वैश्विक परिवर्तनशील स्थिति से बचना चाहते हैं ? आप इसे स्थानीय, अपरिवर्तनीय, या बिल्कुल भी नहीं बना सकते हैं। लैटर सबसे कुशल और लचीला है, इमो। इसे ipleitation hiding के नाम से जाना जाता है।

class ISomeComponent // abstract base class
{
    //...
};

extern ISomeComponent & g_SomeComponent; // will be defined somewhere else;

0

प्रश्न वास्तव में यह प्रतीत होता है कि प्रदर्शन का त्याग किए बिना युग्मन को कैसे कम किया जाए। सभी वैश्विक ऑब्जेक्ट्स (सेवाएँ) आमतौर पर एक प्रकार का संदर्भ बनाते हैं जो खेल के रन-टाइम के दौरान परिवर्तनशील होता है। इस अर्थ में, सेवा लोकेटर पैटर्न, संदर्भ के विभिन्न भागों को अनुप्रयोग के विभिन्न भागों में फैलाता है, जो कि वह चाहते हैं या नहीं हो सकता है। एक अन्य वास्तविक विश्व दृष्टिकोण इस तरह की संरचना की घोषणा करना होगा:

struct sEnvironment
{
    owning<iAudio*> m_Audio;
    owning<iRenderer*> m_Renderer;
    owning<iGameLevel*> m_GameLevel;
    ...
}

और इसे गैर-स्वामित्व वाले कच्चे सूचक के रूप में पास करें sEnvironment*। यहां पॉइंटर्स इंटरफेस की ओर इशारा करते हैं इसलिए सर्विस लोकेटर की तुलना में कपलिंग को इसी तरह कम किया जाता है। हालाँकि, सभी सेवाएँ एक ही स्थान पर हैं (जो अच्छी हो सकती है या नहीं भी)। यह सिर्फ एक और तरीका है।

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