एक मोबाइल गेम के लिए वास्तु समाधान। भाग 1: मॉडल

आदर्श वाक्य:
- यदि आप नहीं जानते कि मैं क्या मूल्यांकन करूँगा?
- खैर, स्क्रीन और बटन होंगे।
- दीमा, आपने अब मेरे पूरे जीवन को तीन शब्दों में वर्णित किया है!
(c) गेमिंग कंपनी में एक रैली में वास्तविक संवाद



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

हमारी कठोर वास्तविकता में, हर किसी ने कम से कम एक बार अपने विचार में एक बड़ी परियोजना तैयार की है कि यह कैसे करना है, इसके बारे में अपने स्वयं के विचार हैं, और अक्सर रक्त की अंतिम बूंद तक अपने विचारों का बचाव करने के लिए तैयार है। दूसरों के लिए यह मुझे मुस्कुराता है, और प्रबंधन अक्सर इस सभी को एक विशाल ब्लैक बॉक्स के रूप में देखता है, जिसने किसी के खिलाफ आराम नहीं किया है। लेकिन क्या होगा अगर मैं आपको बताता हूं कि सही समाधान 2-3 बार नई कार्यक्षमता के निर्माण को कम करने में मदद करेगा, पुरानी 5-10 बार त्रुटियों की खोज करेगा, और आपको कई नए और महत्वपूर्ण काम करने की अनुमति देगा जो पहले से उपलब्ध नहीं थे? यह आपके दिल में वास्तुकला को देने के लिए पर्याप्त है!
एक मोबाइल गेम के लिए वास्तु समाधान। भाग 2: कमान और उनकी कतारें
एक मोबाइल गेम के लिए वास्तु समाधान। भाग 3: जेट जोर पर देखें


आदर्श


खेतों तक पहुंच


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

मैं पूर्ण कोड प्रदान नहीं करता, क्योंकि यह थोड़ा डॉफ़िग है, और सामान्य तौर पर यह उसके बारे में नहीं है। मैं एक साधारण उदाहरण के साथ अपने तर्क का वर्णन करूंगा:

public class PlayerModel { public int money; public InventoryModel inventory; /* Using */ public void SomeTestChanges() { money = 10; inventory.capacity++; } } 

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

हम अपने स्वयं के वर्ग ReactiveProperty <T> का उपयोग करेंगे, जो हुड के नीचे छिपाएंगे उन संदेशों को भेजने के लिए जो हम चाहते हैं। यह कुछ इस तरह दिखेगा:

 public class PlayerModel : Model { public ReactiveProperty<int> money = new ReactiveProperty<int>(); public ReactiveProperty<InventoryModel> inventory = new ReactiveProperty<InventoryModel>(); /* Using */ public void SomeTestChanges() { money.Value = 10; inventory.Value.capacity.Value++; } public void Subscription(Text text) { money.SubscribeWithState(text, (x, t) => t.text = x.ToString()); } } 

यह मॉडल का पहला संस्करण है। यह विकल्प पहले से ही कई प्रोग्रामर के लिए एक सपना है, लेकिन मुझे अभी भी यह पसंद नहीं है। पहली चीज जो मुझे पसंद नहीं है वह यह है कि एक्सेसिंग वैल्यू जटिल है। मैं इस उदाहरण को लिखते समय भ्रमित होने में कामयाब रहा, एक स्थान पर मूल्य को भूल गया, और यह वास्तव में इन डेटा जोड़तोड़ है जो कि हर चीज के शेर का हिस्सा बनाते हैं और मॉडल के साथ भ्रमित होते हैं। यदि आप 4.x भाषा संस्करण का उपयोग कर रहे हैं, तो आप यह कर सकते हैं:

 public ReactiveProperty<int> money { get; private set; } = new ReactiveProperty<int>(); 

लेकिन इससे सभी समस्याओं का समाधान नहीं होता है। मैं केवल लिखना चाहता हूं: इन्वेंट्री.कैपेसिटी ++ ;; मान लीजिए कि हम प्रत्येक मॉडल क्षेत्र के लिए प्राप्त करने की कोशिश करते हैं; सेट; लेकिन घटनाओं की सदस्यता लेने के लिए, हमें स्वयं ReactiveProperty तक भी पहुंचने की आवश्यकता है। स्पष्ट असुविधा और भ्रम का स्रोत। इस तथ्य के बावजूद कि हमें केवल यह इंगित करने की आवश्यकता है कि हम किस क्षेत्र की निगरानी करने जा रहे हैं। और यहाँ मैं एक मुश्किल युद्धाभ्यास के साथ आया था जो मुझे पसंद आया।

देखते हैं कि आपको यह पसंद आता है या नहीं।

यह ReactiveProperty नहीं है जिसे प्रोग्रामर के साथ काम कर रहे कंक्रीट मॉडल में डाला जाता है, इसे डाला जाता है, लेकिन इसका स्टैटिक डिस्क्रिप्टर PValue, अधिक सामान्य प्रॉपर्टी का वारिस, यह फ़ील्ड की पहचान करता है, और अंदर मॉडल कंस्ट्रक्टर के हुड के नीचे वांछित प्रकार के ReactiveProperty का निर्माण और भंडारण छिपा होता है। सबसे अच्छा नाम नहीं है, लेकिन यह हुआ, फिर नाम बदल दिया।

कोड में, यह इस तरह दिखता है:

 public class PlayerModel : Model { public static PValue<int> MONEY = new PValue<int>(); public int money { get { return MONEY.Get(this); } set { MONEY.Set(this, value) } } public static PModel<InventoryModel> INVENTORY = new PModel<InventoryModel>(); public InventoryModel inventory { get { return INVENTORY.Get(this); } set { INVENTORY.Set(this, value) } } /* Using */ public void SomeTestChanges() { money = 10; inventory.capacity++; } public void Subscription(Text text) { this.Get(MONEY).SubscribeWithState(text, (x, t) => t.text = x.ToString()); } } 

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

लेख के अंत में एक पोल होता है जो आपको सबसे अच्छा लगता है।
नीचे वर्णित सब कुछ दोनों संस्करणों में लागू किया जा सकता है।

लेन-देन


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

एक धारणा है कि यदि आप मॉडल से डेटा पढ़ने और लिखने के लिए एक अलग इंटरफ़ेस बनाते हैं, तो यह किसी तरह मदद करेगा। हकीकत में, मॉडल अतिरिक्त फ़ाइलों और थकाऊ अतिरिक्त संचालन के साथ ऊंचा हो गया है। ये सीमाएँ अंतिम हैं। प्रोग्रामर्स को, सबसे पहले, उनके बारे में जानने और लगातार सोचने के लिए मजबूर किया जाता है: "प्रत्येक विशिष्ट फ़ंक्शन, मॉडल या उसके इंटरफ़ेस को क्या देना चाहिए", और दूसरी बात, ऐसे हालात भी उत्पन्न होते हैं जब इन प्रतिबंधों को दरकिनार करना पड़ता है, इसलिए बाहर निकलने पर हमारे पास डी'आर्टगैनन हैं, जो इस सब को सफेद रंग में लेकर आए हैं, और उनके इंजन के कई उपयोगकर्ता, जो प्रोजेक्ट मैनेजर के बुरे रक्षक हैं, और लगातार दुर्व्यवहार के बावजूद, कुछ भी काम नहीं करता है। इसलिए, मैं ऐसी गलती की संभावना को केवल कसकर रोकना पसंद करता हूं। सम्मेलनों की खुराक कम करें, इसलिए बोलने के लिए।

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

 public class PlayerModel : Model { public PlayerModel(ModelRoot gamestate) : base (gamestate) {} } 

लेकिन मॉडल की अन्य विशेषताओं के लिए, यह बहुत उपयोगी है कि मॉडल में एक मूल निर्माण के लिए मूल मॉडल का लिंक है। हमारे उदाहरण में, यह player.inventory.Parent == खिलाड़ी होगा। और फिर इस निर्माण से बचा जा सकता है। कोई भी मॉडल अपने माता-पिता से जादुई स्थान के लिए लिंक प्राप्त करने और कैश करने में सक्षम होगा, और वह भी अपने माता-पिता से, और तब तक जब तक कि अगले माता-पिता उस जादुई स्थान से बाहर न हो जाएं। परिणामस्वरूप, घोषणाओं के स्तर पर, यह सब इस तरह दिखेगा:

 public class ModelRoot : Model { public bool locked { get; private set; } } public partial class Model { public Model Parent { get; protected set; } public ModelRoot Root { get; } } 

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

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

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

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

 public partial class Model { public void DispatchChanges(Command transaction); public void FixChanges(); public void RevertChanges(); } 

इंजन बनाने के चरण में विकल्प अधिक समय लेने वाला होता है, लेकिन तब उपयोग की लागत कम होती है। और सबसे महत्वपूर्ण बात, यह अगले सुधार के लिए संभावना को खोलता है।

मॉडल में किए गए परिवर्तनों के बारे में जानकारी


मुझे मॉडल से ज्यादा चाहिए। किसी भी क्षण मैं आसानी से और आसानी से देखना चाहता हूं कि मेरे कार्यों के परिणामस्वरूप मॉडल की स्थिति में क्या बदलाव आया है। उदाहरण के लिए, इस रूप में:

 {"player":{"money":10, "inventory":{"capacity":11}}} 

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

इसलिए, ReactiveProperty को न केवल अपनी वर्तमान स्थिति, बल्कि पिछले एक को भी स्टोर करना होगा। यह अत्यंत उपयोगी अवसरों के एक पूरे समूह को जन्म देता है। सबसे पहले, ऐसी स्थिति में अंतर का निष्कर्षण तेज है, और हम शांति से भोजन में यह सब डंप कर सकते हैं। दूसरे, आप एक भारी अंतर नहीं पा सकते हैं, लेकिन परिवर्तनों से एक कॉम्पैक्ट थोड़ा हैश, और एक अन्य समान गेमस्टैट में परिवर्तनों के हैश के साथ तुलना कर सकते हैं। यदि यह सहमत नहीं है, तो आपको समस्याएं हैं। तीसरा, यदि कमांड का निष्पादन निष्पादन के साथ गिर गया, तो आप हमेशा परिवर्तनों को रद्द कर सकते हैं और लेनदेन शुरू होने के समय अनिर्धारित स्थिति के बारे में पता लगा सकते हैं। राज्य में लागू टीम के साथ मिलकर, यह जानकारी अमूल्य है क्योंकि आप आसानी से स्थिति को सही ढंग से पुन: पेश कर सकते हैं। बेशक, इसके लिए आपको गेम की स्थिति के सुविधाजनक क्रमांकन और डीरियलाइज़ेशन के लिए तैयार कार्यक्षमता होनी चाहिए, लेकिन आपको वैसे भी आवश्यकता होगी।

मॉडल परिवर्तन का सीरियलाइजेशन


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

 [Flags] public enum ExportMode { all = 0x0, changes = 0x1, serverVerified = 0x2, //    ,    } /**    */ public partial class Model { public bool GetHashCode(ExportMode mode, out int code); public bool Import(BinaryReader binarySerialization); public bool Import(JSONReader json); public void ExportAll(ExportMode mode, BinaryWriter binarySerialization); public void ExportAll(ExportMode mode, JSONWriter json); public bool Export(ExportMode mode, out Dictionary<string, object> data); } 

एक्सपोर्ट विधि (ExportMode मोड, आउट डिक्शनरी <string>, object> data) के हस्ताक्षर कुछ चिंताजनक हैं। और बात यह है: जब आप पूरे पेड़ को क्रमबद्ध करते हैं, तो आप सीधे स्ट्रीम पर, या हमारे मामले में, JSONWriter को लिख सकते हैं, जो स्ट्रिंगराइटर के लिए एक सरल ऐड-ऑन है। लेकिन जब आप परिवर्तन निर्यात करते हैं, तो यह इतना आसान नहीं होता है, क्योंकि जब आप एक पेड़ में गहराई में जाते हैं और शाखाओं में से एक में जाते हैं, तो आप अभी भी यह नहीं जानते हैं कि इसमें से कुछ भी निर्यात करना है या नहीं। इसलिए, इस स्तर पर मैं दो समाधानों के साथ आया, एक सरल, दूसरा अधिक जटिल और किफायती। एक सरल बात यह है कि केवल निर्यात करते समय, आप सभी परिवर्तनों को शब्दकोश से एक पेड़ में बदल देते हैं <string, object> और List <object>। और फिर क्या हुआ, अपने पसंदीदा धारावाहिक को खिलाओ। यह एक सरल तरीका है जिसमें टैम्बोरिन के साथ नृत्य करने की आवश्यकता नहीं होती है। लेकिन इसकी खामी यह है कि ढेर में परिवर्तन निर्यात करने की प्रक्रिया में, एक बार के संग्रह के लिए जगह आवंटित की जाएगी। वास्तव में, बहुत जगह नहीं है, क्योंकि यह पूरा निर्यात एक बड़ा पेड़ देता है, और विशिष्ट कमांड पेड़ में बहुत कम बदलाव छोड़ता है।

हालांकि, कई लोग मानते हैं कि कचरा कलेक्टर को खिलाना क्योंकि ट्रोल अत्यधिक आवश्यकता के बिना आवश्यक नहीं है। उनके लिए, और मेरी अंतरात्मा को शांत करने के लिए, मैंने एक अधिक जटिल समाधान तैयार किया:

 /**    */ public partial class Model { public void ExportAll(ExportMode mode, Type propertyType, JSONWriter writer, bool newModel = false); public bool DetectChanges(ExportMode mode, Stack<Model> ierarchyChanged = null); public void ExportChanges(ExportMode mode, Type propertyType, JSONWriter writer, Queue<Model> ierarchyChanges = null); } 

इस विधि का सार पेड़ से दो बार चलना है। पहली बार, उन सभी मॉडलों को देखें जो स्वयं बदल गए हैं, या बच्चे के मॉडल में परिवर्तन हैं, और उन सभी को क्यू <मॉडल> ​​ierarchyChanges में ठीक उसी क्रम में लिखते हैं, जिस क्रम में वे अपनी वर्तमान स्थिति में पेड़ में दिखाई देते हैं। कई बदलाव नहीं हुए हैं, कतार लंबी नहीं होगी। इसके अलावा, कॉल के बीच स्टैक <मॉडल> ​​और कतार <मॉडल> ​​को रखने के लिए कुछ भी नहीं रोकता है और फिर कॉल के दौरान बहुत कम आवंटन होंगे।

और पहले से ही पेड़ के माध्यम से दूसरी बार गुजर रहा है, हर बार कतार के शीर्ष को देखना संभव होगा, और समझ सकता है कि क्या पेड़ की इस शाखा में जाना आवश्यक है या तुरंत आगे बढ़ें। यह JSONWriter को किसी अन्य मध्यवर्ती परिणाम को वापस किए बिना तुरंत लिखने की अनुमति देता है।

यह बहुत संभावना है कि यह जटिलता वास्तव में आवश्यक नहीं है, क्योंकि बाद में आप देखेंगे कि वृक्ष में परिवर्तन का निर्यात आपको केवल डिबगिंग के लिए या अपवाद के साथ दुर्घटनाग्रस्त होने पर करना होगा। सामान्य ऑपरेशन के दौरान, सब कुछ GetHashCode (ExportMode मोड, आउट कोड) तक सीमित है, जिसमें ये सभी प्रसन्नता गहराई से अलग-अलग हैं।

इससे पहले कि हम अपने मॉडल को जटिल बनाते रहें, इस बारे में बात करते हैं।

यह इतना महत्वपूर्ण क्यों है


सभी प्रोग्रामर कहते हैं कि यह बहुत महत्वपूर्ण है, लेकिन आमतौर पर कोई भी उन पर विश्वास नहीं करता है। क्यों?

सबसे पहले, क्योंकि सभी प्रोग्रामर कहते हैं कि आपको पुराने को बाहर फेंकने और नए में लिखने की आवश्यकता है। वह सब, योग्यता की परवाह किए बिना। यह पता लगाने के लिए कोई प्रबंधकीय तरीका नहीं है कि क्या यह सच है या नहीं और प्रयोग आमतौर पर बहुत महंगे हैं। प्रबंधक को एक प्रोग्रामर चुनने और अपने फैसले पर भरोसा करने के लिए मजबूर किया जाएगा। समस्या यह है कि ऐसा सलाहकार आमतौर पर वह होता है जिसके साथ प्रबंधन लंबे समय से काम कर रहा है और इसका मूल्यांकन करता है कि क्या यह अपने विचारों को महसूस कर पा रहा था। और इसके सभी बेहतरीन विचार पहले से ही वास्तविकता में सन्निहित हैं। तो यह भी एक आदर्श तरीका नहीं है यह पता लगाने का कि अन्य लोगों और विचारों में कितना अच्छा है।

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

तीसरा, भले ही अपने आप में "यह कैसे होना चाहिए" का विचार अच्छा और आदर्श है, लेकिन इसके कार्यान्वयन में कितना समय लगेगा, यह ज्ञात नहीं है। प्रोग्रामर की शीतलता पर खर्च किए गए समय की निर्भरता बहुत गैर-रैखिक है। Seigneur एक सरल कार्य को जूनियर की तुलना में बहुत तेज नहीं करेगा। डेढ़ बार, शायद। लेकिन प्रत्येक प्रोग्रामर की अपनी "जटिलता की सीमा" होती है, जिसके आगे उसका प्रभाव नाटकीय रूप से गिर जाता है। मेरे जीवन में एक मामला था जब मुझे एक जटिल जटिल कार्य का एहसास करने की आवश्यकता थी, और यहां तक ​​कि घर में इंटरनेट बंद करने और एक महीने के लिए तैयार भोजन का आदेश देने के साथ समस्या पर पूरी तरह से ध्यान केंद्रित करने से मदद नहीं मिली। लेकिन दो साल बाद, दिलचस्प किताबें पढ़ने और संबंधित कार्यों को हल करने के बाद। , मैंने तीन दिनों में इस समस्या को हल किया। मुझे यकीन है कि हर कोई अपने करियर में ऐसा कुछ याद रखेगा। और यहाँ पकड़ है! तथ्य यह है कि अगर एक सरल विचार आपके दिमाग में आया जैसा कि यह होना चाहिए, तो सबसे अधिक संभावना है कि यह नया विचार कहीं न कहीं आपकी जटिलता की व्यक्तिगत सीमा पर है, और शायद इससे थोड़ा पीछे भी। प्रबंधन, इस तरह से बार-बार जलने पर, किसी भी नए विचारों पर प्रहार करना शुरू कर देता है। और यदि आप अपने लिए खेल बनाते हैं, तो परिणाम और भी बुरा हो सकता है, क्योंकि आपको रोकने वाला कोई नहीं होगा।

लेकिन फिर, कैसे कोई भी अच्छे समाधान का उपयोग करने का प्रबंधन करता है? इसके कई तरीके हैं।

सबसे पहले, प्रत्येक कंपनी एक तैयार व्यक्ति को नियुक्त करना चाहती है जिसने पहले से ही एक पिछले नियोक्ता के साथ ऐसा किया है। किसी और पर प्रयोग के बोझ को स्थानांतरित करने का यह सबसे आम तरीका है।

दूसरी बात यह है कि जिन कंपनियों या लोगों ने अपना पहला सफल गेम बनाया है, वे सुस्त हैं, और अगले प्रोजेक्ट को शुरू करने वाले बदलाव के लिए तैयार हैं।

तीसरा, ईमानदारी से अपने आप को स्वीकार करें कि कभी-कभी आप वेतन के लिए नहीं, बल्कि प्रक्रिया के आनंद के लिए कुछ करते हैं। मुख्य बात इसके लिए समय निकालना है।

चौथा, यह लोगों के साथ-साथ सिद्ध समाधानों और पुस्तकालयों का एक सेट है, जो गेमिंग कंपनी के मुख्य फंड का निर्माण करते हैं, और यह एकमात्र ऐसी चीज है जो उस समय तक बनी रहेगी जब कोई महत्वपूर्ण व्यक्ति ऑस्ट्रेलिया में जाता है और स्थानांतरित होता है।

बहुत आखिरी, हालांकि सबसे स्पष्ट कारण नहीं है: क्योंकि यह बहुत फायदेमंद है। नए समाधानों को लिखने, उन्हें डीबग करने और त्रुटियों को पकड़ने के लिए समय पर कई समाधानों से कई कमी आती है। मैं आपको एक उदाहरण देता हूं: दो दिन पहले, ग्राहक को एक नई सुविधा में एक निष्पादन था, जिसकी संभावना 1000 में से 1 है, अर्थात, क्यूए को पुन: पेश करने के लिए यातना दी जाएगी, और जब आप इसे देते हैं, तो यह प्रति दिन 200 त्रुटि है। सब कुछ ढहने से पहले आपको स्थिति को फिर से बनाने और एक बिंदु पर ग्राहक को एक बिंदु पर पकड़ने में कितना समय लगेगा? उदाहरण के लिए, मेरे पास 10 मिनट हैं।

आदर्श


मॉडल ट्री


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

 public class PModel<T> : Property<T> where T:Model {} public partial class PlayerModel : Model { public PModel<InventoryModel> INVENTORY = new PModel<InventoryModel>(); public InventoryModel inventory { get { return INVENTORY.Value(this); } set { INVENTORY.Value(this, value); } } } 

अंतर क्या है? जब इस फ़ील्ड में एक नया मॉडल जोड़ा जाता है, तो जिस मॉडल में इसे जोड़ा गया था, उसके मूल फ़ील्ड में लिखा गया है, और हटाए जाने पर, पेरेंट फ़ील्ड रीसेट हो जाता है। सिद्धांत रूप में, सब कुछ ठीक है, लेकिन कई नुकसान हैं। पहले - प्रोग्रामर जो इसका उपयोग करेंगे, उनसे गलती हो सकती है। इससे बचने के लिए, हम अलग-अलग कोणों से इस प्रक्रिया पर छिपी जाँच करते हैं:

  1. हम PValue को ठीक कर देंगे ताकि यह अपने मूल्य के प्रकार की जांच कर ले, और विशेषज्ञों द्वारा शपथ लेते समय इसमें दिए गए मॉडल को संग्रहीत करने का प्रयास करते हुए, यह दर्शाता है कि इसके लिए एक अलग निर्माण का उपयोग करना आवश्यक है, बस भ्रमित होने के लिए नहीं। यह, निश्चित रूप से, एक रनटाइम चेक है, लेकिन यह शुरू करने के पहले ही प्रयास में शपथ लेता है, इसलिए यह करेगा।
  2. PModel Parent - , . . , .

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

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

 public class ModelPath { public Property[] properties; public Object[] indexes; public override ToString(); public static ModelPath FromString(string path); } public partial class Model { public ModelPath Path(); } public partial class ModelRoot : Model { public Model GetByPath(ModelPath path); } 

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

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

 public class Persistent : Model { public int id { get { return ID.Get(this); } set { ID.Set(this, value); } } public static RProperty<int> ID = new RProperty<int>(); } public partial class ModelRoot : Model { public int nextFreePersistentId { get { return NEXT_FREE_PERSISTENT_ID.Get(this); } set { NEXT_FREE_PERSISTENT_ID.Set(this, value); } } public static RProperty<int> NEXT_FREE_PERSISTENT_ID = new RProperty<int>(); public static PDictionaryModel<int, Persistent> PERSISTENT = new PDictionaryModel<int, Persistent>() { notServerVerified = true }; /// <summary>      Id-. </summary> public PersistentT Persistent<PersistentT>(int localId) where PersistentT : Persistent, new(); /// <summary> C    Id. </summary> public PersistentT Persistent<PersistentT>() where PersistentT : Persistent, new(); } 

थोड़ा बोझिल, लेकिन इसका इस्तेमाल किया जा सकता है। पुआल बिछाने के लिए, पर्सेंट्रेटर मॉडलरूट पैरामीटर के साथ कंस्ट्रक्टर को फास्ट कर सकता है, जो अलार्म को बढ़ाएगा यदि वे इस मॉडल को इस मॉडलरूट के तरीकों के माध्यम से नहीं बनाने की कोशिश करते हैं।

मेरे पास मेरे कोड में दोनों विकल्प हैं, और सवाल यह है कि अगर दूसरा पूरी तरह से सभी संभावित मामलों को कवर करता है, तो पहले विकल्प का उपयोग क्यों करें?

इसका उत्तर यह है कि खेल की स्थिति, सबसे पहले, लोगों द्वारा पठनीय होनी चाहिए। यदि संभव हो तो, पहले विकल्प का उपयोग करने पर यह कैसा दिखता है?

 { "persistents":{}, "player":{ "money":10, "inventory":{"capacity":11} } } 

और अब, यह कैसा दिखेगा यदि केवल दूसरे विकल्प का उपयोग किया गया था:
 { "persistents":{ "1":{"money":10, "inventory":2}, "2":{"capacity":11} }, "player":1 } 

व्यक्तिगत रूप से डिबग करने के लिए, मैं पहला विकल्प पसंद करता हूं।

मॉडल गुण तक पहुँचें


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

पहली चीज़ जो डिक्शनरी के बारे में जानने के लिए उपयोगी है, वह यह है कि इसे पढ़ने से शब्दकोश के आकार की परवाह किए बिना इतना समय नहीं लगता है। हम मॉडल में एक निजी स्थिर शब्दकोश बनाएंगे जिसमें प्रत्येक प्रकार के मॉडल को यह वर्णन किया गया है कि इसमें कौन से फ़ील्ड हैं और मॉडल का निर्माण करते समय हम इसे एक बार एक्सेस करेंगे। टाइप कंस्ट्रक्टर में, हम यह देखते हैं कि क्या हमारे प्रकार के लिए कोई विवरण है। यदि नहीं, तो हम इसे बनाते हैं, यदि ऐसा है, तो हम समाप्त एक लेते हैं। इस प्रकार, विवरण प्रत्येक वर्ग के लिए केवल एक बार बनाया जाएगा। विवरण बनाते समय, हम प्रत्येक स्थिर संपत्ति (फ़ील्ड विवरण) में प्रतिबिंब के माध्यम से निकाले गए डेटा - फ़ील्ड का नाम, और सूचकांक जिसके तहत इस फ़ील्ड के डेटा स्टोर सरणी में होंगे। इस तरह सेजब फ़ील्ड विवरण के माध्यम से पहुँचा जाता है, तो इसका भंडारण पहले से ज्ञात इंडेक्स पर सरणी से बाहर ले जाया जाएगा, अर्थात्।

कोड में, यह इस तरह दिखेगा:

 public class Model : IModelInternals { #region Properties protected static Dictionary<Type, Property[]> propertiesDictionary = new Dictionary<Type, Property[]>(); protected static Dictionary<Type, Property[]> propertiesForBinarySerializationDictionary = new Dictionary<Type, Property[]>(); protected Property[] _properties, _propertiesForBinarySerialization; protected BaseStorage[] _storages; public Model() { Type targetType = GetType(); if (!propertiesDictionary.ContainsKey(targetType)) RegisterModelsProperties(targetType, new List<Property>(), new List<Property>()); _properties = propertiesDictionary[targetType]; _storages = new BaseStorage[_properties.Length]; for (var i = 0; i < _storages.Length; i++) _storages[i] = _properties[i].CreateStorage(); } private void RegisterModelsProperties(Type target, List<Property> registered, List<Property> registeredForBinary) { if (!propertiesDictionary.ContainsKey(target)) { if (target.BaseType != typeof(Model) && typeof(Model).IsAssignableFrom(target.BaseType)) RegisterModelsProperties(target.BaseType, registered, registeredForBinary); var fields = target.GetFields(BindingFlags.Public | BindingFlags.Static); // | BindingFlags.DeclaredOnly List<Property> alphabeticSorted = new List<Property>(); for (int i = 0; i < fields.Length; i++) { var field = fields[i]; if (typeof(Property).IsAssignableFrom(field.FieldType)) { var prop = field.GetValue(this) as Property; prop.Name = field.Name; prop.Parent = target; prop.storageIndex = registered.Count; registered.Add(prop); alphabeticSorted.Add(prop); } } alphabeticSorted.Sort((p1, p2) => String.Compare(p1.Name, p2.Name)); registeredForBinary.AddRange(alphabeticSorted); Property[] properties = new Property[registered.Count]; for (int i = 0; i < registered.Count; i++) properties[i] = registered[i]; propertiesDictionary.Add(target, properties); properties = new Property[registered.Count]; for (int i = 0; i < registeredForBinary.Count; i++) properties[i] = registeredForBinary[i]; propertiesForBinarySerializationDictionary.Add(target, properties); } else { registered.AddRange(propertiesDictionary[target]); registeredForBinary.AddRange(propertiesForBinarySerializationDictionary[target]); } } CastType IModelInternals.GetStorage<CastType>(Property property) { try { return (CastType)_storages[property.storageIndex]; } catch { UnityEngine.Debug.LogError(string.Format("{0}.GetStorage<{1}>({2})",GetType().Name, typeof(CastType).Name, property.ToString())); return null; } } #endregion } 

डिजाइन थोड़ा सरल है, क्योंकि इस मॉडल के पूर्वजों में घोषित स्थिर संपत्ति विवरणकों में पहले से ही पंजीकृत भंडारण सूचकांक हो सकते हैं, और Type.GetFields () से लौटने के गुणों के आदेश की गारंटी नहीं है। आदेश के लिए और इसलिए कि गुणों को दो में पुष्ट नहीं किया गया है। समय, आपको खुद पर नजर रखने की जरूरत है।

संग्रह गुण


मॉडल ट्री पर अनुभाग में, एक निर्माण को नोटिस कर सकता है जो पहले उल्लेख नहीं किया गया था: PDEDIAModel <int, Persistent> - संग्रह वाले क्षेत्र के लिए एक विवरणक। यह स्पष्ट है कि हमें संग्रह के लिए अपनी खुद की रिपॉजिटरी बनानी होगी, जो इस बात की जानकारी संग्रहीत करती है कि लेनदेन की शुरुआत से पहले संग्रह कैसा दिखता था और अब कैसा दिखता है। यहाँ पानी के नीचे का कंकड़ पीटर आई के तहत थंडर-स्टोन का आकार है। इस तथ्य में यह शामिल है कि, हाथ पर दो लंबे शब्दकोशों होने के नाते, उनके बीच के अंतर की गणना करना एक नारकीय महंगा काम है। मेरा मानना ​​है कि ऐसे मॉडल का उपयोग मेटा से संबंधित सभी कार्यों के लिए किया जाना चाहिए, जिसका अर्थ है कि उन्हें जल्दी से काम करना चाहिए। दो राज्यों को संग्रहीत करने के बजाय, उन्हें क्लोन करना, और फिर महंगा तुलना करना, मैं एक पेचीदा हुक बनाता हूं - केवल शब्दकोश की वर्तमान स्थिति स्टोर में संग्रहीत की जाती है। अन्य दो शब्दकोश हटाए गए मान हैं।और बदले हुए तत्वों के पुराने मूल्य। अंत में, शब्दकोश में जोड़े गए नए कुंजी का एक सेट संग्रहीत किया जाता है। यह जानकारी आसानी से और जल्दी से भर दी जाती है। इसके साथ सभी आवश्यक भिन्न उत्पन्न करना आसान है, और यदि आवश्यक हो तो यह पिछले वर्षों को बहाल करने के लिए पर्याप्त है। कोड में, यह इस तरह दिखता है:

 public class DictionaryStorage<TKey, TValues> : BaseStorage { public Dictionary<TKey, TValues> current = new Dictionary<TKey, TValues>(); public Dictionary<TKey, TValues> removed = new Dictionary<TKey, TValues>(); public Dictionary<TKey, TValues> changedValues = new Dictionary<TKey, TValues>(); public HashSet<TKey> newKeys = new HashSet<TKey>(); } 

मैंने सूची के लिए समान अद्भुत भंडार के साथ आने का प्रबंधन नहीं किया है, या मेरे पास पर्याप्त समय नहीं है, मेरी दो प्रतियाँ हैं। अंतर के आकार को कम करने के लिए एक अतिरिक्त ऐड-ऑन की आवश्यकता है।

 public class ListStorage<TValue> : BaseStorage { public List<TValue> current = new List<TValue>(); public List<TValue> previouse = new List<TValue>(); //        public List<int> order = new List<int>(); //       . } 

कुल मिलाकर


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

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

पीएस कई वाक्यविन्यास त्रुटियों पर सहयोग और निर्देशों के लिए प्रस्ताव, कृपया पीएम में।

Source: https://habr.com/ru/post/hi434582/


All Articles