साझा प्रबंधन के साथ चरित्र प्रबंधन


प्रोजेक्ट का लिंक

इस लेख में, मैं यह दिखाना चाहता हूं कि आप तीसरे व्यक्ति के चरित्र को नियंत्रित करने के लिए शेयर्ड ईवेंट का उपयोग कैसे कर सकते हैं जो संपत्ति का एक मानक सेट प्रदान करता है। मैंने पिछले लेखों (और यह ) में SharedEvents के बारे में लिखा था।

बिल्ली के लिए आपका स्वागत है!

पहली चीज़ जो आपको चाहिए वह है कार्यान्वित SharedState / SharedEvents के साथ एक परियोजना लेना और परिसंपत्तियों का एक मानक समूह जोड़ना



मैंने प्रोटोटाइपिंग प्रीफैब्स से एक छोटा और बहुत सरल दृश्य बनाया



और मानक सेटिंग्स के साथ सतह नेविगेशन सेंकना



उसके बाद, आपको इस दृश्य में प्रीफ़ैब थर्डपर्सनचैकर को जोड़ना होगा



फिर आप शुरू कर सकते हैं और सुनिश्चित कर सकते हैं कि सब कुछ बॉक्स से बाहर काम करता है। फिर आप पहले से बनाए गए SharedState / SharedEvents बुनियादी ढांचे के उपयोग को कॉन्फ़िगर करने के लिए आगे बढ़ सकते हैं। ऐसा करने के लिए, चरित्र ऑब्जेक्ट से ThirdPersonUserController घटक निकालें।



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

और इसे संभव बनाने के लिए, आपको चरित्र ऑब्जेक्ट में NavMeshAgent घटक को जोड़ना और कॉन्फ़िगर करना होगा



अब आपको एक सरल नियंत्रक बनाने की आवश्यकता है जो चरित्र को नियंत्रित करेगा
माउस AgentMouseController के साथ



using UnityEngine; using UnityEngine.AI; using UnityStandardAssets.Characters.ThirdPerson; public class AgentMouseController : MonoBehaviour { public NavMeshAgent agent; public ThirdPersonCharacter character; public Camera cam; void Start() { //      agent.updateRotation = false; } void Update() { //     if (Input.GetMouseButtonDown(0)) { Ray ray = cam.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(ray, out hit)) { agent.SetDestination(hit.point); } } //    ,     if(agent.remainingDistance > agent.stoppingDistance) { character.Move(agent.desiredVelocity, false, false); } else // ,    { character.Move(Vector3.zero, false, false); } } } 

और इसे चरित्र के ऑब्जेक्ट में जोड़ें, इसे कैमरा, चरित्र के नियंत्रक और एजेंट को लिंक दें। यह सब मंच से उपलब्ध है।



और वह सब है। यह एजेंट को यह बताने के लिए पर्याप्त है कि एजेंट को कहां ले जाना है, माउस का उपयोग करके (बाएं-क्लिक)।

आप शुरू कर सकते हैं और सुनिश्चित कर सकते हैं कि सब कुछ काम करता है



साझाकरण एकीकरण


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

घटक को बुलाया जाएगा, उदाहरण के लिए, MouseHandlerComponent



 using UnityEngine; public class MouseHandlerComponent : SharedStateComponent { public Camera cam; #region MonoBehaviour protected override void OnSharedStateChanged(SharedStateChangedEventData newState) { } protected override void OnStart() { if (cam == null) throw new MissingReferenceException("   "); } protected override void OnUpdate() { //     if (Input.GetMouseButtonDown(0)) { //           var hit = GetMouseHit(); Events.PublishAsync("poittogound", new PointOnGroundEventData { Sender = this, Point = hit.point }); } } #endregion private RaycastHit GetMouseHit() { Ray ray = cam.ScreenPointToRay(Input.mousePosition); RaycastHit hit; Physics.Raycast(ray, out hit); return hit; } } 

इस घटक को सूचनाओं में डेटा भेजने के लिए एक वर्ग की आवश्यकता होती है। ऐसी कक्षाओं के लिए जिनमें केवल सूचनाओं के लिए डेटा होगा, आप एक फ़ाइल बना सकते हैं और इसे DefinedEventsData नाम दे सकते हैं



और माउस के साथ एक क्लिक की स्थिति भेजने के लिए, इसमें एक वर्ग जोड़ें

 using UnityEngine; public class PointOnGroundEventData : EventData { public Vector3 Point { get; set; } } 

अगली बात यह है कि एक घटक जोड़ें जो रैपर या डेकोरेटर होगा, जैसा कि आप चाहते हैं, NavMeshAgent घटक के लिए। चूंकि मैं मौजूदा (3 पार्टी) घटकों को नहीं बदलूंगा , मैं डेकोरेटर्स का उपयोग शेयर्डस्टेट / शेयर्ड ईवेंट्स के साथ एकीकृत करने के लिए करूंगा



यह घटक दृश्य पर कुछ बिंदुओं पर माउस क्लिक के बारे में सूचनाएं प्राप्त करेगा और एजेंट को बताएगा कि कहां स्थानांतरित करना है। और प्रत्येक फ्रेम में एजेंट की स्थिति की स्थिति की निगरानी भी करें और इसके बदलाव के बारे में एक अधिसूचना बनाएं।

यह घटक NavMeshAgent घटक पर निर्भर करेगा

 using UnityEngine; using UnityEngine.AI; [RequireComponent(typeof(NavMeshAgent))] public class AgentWrapperComponent : SharedStateComponent { private NavMeshAgent agent; #region Monobehaviour protected override void OnSharedStateChanged(SharedStateChangedEventData newState) { } protected override void OnStart() { //  agent = GetComponent<NavMeshAgent>(); //      agent.updateRotation = false; Events.Subscribe<PointOnGroundEventData>("pointtoground", OnPointToGroundGot); } protected override void OnUpdate() { //     if (agent.remainingDistance > agent.stoppingDistance) { Events.Publish("agentmoved", new AgentMoveEventData { Sender = this, DesiredVelocity = agent.desiredVelocity }); } else { Events.Publish("agentmoved", new AgentMoveEventData { Sender = this, DesiredVelocity = Vector3.zero }); } } #endregion private void OnPointToGroundGot(PointOnGroundEventData eventData) { //    agent.SetDestination(eventData.Point); } } 


डेटा भेजने के लिए, इस घटक को एक वर्ग की आवश्यकता होती है जिसे DefinedEventsData फ़ाइल में जोड़ा जाना चाहिए
 public class AgentMoveEventData : EventData { public Vector3 DesiredVelocity { get; set; } } 

यह चरित्र को स्थानांतरित करने के लिए पहले से ही पर्याप्त है। लेकिन वह इसे बिना एनीमेशन के करेंगे, क्योंकि हम अभी तक थर्डपर्सनचैटर का उपयोग नहीं कर रहे हैं। और इसके लिए, NavMeshAgent की तरह , आपको एक CharacterWrapperComponent डेकोरेटर बनाने की आवश्यकता है



घटक एजेंट की स्थिति परिवर्तन के बारे में सूचनाएं सुनेंगे, और अधिसूचना (घटना) से प्राप्त दिशा में चरित्र को स्थानांतरित करेंगे।

 using UnityEngine; using UnityStandardAssets.Characters.ThirdPerson; [RequireComponent(typeof(ThirdPersonCharacter))] public class CharacterWrapperComponent : SharedStateComponent { private ThirdPersonCharacter character; #region Monobehaviour protected override void OnSharedStateChanged(SharedStateChangedEventData newState) { } protected override void OnStart() { character = GetComponent<ThirdPersonCharacter>(); Events.Subscribe<AgentMoveEventData>("agentmoved", OnAgentMove); } protected override void OnUpdate() { } #endregion private void OnAgentMove(AgentMoveEventData eventData) { //       character.Move(eventData.DesiredVelocity, false, false); } } 

और वह सब है। यह इन घटकों को चरित्र की गेम ऑब्जेक्ट में जोड़ता है। आपको मौजूदा एक से एक प्रति बनाने की जरूरत है, पुराने AgentMouseControl घटक को हटा दें



और नया MouseHandlerComponent , AgentWrapperComponent और CharacterWrapperComponent जोड़ें

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





आप शुरू कर सकते हैं और सुनिश्चित कर सकते हैं कि सब कुछ काम करता है।

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

SharedEvents के लिए अतुल्यकालिक व्यवहार


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

 //  data    eventName  public async Task PublishAsync<T>(string eventName, T data) where T : EventData { if (_subscribers.ContainsKey(eventName)) { var listOfDelegates = _subscribers[eventName]; var tasks = new List<Task>(); foreach (Action<T> callback in listOfDelegates) { tasks.Add(Task.Run(() => { callback(data); })); } await Task.WhenAll(tasks); } } 

अब आपको SharedStateComponent बेस क्लास में अमूर्त OnUpdate पद्धति को अतुल्यकालिक में बदलने की आवश्यकता है ताकि वह इस विधि के कार्यान्वयन के अंदर शुरू किए गए कार्यों को लौटाए और OnUpdateAsync का नाम बदल दे

 protected abstract Task[] OnUpdateAsync(); 

आपको एक तंत्र की भी आवश्यकता होगी जो वर्तमान से पहले पिछले फ्रेम से कार्यों को पूरा करने को नियंत्रित करेगा

 private Task[] _previosFrameTasks = null; //   private async Task CompletePreviousTasks() { if (_previosFrameTasks != null && _previosFrameTasks.Length > 0) await Task.WhenAll(_previosFrameTasks); } 

आधार वर्ग में अद्यतन पद्धति को async के रूप में चिह्नित करने की आवश्यकता है और पिछले कार्यों के निष्पादन की पूर्व जांच करें

 async void Update() { await CompletePreviousTasks(); //     _previosFrameTasks = OnUpdateAsync(); } 

बेस क्लास में इन परिवर्तनों के बाद, आप पुराने OnUpdate पद्धति के कार्यान्वयन को नए OnUpdateAsync में बदल सकते हैं । पहला घटक जहां यह किया जाएगा, वह AgentWrapperComponent है । अब यह विधि परिणाम की वापसी की उम्मीद करती है। यह परिणाम कार्यों की एक सरणी होगी। एक सरणी क्योंकि विधि में कई को समानांतर में लॉन्च किया जा सकता है और हम उन्हें एक गुच्छा में संसाधित करेंगे।

 protected override Task[] OnUpdateAsync() { //     if (agent.remainingDistance > agent.stoppingDistance) { return new Task[] { Events.PublishAsync("agentmoved", new AgentMoveEventData { Sender = this, DesiredVelocity = agent.desiredVelocity }) }; } else { return new Task[] { Events.PublishAsync("agentmoved", new AgentMoveEventData { Sender = this, DesiredVelocity = Vector3.zero }) }; } } 

OnUpdate पद्धति में परिवर्तन के लिए अगला उम्मीदवार MouseHandlerController है । यहां सिद्धांत एक ही है

  protected override Task[] OnUpdateAsync() { //     if (Input.GetMouseButtonDown(0)) { //           var hit = GetMouseHit(); return new Task[] { Events.PublishAsync("pointtoground", new PointOnGroundEventData { Sender = this, Point = hit.point }) }; } return null; } 

अन्य सभी कार्यान्वयनों में जहां यह विधि खाली थी, इसे इसे बदलने के लिए पर्याप्त है

 protected override Task[] OnUpdateAsync() { return null; } 

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



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



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

 using System; using System.Collections; using System.Collections.Concurrent; using UnityEngine; public class Dispatcher : MonoBehaviour { private static Dispatcher _instance; private volatile bool _queued = false; private ConcurrentQueue<Action> _queue = new ConcurrentQueue<Action>(); private static readonly object _sync_ = new object(); //     public static void RunOnMainThread(Action action) { _instance._queue.Enqueue(action); lock (_sync_) { _instance._queued = true; } } //       () [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void Initialize() { if (_instance == null) { _instance = new GameObject("Dispatcher").AddComponent<Dispatcher>(); DontDestroyOnLoad(_instance.gameObject); } } void Update() { if (_queued) //   { while (!_queue.IsEmpty) { if (_queue.TryDequeue(out Action a)) { StartCoroutine(ActionWrapper(a)); } } lock (_sync_) { _queued = false; } } } //    IEnumerator ActionWrapper(Action a) { a(); yield return null; } } 

अगला काम डिस्पैचर लागू करना है। ऐसा करने के लिए 2 स्थान हैं। 1 चरित्र का डेकोरेटर है, वहां हम उससे दिशा पूछते हैं। CharacterWrapperComponent घटक में

 private void OnAgentMove(AgentMoveEventData eventData) { Dispatcher.RunOnMainThread(() => character.Move(eventData.DesiredVelocity, false, false)); } 

2 एजेंट का डेकोरेटर है, वहां हम एजेंट के लिए स्थिति का संकेत देते हैं। AgentWrapperComponent घटक में

 private void OnPointToGroundGot(PointOnGroundEventData eventData) { //    Dispatcher.RunOnMainThread(() => agent.SetDestination(eventData.Point)); } 

अब कोई त्रुटि नहीं होगी, कोड सही ढंग से काम करेगा। आप इसे शुरू और देख सकते हैं।

थोड़ा सा परावर्तन


सब कुछ तैयार होने और सब कुछ काम करने के बाद, आप कोड को थोड़ा कंघी कर सकते हैं और इसे थोड़ा अधिक सुविधाजनक और सरल बना सकते हैं। इसके लिए थोड़े बदलाव की जरूरत होगी।

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



अंदर, हम एक सरल जेनेरिक एक्सटेंशन विधि बनाएंगे जो किसी भी उदाहरण को किसी सरणी में लपेटेगी

 public static class Extensions { //    public static T[] WrapToArray<T>(this T source) { return new T[] { source }; } } 

अगला परिवर्तन घटकों में डिस्पैचर के प्रत्यक्ष उपयोग को छिपा रहा है। इसके बजाय, SharedStateComponent बेस क्लास में एक विधि बनाएं और वहां से डिस्पैचर का उपयोग करें।

 protected void PerformInMainThread(Action action) { Dispatcher.RunOnMainThread(action); } 

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

 protected override Task[] OnUpdateAsync() { //     if (agent.remainingDistance > agent.stoppingDistance) { return Events.PublishAsync("agentmoved", new AgentMoveEventData { Sender = this, DesiredVelocity = agent.desiredVelocity }) .WrapToArray(); } else { return Events.PublishAsync("agentmoved", new AgentMoveEventData { Sender = this, DesiredVelocity = Vector3.zero }) .WrapToArray(); } } 

और घटक में MouseHandlerComponent

 protected override Task[] OnUpdateAsync() { //     if (Input.GetMouseButtonDown(0)) { //           var hit = GetMouseHit(); return Events.PublishAsync("pointtoground", new PointOnGroundEventData { Sender = this, Point = hit.point }) .WrapToArray(); } return null; } 

अब हम घटकों में डिस्पैचर के प्रत्यक्ष उपयोग से छुटकारा पा लेते हैं और इसके बजाय हम बेस क्लास में PerformInMainThread विधि कहते हैं।

सबसे पहले AgentWrapperComponent में

 private void OnPointToGroundGot(PointOnGroundEventData eventData) { //    PerformInMainThread(() => agent.SetDestination(eventData.Point)); } 

और CharacterWrapperComponent घटक में

 private void OnAgentMove(AgentMoveEventData eventData) { PerformInMainThread(() => character.Move(eventData.DesiredVelocity, false, false)); } 

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

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


All Articles