एकता में षट्कोण मानचित्र: पथ खोजक, खिलाड़ी दस्ते, एनिमेशन

भाग 1-3: जाल, रंग और सेल ऊँचाई

भाग 4-7: धक्कों, नदियों और सड़कों

भाग 8-11: जल, भू-भाग और प्राचीर

भाग 12-15: बचत और लोडिंग, बनावट, दूरियां

भाग 16-19: रास्ता खोजना, खिलाड़ी दस्ते, एनिमेशन

भाग 20-23: कोहरे के युद्ध, मानचित्र अनुसंधान, प्रक्रियात्मक पीढ़ी

भाग 24-27: जल चक्र, अपरदन, बायोम, बेलनाकार नक्शा

भाग 16: रास्ता खोजना


  • कोशिकाओं को हाइलाइट करें
  • खोज लक्ष्य चुनें
  • सबसे छोटा रास्ता खोजें
  • प्राथमिकता कतार बनाएं

कोशिकाओं के बीच की दूरी की गणना करने के बाद, हम उनके बीच के रास्तों को खोजने के लिए आगे बढ़े।

इस भाग के साथ शुरू, षट्भुज मानचित्र ट्यूटोरियल एकता 5.6.0 में बनाया जाएगा। यह ध्यान दिया जाना चाहिए कि 5.6 में एक बग है जो कई प्लेटफार्मों के लिए विधानसभाओं में बनावट के सरणियों को नष्ट कर देता है। आप बनावट सरणी निरीक्षक में पठनीय शामिल करके इसके चारों ओर प्राप्त कर सकते हैं।


एक यात्रा की योजना बना रहा है

हाइलाइट की गई कोशिकाएं


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

बनावट की रूपरेखा


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


काली पृष्ठभूमि पर सेल की रूपरेखा

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


बनावट आयात विकल्प

प्रति सेल एक स्प्राइट


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



Prefab बाल चयन तत्व

अब प्रत्येक कोशिका में एक स्प्राइट है, लेकिन यह बहुत बड़ा होगा। आकृति को कोशिकाओं के केंद्रों से मिलाने के लिए स्प्राइट के परिवर्तन घटक की चौड़ाई और ऊँचाई को 17 तक बदल दें।


चयन राहत से आंशिक रूप से छिपा हुआ है

सब कुछ के ऊपर आरेखण


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

Shader "Custom/Highlight" { Properties { [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {} _Color ("Tint", Color) = (1,1,1,1) [MaterialToggle] PixelSnap ("Pixel snap", Float) = 0 [HideInInspector] _RendererColor ("RendererColor", Color) = (1,1,1,1) [HideInInspector] _Flip ("Flip", Vector) = (1,1,1,1) [PerRendererData] _AlphaTex ("External Alpha", 2D) = "white" {} [PerRendererData] _EnableExternalAlpha ("Enable External Alpha", Float) = 0 } SubShader { Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" "CanUseSpriteAtlas"="True" } Cull Off ZWrite Off Blend One OneMinusSrcAlpha Pass { CGPROGRAM #pragma vertex SpriteVert #pragma fragment SpriteFrag #pragma target 2.0 #pragma multi_compile_instancing #pragma multi_compile _ PIXELSNAP_ON #pragma multi_compile _ ETC1_EXTERNAL_ALPHA #include "UnitySprites.cginc" ENDCG } } } 

पहला बदलाव यह है कि हम गहराई बफर को अनदेखा करते हैं, जिससे जेड-टेस्ट हमेशा सफल होता है।

  ZWrite Off ZTest Always 

दूसरा परिवर्तन यह है कि हम बाकी पारदर्शी ज्यामिति के बाद प्रतिपादन कर रहे हैं। पारदर्शिता कतार में 10 जोड़ने के लिए पर्याप्त है।

  "Queue"="Transparent+10" 

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



हम अपने स्वयं के स्प्राइट सामग्री का उपयोग करते हैं

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


गहराई बफर को अनदेखा करें

चयन नियंत्रण


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


अक्षम छवि घटक

सेल चयन को सक्षम करने के लिए, HexCell में HexCell विधि जोड़ें। इसे अपने uiRect के एकमात्र बच्चे को लेना चाहिए और इसके छवि घटक को शामिल करना चाहिए। हम DisableHighlight मेथड भी बनाएंगे।

  public void DisableHighlight () { Image highlight = uiRect.GetChild(0).GetComponent<Image>(); highlight.enabled = false; } public void EnableHighlight () { Image highlight = uiRect.GetChild(0).GetComponent<Image>(); highlight.enabled = true; } 

अंत में, हम रंग निर्दिष्ट कर सकते हैं ताकि जब चालू हो, तो बैकलाइट को एक ह्यू दें।

  public void EnableHighlight (Color color) { Image highlight = uiRect.GetChild(0).GetComponent<Image>(); highlight.color = color; highlight.enabled = true; } 

unitypackage

रास्ता खोज रहे हैं


अब जब हम कोशिकाओं का चयन कर सकते हैं, तो हमें आगे बढ़ने और दो कोशिकाओं का चयन करने की आवश्यकता है, और फिर उनके बीच का रास्ता खोजें। पहले हमें कोशिकाओं का चयन करने की आवश्यकता है, फिर खोज को उनके बीच एक पथ तक सीमित रखें, और अंत में इस पथ को दिखाएं।

खोज शुरू करें


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

  HexCell previousCell, searchFromCell; 

HandleInput अंदर HandleInput हम Shift कुंजी का परीक्षण करने के लिए Input.GetKey(KeyCode.LeftShift) का उपयोग कर सकते हैं।

  if (editMode) { EditCells(currentCell); } else if (Input.GetKey(KeyCode.LeftShift)) { if (searchFromCell) { searchFromCell.DisableHighlight(); } searchFromCell = currentCell; searchFromCell.EnableHighlight(Color.blue); } else { hexGrid.FindDistancesTo(currentCell); } 


जहां देखो

खोज समापन बिंदु


एक सेल में सभी दूरियों की तलाश करने के बजाय, हम अब दो विशिष्ट कोशिकाओं के बीच का रास्ता तलाश रहे हैं। इसलिए, HexGrid.FindDistancesTo का नाम बदलकर HexGrid.FindDistancesTo कर HexGrid.FindPath और इसे दूसरा HexCell पैरामीटर दें। इसके अलावा, Search विधि को बदलें।

  public void FindPath (HexCell fromCell, HexCell toCell) { StopAllCoroutines(); StartCoroutine(Search(fromCell, toCell)); } IEnumerator Search (HexCell fromCell, HexCell toCell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; } WaitForSeconds delay = new WaitForSeconds(1 / 60f); List<HexCell> frontier = new List<HexCell>(); fromCell.Distance = 0; frontier.Add(fromCell); … } 

अब HexMapEditor.HandleInput को संशोधित विधि को कॉल करना चाहिए, searchFromCell और currentCell का उपयोग तर्क के रूप में करना चाहिए। इसके अलावा, हम केवल तभी खोज सकते हैं जब हम यह जानते हैं कि किस सेल से खोज की जाए। और अगर शुरुआत और अंत अंक मेल खाते हैं तो हमें खोज करने की जहमत नहीं उठानी है।

  if (editMode) { EditCells(currentCell); } else if (Input.GetKey(KeyCode.LeftShift)) { … } else if (searchFromCell && searchFromCell != currentCell) { hexGrid.FindPath(searchFromCell, currentCell); } 

खोज की ओर मुड़ते हुए, हमें पहले सभी पिछले चयनों से छुटकारा पाना होगा। इसलिए, जब दूरी को रीसेट किया HexGrid.Search तो HexGrid.Search को बंद कर HexGrid.Search । चूंकि यह प्रारंभिक सेल की रोशनी को भी बंद कर देता है, तो इसे फिर से चालू करें। इस स्तर पर, हम अंतिम बिंदु को भी उजागर कर सकते हैं। चलो उसे लाल करो।

  IEnumerator Search (HexCell fromCell, HexCell toCell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; cells[i].DisableHighlight(); } fromCell.EnableHighlight(Color.blue); toCell.EnableHighlight(Color.red); … } 


एक संभावित पथ के समापन बिंदु

खोज सीमित करें


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

  while (frontier.Count > 0) { yield return delay; HexCell current = frontier[0]; frontier.RemoveAt(0); if (current == toCell) { break; } for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … } } 


अंत बिंदु पर रुकें

यदि समापन बिंदु तक नहीं पहुंचा जा सकता है तो क्या होता है?
तब एल्गोरिथ्म काम करना जारी रखेगा जब तक कि यह सभी पहुंच योग्य कोशिकाओं को न खोज ले। समय से पहले निकलने की संभावना के बिना, यह पुराने FindDistancesTo विधि के रूप में काम करेगा।

पथ प्रदर्शन


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

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


केंद्र में पथों का वर्णन करने वाला ट्री नेटवर्क

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

  public HexCell PathFrom { get; set; } 

HexGrid.Search सीमा में जोड़ते समय पड़ोसी के PathFrom मान को वर्तमान सेल पर सेट करें। इसके अलावा, हमें पड़ोसी से छोटा रास्ता खोजने पर इस लिंक को बदलना होगा।

  if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; neighbor.PathFrom = current; frontier.Add(neighbor); } else if (distance < neighbor.Distance) { neighbor.Distance = distance; neighbor.PathFrom = current; } 

अंतिम बिंदु पर पहुंचने के बाद, हम इन लिंक को प्रारंभिक सेल में वापस लाकर पथ का अनुमान लगा सकते हैं और उनका चयन कर सकते हैं।

  if (current == toCell) { current = current.PathFrom; while (current != fromCell) { current.EnableHighlight(Color.white); current = current.PathFrom; } break; } 


पथ मिल गया

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

खोज की शुरुआत बदलें


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

  HexCell previousCell, searchFromCell, searchToCell; 

इस क्षेत्र का उपयोग करते हुए, हम एक नई खोज की शुरुआत भी कर सकते हैं।

  else if (Input.GetKey(KeyCode.LeftShift)) { if (searchFromCell) { searchFromCell.DisableHighlight(); } searchFromCell = currentCell; searchFromCell.EnableHighlight(Color.blue); if (searchToCell) { hexGrid.FindPath(searchFromCell, searchToCell); } } else if (searchFromCell && searchFromCell != currentCell) { searchToCell = currentCell; hexGrid.FindPath(searchFromCell, searchToCell); } 

इसके अलावा, हमें समान शुरुआत और समाप्ति बिंदुओं से बचने की आवश्यकता है।

  if (editMode) { EditCells(currentCell); } else if ( Input.GetKey(KeyCode.LeftShift) && searchToCell != currentCell ) { … } 

unitypackage

होशियार खोज


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

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

खोज के आंकड़े


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

  public int SearchHeuristic { get; set; } 

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

चूंकि उत्तराधिकार पहले की गई यात्रा पथ पर निर्भर नहीं करता है, यह खोज प्रक्रिया में स्थिर है। इसलिए, हमें इसे केवल एक बार गणना करने की आवश्यकता है जब HexGrid.Search बॉर्डर पर एक सेल जोड़ता है।

  if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); frontier.Add(neighbor); } 

प्राथमिकता खोजें


अब से, हम सेल की दूरी के आधार पर खोज की प्राथमिकता का निर्धारण करेंगे और साथ ही इसके आंकड़े भी। HexCell में इस मान के लिए एक संपत्ति जोड़ें।

  public int SearchPriority { get { return distance + SearchHeuristic; } } 

इसे काम करने के लिए, HexGrid.Search ताकि यह इस संपत्ति का उपयोग सीमा को छाँटने में करे।

  frontier.Sort( (x, y) => x.SearchPriority.CompareTo(y.SearchPriority) ); 



उत्तराधिकार के बिना और उत्तराधिकार के साथ खोजें

मान्य हेयुरिस्टिक


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

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


Heuristics × 5 का उपयोग करना

हालांकि, अगर नक्शे में सड़कें हैं, तो हम शेष दूरी को कम कर सकते हैं। नतीजतन, एल्गोरिथ्म गलतियां कर सकता है और एक रास्ता बना सकता है जो वास्तव में सबसे छोटा नहीं है।



ओवररेटेड और मान्य उत्तराधिकार

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

कड़ाई से बोलते हुए, कम लागत का उपयोग करना काफी सामान्य है, लेकिन यह केवल हेयुरिस्टिक कमजोर बना देगा। न्यूनतम संभव हेयुरिस्टिक शून्य है, जो हमें सिर्फ दिक्जस्ट्रा के एल्गोरिदम देता है। नॉनज़रो ह्यूरिस्टिक के साथ, एल्गोरिथ्म को ए * कहा जाता है (उच्चारण "ए स्टार")।

इसे A * क्यों कहा जाता है?
नीजल्स निल्सन द्वारा पहले दिक्क्स्ट्रा के एल्गोरिथ्म में हेयूरिस्टिक्स जोड़ने का विचार था। उसने अपने संस्करण का नाम A1 रखा। बर्ट्राम राफेल बाद में सर्वश्रेष्ठ संस्करण के साथ आया जिसे उन्होंने A2 कहा। तब पीटर हार्ट ने यह साबित कर दिया कि एक अच्छा हेयूरिस्टिक ए 2 इष्टतम है, यानी इससे बेहतर कोई संस्करण नहीं हो सकता। इसने उसे यह बताने के लिए कि उसे सुधार नहीं किया जा सकता है, ए 3 या ए 4 दिखाई नहीं देगा, एल्गोरिथ्म ए * को कॉल करने के लिए मजबूर किया। तो हां, ए * एल्गोरिथ्म सबसे अच्छा है जो हम प्राप्त कर सकते हैं, लेकिन यह उतना ही अच्छा है जितना कि इसका अनुमान।

unitypackage

प्राथमिकता कतार


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

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

अपनी खुद की कतार बनाएं


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

 using System.Collections.Generic; public class HexCellPriorityQueue { List<HexCell> list = new List<HexCell>(); public void Enqueue (HexCell cell) { } public HexCell Dequeue () { return null; } public void Change (HexCell cell) { } public void Clear () { list.Clear(); } } 

हम सेल की प्राथमिकताओं को स्वयं कोशिकाओं में संग्रहीत करते हैं। यानी कतार में सेल जोड़ने से पहले उसकी प्राथमिकता तय करनी होगी। लेकिन प्राथमिकता परिवर्तन की स्थिति में, यह जानना उपयोगी होगा कि पुरानी प्राथमिकता क्या थी। तो चलो इसे एक पैरामीटर के रूप में Change में जोड़ें।

  public void Change (HexCell cell, int oldPriority) { } 

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

  int count = 0; public int Count { get { return count; } } public void Enqueue (HexCell cell) { count += 1; } public HexCell Dequeue () { count -= 1; return null; } … public void Clear () { list.Clear(); count = 0; } 

कतार में जोड़ें


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

  public void Enqueue (HexCell cell) { count += 1; int priority = cell.SearchPriority; list[priority] = cell; } 

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

  int priority = cell.SearchPriority; while (priority >= list.Count) { list.Add(null); } list[priority] = cell; 


छेद के साथ सूची

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

  public HexCell NextWithSamePriority { get; set; } 

एक श्रृंखला बनाने के लिए, HexCellPriorityQueue.Enqueue को हटाने से पहले, उसी प्राथमिकता के साथ वर्तमान मूल्य को संदर्भित करने के लिए नए जोड़े गए सेल को मजबूर करें।

  cell.NextWithSamePriority = list[priority]; list[priority] = cell; 


लिंक की गई सूचियों की सूची

कतार से निकालें


एक प्राथमिकता कतार से सेल प्राप्त करने के लिए, हमें सबसे कम गैर-खाली सूचकांक में लिंक की गई सूची तक पहुंचने की आवश्यकता है। इसलिए, जब तक हम इसे नहीं ढूंढते, हम एक लूप में सूची में घूमेंगे। यदि हम नहीं पाते हैं, तो कतार खाली है और हम null

मिली श्रृंखला से, हम किसी भी सेल को वापस कर सकते हैं, क्योंकि इन सभी की प्राथमिकता समान है। श्रृंखला की शुरुआत से एक सेल वापस करने का सबसे आसान तरीका।

  public HexCell Dequeue () { count -= 1; for (int i = 0; i < list.Count; i++) { HexCell cell = list[i]; if (cell != null) { return cell; } } return null; } 

शेष श्रृंखला के लिंक को रखने के लिए, अगली सेल उसी प्राथमिकता के साथ उपयोग करें जैसे नई शुरुआत। यदि इस प्राथमिकता स्तर पर केवल एक सेल था, तो तत्व null हो जाता है और भविष्य में इसे छोड़ दिया जाएगा।

  if (cell != null) { list[i] = cell.NextWithSamePriority; return cell; } 

न्यूनतम ट्रैकिंग


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

  int minimum = int.MaxValue; … public void Clear () { list.Clear(); count = 0; minimum = int.MaxValue; } 

कतार में एक सेल जोड़ते समय, हम आवश्यक के रूप में न्यूनतम बदलते हैं।

  public void Enqueue (HexCell cell) { count += 1; int priority = cell.SearchPriority; if (priority < minimum) { minimum = priority; } … } 

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

  public HexCell Dequeue () { count -= 1; for (; minimum < list.Count; minimum++) { HexCell cell = list[minimum]; if (cell != null) { list[minimum] = cell.NextWithSamePriority; return cell; } } return null; } 

यह प्राथमिकता सूची के लूप को बायपास करने में लगने वाले समय को काफी कम कर देता है।

प्राथमिकताएँ बदलें


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

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

  public void Change (HexCell cell, int oldPriority) { HexCell current = list[oldPriority]; HexCell next = current.NextWithSamePriority; } 

यदि वर्तमान सेल एक परिवर्तित सेल है, तो यह हेड सेल है और हम इसे काट सकते हैं जैसे कि हमने इसे कतार से बाहर खींच लिया था।

  HexCell current = list[oldPriority]; HexCell next = current.NextWithSamePriority; if (current == cell) { list[oldPriority] = next; } 

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

  if (current == cell) { list[oldPriority] = next; } else { while (next != cell) { current = next; next = current.NextWithSamePriority; } } 

इस बिंदु पर, हम लिंक की गई सूची से परिवर्तित सेल को हटा सकते हैं, इसे स्किप कर सकते हैं।

  while (next != cell) { current = next; next = current.NextWithSamePriority; } current.NextWithSamePriority = cell.NextWithSamePriority; 

सेल हटाने के बाद, आपको इसे फिर से जोड़ने की आवश्यकता है ताकि यह अपनी नई प्राथमिकता की सूची में दिखाई दे।

  public void Change (HexCell cell, int oldPriority) { … Enqueue(cell); } 

Enqueue विधि काउंटर को बढ़ाता है, लेकिन वास्तव में हम एक नया सेल नहीं जोड़ रहे हैं। इसलिए, इसकी भरपाई के लिए, हमें काउंटर को कम करना होगा।

  Enqueue(cell); count -= 1; 

कतार का उपयोग


अब हम HexGrid पर अपनी प्राथमिकता कतार का लाभ उठा सकते हैं। यह एक एकल उदाहरण के साथ किया जा सकता है, सभी खोज अभियानों के लिए पुन: प्रयोज्य।

  HexCellPriorityQueue searchFrontier; … IEnumerator Search (HexCell fromCell, HexCell toCell) { if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } else { searchFrontier.Clear(); } … } 

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

  WaitForSeconds delay = new WaitForSeconds(1 / 60f); // List<HexCell> frontier = new List<HexCell>(); fromCell.Distance = 0; // frontier.Add(fromCell); searchFrontier.Enqueue(fromCell); while (searchFrontier.Count > 0) { yield return delay; HexCell current = searchFrontier.Dequeue(); // frontier.RemoveAt(0); … } 

कोड बदलें ताकि यह जोड़ता है और पड़ोसी को बदलता है। बदलाव से पहले हम पुरानी प्राथमिकता को याद करेंगे।

  if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); // frontier.Add(neighbor); searchFrontier.Enqueue(neighbor); } else if (distance < neighbor.Distance) { int oldPriority = neighbor.SearchPriority; neighbor.Distance = distance; neighbor.PathFrom = current; searchFrontier.Change(neighbor, oldPriority); } 

इसके अतिरिक्त, हमें अब सीमा को छाँटने की आवश्यकता नहीं है।

 // frontier.Sort( // (x, y) => x.SearchPriority.CompareTo(y.SearchPriority) // ); 


प्राथमिकता कतार का उपयोग करके खोज करें

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



एकता की प्राथमिकता के साथ क्रमबद्ध सूची और कतार



भाग 17: सीमित आंदोलन


  • हम चरण-दर-चरण आंदोलन के लिए तरीके ढूंढते हैं।
  • तुरंत पथ प्रदर्शित करें।
  • हम एक अधिक प्रभावी खोज बनाते हैं।
  • हम केवल पथ की कल्पना करते हैं।

इस भाग में, हम हरकत को गति में विभाजित करेंगे और खोज को यथासंभव तेज करेंगे।


कई चालों से यात्रा

कदम से कदम आंदोलन


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

गति


सीमित आंदोलन के लिए समर्थन प्रदान करने के लिए, हम पूर्णांक पैरामीटर HexGrid.FindPathमें जोड़ते हैं यह एक चाल के लिए गति की सीमा निर्धारित करता है।HexGrid.Searchspeed

  public void FindPath (HexCell fromCell, HexCell toCell, int speed) { StopAllCoroutines(); StartCoroutine(Search(fromCell, toCell, speed)); } IEnumerator Search (HexCell fromCell, HexCell toCell, int speed) { … } 

खेल में विभिन्न प्रकार की इकाइयाँ विभिन्न गति का उपयोग करती हैं। कैवलरी तेज है, पैदल सेना धीमी है, और इसी तरह। हमारे पास अभी तक इकाइयां नहीं हैं, इसलिए अब हम एक निरंतर गति का उपयोग करेंगे। चलो 24 का मान लेते हैं। यह एक काफी बड़ा मूल्य है, 5 से विभाज्य नहीं (चलती की डिफ़ॉल्ट लागत)। निरंतर गति FindPathसे तर्क के रूप में जोड़ें HexMapEditor.HandleInput

  if (editMode) { EditCells(currentCell); } else if ( Input.GetKey(KeyCode.LeftShift) && searchToCell != currentCell ) { if (searchFromCell) { searchFromCell.DisableHighlight(); } searchFromCell = currentCell; searchFromCell.EnableHighlight(Color.blue); if (searchToCell) { hexGrid.FindPath(searchFromCell, searchToCell, 24); } } else if (searchFromCell && searchFromCell != currentCell) { searchToCell = currentCell; hexGrid.FindPath(searchFromCell, searchToCell, 24); } 

चाल


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

चलो वर्तमान सेल और उसके सभी पड़ोसियों के पाठ्यक्रम का निर्धारण करते हैंHexGrid.Searchवर्तमान सेल के पाठ्यक्रम की गणना केवल एक बार की जा सकती है, पड़ोसी चक्र में घूमने से ठीक पहले। जैसे ही हम उससे दूरी पाते हैं, पड़ोसी की चाल निर्धारित की जा सकती है।

  int currentTurn = current.Distance / speed; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … int distance = current.Distance; if (current.HasRoadThroughEdge(d)) { distance += 1; } else if (current.Walled != neighbor.Walled) { continue; } else { distance += edgeType == HexEdgeType.Flat ? 5 : 10; distance += neighbor.UrbanLevel + neighbor.FarmLevel + neighbor.PlantLevel; } int turn = distance / speed; … } 

खोया हुआ आंदोलन


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

मान लीजिए कि हम एक सजातीय मानचित्र के साथ आगे बढ़ते हैं, अर्थात, प्रत्येक सेल में जाने के लिए आपको आंदोलन की 5 इकाइयों की आवश्यकता होती है। हमारी गति 24 है। चार चरणों के बाद, हमने अपने स्टॉक की आवाजाही से 20 इकाइयाँ खर्च कीं, और 4 बचे हैं। अगले चरण में, 5 इकाइयों की फिर से ज़रूरत है, यानी उपलब्ध लोगों की तुलना में एक अधिक। इस स्तर पर हमें क्या करने की आवश्यकता है?

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

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

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

 // int distance = current.Distance; int moveCost; if (current.HasRoadThroughEdge(d)) { moveCost = 1; } else if (current.Walled != neighbor.Walled) { continue; } else { moveCost = edgeType == HexEdgeType.Flat ? 5 : 10; moveCost += neighbor.UrbanLevel + neighbor.FarmLevel + neighbor.PlantLevel; } int distance = current.Distance + moveCost; int turn = distance / speed; 

यदि परिणामस्वरूप हम कदम की सीमा को पार करते हैं, तो पहले हम वर्तमान चाल के सभी आंदोलन बिंदुओं का उपयोग करते हैं। हम इसे गति द्वारा गति को गुणा करके कर सकते हैं। उसके बाद, हम आगे बढ़ने की लागत को जोड़ते हैं।

  int distance = current.Distance + moveCost; int turn = distance / speed; if (turn > currentTurn) { distance = turn * speed + moveCost; } 

इसके परिणामस्वरूप, हम 4 अप्रयुक्त आंदोलन बिंदुओं के साथ चौथे सेल में पहला कदम पूरा करेंगे। इन खो गए बिंदुओं को पांचवें सेल की लागतों में जोड़ा जाता है, इसलिए इसकी दूरी 29 हो जाती है, 25 नहीं। नतीजतन, दूरी पहले से अधिक हो जाती है। उदाहरण के लिए, दसवें सेल की दूरी 50 थी। लेकिन अब इसमें प्रवेश करने के लिए, हमें दो चाल की सीमाओं को पार करने की जरूरत है, जिससे 8 गति बिंदु खोते हैं, यानी अब यह दूरी 58 हो जाती है।


अपेक्षा

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

दूरियों के बजाय चाल दिखाना


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

सबसे पहले, UpdateDistanceLabelउसके कॉल से छुटकारा पाएं HexCell

  public int Distance { get { return distance; } set { distance = value; // UpdateDistanceLabel(); } } … // void UpdateDistanceLabel () { // UnityEngine.UI.Text label = uiRect.GetComponent<Text>(); // label.text = distance == int.MaxValue ? "" : distance.ToString(); // } 

इसके बजाय, हम HexCellसामान्य विधि में जोड़ देंगे SetLabelजो एक मनमाना स्ट्रिंग प्राप्त करता है।

  public void SetLabel (string text) { UnityEngine.UI.Text label = uiRect.GetComponent<Text>(); label.text = text; } 

हम इस नई विधि का उपयोग HexGrid.Searchसफाई कोशिकाओं में करते हैं। कोशिकाओं को छिपाने के लिए, बस उन्हें असाइन करें null

  for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; cells[i].SetLabel(null); cells[i].DisableHighlight(); } 

फिर हम पड़ोसी के चिह्न को उसकी चाल का मान बताते हैं। उसके बाद, हम यह देखने में सक्षम होंगे कि पूरे रास्ते में कितने अतिरिक्त कदम उठाए जाएंगे।

  if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; neighbor.SetLabel(turn.ToString()); neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); searchFrontier.Enqueue(neighbor); } else if (distance < neighbor.Distance) { int oldPriority = neighbor.SearchPriority; neighbor.Distance = distance; neighbor.SetLabel(turn.ToString()); neighbor.PathFrom = current; searchFrontier.Change(neighbor, oldPriority); } 


एकता पथ के साथ जाने के लिए आवश्यक चालों की संख्या



झटपट रास्ते


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

बिना कोरटीन के


एल्गोरिथ्म के माध्यम से धीमी गति से गुजरने के लिए, हमने कोरुटिन का उपयोग किया। हमें अब ऐसा करने की आवश्यकता नहीं है, इसलिए हमें कॉल StartCoroutineऔर StopAllCoroutinesसी से छुटकारा मिल जाएगा HexGridइसके बजाय, हम इसे Searchएक नियमित विधि के रूप में लागू करते हैं

  public void Load (BinaryReader reader, int header) { // StopAllCoroutines(); … } public void FindPath (HexCell fromCell, HexCell toCell, int speed) { // StopAllCoroutines(); // StartCoroutine(Search(fromCell, toCell, speed)); Search(fromCell, toCell, speed); } 

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

  void Search (HexCell fromCell, HexCell toCell, int speed) { … // WaitForSeconds delay = new WaitForSeconds(1 / 60f); fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); while (searchFrontier.Count > 0) { // yield return delay; HexCell current = searchFrontier.Dequeue(); … } } 


तुरंत परिणाम

समय की परिभाषा खोजें


अब हम तुरंत पथ प्राप्त कर सकते हैं, लेकिन उनकी गणना कितनी तेजी से की जाती है? छोटे रास्ते लगभग तुरंत दिखाई देते हैं, लेकिन बड़े मानचित्रों पर लंबे रास्ते थोड़े धीमे लग सकते हैं।

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

खोज से ठीक पहले, एक नई स्टॉपवॉच बनाएं और इसे शुरू करें। खोज पूर्ण होने के बाद, स्टॉपवॉच बंद करें और कंसोल में बीता हुआ समय प्रदर्शित करें।

  public void FindPath (HexCell fromCell, HexCell toCell, int speed) { System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); sw.Start(); Search(fromCell, toCell, speed); sw.Stop(); Debug.Log(sw.ElapsedMilliseconds); } 

आइए हमारे एल्गोरिथ्म के लिए सबसे खराब स्थिति चुनें - एक बड़े नक्शे के निचले दाएं कोने के निचले बाएं से एक खोज। सभी का सबसे बुरा एक समान नक्शा है, क्योंकि एल्गोरिदम को सभी 4,800 मानचित्र कोशिकाओं को संसाधित करना होगा।


सबसे खराब स्थिति में खोजें। इसके लिए खोजा गया समय

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

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

मैं विधानसभा के लिए डिबग लॉग कहां देख सकता हूं?
Unity , . . , , Unity Log Files .

यदि आवश्यक हो तो ही खोजें।


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

  if (editMode) { EditCells(currentCell); } else if ( Input.GetKey(KeyCode.LeftShift) && searchToCell != currentCell ) { if (searchFromCell != currentCell) { if (searchFromCell) { searchFromCell.DisableHighlight(); } searchFromCell = currentCell; searchFromCell.EnableHighlight(Color.blue); if (searchToCell) { hexGrid.FindPath(searchFromCell, searchToCell, 24); } } } else if (searchFromCell && searchFromCell != currentCell) { if (searchToCell != currentCell) { searchToCell = currentCell; hexGrid.FindPath(searchFromCell, searchToCell, 24); } } 

केवल पथ के लिए लेबल दिखाएं


यात्रा चिह्न प्रदर्शित करना एक महंगा ऑपरेशन है, खासकर क्योंकि हम एक अडॉप्ट किए गए दृष्टिकोण का उपयोग करते हैं। सभी कोशिकाओं के लिए इस ऑपरेशन को निष्पादित करने से निश्चित रूप से निष्पादन धीमा हो जाएगा। तो चलो में लेबलिंग छोड़ें HexGrid.Search

  if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; // neighbor.SetLabel(turn.ToString()); neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); searchFrontier.Enqueue(neighbor); } else if (distance < neighbor.Distance) { int oldPriority = neighbor.SearchPriority; neighbor.Distance = distance; // neighbor.SetLabel(turn.ToString()); neighbor.PathFrom = current; searchFrontier.Change(neighbor, oldPriority); } 

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

  if (current == toCell) { current = current.PathFrom; while (current != fromCell) { int turn = current.Distance / speed; current.SetLabel(turn.ToString()); current.EnableHighlight(Color.white); current = current.PathFrom; } break; } 


केवल पथ कक्षों के लिए लेबल प्रदर्शित करना

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

  fromCell.EnableHighlight(Color.blue); // toCell.EnableHighlight(Color.red); fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); while (searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); if (current == toCell) { // current = current.PathFrom; while (current != fromCell) { int turn = current.Distance / speed; current.SetLabel(turn.ToString()); current.EnableHighlight(Color.white); current = current.PathFrom; } toCell.EnableHighlight(Color.red); break; } … } 


एंडपॉइंट के लिए प्रगति की जानकारी सबसे महत्वपूर्ण है।

इन परिवर्तनों के बाद, सबसे खराब स्थिति का समय संपादक में 23 मिलीसेकंड और 6 मिलीसेकंड में समाप्त हो जाता है। ये 43 पीपीएस और 166 पीपीएस हैं - बहुत बेहतर।

unitypackage

सबसे चतुर खोज


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

सेल खोज चरण


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

  public int SearchPhase { get; set; } 

उदाहरण के लिए, 0 का मतलब है कि कोशिकाएं अभी तक नहीं पहुंची हैं, 1 - कि सेल अभी सीमा में है, और 2 - कि यह पहले ही सीमा से हटा दिया गया है।

सीमा को मारना


के रूप में HexGrid.Searchहम 0 और 1 में सभी कक्षों रीसेट कर सकते हैं हमेशा सीमा के लिए उपयोग किया जाता है। या हम प्रत्येक नई खोज के साथ सीमाओं की संख्या बढ़ा सकते हैं। इसके लिए धन्यवाद, अगर हम हर बार सीमाओं की संख्या दो से बढ़ाते हैं, तो हमें कोशिकाओं के डंपिंग से नहीं निपटना होगा।

  int searchFrontierPhase; … void Search (HexCell fromCell, HexCell toCell, int speed) { searchFrontierPhase += 2; … } 

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

  fromCell.SearchPhase = searchFrontierPhase; fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); 

और हर बार जब हम सीमा पर पड़ोसी को जोड़ते हैं।

  if (neighbor.Distance == int.MaxValue) { neighbor.SearchPhase = searchFrontierPhase; neighbor.Distance = distance; neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); searchFrontier.Enqueue(neighbor); } 

सीमा की जाँच


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

 // if (neighbor.Distance == int.MaxValue) { if (neighbor.SearchPhase < searchFrontierPhase) { neighbor.SearchPhase = searchFrontierPhase; neighbor.Distance = distance; neighbor.PathFrom = current; neighbor.SearchHeuristic = neighbor.coordinates.DistanceTo(toCell.coordinates); searchFrontier.Enqueue(neighbor); } 

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

  for (int i = 0; i < cells.Length; i++) { // cells[i].Distance = int.MaxValue; cells[i].SetLabel(null); cells[i].DisableHighlight(); } 

सीमा को छोड़कर


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

  while (searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.SearchPhase += 1; … } 

अब हम सीमा से हटाई गई कोशिकाओं को छोड़ सकते हैं, व्यर्थ गणना और दूरियों की तुलना से बच सकते हैं।

  for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if ( neighbor == null || neighbor.SearchPhase > searchFrontierPhase ) { continue; } … } 

इस बिंदु पर, हमारा एल्गोरिथ्म अभी भी एक ही परिणाम पैदा करता है, लेकिन अधिक कुशलता से। मेरी मशीन पर, सबसे खराब स्थिति संपादक में 20 एमएस और विधानसभा में 5 एमएस लगती है।

हम यह भी गणना कर सकते हैं कि सेल कितनी बार एल्गोरिथम द्वारा संसाधित किया गया है, सेल की दूरी की गणना करते समय काउंटर बढ़ाता है। पहले, सबसे खराब स्थिति में हमारे एल्गोरिथ्म ने 28,239 दूरी की गणना की। तैयार ए * एल्गोरिथ्म में, हम इसकी 14,120 दूरी की गणना करते हैं। राशि में 50% की कमी हुई। उत्पादकता पर इन संकेतकों के प्रभाव की डिग्री चलती की लागत की गणना करने में लागत पर निर्भर करती है। हमारे मामले में, यहां बहुत काम नहीं है, इसलिए विधानसभा में सुधार बहुत बड़ा नहीं है, लेकिन संपादक में यह बहुत ही ध्यान देने योग्य है।

unitypackage

रास्ता साफ करना


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

केवल खोजें


आइए विज़ुअलाइज़ेशन कोड को पूरी तरह से हटाकर शुरू करें Searchउसे केवल एक पथ खोज करने की आवश्यकता है और यह जानने की जरूरत नहीं है कि हम इस जानकारी के साथ क्या करेंगे।

  void Search (HexCell fromCell, HexCell toCell, int speed) { searchFrontierPhase += 2; if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } else { searchFrontier.Clear(); } // for (int i = 0; i < cells.Length; i++) { // cells[i].SetLabel(null); // cells[i].DisableHighlight(); // } // fromCell.EnableHighlight(Color.blue); fromCell.SearchPhase = searchFrontierPhase; fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); while (searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.SearchPhase += 1; if (current == toCell) { // while (current != fromCell) { // int turn = current.Distance / speed; // current.SetLabel(turn.ToString()); // current.EnableHighlight(Color.white); // current = current.PathFrom; // } // toCell.EnableHighlight(Color.red); // break; } … } } 

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

  bool Search (HexCell fromCell, HexCell toCell, int speed) { searchFrontierPhase += 2; if (searchFrontier == null) { searchFrontier = new HexCellPriorityQueue(); } else { searchFrontier.Clear(); } fromCell.SearchPhase = searchFrontierPhase; fromCell.Distance = 0; searchFrontier.Enqueue(fromCell); while (searchFrontier.Count > 0) { HexCell current = searchFrontier.Dequeue(); current.SearchPhase += 1; if (current == toCell) { return true; } … } return false; } 

रास्ता याद है


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

  HexCell currentPathFrom, currentPathTo; bool currentPathExists; … public void FindPath (HexCell fromCell, HexCell toCell, int speed) { System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); sw.Start(); currentPathFrom = fromCell; currentPathTo = toCell; currentPathExists = Search(fromCell, toCell, speed); sw.Stop(); Debug.Log(sw.ElapsedMilliseconds); } 

फिर से रास्ता दिखाओ


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

  void ShowPath (int speed) { if (currentPathExists) { HexCell current = currentPathTo; while (current != currentPathFrom) { int turn = current.Distance / speed; current.SetLabel(turn.ToString()); current.EnableHighlight(Color.white); current = current.PathFrom; } } currentPathFrom.EnableHighlight(Color.blue); currentPathTo.EnableHighlight(Color.red); } 

FindPathखोज के बाद इस विधि को कॉल करें।

  currentPathExists = Search(fromCell, toCell, speed); ShowPath(speed); 

मिटाना


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

  void ClearPath () { if (currentPathExists) { HexCell current = currentPathTo; while (current != currentPathFrom) { current.SetLabel(null); current.DisableHighlight(); current = current.PathFrom; } current.DisableHighlight(); currentPathExists = false; } currentPathFrom = currentPathTo = null; } 

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

  sw.Start(); ClearPath(); currentPathFrom = fromCell; currentPathTo = toCell; currentPathExists = Search(fromCell, toCell, speed); if (currentPathExists) { ShowPath(speed); } sw.Stop(); 

इसके अलावा, हम एक नया नक्शा बनाते समय रास्ता साफ करेंगे।

  public bool CreateMap (int x, int z) { … ClearPath(); if (chunks != null) { for (int i = 0; i < chunks.Length; i++) { Destroy(chunks[i].gameObject); } } … } 

और दूसरा कार्ड लोड करने से पहले भी।

  public void Load (BinaryReader reader, int header) { ClearPath(); … } 

इस परिवर्तन से पहले, पथ दृश्य पुन: साफ़ हो जाता है। लेकिन अब हम एक अधिक कुशल दृष्टिकोण का उपयोग कर रहे हैं, और खोज के सबसे खराब मामले में, समय 14 मिलीसेकंड तक घट गया है। अधिक बुद्धिमान सफाई के कारण केवल गंभीर सुधार। असेंबली का समय घटकर 3 ms हो गया, जो कि 333 pps है। इसके लिए धन्यवाद, वास्तविक समय में रास्तों की खोज बिल्कुल लागू है।

अब जब हमने पथों की त्वरित खोज कर ली है, तो हम अस्थायी डीबगिंग कोड को हटा सकते हैं।

  public void FindPath (HexCell fromCell, HexCell toCell, int speed) { // System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); // sw.Start(); ClearPath(); currentPathFrom = fromCell; currentPathTo = toCell; currentPathExists = Search(fromCell, toCell, speed); ShowPath(speed); // sw.Stop(); // Debug.Log(sw.ElapsedMilliseconds); } 

unitypackage

भाग 18: इकाइयाँ


  • हम दस्तों को नक्शे पर रखते हैं।
  • सेव और लोड स्क्वाड।
  • हम सैनिकों के लिए रास्ते तलाशते हैं।
  • हम इकाइयों को आगे बढ़ाते हैं।

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


सुदृढ़ीकरण आ गया

दस्ते बनाना


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

प्रीफ़ैब स्क्वाड


दस्तों के साथ काम करने के लिए, एक नए प्रकार का घटक बनाएं HexUnitअभी के लिए, चलो एक खाली के साथ शुरू करते हैं MonoBehaviour, और बाद में इसमें कार्यक्षमता जोड़ते हैं।

 using UnityEngine; public class HexUnit : MonoBehaviour { } 

इस घटक के साथ एक खाली गेम ऑब्जेक्ट बनाएं, जो एक प्रीफैब बनना चाहिए। यह दस्ते का मूल उद्देश्य होगा।


प्रीफैब स्क्वाड।

एक 3 डी मॉडल जोड़ें जो एक चाइल्ड ऑब्जेक्ट के रूप में टुकड़ी का प्रतीक है। मैंने एक साधारण स्केल्ड क्यूब का इस्तेमाल किया, जिसके लिए मैंने ब्लू मटेरियल बनाया। रूट ऑब्जेक्ट टुकड़ी के जमीनी स्तर को निर्धारित करता है, इसलिए, हम तदनुसार बाल तत्व को विस्थापित करते हैं।



बाल घन तत्व

स्क्वाड में एक कोलाइडर जोड़ें ताकि भविष्य में चयन करना आसान हो। मानक घन का कोलाइडर हमारे लिए काफी उपयुक्त है, बस एक सेल में कोलाइडर फिट करें।

स्क्वाड के उदाहरण बनाना


चूंकि हमारे पास अभी तक गेमप्ले नहीं है, इसलिए इकाइयों का निर्माण संपादन मोड में होता है। इसलिए, इस पर ध्यान दिया जाना चाहिए HexMapEditorऐसा करने के लिए, उसे एक प्रीफैब चाहिए, इसलिए एक फ़ील्ड जोड़ें HexUnit unitPrefabऔर इसे कनेक्ट करें।

  public HexUnit unitPrefab; 


प्रीफ़ैब कनेक्ट करना

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

  HexCell GetCellUnderCursor () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { return hexGrid.GetCell(hit.point); } return null; } 

अब हम HandleInputइसे सरल बनाने में इस विधि का उपयोग कर सकते हैं

  void HandleInput () { // Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); // RaycastHit hit; // if (Physics.Raycast(inputRay, out hit)) { // HexCell currentCell = hexGrid.GetCell(hit.point); HexCell currentCell = GetCellUnderCursor(); if (currentCell) { … } else { previousCell = null; } } 

इसके बाद, एक नई विधि जोड़ें जिसका CreateUnitउपयोग भी किया जाता है GetCellUnderCursorयदि कोई सेल है, तो हम एक नई टीम बनाएंगे।

  void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell) { Instantiate(unitPrefab); } } 

पदानुक्रम को साफ रखने के लिए, स्क्वायड में सभी गेम ऑब्जेक्ट्स के लिए अभिभावक के रूप में ग्रिड का उपयोग करें।

  void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell) { HexUnit unit = Instantiate(unitPrefab); unit.transform.SetParent(hexGrid.transform, false); } } 

HexMapEditorइकाइयों को बनाने के लिए समर्थन जोड़ने का सबसे आसान तरीका एक कुंजी दबाकर है। यू कुंजी को दबाए जाने पर Updateइसे कॉल करने की विधि बदलें CreateUnit। जैसा कि c के साथ HandleInputहोता है, ऐसा तब होना चाहिए जब कर्सर GUI तत्व के शीर्ष पर न हो। सबसे पहले, हम जाँचेंगे कि क्या हमें मानचित्र को संपादित करना चाहिए, और यदि नहीं, तो हम जाँचेंगे कि क्या हमें एक दल जोड़ना चाहिए। यदि ऐसा है, तो कॉल करें CreateUnit

  void Update () { // if ( // Input.GetMouseButton(0) && // !EventSystem.current.IsPointerOverGameObject() // ) { // HandleInput(); // } // else { // previousCell = null; // } if (!EventSystem.current.IsPointerOverGameObject()) { if (Input.GetMouseButton(0)) { HandleInput(); return; } if (Input.GetKeyDown(KeyCode.U)) { CreateUnit(); return; } } previousCell = null; } 


दस्ते का बनाया गया उदाहरण

ट्रूप प्लेसमेंट


अब हम इकाइयाँ बना सकते हैं, लेकिन वे नक्शे के मूल में दिखाई देते हैं। हमें उन्हें सही जगह पर रखने की जरूरत है। इसके लिए यह आवश्यक है कि सैनिक अपनी स्थिति से अवगत हों। इसलिए, हम उस HexUnitसंपत्ति Locationको जोड़ते हैं, जिस पर वे कब्जा करते हैं। संपत्ति सेट करते समय, हम दस्ते की स्थिति को बदल देंगे ताकि यह सेल की स्थिति से मेल खाए।

  public HexCell Location { get { return location; } set { location = value; transform.localPosition = value.Position; } } HexCell location; 

अब मुझे HexMapEditor.CreateUnitकर्सर के नीचे स्क्वाड सेल की स्थिति निर्दिष्ट करनी चाहिए। फिर इकाइयाँ वही होंगी जहाँ उन्हें होना चाहिए।

  void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell) { HexUnit unit = Instantiate(unitPrefab); unit.transform.SetParent(hexGrid.transform, false); unit.Location = cell; } } 


नक्शे पर दस्ते

यूनिट ओरिएंटेशन


अब तक, सभी इकाइयों में एक ही अभिविन्यास है, जो अप्राकृतिक दिखता है। उन्हें पुनर्जीवित करने के लिए, HexUnitसंपत्ति में जोड़ें Orientationयह एक फ्लोट मूल्य है जो वाई अक्ष में स्क्वाड के रोटेशन को डिग्री में इंगित करता है। इसे सेट करते समय, हम तदनुसार गेम ऑब्जेक्ट के रोटेशन को बदल देंगे।

  public float Orientation { get { return orientation; } set { orientation = value; transform.localRotation = Quaternion.Euler(0f, value, 0f); } } float orientation; 

HexMapEditor.CreateUnit0 से 360 डिग्री से असाइन यादृच्छिक रोटेशन।

  void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell) { HexUnit unit = Instantiate(unitPrefab); unit.transform.SetParent(hexGrid.transform, false); unit.Location = cell; unit.Orientation = Random.Range(0f, 360f); } } 


विभिन्न इकाई झुकाव

प्रति सेल एक स्क्वाड


इकाइयाँ अच्छी लगती हैं यदि वे एक सेल में नहीं बनाई जाती हैं। इस मामले में, हमें अजीब दिखने वाले क्यूब्स का एक समूह मिलता है।


ओवरलैड इकाइयां

कुछ खेल एक ही स्थान पर कई इकाइयों की नियुक्ति की अनुमति देते हैं, अन्य नहीं। चूंकि प्रति स्क्वाड के साथ काम करना आसान है, मैं इस विकल्प को चुनूंगा। इसका मतलब है कि हमें एक नया दस्ता तभी बनाना चाहिए जब वर्तमान सेल पर कब्जा न हो। ताकि आप पता लगा सकें, HexCellमानक संपत्ति में जोड़ें Unit

  public HexUnit Unit { get; set; } 

हम इस संपत्ति का उपयोग HexUnit.Locationसेल को यह बताने के लिए करते हैं कि यूनिट उस पर है या नहीं।

  public HexCell Location { get { return location; } set { location = value; value.Unit = this; transform.localPosition = value.Position; } } 

अब यह HexMapEditor.CreateUnitजांच कर सकता है कि क्या वर्तमान सेल मुक्त है।

  void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell && !cell.Unit) { HexUnit unit = Instantiate(unitPrefab); unit.Location = cell; unit.Orientation = Random.Range(0f, 360f); } } 

व्यस्त कोशिकाओं का संपादन


प्रारंभ में, इकाइयों को सही ढंग से रखा गया है, लेकिन अगर उनकी कोशिकाओं को बाद में संपादित किया जाता है तो सब कुछ बदल सकता है। यदि सेल की ऊंचाई बदलती है, तो उस पर कब्जा करने वाली इकाई या तो इसके ऊपर लटकेगी या इसमें डुबकी लगाएगी।


हैंगिंग और ड्रॉव्ड स्क्वैड्स

समाधान परिवर्तन करने के बाद स्क्वाड स्थिति की जांच करना है। ऐसा करने के लिए, विधि जोड़ें HexUnitअब तक, हम केवल टीम की स्थिति में रुचि रखते हैं, इसलिए इसे फिर से पूछें।

  public void ValidateLocation () { transform.localPosition = location.Position; } 

हमें सेल को अपडेट करते समय टुकड़ी की स्थिति को समन्वित करना चाहिए, जब विधियों Refreshया RefreshSelfOnlyऑब्जेक्ट को HexCellबुलाया जाता है तो क्या होता है बेशक, यह केवल तभी आवश्यक है जब वास्तव में सेल में एक टुकड़ी हो।

  void Refresh () { if (chunk) { chunk.Refresh(); … if (Unit) { Unit.ValidateLocation(); } } } void RefreshSelfOnly () { chunk.Refresh(); if (Unit) { Unit.ValidateLocation(); } } 

दस्तों को दूर करना


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

  void DestroyUnit () { HexCell cell = GetCellUnderCursor(); if (cell && cell.Unit) { Destroy(cell.Unit.gameObject); } } 

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

स्क्वाड को नष्ट करने के लिए Updateबाएं Shift + U के संयोजन का उपयोग करें।

  if (Input.GetKeyDown(KeyCode.U)) { if (Input.GetKey(KeyCode.LeftShift)) { DestroyUnit(); } else { CreateUnit(); } return; } 

मामले में जब हम कई इकाइयां बनाते हैं और नष्ट करते हैं, तो इकाई को हटाते समय सावधानी बरतें और संपत्ति को साफ करें। यही है, हम स्पष्ट रूप से स्क्वाड के सेल लिंक को साफ कर देते हैं। इससे निपटने के HexUnitतरीके के Dieसाथ-साथ अपने स्वयं के गेम ऑब्जेक्ट को नष्ट करने की विधि में जोड़ें

  public void Die () { location.Unit = null; Destroy(gameObject); } 

हम इस विधि को कॉल करेंगे HexMapEditor.DestroyUnit, और स्क्वाड को सीधे नष्ट नहीं करेंगे।

  void DestroyUnit () { HexCell cell = GetCellUnderCursor(); if (cell && cell.Unit) { // Destroy(cell.Unit.gameObject); cell.Unit.Die(); } } 

unitypackage

बचत और लोडिंग स्क्वॉड


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

यूनिट ट्रैकिंग


सभी इकाइयों को एक साथ रखने के लिए, हमें उन्हें ट्रैक करने की आवश्यकता है। हम इसे HexGridइकाइयों की सूची में जोड़कर करेंगे इस सूची में मानचित्र पर सभी इकाइयाँ होनी चाहिए।

  List<HexUnit> units = new List<HexUnit>(); 

नया नक्शा बनाते या लोड करते समय, हमें मानचित्र पर सभी इकाइयों से छुटकारा पाना होगा। इस प्रक्रिया को सरल बनाने के लिए, एक ऐसा तरीका बनाएं ClearUnitsजो सूची में मौजूद सभी को मार डाले और उसे साफ कर दे।

  void ClearUnits () { for (int i = 0; i < units.Count; i++) { units[i].Die(); } units.Clear(); } 

हम इस विधि को CreateMapऔर में कहते हैं Loadरास्ता साफ करने के बाद इसे करते हैं।

  public bool CreateMap (int x, int z) { … ClearPath(); ClearUnits(); … } … public void Load (BinaryReader reader, int header) { ClearPath(); ClearUnits(); … } 

ग्रिड में स्क्वाड जोड़ना


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

  public void AddUnit (HexUnit unit, HexCell location, float orientation) { units.Add(unit); unit.transform.SetParent(transform, false); unit.Location = location; unit.Orientation = orientation; } 

अब यह टुकड़ी के नए उदाहरण, इसके स्थान और यादृच्छिक अभिविन्यास के साथ HexMapEditor.CreatUnitकॉल करने के लिए पर्याप्त होगा AddUnit

  void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell && !cell.Unit) { // HexUnit unit = Instantiate(unitPrefab); // unit.transform.SetParent(hexGrid.transform, false); // unit.Location = cell; // unit.Orientation = Random.Range(0f, 360f); hexGrid.AddUnit( Instantiate(unitPrefab), cell, Random.Range(0f, 360f) ); } } 

ग्रिड से दस्तों को हटाना


स्क्वाड और सी को हटाने के लिए एक विधि जोड़ें HexGridबस सूची से दस्ते को हटा दें और इसे मरने का आदेश दें।

  public void RemoveUnit (HexUnit unit) { units.Remove(unit); unit.Die(); } 

हम इस विधि को HexMapEditor.DestroyUnitसीधे दस्ते को नष्ट करने के बजाय अंदर बुलाते हैं।

  void DestroyUnit () { HexCell cell = GetCellUnderCursor(); if (cell && cell.Unit) { // cell.Unit.Die(); hexGrid.RemoveUnit(cell.Unit); } } 

बचत इकाइयों


चूंकि हम सभी इकाइयों को एक साथ रखने जा रहे हैं, इसलिए हमें यह याद रखने की जरूरत है कि वे किन कोशिकाओं पर कब्जा करते हैं। सबसे विश्वसनीय तरीका उनके स्थान के निर्देशांक को सहेजना है। इसे संभव बनाने के लिए, हम फ़ील्ड X और Z को उस HexCoordinatesविधि में जोड़ते Saveहैं जो इसे लिखता है।

 using UnityEngine; using System.IO; [System.Serializable] public struct HexCoordinates { … public void Save (BinaryWriter writer) { writer.Write(x); writer.Write(z); } } 

विधि Saveके लिए HexUnitकर सकते हैं अब निर्देशांक और यूनिट के उन्मुखीकरण रिकॉर्ड है। यह उन सभी इकाइयों का डेटा है जो हमारे पास इस समय है।

 using UnityEngine; using System.IO; public class HexUnit : MonoBehaviour { … public void Save (BinaryWriter writer) { location.coordinates.Save(writer); writer.Write(orientation); } } 

चूंकि यह HexGridइकाइयों को ट्रैक करता है, इसलिए इसकी विधि Saveइकाइयों के डेटा को रिकॉर्ड करेगी। सबसे पहले, इकाइयों की कुल संख्या को लिखें, और फिर उन सभी को एक लूप में घुमाएं।

  public void Save (BinaryWriter writer) { writer.Write(cellCountX); writer.Write(cellCountZ); for (int i = 0; i < cells.Length; i++) { cells[i].Save(writer); } writer.Write(units.Count); for (int i = 0; i < units.Count; i++) { units[i].Save(writer); } } 

हमने संग्रहीत डेटा को बदल दिया है, इसलिए हम संस्करण संख्या SaveLoadMenu.Saveको 2 तक बढ़ाएंगे । पुराना बूट कोड अभी भी काम करेगा, क्योंकि यह स्क्वाड डेटा को आसानी से नहीं पढ़ेगा। हालाँकि, आपको यह इंगित करने के लिए संस्करण संख्या को बढ़ाने की आवश्यकता है कि फ़ाइल में इकाई जानकारी है।

  void Save (string path) { using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(2); hexGrid.Save(writer); } } 

लोडिंग दस्ते


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

  public static HexCoordinates Load (BinaryReader reader) { HexCoordinates c; cx = reader.ReadInt32(); cz = reader.ReadInt32(); return c; } 

चूंकि इकाइयों की संख्या परिवर्तनशील है, इसलिए हमारे पास पहले से मौजूद इकाइयाँ नहीं हैं जिनमें डेटा लोड किया जा सकता है। हम उनके डेटा को लोड करने से पहले इकाइयों के नए उदाहरण बना सकते हैं, लेकिन इसके लिए यह आवश्यक होगा कि हम HexGridबूट समय पर नई इकाइयों के उदाहरण बनाएँ। इसलिए इसे छोड़ना बेहतर है HexUnitहम स्थैतिक विधि का भी उपयोग करते हैं HexUnit.Loadचलो बस इन दस्तों को पढ़कर शुरू करते हैं। अभिविन्यास फ्लोट के मूल्य को पढ़ने के लिए, हम विधि का उपयोग करते हैं BinaryReader.ReadSingle

सिंगल ही क्यों?
float , . , double , . Unity .

  public static void Load (BinaryReader reader) { HexCoordinates coordinates = HexCoordinates.Load(reader); float orientation = reader.ReadSingle(); } 

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

  public static HexUnit unitPrefab; 

इस लिंक को सेट करने के लिए, इसे फिर से उपयोग करें HexGrid, जैसा कि हमने शोर बनावट के साथ किया था। जब हमें कई प्रकार की इकाइयों का समर्थन करने की आवश्यकता होती है, तो हम बेहतर समाधान की ओर बढ़ेंगे।

  public HexUnit unitPrefab; … void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexUnit.unitPrefab = unitPrefab; CreateMap(cellCountX, cellCountZ); } … void OnEnable () { if (!HexMetrics.noiseSource) { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexUnit.unitPrefab = unitPrefab; } } 


हम यूनिट के प्रीफैब को पास करते हैं।

क्षेत्र को जोड़ने के बाद, हमें अब सीधे लिंक की आवश्यकता नहीं है HexMapEditorइसके बजाय, वह उपयोग कर सकता है HexUnit.unitPrefab

 // public HexUnit unitPrefab; … void CreateUnit () { HexCell cell = GetCellUnderCursor(); if (cell && !cell.Unit) { hexGrid.AddUnit( Instantiate(HexUnit.unitPrefab), cell, Random.Range(0f, 360f) ); } } 

अब हम नए दस्ते का एक उदाहरण बना सकते हैं HexUnit.Loadइसे वापस करने के बजाय, हम इसे ग्रिड में जोड़ने के लिए लोड किए गए निर्देशांक और अभिविन्यास का उपयोग कर सकते हैं। इसे संभव बनाने के लिए, एक पैरामीटर जोड़ें HexGrid

  public static void Load (BinaryReader reader, HexGrid grid) { HexCoordinates coordinates = HexCoordinates.Load(reader); float orientation = reader.ReadSingle(); grid.AddUnit( Instantiate(unitPrefab), grid.GetCell(coordinates), orientation ); } 

अंत में, HexGrid.Loadहम इकाइयों की संख्या की गणना करते हैं और इसका उपयोग सभी संग्रहीत इकाइयों को लोड करने के लिए करते हैं, खुद को एक अतिरिक्त तर्क के रूप में पारित करते हैं।

  public void Load (BinaryReader reader, int header) { … int unitCount = reader.ReadInt32(); for (int i = 0; i < unitCount; i++) { HexUnit.Load(reader, this); } } 

बेशक, यह केवल 2 से कम संस्करण वाली फ़ाइलों को बचाने के लिए काम करेगा, युवा संस्करणों में लोड करने के लिए कोई इकाई नहीं है।

  if (header >= 2) { int unitCount = reader.ReadInt32(); for (int i = 0; i < unitCount; i++) { HexUnit.Load(reader, this); } } 

अब हम संस्करण 2 फ़ाइलों को सही ढंग से अपलोड कर सकते हैं, इसलिए SaveLoadMenu.Loadसमर्थित संस्करण की संख्या 2 तक बढ़ा सकते हैं।

  void Load (string path) { if (!File.Exists(path)) { Debug.LogError("File does not exist " + path); return; } using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { int header = reader.ReadInt32(); if (header <= 2) { hexGrid.Load(reader, header); HexMapCamera.ValidatePosition(); } else { Debug.LogWarning("Unknown map format " + header); } } } 

unitypackage

ट्रूप मूवमेंट


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

मानचित्र संपादक सफाई


पथों के साथ चलती इकाइयाँ गेमप्ले का हिस्सा हैं, यह मानचित्र संपादक पर लागू नहीं होता है। इसलिए, हम HexMapEditorरास्ता खोजने से जुड़े सभी कोड से छुटकारा पा लेंगे

 // HexCell previousCell, searchFromCell, searchToCell; HexCell previousCell; … void HandleInput () { HexCell currentCell = GetCellUnderCursor(); if (currentCell) { if (previousCell && previousCell != currentCell) { ValidateDrag(currentCell); } else { isDrag = false; } if (editMode) { EditCells(currentCell); } // else if ( // Input.GetKey(KeyCode.LeftShift) && searchToCell != currentCell // ) { // if (searchFromCell != currentCell) { // if (searchFromCell) { // searchFromCell.DisableHighlight(); // } // searchFromCell = currentCell; // searchFromCell.EnableHighlight(Color.blue); // if (searchToCell) { // hexGrid.FindPath(searchFromCell, searchToCell, 24); // } // } // } // else if (searchFromCell && searchFromCell != currentCell) { // if (searchToCell != currentCell) { // searchToCell = currentCell; // hexGrid.FindPath(searchFromCell, searchToCell, 24); // } // } previousCell = currentCell; } else { previousCell = null; } } 

इस कोड को हटाने के बाद, संपादक को सक्रिय छोड़ने का कोई मतलब नहीं है जब हम संपादन मोड में नहीं हैं। इसलिए, एक मोड ट्रैकिंग फ़ील्ड के बजाय, हम बस घटक को सक्षम या अक्षम कर सकते हैं HexMapEditorइसके अलावा, संपादक को अब यूआई लेबल से निपटने की आवश्यकता नहीं है।

 // bool editMode; … public void SetEditMode (bool toggle) { // editMode = toggle; // hexGrid.ShowUI(!toggle); enabled = toggle; } … void HandleInput () { HexCell currentCell = GetCellUnderCursor(); if (currentCell) { if (previousCell && previousCell != currentCell) { ValidateDrag(currentCell); } else { isDrag = false; } // if (editMode) { EditCells(currentCell); // } previousCell = currentCell; } else { previousCell = null; } } 

चूंकि डिफ़ॉल्ट रूप से हम मैप एडिटिंग मोड में नहीं हैं, इसलिए अवेक में हम एडिटर को डिसेबल कर देंगे।

  void Awake () { terrainMaterial.DisableKeyword("GRID_ON"); SetEditMode(false); } 

मानचित्र को संपादित करते समय, और इकाइयों को प्रबंधित करने के लिए कर्सर के नीचे वर्तमान सेल की खोज करने के लिए रेकास्ट का उपयोग करना आवश्यक है। शायद भविष्य में यह हमारे लिए किसी और चीज के लिए उपयोगी होगा। चलो किरण पैरामीटर के साथ एक HexGridनई विधि से रेकास्टिंग तर्क को स्थानांतरित करते हैं GetCell

  public HexCell GetCell (Ray ray) { RaycastHit hit; if (Physics.Raycast(ray, out hit)) { return GetCell(hit.point); } return null; } 

HexMapEditor.GetCellUniderCursor कर्सर बीम के साथ बस इस विधि को कॉल कर सकते हैं।

  HexCell GetCellUnderCursor () { return hexGrid.GetCell(Camera.main.ScreenPointToRay(Input.mousePosition)); } 

खेल यूआई


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

 using UnityEngine; using UnityEngine.EventSystems; public class HexGameUI : MonoBehaviour { public HexGrid grid; } 

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



गेम यूआई ऑब्जेक्ट

एक HexGameUIविधि जोड़ें SetEditMode, जैसे कि HexMapEditorजब हम एडिट मोड में नहीं होते हैं तो गेम यूआई को चालू करना चाहिए। इसके अलावा, लेबल को यहां शामिल करने की आवश्यकता है क्योंकि गेम यूआई पथों के साथ काम करता है।

  public void SetEditMode (bool toggle) { enabled = !toggle; grid.ShowUI(!toggle); } 

एडिट मोड स्विच की इवेंट सूची के साथ गेम यूआई विधि जोड़ें। इसका मतलब यह होगा कि जब खिलाड़ी मोड बदलता है, तो दोनों विधियों को बुलाया जाता है।


कई आयोजन विधियाँ।

वर्तमान सेल को ट्रैक करें


स्थिति के आधार पर, HexGameUIआपको यह जानना होगा कि वर्तमान में कर्सर के नीचे कौन सी सेल है। इसलिए, हम इसमें एक फ़ील्ड जोड़ते हैं currentCell

  HexCell currentCell; 

इस फ़ील्ड को अद्यतन करने के UpdateCurrentCellलिए HexGrid.GetCellकर्सर बीम का उपयोग करने वाली विधि बनाएँ

  void UpdateCurrentCell () { currentCell = grid.GetCell(Camera.main.ScreenPointToRay(Input.mousePosition)); } 

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

  bool UpdateCurrentCell () { HexCell cell = grid.GetCell(Camera.main.ScreenPointToRay(Input.mousePosition)); if (cell != currentCell) { currentCell = cell; return true; } return false; } 

इकाई चयन


एक दस्ते को स्थानांतरित करने से पहले, इसे चुनना और ट्रैक करना होगा। इसलिए, एक फ़ील्ड जोड़ें selectedUnit

  HexUnit selectedUnit; 

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

  void DoSelection () { UpdateCurrentCell(); if (currentCell) { selectedUnit = currentCell.Unit; } } 

हमें माउस की एक साधारण क्लिक के साथ इकाइयों की पसंद का एहसास होता है। इसलिए, हम एक विधि जोड़ते हैं Updateजो माउस बटन सक्रिय होने पर एक चयन करता है। निश्चित रूप से, हमें इसे तभी निष्पादित करना होगा जब कर्सर GUI तत्व से ऊपर न हो।

  void Update () { if (!EventSystem.current.IsPointerOverGameObject()) { if (Input.GetMouseButtonDown(0)) { DoSelection(); } } } 

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

दस्ते की खोज


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

  void Update () { if (!EventSystem.current.IsPointerOverGameObject()) { if (Input.GetMouseButtonDown(0)) { DoSelection(); } else if (selectedUnit) { DoPathfinding(); } } } 

DoPathfindingHexGrid.FindPathअगर कोई समापन बिंदु है तो बस वर्तमान सेल को अपडेट करता है और कॉल करता है। हम फिर से 24 की निरंतर गति का उपयोग करते हैं।

  void DoPathfinding () { UpdateCurrentCell(); grid.FindPath(selectedUnit.Location, currentCell, 24); } 

कृपया ध्यान दें कि जब भी हम अपडेट करते हैं, हमें हर बार एक नया रास्ता नहीं मिलना चाहिए, लेकिन केवल तब जब वर्तमान सेल बदल जाए।

  void DoPathfinding () { if (UpdateCurrentCell()) { grid.FindPath(selectedUnit.Location, currentCell, 24); } } 


एक दस्ते के लिए एक रास्ता ढूँढना

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

  void DoPathfinding () { if (UpdateCurrentCell()) { if (currentCell) { grid.FindPath(selectedUnit.Location, currentCell, 24); } else { grid.ClearPath(); } } } 

बेशक, इसके लिए यह आवश्यक है कि यह HexGrid.ClearPathआम हो, इसलिए हम ऐसा बदलाव करते हैं।

  public void ClearPath () { … } 

दूसरे, हम एक टुकड़ी का चयन करते समय पुराने रास्ते को साफ कर देंगे।

  void DoSelection () { grid.ClearPath(); UpdateCurrentCell(); if (currentCell) { selectedUnit = currentCell.Unit; } } 

अंत में, हम संपादन मोड को बदलते समय रास्ता साफ करेंगे।

  public void SetEditMode (bool toggle) { enabled = !toggle; grid.ShowUI(!toggle); grid.ClearPath(); } 

केवल मान्य समापन बिंदुओं के लिए खोजें


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

  public bool IsValidDestination (HexCell cell) { return !cell.IsUnderwater; } 

इसके अलावा, हमने केवल एक इकाई को सेल में खड़े होने की अनुमति दी। इसलिए, यदि वह व्यस्त है तो अंतिम सेल मान्य नहीं होगा।

  public bool IsValidDestination (HexCell cell) { return !cell.IsUnderwater && !cell.Unit; } 

हम इस पद्धति का उपयोग HexGameUI.DoPathfindingअमान्य समापन बिंदुओं को अनदेखा करने के लिए करते हैं।

  void DoPathfinding () { if (UpdateCurrentCell()) { if (currentCell && selectedUnit.IsValidDestination(currentCell)) { grid.FindPath(selectedUnit.Location, currentCell, 24); } else { grid.ClearPath(); } } } 

समाप्ति बिंदु पर ले जाएं


यदि हमारे पास एक वैध रास्ता है, तो हम दस्ते को अंतिम बिंदु पर ले जाने में सक्षम हैं। HexGridजानता है कि यह कब किया जा सकता है। हम इसे एक नई रीड-ओनली प्रॉपर्टी में इस जानकारी को पास करते हैं HasPath

  public bool HasPath { get { return currentPathExists; } } 

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

  void DoMove () { if (grid.HasPath) { selectedUnit.Location = currentCell; grid.ClearPath(); } } 

कमांड सबमिट करने के लिए माउस बटन 1 (राइट क्लिक) का उपयोग करें। अगर टुकड़ी का चयन किया जाता है तो हम इसकी जांच करेंगे। यदि बटन दबाया नहीं जाता है, तो हम रास्ता खोजते हैं।

  void Update () { if (!EventSystem.current.IsPointerOverGameObject()) { if (Input.GetMouseButtonDown(0)) { DoSelection(); } else if (selectedUnit) { if (Input.GetMouseButtonDown(1)) { DoMove(); } else { DoPathfinding(); } } } } 

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

  public HexCell Location { get { return location; } set { if (location) { location.Unit = null; } location = value; value.Unit = this; transform.localPosition = value.Position; } } 

दस्तों से बचें


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


रास्ते में

इकाइयों को नजरअंदाज कर दिया जाता है। एक ही गुट की इकाइयां आमतौर पर एक दूसरे के माध्यम से आगे बढ़ सकती हैं, लेकिन अभी तक हमारे पास कोई गुट नहीं है। इसलिए, आइए सभी इकाइयों को एक-दूसरे से अलग होने और रास्तों को अवरुद्ध करने पर विचार करें। यह व्यस्त कोशिकाओं को लंघन द्वारा कार्यान्वित किया जा सकता है HexGrid.Search

  if ( neighbor == null || neighbor.SearchPhase > searchFrontierPhase ) { continue; } if (neighbor.IsUnderwater || neighbor.Unit) { continue; } 


बचें टुकड़ी

unitypackage

भाग 19: मोशन एनिमेशन


  • हम कोशिकाओं के बीच इकाइयों को स्थानांतरित करते हैं।
  • यात्रा के मार्ग की कल्पना करें।
  • हम सेना को कर्व्स पर ले जाते हैं।
  • हम सैनिकों को आंदोलन की दिशा में देखने के लिए मजबूर करते हैं।

इस भाग में, हम पटरियों के साथ स्थानांतरित करने के लिए टेलीपोर्टेशन के बजाय इकाइयों को बाध्य करेंगे।


रास्ते में दस्ते

रास्ते में हलचल


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

मोड़ों के साथ त्रुटि


एक ओवरसाइट के कारण, हम उस पाठ्यक्रम की गलत गणना करते हैं जिस पर सेल पहुंचेगा। अब हम दस्ते की गति से कुल दूरी को विभाजित करके पाठ्यक्रम का निर्धारण करते हैंt = d / s , और शेष को छोड़ना। त्रुटि तब होती है जब सेल में प्राप्त करने के लिए आपको प्रति चाल के सभी शेष आंदोलन बिंदुओं को बिल्कुल खर्च करने की आवश्यकता होती है। उदाहरण के लिए, जब प्रत्येक चरण की लागत 1 होती है, और गति 3 होती है, तो हम प्रति बारी तीन कोशिकाओं को स्थानांतरित कर सकते हैं। हालांकि, मौजूदा गणना के साथ, हम पहले कदम में केवल दो कदम उठा सकते हैं, क्योंकि तीसरे चरण के लिए

टी = / एस = 3 / 3 = 1


गलत तरीके से परिभाषित चाल के साथ चलने की अभिव्यक्त लागत, गति 3 चाल

की सही गणना के लिए हमें प्रारंभिक सेल से सीमा को एक कदम आगे बढ़ाने की आवश्यकता है। हम इस कदम की गणना करने से पहले दूरी को कम करके 1 कर सकते हैं। फिर तीसरे चरण के लिए कदम होगाटी = 2 / 3 = 0


सही मूव्स

हम गणना फॉर्मूला को बदलकर ऐसा कर सकते हैंटी = ( डी - 1 ) / एसइस परिवर्तन Vnesom HexGrid.Search

  bool Search (HexCell fromCell, HexCell toCell, int speed) { … while (searchFrontier.Count > 0) { … int currentTurn = (current.Distance - 1) / speed; for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … int distance = current.Distance + moveCost; int turn = (distance - 1) / speed; if (turn > currentTurn) { distance = turn * speed + moveCost; } … } } return false; } 

हम चाल के निशान भी बदलते हैं।

  void ShowPath (int speed) { if (currentPathExists) { HexCell current = currentPathTo; while (current != currentPathFrom) { int turn = (current.Distance - 1) / speed; … } } … } 

ध्यान दें कि इस दृष्टिकोण के साथ, प्रारंभिक सेल पथ ,1 है। यह सामान्य है, क्योंकि हम इसे प्रदर्शित नहीं करते हैं, और खोज एल्गोरिदम चालू रहता है।

रास्ता मिल रहा है


रास्ते के साथ चलना स्क्वाड का काम है। ऐसा करने के लिए उसे रास्ता जानने की जरूरत है। हमारे पास यह जानकारी है HexGrid, तो चलो कोशिकाओं की सूची के रूप में वर्तमान पथ प्राप्त करने के लिए इसमें एक विधि जोड़ते हैं। वह इसे सूचियों के पूल से ले सकता है और अगर वास्तव में एक रास्ता है तो वापस आ सकता है।

  public List<HexCell> GetPath () { if (!currentPathExists) { return null; } List<HexCell> path = ListPool<HexCell>.Get(); return path; } 

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

  List<HexCell> path = ListPool<HexCell>.Get(); for (HexCell c = currentPathTo; c != currentPathFrom; c = c.PathFrom) { path.Add(c); } return path; 

इस मामले में, हमें पूरे रास्ते की आवश्यकता है, जिसमें प्रारंभिक सेल शामिल है।

  for (HexCell c = currentPathTo; c != currentPathFrom; c = c.PathFrom) { path.Add(c); } path.Add(currentPathFrom); return path; 

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

  path.Add(currentPathFrom); path.Reverse(); return path; 

मोशन रिक्वेस्ट


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

 using UnityEngine; using System.Collections.Generic; using System.IO; public class HexUnit : MonoBehaviour { … public void Travel (List<HexCell> path) { Location = path[path.Count - 1]; } … } 

आंदोलन का अनुरोध करने के लिए, हम इसे बदल देते हैं HexGameUI.DoMoveताकि यह वर्तमान पथ के साथ एक नई विधि को बुलाए, और न केवल इकाई का स्थान निर्धारित करे।

  void DoMove () { if (grid.HasPath) { // selectedUnit.Location = currentCell; selectedUnit.Travel(grid.GetPath()); grid.ClearPath(); } } 

पथ दृश्य


इससे पहले कि हम दस्ते को नजरअंदाज करना शुरू करें, आइए देखें कि रास्ते सही हैं। हम इसे HexUnitउस पथ को याद करने का आदेश देकर करेंगे , जिसके साथ इसे चलना होगा, ताकि इसे गिज़्मोस का उपयोग करके कल्पना की जा सके।

  List<HexCell> pathToTravel; … public void Travel (List<HexCell> path) { Location = path[path.Count - 1]; pathToTravel = path; } 

OnDrawGizmosजाने के लिए अंतिम रास्ता दिखाने के लिए एक विधि जोड़ें (यदि यह मौजूद है)। यदि इकाई अभी तक स्थानांतरित नहीं हुई है, तो पथ समान होना चाहिए nullलेकिन प्ले मोड में पुनःसंक्रमण के बाद संपादन के दौरान एकता की क्रमबद्धता के कारण, यह एक खाली सूची भी हो सकती है।

  void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } } 

पथ दिखाने का सबसे आसान तरीका पथ के प्रत्येक सेल के लिए एक सुविधाजनक क्षेत्र बनाना है। 2 इकाइयों के दायरे वाला एक गोला हमारे लिए उपयुक्त है।

  void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } for (int i = 0; i < pathToTravel.Count; i++) { Gizmos.DrawSphere(pathToTravel[i].Position, 2f); } } 

चूंकि हम टुकड़ी के लिए रास्ते दिखाएंगे, इसलिए हम इसके सभी अंतिम रास्तों को एक साथ देख पाएंगे।


गिज़मोस ने यात्रा के अंतिम रास्तों को प्रदर्शित किया।

सेल कनेक्शन को बेहतर ढंग से दिखाने के लिए, हम पिछली और वर्तमान कोशिकाओं के बीच की रेखा पर एक लूप में कई गोले खींचते हैं। ऐसा करने के लिए, हमें दूसरी सेल से प्रक्रिया शुरू करने की आवश्यकता है। 0.1 इकाइयों की वृद्धि के साथ रेखीय प्रक्षेप का उपयोग करके क्षेत्रों को व्यवस्थित किया जा सकता है, ताकि हमें प्रति खंड दस गोले मिलें।

  for (int i = 1; i < pathToTravel.Count; i++) { Vector3 a = pathToTravel[i - 1].Position; Vector3 b = pathToTravel[i].Position; for (float t = 0f; t < 1f; t += 0.1f) { Gizmos.DrawSphere(Vector3.Lerp(a, b, t), 2f); } } 


अधिक स्पष्ट तरीके

रास्ते में फिसलना


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

 using UnityEngine; using System.Collections; using System.Collections.Generic; using System.IO; public class HexUnit : MonoBehaviour { … IEnumerator TravelPath () { for (int i = 1; i < pathToTravel.Count; i++) { Vector3 a = pathToTravel[i - 1].Position; Vector3 b = pathToTravel[i].Position; for (float t = 0f; t < 1f; t += Time.deltaTime) { transform.localPosition = Vector3.Lerp(a, b, t); yield return null; } } } … } 

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

  public void Travel (List<HexCell> path) { Location = path[path.Count - 1]; pathToTravel = path; StopAllCoroutines(); StartCoroutine(TravelPath()); } 

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

  const float travelSpeed = 4f; … IEnumerator TravelPath () { for (int i = 1; i < pathToTravel.Count; i++) { Vector3 a = pathToTravel[i - 1].Position; Vector3 b = pathToTravel[i].Position; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Vector3.Lerp(a, b, t); yield return null; } } } 

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


चलती इकाइयाँ।

ऊंचाई में अंतर के बारे में क्या?
, . , . , . , . , Endless Legend, , . , .

संकलन के बाद की स्थिति


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

  void OnEnable () { if (location) { transform.localPosition = location.Position; } } 

unitypackage

चिकना आंदोलन


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

पसली से पसली की ओर बढ़ना


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


किनारे से किनारे तक जाने के तीन तरीके इस तरह से उत्पन्न रास्तों को प्रदर्शित करने

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

  void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } Vector3 a, b = pathToTravel[0].Position; for (int i = 1; i < pathToTravel.Count; i++) { // Vector3 a = pathToTravel[i - 1].Position; // Vector3 b = pathToTravel[i].Position; a = b; b = (pathToTravel[i - 1].Position + pathToTravel[i].Position) * 0.5f; for (float t = 0f; t < 1f; t += 0.1f) { Gizmos.DrawSphere(Vector3.Lerp(a, b, t), 2f); } } } 

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

  void OnDrawGizmos () { … for (int i = 1; i < pathToTravel.Count; i++) { … } a = b; b = pathToTravel[pathToTravel.Count - 1].Position; for (float t = 0f; t < 1f; t += 0.1f) { Gizmos.DrawSphere(Vector3.Lerp(a, b, t), 2f); } } 


रिब-आधारित

पथ परिणामी पथ zigzags की तरह कम होते हैं, और अधिकतम मोड़ कोण 120 ° से 90 ° तक कम हो जाता है। इसे एक सुधार माना जा सकता है, इसलिए हम coroutine में समान परिवर्तन लागू करते हैं ताकि यह देखा जा सके TravelPathकि यह एनीमेशन में कैसा दिखता है।

  IEnumerator TravelPath () { Vector3 a, b = pathToTravel[0].Position; for (int i = 1; i < pathToTravel.Count; i++) { // Vector3 a = pathToTravel[i - 1].Position; // Vector3 b = pathToTravel[i].Position; a = b; b = (pathToTravel[i - 1].Position + pathToTravel[i].Position) * 0.5f; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Vector3.Lerp(a, b, t); yield return null; } } a = b; b = pathToTravel[pathToTravel.Count - 1].Position; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Vector3.Lerp(a, b, t); yield return null; } } 


एक बदलती गति के साथ चलना

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

निम्नलिखित घटता है


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


किनारे से किनारे तक वक्र एक द्विघात वक्र पर अंक प्राप्त करने के लिए एक विधि के साथ एक

सहायक वर्ग बनाएं Bezierजैसा कि कर्व्स और स्प्लिन्स ट्यूटोरियल में बताया गया है , इसके लिए सूत्र का उपयोग किया जाता है( 1 - टी ) 2+ 2 ( 1 - टी ) टी बी + टी 2 सी जहाँ और C नियंत्रण बिंदु हैं, और टी इंटरपोलर है।

 using UnityEngine; public static class Bezier { public static Vector3 GetPoint (Vector3 a, Vector3 b, Vector3 c, float t) { float r = 1f - t; return r * r * a + 2f * r * t * b + t * t * c; } } 

GetPoint 0-1 तक सीमित नहीं होना चाहिए?
0-1, . . , GetPointClamped , t . , GetPointUnclamped .

वक्र पथ को दिखाने के लिए OnDrawGizmos, हमें दो नहीं, बल्कि तीन बिंदुओं को ट्रैक करना होगा। अतिरिक्त बिंदु - यह, सेल, जिसके साथ हम वर्तमान यात्रा पर काम कर रहे का केंद्र है एक सूचकांक है i - 1, क्योंकि चक्र 1. पर शुरू होता है अंक के सभी के बाद, हम जगह ले सकता है Vector3.Lerpपर Bezier.GetPoint

प्रारंभ और अंत कोशिकाओं में, अंत और मध्य बिंदु के बजाय, हम बस सेल के केंद्र का उपयोग कर सकते हैं।

  void OnDrawGizmos () { if (pathToTravel == null || pathToTravel.Count == 0) { return; } Vector3 a, b, c = pathToTravel[0].Position; for (int i = 1; i < pathToTravel.Count; i++) { a = c; b = pathToTravel[i - 1].Position; c = (b + pathToTravel[i].Position) * 0.5f; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { Gizmos.DrawSphere(Bezier.GetPoint(a, b, c, t), 2f); } } a = c; b = pathToTravel[pathToTravel.Count - 1].Position; c = b; for (float t = 0f; t < 1f; t += 0.1f) { Gizmos.DrawSphere(Bezier.GetPoint(a, b, c, t), 2f); } } 


बेजियर कर्व्स का उपयोग करके बनाए गए रास्ते एक घुमावदार

रास्ता ज्यादा बेहतर दिखता है। हम उसी परिवर्तनों को लागू करते हैं TravelPathऔर देखते हैं कि इकाइयां इस दृष्टिकोण के साथ कैसे एनिमेटेड हैं।

  IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; for (int i = 1; i < pathToTravel.Count; i++) { a = c; b = pathToTravel[i - 1].Position; c = (b + pathToTravel[i].Position) * 0.5f; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); yield return null; } } a = c; b = pathToTravel[pathToTravel.Count - 1].Position; c = b; for (float t = 0f; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); yield return null; } } 


हम कर्व्स के साथ चलते हैं।

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

समय ट्रैकिंग


इस बिंदु तक, हमने 0 से प्रत्येक खंड पर पुनरावृत्ति शुरू कर दी, जब तक कि हम तक नहीं पहुंचते। 1. स्थिर मूल्य से बढ़ने पर यह ठीक काम करता है, लेकिन हमारा पुनरावृत्ति समय डेल्टा पर निर्भर करता है। जब एक खंड पर चलना पूरा हो जाता है, तो हम समय के डेल्टा के आधार पर कुछ राशि से 1 से अधिक होने की संभावना है। यह उच्च फ्रेम दर पर अदृश्य है, लेकिन कम फ्रेम दर पर झटके का कारण बन सकता है।

समय की हानि से बचने के लिए, हमें शेष समय को एक सेगमेंट से दूसरे में स्थानांतरित करना होगा। यह tपूरे पथ के साथ ट्रैकिंग के द्वारा किया जा सकता है , न कि प्रत्येक खंड में। फिर प्रत्येक खंड के अंत में हम इसे 1 से घटाएंगे।

  IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; float t = 0f; for (int i = 1; i < pathToTravel.Count; i++) { a = c; b = pathToTravel[i - 1].Position; c = (b + pathToTravel[i].Position) * 0.5f; for (; t < 1f; t += Time.deltaTime * travelSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); yield return null; } t -= 1f; } a = c; b = pathToTravel[pathToTravel.Count - 1].Position; c = b; for (; t < 1f; t += Time.deltaTime * traveSpeed) { transform.localPosition = Bezier.GetPoint(a, b, c, t); yield return null; } } 

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

  float t = Time.deltaTime * travelSpeed; 

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

  IEnumerator TravelPath () { … transform.localPosition = location.Position; } 

unitypackage

ओरिएंटेशन एनीमेशन


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

आगे देख रहे हैं


जैसा कि कर्व्स और स्प्लिन्स ट्यूटोरियल में , हम यूनिट के ओरिएंटेशन को निर्धारित करने के लिए वक्र के व्युत्पन्न का उपयोग कर सकते हैं। द्विघाती बेज़ियर वक्र के व्युत्पन्न के लिए सूत्र:2 ( ( 1 - टी ) ( बी - ) + टी ( सी - बी ) )Bezierइसे गणना करने के लिए विधि में जोड़ें

  public static Vector3 GetDerivative ( Vector3 a, Vector3 b, Vector3 c, float t ) { return 2f * ((1f - t) * (b - a) + t * (c - b)); } 

व्युत्पन्न वेक्टर गति की दिशा के साथ एक सीधी रेखा पर स्थित है। हम Quaternion.LookRotationइसे स्क्वाड मोड़ में बदलने के लिए विधि का उपयोग कर सकते हैं हम इसे हर कदम पर पूरा करेंगे HexUnit.TravelPath

  transform.localPosition = Bezier.GetPoint(a, b, c, t); Vector3 d = Bezier.GetDerivative(a, b, c, t); transform.localRotation = Quaternion.LookRotation(d); yield return null; … transform.localPosition = Bezier.GetPoint(a, b, c, t); Vector3 d = Bezier.GetDerivative(a, b, c, t); transform.localRotation = Quaternion.LookRotation(d); yield return null; 

क्या रास्ते की शुरुआत में कोई गलती नहीं है?
, . और , . , t=0 , , Quaternion.LookRotation . , , t=0 . . , t>0 .
, t<1

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

  transform.localPosition = location.Position; orientation = transform.localRotation.eulerAngles.y; 

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

  Vector3 d = Bezier.GetDerivative(a, b, c, t); dy = 0f; transform.localRotation = Quaternion.LookRotation(d); … Vector3 d = Bezier.GetDerivative(a, b, c, t); dy = 0f; transform.localRotation = Quaternion.LookRotation(d); 


आगे बढ़ते हुए देखना

हम बिंदु को देखते हैं


पूरे रास्ते में, इकाइयाँ आगे देख रही हैं, लेकिन आगे बढ़ने से पहले वे दूसरी दिशा में देख सकती हैं। इस मामले में, वे तुरंत अपने अभिविन्यास को बदलते हैं। आंदोलन शुरू होने से पहले वे मार्ग की दिशा में मुड़ें तो बेहतर होगा।

सही दिशा में LookAtदेखना अन्य स्थितियों में उपयोगी हो सकता है, इसलिए आइए एक विधि बनाएं जो एक निश्चित बिंदु पर देखने के लिए अभिविन्यास को बदलने के लिए दस्ते को मजबूर करता है। आवश्यक घुमाव को विधि का उपयोग करके सेट किया जा सकता है Transform.LookAt, पहले टुकड़ी के समान ऊर्ध्वाधर स्थिति में बिंदु बनाकर। उसके बाद हम दस्ते के उन्मुखीकरण को पुनः प्राप्त कर सकते हैं।

  void LookAt (Vector3 point) { point.y = transform.localPosition.y; transform.LookAt(point); orientation = transform.localRotation.eulerAngles.y; } 

ताकि टुकड़ी वास्तव में बदल जाए, हम विधि को एक और कोरुटिन में बदल देंगे जो इसे एक स्थिर गति से घुमाएगा। मोड़ की गति को भी समायोजित किया जा सकता है, लेकिन हम निरंतर का उपयोग करेंगे। घुमाव तेज होना चाहिए, प्रति सेकंड लगभग 180 °।

  const float rotationSpeed = 180f; … IEnumerator LookAt (Vector3 point) { … } 

मोड़ के त्वरण के साथ टिंकर करना आवश्यक नहीं है, क्योंकि यह अगोचर है। यह हमारे लिए बस दो झुकावों के बीच अंतर करने के लिए पर्याप्त होगा। दुर्भाग्य से, यह दो नंबरों के मामले में उतना सरल नहीं है, क्योंकि कोण परिपत्र हैं। उदाहरण के लिए, 350 ° से 10 ° तक के संक्रमण का परिणाम 20 ° दक्षिणावर्त घुमाव में होना चाहिए, लेकिन सरल प्रक्षेप एक वामावर्त दिशा में 340 ° घूमने पर मजबूर करेगा।

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

  IEnumerator LookAt (Vector3 point) { point.y = transform.localPosition.y; Quaternion fromRotation = transform.localRotation; Quaternion toRotation = Quaternion.LookRotation(point - transform.localPosition); for (float t = Time.deltaTime; t < 1f; t += Time.deltaTime) { transform.localRotation = Quaternion.Slerp(fromRotation, toRotation, t); yield return null; } transform.LookAt(point); orientation = transform.localRotation.eulerAngles.y; } 

यह काम करेगा, लेकिन रोटेशन के कोण की परवाह किए बिना, प्रक्षेप हमेशा 0 से 1 तक जाता है। एकसमान कोणीय वेग सुनिश्चित करने के लिए, हमें प्रक्षेप को धीमा करना होगा क्योंकि रोटेशन का कोण बढ़ता है।

  Quaternion fromRotation = transform.localRotation; Quaternion toRotation = Quaternion.LookRotation(point - transform.localPosition); float angle = Quaternion.Angle(fromRotation, toRotation); float speed = rotationSpeed / angle; for ( float t = Time.deltaTime * speed; t < 1f; t += Time.deltaTime * speed ) { transform.localRotation = Quaternion.Slerp(fromRotation, toRotation, t); yield return null; } 

कोण को जानने के बाद, यदि हम शून्य हो जाते हैं, तो हम पूरी तरह से मोड़ को छोड़ सकते हैं।

  float angle = Quaternion.Angle(fromRotation, toRotation); if (angle > 0f) { float speed = rotationSpeed / angle; for ( … ) { … } } 

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

  IEnumerator TravelPath () { Vector3 a, b, c = pathToTravel[0].Position; yield return LookAt(pathToTravel[1].Position); float t = Time.deltaTime * travelSpeed; … } 

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

  Vector3 a, b, c = pathToTravel[0].Position; transform.localPosition = c; yield return LookAt(pathToTravel[1].Position); 


जाने से पहले मुड़ें

मिटाना


हमें जिस आंदोलन की आवश्यकता है, उसे प्राप्त करने के बाद, हम विधि से छुटकारा पा सकते हैं OnDrawGizmosइसे हटा दें या हमें भविष्य में रास्ते देखने की जरूरत है।

 // void OnDrawGizmos () { // … // } 

चूंकि हमें अब यह याद रखने की आवश्यकता नहीं है कि हम किस रास्ते पर बढ़ रहे थे, अंत में TravelPathआप कोशिकाओं की सूची को मुक्त कर सकते हैं।

  IEnumerator TravelPath () { … ListPool<HexCell>.Add(pathToTravel); pathToTravel = null; } 

वास्तविक दस्ते के एनिमेशन के बारे में क्या?
, . 3D- . . , . Mecanim, TravelPath .

unitypackage

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


All Articles