paint-brush
ऑब्जेक्टपूल पैटर्न का उपयोग करके अपने गेम प्रोजेक्ट को कैसे बढ़ाएँ? (एक एकता गाइड)द्वारा@jokeresdeu
906 रीडिंग
906 रीडिंग

ऑब्जेक्टपूल पैटर्न का उपयोग करके अपने गेम प्रोजेक्ट को कैसे बढ़ाएँ? (एक एकता गाइड)

द्वारा Les Dibrivniy16m2023/05/10
Read on Terminal Reader

बहुत लंबा; पढ़ने के लिए

ऑब्जेक्ट पूल 99% गेम प्रोजेक्ट्स के अनुकूलन का आधार है। यह एक प्रारंभिक सिद्धांत पर आधारित है: अपना कार्य पूरा करने के बाद, वस्तु को हटाया नहीं जाता है बल्कि एक अलग वातावरण में ले जाया जाता है और इसे पुनः प्राप्त और पुन: उपयोग किया जा सकता है। लेस डिब्रिवनी बताते हैं कि यह पैटर्न क्यों जरूरी है।
featured image - ऑब्जेक्टपूल पैटर्न का उपयोग करके अपने गेम प्रोजेक्ट को कैसे बढ़ाएँ? (एक एकता गाइड)
Les Dibrivniy HackerNoon profile picture
0-item
1-item

नमस्ते, मेरा नाम ओल्स डिब्रीवनी है, और मैं कीकी में यूनिटी डेवलपर हूं। हम बच्चों के विकास के लिए एडटेक और गेमडेव - ऐप्स और वेब उत्पादों के चौराहे पर उत्पाद बनाते हैं। अब हमारे उत्पादों के चार मिलियन से अधिक उपयोगकर्ता हैं।

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

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

यह लेख प्रोग्रामिंग गुरुओं के लिए प्रासंगिक नहीं हो सकता है, लेकिन शुरुआती लोगों के लिए इसके विपरीत। यहाँ मैं यह समझाने के लिए उदाहरणों का उपयोग करूँगा कि यह पैटर्न क्यों आवश्यक है।


ऑब्जेक्टपूल के बिना चीजें कैसे चलती हैं?

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

आइए विचार करें कि आग का गोला कैसा दिखेगा। मैदान

 Rigidbody2D
और
 _speed
आग के गोले की गति के लिए जिम्मेदार होंगे;
 OnTriggerEnter2D
टक्कर के बाद विधि काम करेगी - फायरबॉल घटक वाली वस्तु नष्ट हो जाएगी:

 public class FireBall : MonoBehaviour 2 { 3 [SerializeField] private Rigidbody2D _rigidbody; 4 [SerializeField] private float _speed; 5   
6 public void FlyInDirection(Vector2 flyDirection) 7 { 8 _rigidbody.velocity = flyDirection * _speed; 9 } 10   
11 private void OnTriggerEnter2D(Collider2D other) 12 { 13 Destroy(gameObject); 14 } 15 }

दुश्मन भी काफी साधारण दिखेगा:

 public class Enemy : MonoBehaviour 2 { 3 [SerializeField] private FireBall _fireBall; 4 [SerializeField] private Transform _castPoint; 5   
6 public void Cast() 7 { 8 FireBall fireBall = Instantiate(_fireBall, _castPoint.position, Quaternion.identity); 9 fireBall.FlyInDirection(transform.right); 10 } 11 }

आप कॉल कर सकते हैं

 Cast
आपके प्रोजेक्ट या एनीमेशन इवेंट्स में किसी भी बिंदु से विधि। हमारे पास निम्न प्रवाह है: दुश्मन ने एक आग का गोला बनाया और उसे अपनी टकटकी की दिशा में प्रक्षेपित किया। रास्ते में आने वाली पहली बाधा तक पहुंचकर आग का गोला नष्ट हो जाता है।

यह दृष्टिकोण इष्टतम लगता है, लेकिन यह सबसे खराब विकल्पों में से एक है। आइए हर चीज का बिंदुवार विश्लेषण करें:

1. डिवाइस के सीपीयू और मेमोरी का अक्षम उपयोग।

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

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

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

2. आपके द्वारा बनाई गई हर चीज को नष्ट और साफ किया जाना चाहिए।

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

3. कोड नियंत्रण। एक वस्तु को बनाने और नष्ट करने के लिए विभिन्न वर्ग जिम्मेदार हैं, और उनमें से दर्जनों परियोजना में हो सकते हैं। क्या और कहाँ बनाया या हटाया गया है यह खोजना कभी-कभी एक तुच्छ कार्य नहीं है - और मैं अब पदानुक्रम में वस्तुओं को नियंत्रित करने के बारे में चुप हूँ।

चलिए ObjectPool को प्रोजेक्ट में एकीकृत करते हैं!

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

किसी वस्तु को बनाने, पुन: उपयोग करने और नष्ट करने के लिए एक इकाई जिम्मेदार होगी - हम इसे कहेंगे

 ObjectPool
. आग के गोले के साथ काम करने के लिए हम इसे ऐसा दिखा सकते हैं:

 public class ObjectPool : MonoBehaviour 2 { 3 [SerializeField] private FireBall _fireBallPrefab; 4   
5 private readonly List <FireBall> _freeFireBalls = new List <FireBall>(); 6   
7 public FireBall GetFireBall() 8 { 9 FireBall fireBall; 10       if (_freeFireBalls.Count > 0 ) 11 { 12 fireBall = _freeFireBalls[ 0 ]; 13 _freeFireBalls. Remove(fireBall);
14 } 15       else
16 { 17 fireBall = Instantiate(_fireBallPrefab, transform); 18 } 19       return fireBall; 20 } 21   
22 private void ReturnFireBall(FireBall fireBall) 23 { 24 _freeFireBalls.Add(fireBall); 25 } 26 }

 _freeFireBalls
सूची इस कोड में दिखाई देती है। हम बनाए गए आग के गोले को वहां जमा करेंगे जिन्होंने अपना काम किया है। दुश्मन अब ऐसा दिखेगा:

 public class Enemy : MonoBehaviour 2 { 3 [SerializeField] private ObjectPool _objectPool; 4 [SerializeField] private Transform _castPoint; 5   
6 public void Cast() 7 { 8 FireBall fireBall = _objectPool.GetFireBall(); 9 fireBall.transform.position = _castPoint.position; 10 fireBall.FlyInDirection(transform.right); 11 } 12 }

हमारी समस्या की जड़: हम आग के गोले को पूल में कैसे लौटा सकते हैं? हम दुश्मन पर भरोसा नहीं कर सकते; वह नहीं जानता कि आग का गोला कब नष्ट हो जाएगा। हम आग का गोला ज्ञान भी नहीं देना चाहते हैं

 ObjectPool
, क्योंकि यह अनावश्यक कनेक्शन बनाएगा।

मुझे आशा है कि आपने ध्यान दिया होगा कि मैंने बनाया है

 ReturnFireBall
विधि निजी। इसलिए, हम मूल सी # पैटर्न में से एक - पर्यवेक्षक, और इसके कार्यान्वयन - घटनाओं का उपयोग करेंगे। आग का गोला अब ऐसा दिखेगा:

 public class FireBall : MonoBehaviour 2 { 3 [SerializeField] private Rigidbody2D _rigidbody; 4 [SerializeField] private float _speed; 5   
6 public event Action<FireBall> Destroyed; 7   
8 public void FlyInDirection(Vector2 flyDirection) 9 { 10 _rigidbody.velocity = flyDirection * _speed; 11 } 12   
13 private void OnTriggerEnter2D(Collider2D other) 14 { 15 Destroyed?.Invoke(this); 16 } 17 }

 ObjectPool
की सदस्यता लेगा
 Destroyed
दुनिया को वस्तु पास करने के बाद की घटना:

 public class ObjectPool : MonoBehaviour 2 { 3 [SerializeField] private FireBall _fireBallPrefab; 4 private readonly List <FireBall> _freeFireBalls = new List <FireBall>(); 5   
6 public FireBall GetFireBall() 7 { 8 FireBall fireBall; 9       if (_freeFireBalls.Count > 0 ) 10 { 11 fireBall = _freeFireBalls[ 0 ]; 12 _freeFireBalls. Remove(fireBall);
13 } 14       else
15 { 16 fireBall = Instantiate(_fireBallPrefab, transform); 17 } 18 fireBall.Destroyed += ReturnFireBall; 19       return fireBall; 20 } 21   
22 private void ReturnFireBall(FireBall fireBall) 23 { 24 fireBall.Destroyed -= ReturnFireBall; 25 _freeFireBalls.Add(fireBall); 26 } 27 }

यह ऑब्जेक्ट पूल को ऑब्जेक्ट पर टांगने के लिए रहता है

 Enemy
घटक - बधाई! कचरा संग्राहक तब तक आराम करेगा जब तक आप किसी अन्य दृश्य पर नहीं जाते या खेल बंद नहीं करते। हमारे पास इसके मूल कार्यान्वयन में ऑब्जेक्टपूल है, लेकिन हम इसे सुधार सकते हैं।

इंटरफेस और जेनरिक: ऑब्जेक्टपूल के लिए एकदम सही मेल

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

प्रत्येक वस्तु जिसके माध्यम से हम चलते हैं

 ObjectPool
एक विशिष्ट प्रकार के लिए बाध्य होना चाहिए। उन्हें विशेष कार्यान्वयन से स्वतंत्र रूप से संसाधित किया जाना चाहिए। पूल द्वारा संसाधित की जाने वाली वस्तुओं की मूल विरासत पदानुक्रम को बनाए रखना आवश्यक है। वे से विरासत में मिलेगा
 MonoBehaviour
, कम से कम। आईपूलेबल इंटरफ़ेस का उपयोग करें:

 public interface IPoolable 2 { 3 GameObject GameObject { get ; } 4 event Action<IPoolable> Destroyed; 5 void Reset (); 6 }

हमें विरासत में मिला है

 FireBall
यह से:

 public class FireBall : MonoBehaviour, IPoolable 2 { 3 [SerializeField] private Rigidbody2D _rigidbody; 4 [SerializeField] private float _speed; 5 public GameObject GameObject => gameObject; 6   
7 public event Action<IPoolable> Destroyed; 8   
9 private void OnTriggerEnter2D(Collider2D other) 10 { 11       Reset (); 12 } 13 public void Reset () 14 { 15 Destroyed?.Invoke(this); 16 } 17   
18 public void FlyInDirection(Vector2 flyDirection) 19 { 20 _rigidbody.velocity = flyDirection * _speed; 21 } 22 }

आखिरी काम है पढ़ाना

 ObjectPool
किसी भी वस्तु के साथ काम करने के लिए। आईपूल करने योग्य पर्याप्त नहीं होगा क्योंकि इंस्टेंटिएट केवल ऑब्जेक्ट की प्रतिलिपि बना सकता है
 gameObject
संपत्ति। हम इस्तेमाल करेंगे
 Component
; ऐसी प्रत्येक वस्तु इस वर्ग से प्राप्त होती है।
 MonoBehaviour
से भी विरासत में मिला है। नतीजतन, हम निम्नलिखित प्राप्त करेंगे
 ObjectPool
:

 public class ObjectPool<T> where T : Component, IPoolable 2 { 3 private readonly List <IPoolable> _freeObjects; 4 private readonly Transform _container; 5 private readonly T _prefab; 6   
7 public ObjectPool(T prefab) 8 { 9 _freeObjects = new List <IPoolable>(); 10 _container = new GameObject().transform; 11 _container. name = prefab.GameObject. name ; 12 _prefab = prefab; 13 } 14   
15 public IPoolable GetFreeObject() 16 { 17 IPoolable poolable; 18       if (_freeObjects.Count > 0 ) 19 { 20 poolable = _freeObjects[ 0 ] as T; 21 _freeObjects. RemoveAt(0);
22 } 23       else
24 { 25 poolable = Object.Instantiate(_prefab, _container); 26 } 27 poolable.GameObject.SetActive(true); 28 poolable.Destroyed += ReturnToPool; 29       return poolable; 30 } 31   
32 private void ReturnToPool(IPoolable poolable) 33 { 34 _freeObjects.Add(poolable); 35 poolable.Destroyed -= ReturnToPool; 36 poolable.GameObject.SetActive(false); 37 poolable.GameObject.transform.SetParent(_container); 38 } 39 }

अब हम एक बना सकते हैं

 ObjectPool
किसी वस्तु के लिए जो विरासत की शर्तों को पूरा करती है
 Component
और आईपूलेबल। मैंने हटा दिया
 ObjectPool
से विरासत
 MonoBehaviour
, इस प्रकार घटकों की संख्या को कम करके हम वस्तुओं पर लटकेंगे।

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

 ObjectPool
— संसाधनों की बहुत अधिक बचत कभी नहीं हो सकती! एक विशिष्ट वस्तु प्रकार के लिए एक वर्ग से पूछना भी काफी सुविधाजनक है। ऐसा वर्ग वस्तु की पीढ़ी, नियंत्रण और अंतिम निपटान को संभाल लेगा। यह विकल्प हर प्रबंधन के रूप में सबसे अच्छा विकल्प है
 ObjectPool
परियोजना में व्यक्तिगत रूप से चुनौतीपूर्ण है।

की बुनियादी जरूरतों को पूरा करने के लिए

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

हम एक अतिरिक्त इकाई बनाएंगे

 PoolTask
के अंतिम कार्यान्वयन में
 ObjectPool
. यह वर्ग एक प्रीफ़ैब से निर्मित वस्तुओं के साथ कार्य को नियंत्रित करेगा:

 public class PoolTask 2 { 3 private readonly List <IPoolable> _freeObjects; 4 private readonly List <IPoolable> _objectsInUse; 5 private readonly Transform _container; 6   
7 public PoolTask(Transform container) 8 { 9 _container = container; 10 _objectsInUse = new List <IPoolable>(); 11 _freeObjects = new List <IPoolable>(); 12 } 13   
14 public void CreateFreeObjects<T>(T prefab, int count) where T : Component, IPoolable 15 { 16       for (var i = 0 ; i < count; i++) 17 { 18 var poolable = Object.Instantiate(prefab, _container); 19 _freeObjects.Add(poolable); 20 } 21 } 22   
23 public T GetFreeObject<T>(T prefab) where T : Component, IPoolable 24 { 25 T poolable; 26       if (_freeObjects.Count > 0 ) 27 { 28 poolable = _freeObjects[ 0 ] as T; 29 _freeObjects. RemoveAt(0);
30 } 31       else
32 { 33 poolable = Object.Instantiate(prefab, _container); 34 } 35 poolable.Destroyed += ReturnToPool; 36 poolable.GameObject.SetActive(true); 37 _objectsInUse.Add(poolable); 38       return poolable; 39 } 40   
41 public void ReturnAllObjectsToPool() 42 { 43 foreach (var poolable in _objectsInUse) 44 poolable. Reset (); 45 } 46   
47 public void Dispose() 48 { 49 foreach (var poolable in _objectsInUse) 50 Object.Destroy(poolable.GameObject); 51       
52 foreach (var poolable in _freeObjects) 53 Object.Destroy(poolable.GameObject); 54 } 55   
56 private void ReturnToPool(IPoolable poolable) 57 { 58 _objectsInUse. Remove(poolable);
59 _freeObjects.Add(poolable); 60 poolable.Destroyed -= ReturnToPool; 61 poolable.GameObject.SetActive(false); 62 poolable.GameObject.transform.SetParent(_container); 63 } 64 }

पूलटास्क में अतिरिक्त विशेषताएं होंगी:

1. उन वस्तुओं को ट्रैक करना जिन्हें हमने दुनिया में जारी किया है, ताकि यदि आवश्यक हो, तो उन्हें नष्ट किया जा सके या एक विशिष्ट क्षण में पूल में लौटाया जा सके;

2. मुक्त वस्तुओं की पूर्व निर्धारित मात्रा उत्पन्न करना।

अंत में, चलिए एक ObjectPool बनाते हैं जो हमारी सभी जरूरतों को पूरा करेगा और वस्तुओं के नियंत्रण और निर्माण को पूरी तरह से अपने हाथ में ले लेगा:

 public class ObjectPool 2 { 3 private static ObjectPool _instance; 4 public static ObjectPool Instance => _instance ??= new ObjectPool(); 5   
6 private readonly Dictionary<Component, PoolTask> _activePoolTasks; 7 private readonly Transform _container; 8  
9 private ObjectPool() 10 { 11 _activePoolTasks = new Dictionary<Component, PoolTask>(); 12 _container = new GameObject().transform; 13 _container. name = nameof(ObjectPool); 14 } 15   
16 public void CreateFreeObjects<T>(T prefab, int count) where T : Component, IPoolable 17 { 18       if (!_activePoolTasks.TryGetValue(prefab, out var poolTask)) 19 AddTaskToPool(prefab, out poolTask); 20       
21 poolTask.CreateFreeObjects(prefab, count); 22 } 23   
24 public T GetObject<T>(T prefab) where T : Component, IPoolable 25 { 26       if (!_activePoolTasks.TryGetValue(prefab, out var poolTask)) 27 AddTaskToPool(prefab, out poolTask); 28       return poolTask.GetFreeObject(prefab); 29 } 30   
31 public void Dispose() 32 { 33 foreach (var poolTask in _activePoolTasks.Values) 34 poolTask.Dispose(); 35 } 36   
37 private void AddTaskToPool<T>(T prefab, out PoolTask poolTask) where T : Component, IPoolable 38 { 39 var taskContainer = new GameObject 40 { 41           name = $ "{prefab.name}_pool" , 42 transform = 43 { 44 parent = _container 45 } 46 }; 47 poolTask = new PoolTask(taskContainer.transform); 48 _activePoolTasks.Add(prefab, poolTask); 49 } 50 }

सिंगलटन का उपयोग आपकी नज़र में आ सकता है - यह केवल एक उदाहरण है। आप अपने प्रोजेक्ट में उपयोग को अनुकूलित कर सकते हैं — यह चल सकता है

 ObjectPool
ज़ेनजेक्ट के माध्यम से कंस्ट्रक्टर या इंजेक्शन के माध्यम से।

हमारे पास इसका अंतिम संस्करण है

 Cast
विधि में
 Enemy
:

 public class Enemy : MonoBehaviour 2 { 3 [SerializeField] private FireBall _prefab; 4 [SerializeField] private Transform _castPoint; 5   
6 public void Cast() 7 { 8 FireBall fireBall = ObjectPool.Instance.GetObject(_prefab); 9 fireBall.transform.position = _castPoint.position; 10 fireBall.FlyInDirection(transform.right); 11 } 12 }

जेनेरिक का उपयोग करने के कारण हमें तुरंत प्रसंस्करण के लिए आवश्यक प्रकार की वस्तु मिलती है।

 ObjectPool
प्रीफ़ैब के अनुसार आंतरिक कार्यों को समूहित करेगा - यदि कई हैं
 FireBall
घटक, पूल उन्हें सही ढंग से संसाधित करेगा और आपको सही देगा। यह दृष्टिकोण खेल के दृश्य के लिए किसी वस्तु को उत्पन्न करने में मदद करेगा।

हालांकि, यूआई तत्वों के साथ काम करते समय सावधान रहें: माता-पिता के बीच किसी वस्तु को स्थानांतरित करते समय अलग-अलग परिवर्तन होते हैं

 localScale
, द
 localScale
वस्तु का स्वरूप ही बदल जाएगा। यदि आपकी परियोजना में एक अनुकूली यूआई है, तो कैनवास घटक के साथ रूपांतरण उनकी बदल जाएगा
 localScale
विस्तार के आधार पर। मैं आपको यह सरल ऑपरेशन करने की सलाह देता हूं:

 poolable.GameObject.transform.localScale = Vector2. on e ;

आप अन्य विकल्पों में 3 स्क्रिप्ट का उपयोग कर सकते हैं:

 ObjectPool
,
 PoolTask
, और
 IPoolable
. तो बेझिझक उन्हें अपनी परियोजना में जोड़ें और 100% के लिए ऑब्जेक्ट पूल पैटर्न का उपयोग करें!