एकता पर मोबाइल उपकरणों के लिए बड़ा शहर। विकास और अनुकूलन में अनुभव



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

खेल


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

आर्किटेक्चर


छवि

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

  • AudioManager
  • UIManager
  • InputManager

मैंने कट्टरता से ऐसी वास्तुकला बनाने की कोशिश की जिसमें मैं प्रबंधक से विभिन्न घटकों का प्रबंधन कर सकता हूं। उदाहरण के लिए, AudioManager सभी ध्वनियों का प्रबंधन करता है, UIManager में प्रबंधन के लिए सभी UI तत्व और विधियाँ हैं। सभी इनपुट घटनाओं और प्रतिनिधियों का उपयोग करके इनपुट प्रबंधक के माध्यम से संसाधित किया जाता है।

सरलीकृत AudioManager। यह आपको गेम ऑब्जेक्ट में कई ऑडियो घटकों को जोड़ने की अनुमति देता है और यदि आवश्यक हो, तो ध्वनि चलाएं:

public class AudioManager : MonoBehaviour { public static AudioManager instance = null; //  public AudioClip metalHitAC; //   private AudioSource metalHitAS; //    public bool isMetalHit = false; private void Awake() { if (instance == null) instance = this; else if (instance == this) Destroy(gameObject); } void Start() { metalHitAS = AddAudio(metalHitAC, false, false, 0.3f, 1); } void LateUpdate() { if (isMetalHit) { metalHitAS.Play(); isMetalHit = false; } } AudioSource AddAudio(AudioClip clip, bool loop, bool playAwake, float vol, float pitch) { var newAudio = gameObject.AddComponent<AudioSource>(); newAudio.clip = clip; newAudio.loop = loop; newAudio.playOnAwake = playAwake; newAudio.volume = vol; newAudio.pitch = pitch; newAudio.minDistance = 10; return newAudio; } public AudioSource AddAudioToGameObject(AudioClip clip, bool loop, bool playAwake, float vol, float pitch, float minDistance, float maxDistance, GameObject go) { var newAudio = go.AddComponent<AudioSource>(); newAudio.spatialBlend = 1; newAudio.clip = clip; newAudio.loop = loop; newAudio.playOnAwake = playAwake; newAudio.volume = vol; newAudio.pitch = pitch; newAudio.minDistance = minDistance; newAudio.maxDistance = maxDistance; return newAudio; } } 

स्टार्टअप पर, AddAudio विधि एक घटक को जोड़ती है, और फिर कहीं से भी हम उस ध्वनि को खेल सकते हैं जिसकी हमें आवश्यकता है:

 AudioManager.instance.isMetalHit = true; 

इस उदाहरण में, यह विधि में वापस खेलने के लिए समझदार होगा।

एक सरलीकृत InputManager कैसा दिखता है:

 public class InputManager : MonoBehaviour { public static InputManager instance = null; public float horizontal, vertical; public delegate void ClickAction(); public static event ClickAction OnAimKeyClicked; //public delegate void ClickActionFloatArg(float arg); //public static event ClickActionFloatArg OnRSliderValueChange, OnGSliderValueChange, OnBSliderValueChange; public void AimKeyDown() { OnAimKeyClicked(); } } 

मैं बटन पर AimKeyDown विधि डालता हूं, और OnAimKeyClicked पर हथियार नियंत्रण स्क्रिप्ट पर हस्ताक्षर करता हूं:

 InputManager.instance.OnAimKeyClicked += GunShot; 

मेरा पूरा इनपुट सिस्टम एक समान तरीके से लागू किया गया है। मैंने तेजी के साथ कोई समस्या नहीं देखी। इसने हमें सभी क्लिक हैंडलर को एक स्थान पर एकत्रित करने की अनुमति दी - InputManager।

अनुकूलन


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

1. घटक कैशिंग (सरल मूल के साथ शुरू)

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

2. SendMessage का उपयोग करना

SendMessage () का उपयोग करना GetComponent () की तुलना में धीमा है। SendMessage स्ट्रिंग स्क्रिप्ट का उपयोग करके वांछित नाम के साथ विधि खोजने के लिए प्रत्येक स्क्रिप्ट के माध्यम से जाते हैं। GetComponent, स्क्रिप्ट की तुलना प्रकार के माध्यम से करता है और सीधे विधि को कॉल करता है।

3. ऑब्जेक्ट टैग की तुलना

Obj.tag == "string" के बजाय ComparTag विधि का उपयोग करें। एकता में, गेम ऑब्जेक्ट्स से स्ट्रिंग्स निकालने से डुप्लिकेट स्ट्रिंग बनती है, जो कचरा कलेक्टर को काम जोड़ती है। गेम ऑब्जेक्ट का नाम लेने से बचने के लिए बेहतर है। आप अद्यतन में तुलनात्मक कॉल नहीं कर सकते हैं और साथ ही साथ भारी संचालन पढ़ सकते हैं।

4. सामग्री

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

5. भौतिकी

एफपीएस को नीचे खींचना आसान है। 10. यह पता चला है कि स्थिर वस्तुएं भी बातचीत करती हैं और गणना में भाग लेती हैं। मैंने गलती से सोचा था कि स्थैतिक भौतिक वस्तुएं (जिन वस्तुओं में एक रिगिडबॉडी घटक है) पूरी तरह से मांग पर निष्क्रिय हैं। मैं पुराने ट्यूटोरियल से भटक गया था जिसमें कहा गया था कि जहां भी कोई राइडर हो वहां रिगिडबॉडी होनी चाहिए। अब मेरी सभी स्थिर वस्तुएं Static + BoxCollider हैं। जहां मुझे भौतिकी की आवश्यकता है, उदाहरण के लिए, लैम्पपोस्ट जिन्हें खटखटाया जा सकता है, मुझे लगता है कि आवश्यक होने पर रिगिडबॉडी घटक को काट दिया जाए।

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

6. समावेश culling + Lod

एक बड़े दृश्य के साथ, रोना रोना अपरिहार्य है। वस्तुओं (पेड़ों, डंडों आदि) को बड़ी दूरी पर निष्क्रिय करने के लिए, मैं लॉड का उपयोग करता हूं।

छवि

छवि

7. ऑब्जेक्ट पूल

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

मुकाबला बॉट के प्रबंधन के लिए सरल पूल:

  public List<Enemy> enemyPool = new List<Enemy>(); private void Start() { //    Enemy Transform enemyGameObjectContainer = Application.instance.objectPool.Find("Enemy"); //  enemyPool  for (int i = 0; i < enemyGameObjectContainer.childCount; i++) { enemyPool.Add(new Enemy() { Id = i, ParentRoomId = 0, GameObj = enemyGameObjectContainer.GetChild(i).gameObject }); } } public void SpawnEnemyForRoom(int roomId, int amount, Transform spawnPosition, bool combatMode) { //Stopwatch sw = new Stopwatch(); //sw.Start(); foreach (Enemy enemy in enemyPool) { if (amount > 0) { if (enemy.ParentRoomId == 0 && enemy.GameObj.activeSelf == false) { // id   enemy.ParentRoomId = roomId; enemy.GameObj.transform.position = spawnPosition.position; enemy.GameObj.transform.rotation = spawnPosition.rotation; enemy.AICombat = enemy.GameObj.GetComponent<AICombat>(); enemy.AICombat.parentRoomId = roomId; // id  enemy.AICombat.id = enemy.Id; //   enemy.GameObj.SetActive(true); //      if (combatMode) enemy.AICombat.ActivateCombatMode(); amount--; } } if (amount == 0) break; } } 

डेटाबेस


मैं एक डेटाबेस के रूप में sqlite का उपयोग करता हूं - आसानी से और जल्दी से। डेटा तालिका के रूप में प्रस्तुत किया गया है, आप जटिल प्रश्न बना सकते हैं। डेटाबेस के साथ काम करने के लिए कक्षा में, 800 लाइनें जब। मैं कल्पना नहीं कर सकता कि यह XML / JSON में कैसा दिखेगा।

भविष्य के लिए समस्याएं और योजनाएं


शहर से "कमरों" में जाने के लिए मैंने "टेलीपोर्ट" का कार्यान्वयन चुना। खिलाड़ी दरवाजे के पास जाता है, दृश्य कक्ष लोड किया जाता है और खिलाड़ी को टेलीपोर्ट किया जाता है। यह आपको शहर में कमरे रखने से बचाता है। यदि आप शहर में कमरे लागू करते हैं, जो भरने के साथ +15 कमरे हैं, तो मेमोरी की खपत न्यूनतम 1GB तक बढ़ जाएगी। मुझे यह कार्यान्वयन पसंद नहीं है, यह यथार्थवादी नहीं है और प्रतिबंधों का एक गुच्छा लगाता है। एकता ने हाल ही में अपनी मेगासिटी का डेमो दिखाया, यह प्रभावशाली है। मैं इमारतों और परिसर को लोड करने के लिए मेगासिटी से प्रौद्योगिकी का उपयोग करने के लिए धीरे-धीरे खेल को स्थानांतरित करना चाहता हूं। यह एक आकर्षक और दिलचस्प अनुभव है, मुझे लगता है कि यह वास्तव में जीवंत शहर बन जाएगा। मैंने async लोड दृश्य का उपयोग क्यों नहीं किया? यह सरल है, यह काम नहीं करता है, 2018.3 संस्करण में बॉक्स से बाहर कोई async लोड दृश्य नहीं है। प्रारंभ में, मुझे उम्मीद थी कि किसी शहर की योजना बनाते समय async लोड सीन, लेकिन जैसा कि यह पता चलता है, बड़े दृश्यों पर यह गेम को एक नियमित लोड दृश्य की तरह जमा देता है। एकता मंच पर इसकी पुष्टि की गई थी, आप चारों ओर पहुंच सकते हैं, लेकिन बैसाखी की जरूरत है।

कुछ आँकड़े:

बनावट: 304 / 374.3 एमबी
मेष: 295 / 304.0 एमबी
सामग्री: 101 / 148.0 KB (संभावित विसंगति यहां)
एनिमेशनक्लिप्स: 24 / 2.8 एमबी
ऑडियो क्लिप: 22 / 30.3 एमबी
एसेट्स: 21761
खेल के दृश्य दृश्य में: 29450
दृश्य में कुल वस्तुएँ: 111645
कुल वस्तु गणना: 133406
प्रति फ्रेम जीसी आवंटन: 70 / 2.0 केबी
C # कोड की कुल 4800 लाइनें।

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

आप एक ओपन बीटा टेस्ट चला सकते हैं और इसे यहां महसूस कर सकते हैं: play.google.com/store/apps/details?id=com.ag.mafiaProject01 (यदि असेंबली काम नहीं करती है, तो आपको इसे थोड़ा सा निहारने की जरूरत है, हर रात अपडेट)। मुझे उम्मीद है कि इसे विज्ञापन लिंक नहीं माना जाएगा, क्योंकि यह बीटा और डाउनलोड मुझे रेटिंग और लाभांश नहीं लाएंगे। इसके अलावा, मुझे नहीं लगता कि हब्र मेरे खेल के लक्षित दर्शक हैं।

स्क्रीनशॉट:



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


All Articles