Hexagon Maps in Unity: Path Finder, Pemain Regu, Animasi

Bagian 1-3: jala, warna, dan tinggi sel

Bagian 4-7: gundukan, sungai, dan jalan

Bagian 8-11: air, bentang alam, dan benteng

Bagian 12-15: menyimpan dan memuat, tekstur, jarak

Bagian 16-19: menemukan jalan, regu pemain, animasi

Bagian 20-23: Kabut Perang, Penelitian Peta, Generasi Prosedural

Bagian 24-27: siklus air, erosi, bioma, peta silindris

Bagian 16: menemukan jalan


  • Sorot sel
  • Pilih target pencarian
  • Temukan jalur terpendek
  • Buat antrian prioritas

Setelah menghitung jarak antar sel, kami melanjutkan untuk menemukan jalur di antara mereka.

Dimulai dengan bagian ini, tutorial peta segi enam akan dibuat di Unity 5.6.0. Perlu dicatat bahwa dalam 5.6 ada bug yang menghancurkan array tekstur dalam rakitan untuk beberapa platform. Anda dapat menyiasatinya dengan memasukkan Apakah Dapat Dibaca di inspektur array tekstur.


Merencanakan perjalanan

Sel yang disorot


Untuk mencari jalur antara dua sel, pertama-tama kita harus memilih sel-sel ini. Lebih dari sekadar memilih satu sel dan memantau pencarian di peta. Sebagai contoh, pertama-tama kita akan memilih sel awal, dan kemudian yang terakhir. Dalam hal ini, akan lebih mudah bagi mereka untuk disorot. Karena itu, mari kita tambahkan fungsionalitas tersebut. Sampai kita menciptakan cara penyorotan yang canggih atau efisien, kita hanya menciptakan sesuatu untuk membantu kita dalam pengembangan.

Tekstur garis besar


Salah satu cara sederhana untuk memilih sel adalah dengan menambahkan path ke dalamnya. Cara termudah untuk melakukan ini adalah dengan tekstur yang mengandung garis heksagonal. Di sini Anda dapat mengunduh tekstur seperti itu. Ini transparan kecuali untuk garis putih segi enam. Setelah membuatnya putih, di masa depan kita akan dapat mewarnainya sesuai kebutuhan.


Garis sel pada latar belakang hitam

Impor tekstur dan atur Jenis Teksturnya ke Sprite . Mode Sprite- nya akan diatur ke Tunggal dengan pengaturan default. Karena ini adalah tekstur yang sangat putih, kita tidak perlu mengonversi ke sRGB . Saluran alfa menunjukkan transparansi, jadi aktifkan Alpha adalah Transparansi . Saya juga mengatur tekstur Mode Filter ke Trilinear , karena kalau tidak transisi mip untuk jalur mungkin menjadi terlalu mencolok.


Opsi Impor Tekstur

Satu sprite per sel


Cara tercepat adalah menambahkan kontur yang mungkin ke sel, menambahkan masing-masing sprite sendiri. Buat objek game baru, tambahkan komponen Gambar ( Komponen / UI / Gambar ) ke dalamnya dan tetapkan sprite garis besar kami. Lalu masukkan contoh cetakan Hex Cell Label ke dalam adegan, buat objek sprite menjadi anak dari itu, terapkan perubahan pada cetakan, dan kemudian singkirkan cetakan.



Elemen Pemilihan Anak Prefab

Sekarang setiap sel memiliki sprite, tetapi akan terlalu besar. Untuk membuat kontur cocok dengan pusat sel, ubah Lebar dan Tinggi komponen transformasi sprite menjadi 17.


Pilihan sprite sebagian disembunyikan oleh lega

Menggambar di atas segalanya


Karena kontur ditumpangkan pada area tepi sel, sering muncul di bawah geometri relief. Karena itu, bagian dari sirkuit menghilang. Ini dapat dihindari dengan sedikit menaikkan sprite secara vertikal, tetapi tidak dalam kasus istirahat. Sebagai gantinya, kita dapat melakukan hal berikut: selalu menggambar sprite di atas segalanya. Untuk melakukan ini, buat spader shader Anda sendiri. Cukup bagi kita untuk menyalin sprite shader Unity standar dan membuat beberapa perubahan padanya.

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 } } } 

Perubahan pertama adalah kita mengabaikan buffer kedalaman, membuat uji-Z selalu berhasil.

  ZWrite Off ZTest Always 

Perubahan kedua adalah bahwa kita membuat setelah sisa dari geometri transparan. Cukup menambahkan 10 ke antrian transparansi.

  "Queue"="Transparent+10" 

Buat materi baru yang akan digunakan shader ini. Kami dapat mengabaikan semua propertinya, mengikuti nilai default. Kemudian buat prefab sprite menggunakan bahan ini.



Kami menggunakan bahan sprite kami sendiri

Sekarang kontur seleksi selalu terlihat. Bahkan jika sel disembunyikan di bawah lega yang lebih tinggi, garis besarnya masih akan digambarkan di atas segalanya. Itu mungkin tidak terlihat indah, tetapi sel-sel yang dipilih akan selalu terlihat, yang berguna bagi kita.


Abaikan buffer kedalaman

Kontrol pemilihan


Kami tidak ingin semua sel disorot secara bersamaan. Bahkan, pada awalnya mereka semua harus tidak dipilih. Kita dapat menerapkan ini dengan menonaktifkan komponen Gambar dari objek cetakan awal.


Komponen Gambar Dinonaktifkan

Untuk mengaktifkan pemilihan sel, tambahkan metode EnableHighlight ke EnableHighlight . Ini harus mengambil satu-satunya anak dari uiRect dan memasukkan komponen uiRect . Kami juga akan membuat metode 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; } 

Akhirnya, kita bisa menentukan warna sehingga ketika dihidupkan, beri rona latar.

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

paket unity

Menemukan jalan


Sekarang kita dapat memilih sel, kita perlu bergerak dan memilih dua sel, dan kemudian menemukan jalur di antara mereka. Pertama-tama kita harus memilih sel, kemudian membatasi pencarian ke jalur di antara mereka, dan akhirnya menunjukkan jalur ini.

Mulai pencarian


Kita perlu memilih dua sel yang berbeda, titik awal dan akhir pencarian. Misalkan untuk memilih sel pencarian awal, tahan tombol Shift kiri sambil mengklik mouse. Dalam hal ini, sel disorot dengan warna biru. Kami perlu menyimpan tautan ke sel ini untuk pencarian lebih lanjut. Selain itu, ketika memilih sel awal yang baru, pemilihan yang lama harus dinonaktifkan. Oleh karena itu, kami menambahkan bidang searchFromCell ke searchFromCell .

  HexCell previousCell, searchFromCell; 

Di dalam HandleInput kita bisa menggunakan Input.GetKey(KeyCode.LeftShift) untuk menguji tombol Shift yang ditekan.

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


Di mana mencarinya

Cari titik akhir


Alih-alih mencari semua jarak ke sel, kami sekarang mencari jalur antara dua sel tertentu. Oleh karena itu, ganti nama HexGrid.FindDistancesTo menjadi HexGrid.FindPath dan berikan parameter HexCell , ubah metode 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); … } 

Sekarang HexMapEditor.HandleInput harus memanggil metode yang dimodifikasi, menggunakan searchFromCell dan currentCell sebagai argumen. Selain itu, kita dapat mencari hanya ketika kita tahu dari sel mana untuk mencari. Dan kita tidak perlu repot mencari jika titik awal dan akhir bertepatan.

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

Beralih ke pencarian, pertama-tama kita harus menyingkirkan semua pilihan sebelumnya. Karena itu, buat HexGrid.Search matikan pilihan saat mengatur ulang jarak. Karena ini juga mematikan iluminasi sel awal, kemudian hidupkan kembali. Pada tahap ini, kita juga bisa menyoroti titik akhir. Mari kita membuatnya merah.

  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); … } 


Titik akhir dari jalur potensial

Batasi pencarian


Pada titik ini, algoritma pencarian kami masih menghitung jarak ke semua sel yang dapat dijangkau dari sel awal. Tapi kami tidak membutuhkannya lagi. Kita bisa berhenti begitu menemukan jarak terakhir ke sel terakhir. Yaitu, ketika sel saat ini terbatas, kita dapat keluar dari loop algoritma.

  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++) { … } } 


Berhenti di titik akhir

Apa yang terjadi jika titik akhir tidak dapat dijangkau?
Kemudian algoritma akan terus bekerja sampai menemukan semua sel yang dapat dijangkau. Tanpa kemungkinan keluar prematur, ini akan berfungsi sebagai metode FindDistancesTo lama.

Tampilan jalur


Kita dapat menemukan jarak antara awal dan akhir jalan, tetapi belum tahu jalan yang sebenarnya. Untuk menemukannya, Anda perlu melacak bagaimana setiap sel tercapai. Tetapi bagaimana cara melakukannya?

Saat menambahkan sel ke perbatasan, kami melakukan ini karena itu adalah tetangga sel saat ini. Satu-satunya pengecualian adalah sel awal. Semua sel lain telah dijangkau melalui sel saat ini. Jika kita melacak dari sel mana masing-masing sel itu berasal, kita mendapatkan jaringan sel sebagai hasilnya. Lebih tepatnya, jaringan seperti pohon, yang root adalah titik awal. Kita dapat menggunakannya untuk membangun jalur setelah mencapai titik akhir.


Jaringan pohon menggambarkan jalur ke pusat

Kami dapat menyimpan informasi ini dengan menambahkan tautan ke sel lain di HexCell . Kami tidak perlu membuat serialisasi data ini, jadi kami menggunakan properti standar untuk ini.

  public HexCell PathFrom { get; set; } 

Dalam HexGrid.Search atur nilai PathFrom dari tetangga ke sel saat ini ketika menambahkannya ke perbatasan. Selain itu, kami perlu mengubah tautan ini ketika kami menemukan cara yang lebih pendek ke tetangga.

  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; } 

Setelah mencapai titik akhir, kita dapat memvisualisasikan jalur dengan mengikuti tautan ini kembali ke sel awal dan memilihnya.

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


Path ditemukan

Perlu mempertimbangkan bahwa seringkali ada beberapa jalur terpendek. Yang ditemukan tergantung pada urutan pemrosesan sel. Beberapa jalur mungkin terlihat bagus, yang lain mungkin buruk, tetapi tidak pernah ada jalan yang lebih pendek. Kami akan kembali ke sini nanti.

Ubah awal pencarian


Setelah memilih titik awal, mengubah titik akhir akan memicu pencarian baru. Hal yang sama harus terjadi ketika memilih sel awal yang baru. Untuk memungkinkan ini, HexMapEditor juga harus mengingat titik akhir.

  HexCell previousCell, searchFromCell, searchToCell; 

Dengan menggunakan bidang ini, kami juga dapat memulai pencarian baru saat memilih awal yang baru.

  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); } 

Selain itu, kita perlu menghindari titik awal dan akhir yang sama.

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

paket unity

Pencarian Lebih Cerdas


Meskipun algoritme kami menemukan jalur terpendek, algoritma ini menghabiskan banyak waktu menjelajahi titik yang jelas tidak akan menjadi bagian dari jalur ini. Setidaknya sudah jelas bagi kami. Algoritme tidak dapat melihat ke bawah pada peta, tidak dapat melihat bahwa pencarian di beberapa arah tidak akan berarti. Dia lebih suka bergerak di jalan, terlepas dari kenyataan bahwa mereka menuju ke arah yang berlawanan dari titik akhir. Apakah mungkin membuat pencarian lebih pintar?

Saat ini, ketika memilih sel untuk diproses berikutnya, kami hanya mempertimbangkan jarak dari sel ke awal. Jika kita ingin berbuat lebih pintar, maka kita juga harus mempertimbangkan jarak ke titik akhir. Sayangnya, kita belum mengenalnya. Tapi kita bisa membuat estimasi jarak yang tersisa. Menambahkan perkiraan ini pada jarak ke sel memberi kita pemahaman tentang total panjang jalan yang melewati sel ini. Kemudian kita dapat menggunakannya untuk memprioritaskan pencarian sel.

Cari Heuristik


Saat kami menggunakan estimasi atau dugaan alih-alih data yang diketahui secara tepat, ini disebut menggunakan heuristik pencarian. Heuristik ini merupakan tebakan terbaik dari jarak yang tersisa. Kita harus menentukan nilai ini untuk setiap sel yang kita cari, jadi kita akan menambahkan properti integer HexCell untuknya. Kita tidak perlu membuat cerita bersambung, jadi properti standar lain sudah cukup.

  public int SearchHeuristic { get; set; } 

Bagaimana kita membuat asumsi tentang jarak yang tersisa? Dalam kasus paling ideal, kita akan memiliki jalan yang mengarah langsung ke titik akhir. Jika demikian, maka jaraknya sama dengan jarak yang tidak berubah antara koordinat sel ini dan sel akhir. Mari kita manfaatkan hal ini dalam heuristik kita.

Karena heuristik tidak bergantung pada jalur yang telah dilalui sebelumnya, maka ia konstan dalam proses pencarian. Oleh karena itu, kita perlu menghitungnya hanya sekali ketika HexGrid.Search menambahkan sel ke perbatasan.

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

Prioritas Pencarian


Mulai sekarang, kami akan menentukan prioritas pencarian berdasarkan jarak ke sel plus heuristiknya. Mari menambahkan properti untuk nilai ini di HexCell .

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

Agar ini berfungsi, HexGrid.Search sehingga ia menggunakan properti ini untuk mengurutkan perbatasan.

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



Cari tanpa heuristik dan dengan heuristik

Heuristik yang valid


Berkat prioritas pencarian baru, kami sebenarnya akan mengunjungi lebih sedikit sel sebagai hasilnya. Namun, pada peta yang seragam, algoritme masih memproses sel yang berada di arah yang salah. Ini karena, secara default, biaya untuk setiap langkah adalah 5, dan heuristik per langkah hanya menambahkan 1. Artinya, pengaruh heuristik tidak terlalu kuat.

Jika biaya pemindahan semua kartu sama, maka kita dapat menggunakan biaya yang sama saat menentukan heuristik. Dalam kasus kami, ini akan menjadi heuristik saat ini dikalikan dengan 5. Ini akan secara signifikan mengurangi jumlah sel yang diproses.


Menggunakan heuristik × 5

Namun, jika ada jalan di peta, maka kita bisa melebih-lebihkan jarak yang tersisa. Alhasil, algoritma bisa membuat kesalahan dan membuat jalur yang sebenarnya bukan yang terpendek.



Heuristik berlebihan dan valid

Untuk memastikan bahwa jalur terpendek ditemukan, kita perlu memastikan bahwa kita tidak pernah melebih-lebihkan jarak yang tersisa. Pendekatan ini disebut heuristik yang valid. Karena biaya minimum untuk bergerak adalah 1, kami tidak punya pilihan selain menggunakan biaya yang sama dalam menentukan heuristik.

Sebenarnya, sangat normal untuk menggunakan biaya yang lebih rendah, tetapi ini hanya akan membuat heuristik lebih lemah. Heuristik minimum yang mungkin adalah nol, yang hanya memberi kita algoritma Dijkstra. Dengan heuristik yang bukan nol, algoritma ini disebut A * (dilafalkan "A star").

Mengapa disebut A *?
Gagasan menambahkan heuristik ke algoritma Dijkstra pertama kali diusulkan oleh Niels Nilsson. Dia menamai versinya A1. Bertram Rafael kemudian datang dengan versi terbaik yang ia sebut A2. Kemudian Peter Hart membuktikan bahwa dengan heuristik A2 yang baik adalah optimal, artinya, tidak ada versi yang lebih baik. Ini memaksanya untuk memanggil algoritma A * untuk menunjukkan bahwa itu tidak dapat diperbaiki, yaitu, A3 atau A4 tidak akan muncul. Jadi ya, algoritma A * adalah yang terbaik yang bisa kita dapatkan, tetapi sama bagusnya dengan heuristiknya.

paket unity

Antrian prioritas


Meskipun A * adalah algoritma yang baik, implementasi kami tidak begitu efektif, karena kami menggunakan daftar untuk menyimpan perbatasan, yang perlu disortir di setiap iterasi. Seperti disebutkan di bagian sebelumnya, kita perlu antrian prioritas, tetapi penerapan standarnya tidak ada. Karena itu, mari kita buat sendiri.

Giliran kita harus mendukung operasi pengaturan dan pengecualian dari antrian berdasarkan prioritas. Ini juga harus mendukung perubahan prioritas sel yang sudah ada dalam antrian. Idealnya, kami menerapkannya, meminimalkan pencarian untuk menyortir dan mengalokasikan memori. Selain itu, harus tetap sederhana.

Buat antrian Anda sendiri


Buat kelas HexCellPriorityQueue baru dengan metode umum yang diperlukan. Kami menggunakan daftar sederhana untuk melacak konten antrian. Selain itu, kami akan menambahkan metode Clear untuk menghapus antrian sehingga dapat digunakan berulang kali.

 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(); } } 

Kami menyimpan prioritas sel di dalam sel itu sendiri. Artinya, sebelum menambahkan sel ke antrian, prioritasnya harus ditetapkan. Tetapi jika terjadi perubahan prioritas, mungkin akan berguna untuk mengetahui apa prioritas lama itu. Jadi mari kita tambahkan ini ke Change sebagai parameter.

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

Ini juga berguna untuk mengetahui berapa banyak sel dalam antrian, jadi mari kita tambahkan properti Count untuk ini. Cukup gunakan bidang yang kami akan melakukan kenaikan dan penurunan yang sesuai.

  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; } 

Tambahkan ke Antrean


Ketika sel ditambahkan ke antrian, pertama mari kita gunakan prioritasnya sebagai indeks, memperlakukan daftar sebagai array sederhana.

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

Namun, ini hanya berfungsi jika daftar tersebut cukup panjang, jika tidak kita akan melampaui batas. Anda dapat menghindari ini dengan menambahkan item kosong ke daftar hingga mencapai panjang yang diperlukan. Elemen kosong ini tidak mereferensikan sel, jadi Anda bisa membuatnya dengan menambahkan null ke daftar.

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


Daftar dengan lubang

Tetapi ini adalah bagaimana kami menyimpan hanya satu sel per prioritas, dan kemungkinan besar akan ada beberapa. Untuk melacak semua sel dengan prioritas yang sama, kita perlu menggunakan daftar lain. Meskipun kami dapat menggunakan daftar nyata untuk setiap prioritas, kami juga dapat menambahkan properti ke HexCell untuk mengikat mereka bersama. Ini memungkinkan kami membuat rantai sel yang disebut daftar tertaut.

  public HexCell NextWithSamePriority { get; set; } 

Untuk membuat rantai, biarkan HexCellPriorityQueue.Enqueue memaksa sel yang baru ditambahkan untuk merujuk ke nilai saat ini dengan prioritas yang sama, sebelum menghapusnya.

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


Daftar daftar tertaut

Hapus dari antrian


Untuk mendapatkan sel dari antrian prioritas, kita perlu mengakses daftar tertaut di indeks non-kosong terendah. Oleh karena itu, kita akan berkeliling daftar dalam satu lingkaran sampai kita menemukannya. Jika kami tidak menemukan, maka antriannya kosong dan kami mengembalikan null .

Dari rantai yang ditemukan, kita dapat mengembalikan sel apa pun, karena semuanya memiliki prioritas yang sama. Cara termudah adalah mengembalikan sel dari awal rantai.

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

Untuk menjaga tautan ke rantai yang tersisa, gunakan sel berikutnya dengan prioritas yang sama dengan awal yang baru. Jika hanya ada satu sel pada tingkat prioritas ini, maka elemen menjadi null dan akan dilewati di masa depan.

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

Pelacakan minimum


Pendekatan ini berfungsi, tetapi beralih melalui daftar setiap kali sel diterima. Kita tidak dapat menghindari menemukan indeks non -tyty terkecil, tetapi kita tidak diharuskan mulai dari awal setiap waktu. Sebagai gantinya, kami dapat melacak prioritas minimum dan memulai pencarian dengannya. Awalnya, minimum pada dasarnya sama dengan tak terbatas.

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

Saat menambahkan sel ke antrian, kami mengubah minimum yang diperlukan.

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

Dan ketika menarik diri dari antrian, kami menggunakan setidaknya daftar untuk iterasi, dan jangan mulai dari awal.

  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; } 

Ini secara signifikan mengurangi jumlah waktu yang diperlukan untuk memotong dalam loop daftar prioritas.

Ubah Prioritas


Saat mengubah prioritas sel, itu harus dihapus dari daftar tertaut yang merupakan bagiannya. Untuk melakukan ini, kita perlu mengikuti rantai sampai kita menemukannya.

Mari kita mulai dengan menyatakan bahwa kepala daftar prioritas lama akan menjadi sel saat ini, dan kami juga akan melacak sel berikutnya. Kita dapat segera mengambil sel berikutnya, karena kita tahu bahwa setidaknya ada satu sel dengan indeks ini.

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

Jika sel saat ini adalah sel yang diubah, maka ini adalah sel kepala dan kita dapat memotongnya seolah-olah kita telah mengeluarkannya dari antrian.

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

Jika ini tidak terjadi, maka kita perlu mengikuti rantai sampai kita berada di sel di depan sel yang berubah. Ini berisi tautan ke sel yang telah dimodifikasi.

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

Pada titik ini, kita dapat menghapus sel yang diubah dari daftar tertaut, melompati sel.

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

Setelah menghapus sel, Anda perlu menambahkannya lagi sehingga muncul dalam daftar prioritas barunya.

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

Metode Enqueue menambah penghitung, tetapi dalam kenyataannya kami tidak menambahkan sel baru. Karena itu, untuk mengkompensasi ini, kita harus mengurangi penghitung.

  Enqueue(cell); count -= 1; 

Penggunaan antrian


Sekarang kita dapat memanfaatkan antrian prioritas kami di HexGrid . Ini dapat dilakukan dengan satu instance, dapat digunakan kembali untuk semua operasi pencarian.

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

Sebelum memulai loop, metode Searchterlebih dahulu harus ditambahkan ke antrian fromCell, dan setiap iterasi dimulai dengan output sel dari antrian. Ini akan menggantikan kode perbatasan yang lama.

  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); … } 

Ubah kode sehingga menambah dan mengubah tetangga. Sebelum perubahan kita akan mengingat prioritas lama.

  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); } 

Selain itu, kita tidak perlu lagi menyortir perbatasan.

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


Pencarian menggunakan antrian prioritas

Seperti yang disebutkan sebelumnya, jalur terpendek yang ditemukan tergantung pada urutan pemrosesan sel. Giliran kita menciptakan urutan yang berbeda dari urutan daftar yang diurutkan, sehingga kita bisa mendapatkan cara lain. Karena kami menambah dan menghapus dari kepala daftar yang ditautkan untuk setiap prioritas, mereka lebih seperti tumpukan daripada antrian. Sel yang ditambahkan terakhir diproses terlebih dahulu. Efek samping dari pendekatan ini adalah bahwa algoritma tersebut rawan zigzag. Oleh karena itu, kemungkinan jalur zigzag juga meningkat. Untungnya, jalur seperti itu biasanya terlihat lebih baik, sehingga efek samping ini menguntungkan kita.



Daftar dan antrian diurutkan dengan prioritas

unitypackage

Bagian 17: gerakan terbatas


  • Kami menemukan cara untuk gerakan langkah demi langkah.
  • Segera tampilkan jalurnya.
  • Kami membuat pencarian yang lebih efektif.
  • Kami memvisualisasikan hanya jalur.

Di bagian ini, kami akan membagi gerakan menjadi gerakan dan mempercepat pencarian sebanyak mungkin.


Perjalanan dari beberapa gerakan

Gerakan langkah demi langkah


Game strategi yang menggunakan jaring segi enam hampir selalu berbasis giliran. Unit yang bergerak di peta memiliki kecepatan terbatas, yang membatasi jarak yang ditempuh dalam satu putaran.

Kecepatan


Untuk memberikan dukungan untuk gerakan terbatas, kami menambahkan HexGrid.FindPathdan ke dalam HexGrid.Searchparameter integer speed. Ini menentukan rentang gerakan untuk satu gerakan.

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

Berbagai jenis unit dalam permainan menggunakan kecepatan yang berbeda. Kavaleri cepat, infanteri lambat, dan sebagainya. Kami belum memiliki unit, jadi untuk saat ini kami akan menggunakan kecepatan konstan. Mari kita ambil nilai 24. Ini adalah nilai yang cukup besar, tidak habis dibagi 5 (biaya default untuk pindah). Menambahkan argumen untuk FindPathdi HexMapEditor.HandleInputkecepatan konstan.

  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); } 

Bergerak


Selain melacak total biaya bergerak di sepanjang jalan, kita sekarang juga perlu tahu berapa banyak gerakan yang diperlukan untuk bergerak di sepanjang jalan. Tetapi kita tidak perlu menyimpan informasi ini di setiap sel. Itu bisa diperoleh dengan membagi jarak yang ditempuh dengan kecepatan. Karena ini adalah bilangan bulat, kami akan menggunakan divisi bilangan bulat. Artinya, total jarak tidak lebih dari 24 sesuai dengan jalur 0. Ini berarti bahwa keseluruhan jalur dapat diselesaikan dalam jalur saat ini. Jika titik akhir berada pada jarak 30, maka ini harus belok 1. Untuk sampai ke titik akhir, unit harus menghabiskan semua gerakannya di belokan saat ini dan di bagian dari belokan berikutnya.

Mari kita tentukan jalannya sel saat ini dan semua tetangganya di dalamHexGrid.Search. Perjalanan sel saat ini dapat dihitung hanya sekali, tepat sebelum berkeliling dalam siklus tetangga. Langkah tetangga dapat ditentukan segera setelah kami menemukan jarak dengannya.

  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; … } 

Gerakan yang hilang


Jika langkah tetangga lebih besar dari langkah saat ini, maka kami melewati batas langkah tersebut. Jika gerakan yang diperlukan untuk mencapai tetangga adalah 1, maka semuanya baik-baik saja. Tetapi jika pindah ke sel berikutnya lebih mahal, maka semuanya menjadi lebih rumit.

Misalkan kita bergerak di sepanjang peta homogen, yaitu, untuk masuk ke setiap sel Anda membutuhkan 5 unit gerakan. Kecepatan kami adalah 24. Setelah empat langkah, kami menghabiskan 20 unit dari stok pergerakan kami, dan ada 4 yang tersisa. Pada langkah berikutnya, 5 unit dibutuhkan lagi, yaitu, satu lebih banyak dari yang tersedia. Apa yang perlu kita lakukan pada tahap ini?

Ada dua pendekatan untuk situasi ini. Yang pertama adalah memungkinkan unit untuk memasuki sel kelima pada giliran saat ini, bahkan jika kita tidak memiliki cukup gerakan. Yang kedua adalah untuk melarang gerakan selama gerakan saat ini, yaitu, poin gerakan yang tersisa tidak dapat digunakan dan mereka akan hilang.

Pilihan opsi tergantung pada gim. Secara umum, pendekatan pertama lebih tepat untuk game di mana unit hanya dapat bergerak beberapa langkah per giliran, misalnya, untuk game dalam seri Civilization. Ini memastikan bahwa unit selalu dapat bergerak setidaknya satu sel per putaran. Jika unit dapat memindahkan banyak sel per giliran, seperti di Age of Wonders atau di Battle for Wesnoth, maka opsi kedua lebih baik.

Karena kita menggunakan kecepatan 24, mari kita pilih pendekatan kedua. Agar mulai bekerja, kita perlu mengisolasi biaya masuk ke sel berikutnya sebelum menambahkannya ke jarak saat ini.

 // 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; 

Jika akibatnya kita melewati batas gerakan, maka pertama-tama kita menggunakan semua titik gerakan dari gerakan saat ini. Kita bisa melakukan ini hanya dengan mengalikan langkah dengan kecepatan. Setelah itu, kami menambahkan biaya pemindahan.

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

Sebagai hasil dari ini, kami akan menyelesaikan langkah pertama di sel keempat dengan 4 titik gerakan yang tidak digunakan. Titik-titik yang hilang ini ditambahkan ke biaya sel kelima, sehingga jaraknya menjadi 29, bukan 25. Akibatnya, jaraknya lebih besar dari sebelumnya. Sebagai contoh, sel kesepuluh memiliki jarak 50. Tapi sekarang untuk masuk ke dalamnya, kita perlu melintasi batas dua gerakan, kehilangan 8 titik gerakan, yaitu jarak ke sekarang menjadi 58.


Lebih lama dari yang diharapkan

Karena titik pergerakan yang tidak digunakan ditambahkan ke jarak ke sel, mereka diperhitungkan saat menentukan jalur terpendek. Cara paling efektif adalah membuang poin sesedikit mungkin. Karenanya, pada kecepatan yang berbeda, kita bisa mendapatkan jalur yang berbeda.

Menampilkan gerakan alih-alih jarak


Saat kami memainkan game, kami tidak terlalu tertarik dengan nilai jarak yang digunakan untuk menemukan jalur terpendek. Kami tertarik pada jumlah gerakan yang diperlukan untuk mencapai titik akhir. Karena itu, alih-alih jarak, mari kita tampilkan gerakan.

Pertama, singkirkan UpdateDistanceLabelpanggilannya 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(); // } 

Sebagai gantinya, kami akan menambah HexCellmetode umum SetLabelyang menerima string arbitrer.

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

Kami menggunakan metode baru ini dalam HexGrid.Searchmembersihkan sel. Untuk menyembunyikan sel, cukup tetapkan null.

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

Lalu kami memberikan nilai tetangganya kepada tetangganya. Setelah itu, kita akan dapat melihat berapa banyak gerakan tambahan yang diperlukan untuk melakukan semuanya.

  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); } 


Jumlah gerakan yang diperlukan untuk bergerak di sepanjang jalur paket

unity

Jalur instan


Selain itu, saat kami bermain game, kami tidak peduli bagaimana algoritma pencarian jalur menemukan jalannya. Kami ingin segera melihat jalur yang diminta. Saat ini, kami dapat memastikan bahwa algoritme berfungsi, jadi mari kita singkirkan visualisasi pencarian.

Tanpa corutin


Untuk bagian yang lambat melalui algoritma, kami menggunakan corutin. Kami tidak perlu melakukan ini lagi, jadi kami akan menyingkirkan panggilan StartCoroutinedan StopAllCoroutinesc HexGrid. Sebagai gantinya, kami hanya memintanya Searchsebagai metode biasa.

  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); } 

Karena kita tidak lagi menggunakannya Searchsebagai coroutine, maka tidak perlu hasil, jadi kita akan menyingkirkan operator ini. Ini berarti bahwa kami juga akan menghapus deklarasi WaitForSecondsdan mengubah tipe pengembalian metode menjadi 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(); … } } 


Hasil instan

Definisi waktu pencarian


Sekarang kita bisa mendapatkan jalur secara instan, tetapi seberapa cepat mereka dihitung? Jalur pendek muncul hampir dengan segera, tetapi jalur panjang di peta besar mungkin tampak agak lambat.

Mari kita mengukur berapa banyak waktu yang diperlukan untuk menemukan dan menampilkan jalan. Kita dapat menggunakan profiler untuk menentukan waktu pencarian, tetapi ini sedikit terlalu banyak dan menciptakan biaya tambahan. Mari kita gunakan Stopwatch, yang ada di namespace System.Diagnostics. Karena kami hanya menggunakannya untuk sementara, saya tidak akan menambahkan konstruk usingke awal skrip.

Tepat sebelum pencarian, buat stopwatch baru dan mulai saja. Setelah pencarian selesai, hentikan stopwatch dan tampilkan waktu yang berlalu di konsol.

  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); } 

Mari kita pilih kasus terburuk untuk algoritma kami - pencarian dari kiri bawah ke sudut kanan atas peta besar. Yang terburuk adalah peta yang seragam, karena algoritme harus memproses semua 4.800 sel peta.


Cari dalam kasus terburuk Waktu yang

dihabiskan untuk mencari bisa berbeda, karena editor Unity bukan satu-satunya proses yang berjalan pada mesin Anda. Jadi cobalah beberapa kali untuk mendapatkan pemahaman tentang durasi rata-rata. Dalam kasus saya, pencarian membutuhkan waktu sekitar 45 milidetik. Ini tidak terlalu banyak dan sesuai dengan 22,22 jalur per detik; menyatakan ini sebagai 22 pps (jalur per detik). Ini berarti bahwa frame rate dari permainan juga akan berkurang hingga maksimum 22 fps dalam frame itu ketika jalur ini dihitung. Dan ini tanpa memperhitungkan semua pekerjaan lain, misalnya, merender bingkai itu sendiri. Artinya, kami mendapatkan penurunan frame rate yang cukup besar, itu akan turun menjadi 20 fps.

Saat melakukan tes kinerja seperti itu, Anda perlu mempertimbangkan bahwa kinerja editor Unity tidak akan setinggi kinerja aplikasi yang sudah selesai. Jika saya melakukan tes yang sama dengan perakitan, maka rata-rata hanya butuh 15 ms. Itu adalah 66 pps, yang jauh lebih baik. Namun demikian, ini masih merupakan bagian besar dari sumber daya yang dialokasikan per frame, sehingga frame rate akan menjadi lebih rendah dari 60 fps.

Di mana saya dapat melihat log debug untuk perakitan?
Unity , . . , , Unity Log Files .

Cari hanya jika perlu.


Kami dapat membuat optimasi sederhana - melakukan pencarian hanya saat diperlukan. Sementara kami memulai pencarian baru di setiap frame di mana tombol mouse ditekan. Oleh karena itu, frame rate akan terus-menerus diremehkan saat diseret dan dijatuhkan. Kita dapat menghindari ini dengan memulai pencarian baru HexMapEditor.HandleInputhanya ketika kita benar-benar berurusan dengan titik akhir yang baru. Jika tidak, maka jalur yang terlihat saat ini masih valid.

  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); } } 

Tampilkan label hanya untuk jalur


Menampilkan tanda perjalanan adalah operasi yang agak mahal, terutama karena kami menggunakan pendekatan yang tidak dioptimalkan. Melakukan operasi ini untuk semua sel pasti akan memperlambat eksekusi. Jadi mari kita lewati label 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); } 

Kita perlu melihat informasi ini hanya untuk jalur yang ditemukan. Oleh karena itu, setelah mencapai titik akhir, kami akan menghitung kursus dan mengatur label hanya sel-sel yang ada di jalan.

  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; } 


Menampilkan label untuk sel path saja

Sekarang kami hanya menyertakan label sel antara awal dan akhir. Tetapi titik akhirnya adalah hal yang paling penting, kita juga harus menetapkan label untuk itu. Anda bisa melakukan ini dengan memulai siklus jalur dari sel tujuan, dan bukan dari sel di depannya. Dalam kasus ini, iluminasi titik akhir dari merah akan berubah menjadi putih, jadi kami akan menghapus cahaya latar di bawah siklus.

  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; } … } 


Informasi kemajuan paling penting untuk titik akhir.Setelah

perubahan ini, waktu kasus terburuk dikurangi menjadi 23 milidetik dalam editor dan 6 milidetik dalam perakitan selesai. Ini adalah 43 pps dan 166 pps - jauh lebih baik.

paket unity

Pencarian paling cerdas


Pada bagian sebelumnya, kami membuat prosedur pencarian lebih pintar dengan menerapkan algoritma A * . Namun, pada kenyataannya kami masih belum melakukan pencarian dengan cara yang paling optimal. Dalam setiap iterasi, kami menghitung jarak dari sel saat ini ke semua tetangganya. Ini berlaku untuk sel yang belum atau saat ini menjadi bagian dari batas pencarian. Tetapi sel-sel yang telah dihilangkan dari perbatasan, tidak perlu lagi dipertimbangkan, karena kami telah menemukan jalur terpendek ke sel-sel ini. Implementasi yang benar dari A * melewatkan sel-sel ini, sehingga kita dapat melakukan hal yang sama.

Fase Pencarian Sel


Bagaimana kita tahu jika sel telah meninggalkan perbatasan? Meskipun kita tidak bisa menentukan ini. Karena itu, Anda perlu melacak pada fase apa pencarian sel itu. Dia belum berada di perbatasan, atau di dalamnya sekarang, atau di luar negeri. Kami dapat melacak ini dengan menambahkan ke HexCellproperti integer sederhana.

  public int SearchPhase { get; set; } 

Misalnya, 0 berarti bahwa sel belum mencapai, 1 - bahwa sel berada di perbatasan sekarang, dan 2 - bahwa ia telah dihapus dari perbatasan.

Memukul perbatasan


Di HexGrid.Searchkita dapat mengatur ulang semua sel ke 0 dan selalu menggunakan 1 untuk perbatasan. Atau kami dapat menambah jumlah perbatasan dengan setiap pencarian baru. Berkat ini, kita tidak harus berurusan dengan pembuangan sel jika kita setiap kali menambah jumlah perbatasan dua.

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

Sekarang kita perlu mengatur fase pencarian sel saat menambahkannya ke perbatasan. Proses dimulai dengan sel awal, yang ditambahkan ke perbatasan.

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

Dan juga setiap kali kita menambahkan tetangga ke perbatasan.

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

Pemeriksaan perbatasan


Sampai sekarang, untuk memverifikasi bahwa sel belum ditambahkan ke perbatasan, kami menggunakan jarak yang sama dengan int.MaxValue. Sekarang kita dapat membandingkan fase pencarian sel dengan perbatasan saat ini.

 // 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); } 

Ini berarti bahwa kita tidak perlu lagi mengatur ulang jarak sel sebelum mencari, artinya, kita harus melakukan lebih sedikit pekerjaan, yang bagus.

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

Meninggalkan perbatasan


Ketika sel dihapus dari batas, kami menyatakan ini dengan peningkatan fase pencariannya. Ini menempatkannya di luar perbatasan saat ini dan sebelum yang berikutnya.

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

Sekarang kita dapat melewati sel yang dihapus dari perbatasan, menghindari perhitungan yang tidak berguna dan perbandingan jarak.

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

Pada titik ini, algoritma kami masih menghasilkan hasil yang sama, tetapi lebih efisien. Di mesin saya, pencarian terburuk membutuhkan 20 ms di editor dan 5 ms di majelis.

Kami juga dapat menghitung berapa kali sel diproses oleh algoritma, meningkatkan penghitung saat menghitung jarak ke sel. Sebelumnya, algoritma kami dalam kasus terburuk menghitung 28.239 jarak. Dalam algoritma A * siap pakai, kami menghitung 14.120 jaraknya. Jumlahnya berkurang 50%. Tingkat dampak indikator-indikator ini terhadap produktivitas tergantung pada biaya dalam menghitung biaya pemindahan. Dalam kasus kami, tidak ada banyak pekerjaan di sini, jadi peningkatan dalam perakitan tidak terlalu besar, tetapi sangat terlihat di editor.

paket unity

Membersihkan jalan


Saat memulai pencarian baru, pertama-tama kita perlu menghapus visualisasi dari jalur sebelumnya. Saat kami melakukan ini, matikan pilihan dan hapus label dari setiap sel kisi. Ini adalah pendekatan yang sangat sulit. Idealnya, kita hanya perlu membuang sel-sel yang merupakan bagian dari jalur sebelumnya.

Cari Saja


Mari kita mulai dengan menghapus kode visualisasi sepenuhnya Search. Dia hanya perlu melakukan pencarian jalur dan tidak harus tahu apa yang akan kita lakukan dengan informasi ini.

  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; } … } } 

Untuk melaporkan bahwa Searchkami telah menemukan cara, kami akan mengembalikan boolean.

  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; } 

Ingat jalannya


Ketika jalan ditemukan, kita perlu mengingatnya. Berkat ini, kita akan dapat membersihkannya di masa depan. Oleh karena itu, kami akan melacak titik akhir dan apakah ada jalur di antara mereka.

  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); } 

Tampilkan jalan lagi


Kita dapat menggunakan data pencarian yang kita rekam untuk memvisualisasikan jalan lagi. Mari kita buat metode baru untuk ini ShowPath. Ini akan melalui siklus dari ujung ke awal jalan, menyorot sel dan menetapkan nilai stroke ke label mereka. Untuk melakukan ini, kita perlu mengetahui kecepatannya, jadi buatlah parameter. Jika kami tidak memiliki jalur, maka metode hanya memilih titik akhir.

  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); } 

Panggil metode ini FindPathsetelah pencarian.

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

Sapu


Kami melihat jalan lagi, tetapi sekarang tidak bergerak menjauh. Untuk menghapusnya, buat metode ClearPath. Bahkan, itu adalah salinan ShowPath, kecuali bahwa itu menonaktifkan pemilihan dan label, tetapi tidak termasuk mereka. Setelah melakukan ini, ia harus menghapus data jalur yang direkam yang tidak lagi valid.

  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; } 

Dengan menggunakan metode ini, kita dapat menghapus visualisasi jalur lama dengan hanya mengunjungi sel yang diperlukan, ukuran peta tidak lagi penting. Kami akan memanggilnya FindPathsebelum memulai pencarian baru.

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

Selain itu, kami akan menghapus jalur saat membuat peta baru.

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

Dan juga sebelum memuat kartu lain.

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

Visualisasi jalur dihapus lagi, seperti sebelum perubahan ini. Tetapi sekarang kami menggunakan pendekatan yang lebih efisien, dan dalam kasus pencarian terburuk, waktu telah berkurang menjadi 14 milidetik. Perbaikan yang cukup serius hanya karena pembersihan yang lebih cerdas. Waktu perakitan menurun hingga 3 ms, yaitu 333 pps. Berkat ini, pencarian jalur tepat berlaku di waktu nyata.

Sekarang kami telah melakukan pencarian cepat untuk jalur, kami dapat menghapus kode debug sementara.

  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); } 

paket unity

Bagian 18: unit


  • Kami menempatkan regu di peta.
  • Simpan dan muat regu.
  • Kami menemukan cara untuk pasukan.
  • Kami memindahkan unit.

Sekarang kami telah menemukan cara untuk mencari jalur, mari kita tempatkan pasukan di peta.


Bala bantuan tiba

Membuat regu


Sejauh ini kita hanya berurusan dengan sel dan objek tetapnya. Unit berbeda dari mereka dalam hal mereka mobile. Pasukan dapat berarti apa saja dari skala apa pun, dari satu orang atau kendaraan hingga seluruh pasukan. Dalam tutorial ini, kami membatasi diri untuk jenis unit sederhana sederhana. Setelah itu, kami akan beralih ke kombinasi pendukung beberapa jenis unit.

Pasukan Prefab


Untuk bekerja dengan regu, buat tipe komponen baru HexUnit. Untuk sekarang, mari kita mulai dengan yang kosong MonoBehaviour, dan kemudian menambahkan fungsionalitasnya.

 using UnityEngine; public class HexUnit : MonoBehaviour { } 

Buat objek game kosong dengan komponen ini, yang seharusnya menjadi cetakan. Ini akan menjadi objek root dari skuad.


Pasukan Prefab.

Tambahkan model 3D yang melambangkan detasemen sebagai objek anak. Saya menggunakan kubus skala sederhana yang saya buat bahan biru. Objek root menentukan tingkat dasar detasemen, oleh karena itu, kami memindahkan elemen anak.



Elemen kubus anak

Tambahkan collider ke skuad sehingga lebih mudah untuk memilih di masa depan. Penumbuk kubus standar cukup cocok untuk kita, cukup buat penumbuk pas dalam satu sel.

Membuat instance regu


Karena kami belum memiliki gameplay, pembuatan unit dilakukan dalam mode pengeditan. Karena itu, ini harus diatasi HexMapEditor. Untuk melakukan ini, ia memerlukan cetakan, jadi tambahkan bidang HexUnit unitPrefabdan hubungkan.

  public HexUnit unitPrefab; 


Menghubungkan prefab

Saat membuat unit, kami akan menempatkannya di sel di bawah kursor. Ada HandleInputkode untuk menemukan sel ini saat mengedit medan. Sekarang kita juga membutuhkannya untuk regu, jadi kita akan memindahkan kode yang sesuai ke metode yang terpisah.

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

Sekarang kita dapat menggunakan metode ini dalam HandleInputmenyederhanakannya.

  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; } } 

Selanjutnya, tambahkan metode baru CreateUnityang juga menggunakan GetCellUnderCursor. Jika ada sel, kami akan membuat skuad baru.

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

Untuk menjaga hierarki tetap bersih, mari gunakan kisi sebagai induk untuk semua objek gim di regu.

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

Cara termudah untuk menambahkan HexMapEditordukungan untuk membuat unit adalah dengan menekan tombol. Ubah metode Updatesehingga panggilan CreateUnitketika Anda menekan tombol U. Seperti halnya c HandleInput, ini harus terjadi jika kursor tidak di atas elemen GUI. Pertama, kami akan memeriksa apakah kami harus mengedit peta, dan jika tidak, kami akan memeriksa apakah kami harus menambahkan skuad. Jika demikian, teleponlah 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; } 


Dibuat contoh skuad

Penempatan Pasukan


Sekarang kita dapat membuat unit, tetapi mereka muncul di awal peta. Kita harus menempatkan mereka di tempat yang tepat. Untuk ini, perlu bahwa pasukan menyadari posisi mereka. Oleh karena itu, kami menambah HexUnitproperti yang Locationmenunjukkan sel yang mereka tempati. Saat mengatur properti, kami akan mengubah posisi skuad sehingga cocok dengan posisi sel.

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

Sekarang saya HexMapEditor.CreateUnitharus menetapkan posisi sel skuadron di bawah kursor. Maka unit akan berada di tempat yang seharusnya.

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


Pasukan di peta

Orientasi Unit


Sejauh ini, semua unit memiliki orientasi yang sama, yang terlihat agak tidak wajar. Untuk menghidupkannya kembali, tambahkan ke HexUnitproperti Orientation. Ini adalah nilai float yang menunjukkan rotasi skuad sepanjang sumbu Y dalam derajat. Saat mengaturnya, kami akan mengubah rotasi objek game itu sendiri.

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

Dalam HexMapEditor.CreateUnitmenetapkan rotasi acak dari 0 hingga 360 derajat.

  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); } } 


Orientasi unit yang berbeda

Satu regu per sel


Unit terlihat bagus jika tidak dibuat dalam satu sel. Dalam hal ini, kami mendapatkan sekelompok kubus yang terlihat aneh.


Overlay unit

Beberapa game memungkinkan penempatan beberapa unit di satu tempat, yang lain tidak. Karena lebih mudah untuk bekerja dengan satu regu per sel, saya akan memilih opsi ini. Ini berarti bahwa kita harus membuat skuad baru hanya ketika sel saat ini tidak ditempati. Agar Anda bisa mengetahuinya, tambahkan ke HexCellproperti standar Unit.

  public HexUnit Unit { get; set; } 

Kami menggunakan properti ini HexUnit.Locationuntuk memberi tahu sel jika unit ada di dalamnya.

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

Sekarang HexMapEditor.CreateUnitdapat memeriksa apakah sel saat ini gratis.

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

Mengedit Sel Sibuk


Awalnya, unit ditempatkan dengan benar, tetapi semuanya dapat berubah jika selnya diedit nanti. Jika ketinggian sel berubah, maka unit yang menempatinya akan menggantung di atasnya atau masuk ke dalamnya.


Pasukan yang tergantung dan tenggelam

Solusinya adalah memeriksa posisi pasukan setelah melakukan perubahan. Untuk melakukan ini, tambahkan metode ke HexUnit. Sejauh ini, kami hanya tertarik dengan posisi pasukan, jadi tanyakan saja lagi.

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

Kita harus mengoordinasikan posisi detasemen ketika memperbarui sel, apa yang terjadi ketika metode Refreshatau RefreshSelfOnlyobjek HexCelldipanggil. Tentu saja, ini diperlukan hanya ketika benar-benar ada detasemen dalam sel.

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

Menghapus regu


Selain membuat unit, akan berguna untuk menghancurkannya. Karena itu, tambahkan ke HexMapEditormetode DestroyUnit. Dia harus memeriksa apakah ada detasemen dalam sel di bawah kursor, dan jika demikian, hancurkan objek permainan detasemen.

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

Harap dicatat, untuk sampai ke pasukan, kami melewati sel. Untuk berinteraksi dengan skuad, cukup gerakkan mouse di atas selnya. Oleh karena itu, agar ini berfungsi, skuad tidak harus memiliki collider. Namun, menambahkan collider membuatnya lebih mudah untuk dipilih karena menghalangi sinar yang akan bertabrakan dengan sel di belakang regu.

Mari kita Updategunakan kombinasi Shift + U kiri untuk menghancurkan skuad .

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

Dalam kasus ketika kita membuat dan menghancurkan beberapa unit, mari kita berhati-hati dan membersihkan properti saat melepas unit. Yaitu, kami secara eksplisit menghapus tautan sel ke skuad. Tambahkan ke HexUnitmetode Dieyang berhubungan dengan ini, serta penghancuran objek game Anda sendiri.

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

Kami akan memanggil metode ini HexMapEditor.DestroyUnit, dan tidak menghancurkan pasukan secara langsung.

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

paket unity

Menyimpan dan memuat regu


Sekarang kita dapat memiliki unit di peta, kita harus memasukkannya dalam proses penyimpanan dan pemuatan. Kami dapat mendekati tugas ini dengan dua cara. Yang pertama adalah untuk merekam data regu saat merekam sel sehingga sel dan data regu dicampur. Cara kedua adalah menyimpan data sel dan skuad secara terpisah. Meskipun tampaknya pendekatan pertama lebih mudah diimplementasikan, yang kedua memberi kita lebih banyak data terstruktur. Jika kami berbagi data, maka akan lebih mudah untuk bekerja dengan mereka di masa mendatang.

Pelacakan Unit


Untuk menyatukan semua unit, kita perlu melacaknya. Kami akan melakukan ini dengan menambahkan ke HexGriddaftar unit. Daftar ini harus berisi semua unit di peta.

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

Saat membuat atau memuat peta baru, kita harus menyingkirkan semua unit di peta. Untuk menyederhanakan proses ini, buat metode ClearUnitsyang membunuh semua orang dalam daftar dan membersihkannya.

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

Kami menyebut metode ini masuk CreateMapdan masuk Load. Mari kita lakukan setelah membersihkan jalan.

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

Menambahkan regu ke grid


Sekarang, saat membuat unit baru, kita perlu menambahkannya ke daftar. Mari kita menetapkan metode untuk ini AddUnit, yang juga akan berurusan dengan lokasi skuad dan parameter objek induknya.

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

Sekarang HexMapEditor.CreatUnitcukup menelepon AddUnitdengan instance baru detasemen, lokasi, dan orientasi acak.

  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) ); } } 

Menghapus regu dari grid


Tambahkan metode untuk menghapus regu dan c HexGrid. Hapus saja skuad dari daftar dan suruh mati.

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

Kami memanggil metode ini HexMapEditor.DestroyUnit, alih-alih menghancurkan pasukan secara langsung.

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

Menyimpan unit


Karena kita akan menyatukan semua unit, kita perlu mengingat sel mana yang mereka tempati. Cara yang paling dapat diandalkan adalah dengan menyimpan koordinat lokasi mereka. Untuk memungkinkan ini, kami menambahkan bidang X dan Z ke HexCoordinatesmetode Saveyang menulisnya.

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

Metode Saveuntuk HexUnitsekarang dapat merekam koordinat dan orientasi skuad. Ini semua data unit yang kita miliki saat ini.

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

Karena ia HexGridmelacak unit, metodenya Saveakan merekam data unit. Pertama, tulis jumlah total unit, dan kemudian lewati semuanya dalam satu lingkaran.

  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); } } 

Kami mengubah data yang disimpan, jadi kami akan menambah nomor versi SaveLoadMenu.Savemenjadi 2. Kode booting lama akan tetap berfungsi, karena itu hanya tidak akan membaca data skuad. Namun, Anda perlu menambah nomor versi untuk menunjukkan bahwa ada informasi unit dalam file.

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

Memuat regu


Karena ini HexCoordinatesadalah struktur, tidak masuk akal untuk menambahkan metode yang biasa Load. Mari kita menjadikannya metode statis yang membaca dan mengembalikan koordinat yang tersimpan.

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

Karena jumlah unit adalah variabel, kami tidak memiliki unit yang sudah ada sebelumnya di mana data dapat dimuat. Kita dapat membuat instance unit baru sebelum memuat datanya, tetapi ini mengharuskan kita HexGridmembuat instance unit baru pada saat boot. Jadi lebih baik meninggalkannya HexUnit. Kami juga menggunakan metode statis HexUnit.Load. Mari kita mulai dengan hanya membaca regu ini. Untuk membaca nilai float orientasi, kami menggunakan metode BinaryReader.ReadSingle.

Kenapa lajang?
float , . , double , . Unity .

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

Langkah selanjutnya adalah membuat instance dari skuad baru. Namun, untuk ini kita perlu tautan ke cetakan unit. Agar tidak menyulitkannya, mari kita tambahkan HexUnitmetode statis untuk ini .

  public static HexUnit unitPrefab; 

Untuk mengatur tautan ini, mari kita gunakan lagi HexGrid, seperti yang kita lakukan dengan tekstur noise. Ketika kita perlu mendukung banyak jenis unit, kita akan beralih ke solusi yang lebih baik.

  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; } } 


Kami melewati cetakan unit.

Setelah menghubungkan bidang, kami tidak lagi memerlukan tautan langsung ke HexMapEditor. Sebaliknya, dia bisa menggunakan HexUnit.unitPrefab.

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

Sekarang kita dapat membuat instance dari skuad baru di HexUnit.Load. Alih-alih mengembalikannya, kita dapat menggunakan koordinat dan orientasi yang dimuat untuk menambahkannya ke kisi. Untuk memungkinkan ini, tambahkan parameter 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 ); } 

Pada akhirnya, HexGrid.Loadkami menghitung jumlah unit dan menggunakannya untuk memuat semua unit yang tersimpan, menyerahkan diri sebagai argumen tambahan.

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

Tentu saja, ini hanya akan berfungsi untuk menyimpan file dengan versi tidak lebih rendah dari 2, dalam versi yang lebih muda tidak ada unit untuk memuat.

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

Sekarang kita dapat mengunggah file versi 2 dengan benar, sehingga SaveLoadMenu.Loadmenambah jumlah versi yang didukung menjadi 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); } } } 

paket unity

Gerakan Pasukan


Regu bersifat mobile, jadi kita harus bisa memindahkannya di sekitar peta. Kami sudah memiliki kode pencarian jalur, tetapi sejauh ini kami telah mengujinya hanya untuk tempat yang sewenang-wenang. Sekarang kita perlu menghapus UI uji lama dan membuat UI baru untuk manajemen pasukan.

Pembersihan Editor Peta


Memindahkan unit di sepanjang jalur adalah bagian dari gameplay, itu tidak berlaku untuk editor peta. Oleh karena itu, kami akan menyingkirkan HexMapEditorsemua kode yang terkait dengan menemukan jalan.

 // 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; } } 

Setelah menghapus kode ini, tidak lagi masuk akal untuk membiarkan editor aktif ketika kita tidak dalam mode edit. Oleh karena itu, alih-alih bidang pelacakan mode, kita cukup mengaktifkan atau menonaktifkan komponen HexMapEditor. Selain itu, editor sekarang tidak harus berurusan dengan label UI.

 // 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; } } 

Karena secara default kita tidak dalam mode pengeditan peta, di Sedarlah kita akan menonaktifkan editor.

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

Diperlukan raycast untuk mencari sel saat ini di bawah kursor ketika mengedit peta, dan untuk mengelola unit. Mungkin di masa depan itu akan berguna bagi kita untuk hal lain. Mari kita pindahkan logika raycasting dari HexGridke metode baru GetCelldengan parameter balok.

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

HexMapEditor.GetCellUniderCursor mungkin hanya memanggil metode ini dengan sinar kursor.

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

Game UI


Untuk mengontrol UI mode game, kami akan menggunakan komponen baru. Sementara dia hanya akan berurusan dengan pemilihan dan pergerakan unit. Buat tipe komponen baru untuk itu HexGameUI. Untuk melakukan pekerjaannya, tautan ke jaringan sudah cukup baginya.

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

Tambahkan komponen ini ke objek game baru di hierarki UI. Dia tidak harus memiliki objek sendiri, tetapi akan jelas bagi kita bahwa ada UI terpisah untuk permainan.



Game UI Object

Menambahkan HexGameUImetode SetEditMode, seperti pada HexMapEditor. Game UI harus dihidupkan ketika kita tidak dalam mode edit. Juga, label perlu dimasukkan di sini karena UI game bekerja dengan jalur.

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

Tambahkan metode UI game dengan daftar acara sakelar mode edit. Ini berarti bahwa ketika pemain mengubah mode, kedua metode dipanggil.


Beberapa metode acara.

Lacak sel saat ini


Bergantung pada situasinya, HexGameUIAnda perlu tahu sel mana yang saat ini di bawah kursor. Karena itu, kami menambahkan bidang ke dalamnya currentCell.

  HexCell currentCell; 

Buat metode UpdateCurrentCellyang menggunakan HexGrid.GetCellsinar kursor untuk memperbarui bidang ini.

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

Saat memperbarui sel saat ini, kami mungkin perlu mencari tahu apakah itu telah berubah. Paksa untuk UpdateCurrentCellmengembalikan informasi ini.

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

Pemilihan Unit


Sebelum memindahkan regu, regu harus dipilih dan dilacak. Karena itu, tambahkan bidang selectedUnit.

  HexUnit selectedUnit; 

Ketika kami mencoba membuat pilihan, kami perlu memulai dengan memperbarui sel saat ini. Jika sel saat ini maka unit yang menempati sel ini menjadi unit yang dipilih. Jika tidak ada unit dalam sel, maka tidak ada unit yang dipilih. Mari kita buat metode untuk ini DoSelection.

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

Kami menyadari pilihan unit dengan klik sederhana mouse. Oleh karena itu, kami menambahkan metode Updateyang membuat pilihan ketika tombol mouse diaktifkan. Tentu saja, kita perlu menjalankannya hanya ketika kursor tidak di atas elemen GUI.

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

Pada tahap ini, kami belajar cara memilih satu unit sekaligus dengan mengklik mouse. Ketika Anda mengklik sel kosong, pemilihan unit apa pun dihapus. Tetapi sementara kami tidak menerima konfirmasi visual apa pun tentang ini.

Pencarian pasukan


Ketika sebuah unit dipilih, kita dapat menggunakan lokasinya sebagai titik awal untuk menemukan jalur. Untuk mengaktifkan ini, kami tidak akan memerlukan klik tombol mouse lagi. Sebagai gantinya, kami akan secara otomatis menemukan dan menunjukkan jalur antara posisi regu dan sel saat ini. Kami akan selalu melakukan ini Update, kecuali ketika pilihan dibuat. Untuk melakukan ini, ketika kami memiliki detasemen, kami memanggil metode DoPathfinding.

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

DoPathfindingbaru saja memperbarui sel saat ini dan menelepon HexGrid.FindPathjika ada titik akhir. Kami kembali menggunakan kecepatan konstan 24.

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

Harap perhatikan bahwa kami tidak boleh menemukan jalur baru setiap kali kami memperbarui, tetapi hanya ketika sel saat ini berubah.

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


Menemukan jalur untuk sebuah regu

Sekarang kita melihat jalur yang muncul ketika Anda memindahkan kursor setelah memilih regu. Berkat ini, jelas unit mana yang dipilih. Namun, jalur tidak selalu dibersihkan dengan benar. Pertama, mari kita hapus jalur lama jika kursor berada di luar peta.

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

Tentu saja, ini mensyaratkan bahwa itu HexGrid.ClearPathadalah hal biasa, jadi kami membuat perubahan seperti itu.

  public void ClearPath () { … } 

Kedua, kita akan membersihkan jalan lama saat memilih detasemen.

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

Akhirnya, kami akan menghapus jalur saat mengubah mode pengeditan.

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

Cari hanya titik akhir yang valid


Kita tidak selalu dapat menemukan jalannya, karena kadang-kadang tidak mungkin mencapai sel terakhir. Ini normal. Tetapi kadang-kadang sel terakhir itu sendiri tidak dapat diterima. Sebagai contoh, kami memutuskan bahwa jalur tidak dapat memasukkan sel bawah air. Tetapi mungkin tergantung pada unit. Mari kita tambahkan HexUnitmetode yang memberi tahu kita apakah sebuah sel adalah titik akhir yang valid. Sel bawah laut tidak.

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

Selain itu, kami hanya mengizinkan satu unit untuk berdiri di dalam sel. Karenanya, sel terakhir tidak akan valid jika sedang sibuk.

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

Kami menggunakan metode ini HexGameUI.DoPathfindinguntuk mengabaikan titik akhir yang tidak valid.

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

Pindah ke titik akhir


Jika kami memiliki jalur yang valid, maka kami dapat memindahkan skuad ke titik akhir. HexGridtahu kapan ini bisa dilakukan. Kami membuatnya meneruskan informasi ini di properti baca-saja yang baru HasPath.

  public bool HasPath { get { return currentPathExists; } } 

Untuk memindahkan skuad, tambahkan ke HexGameUImetode DoMove. Metode ini akan dipanggil ketika perintah dikeluarkan dan jika unit dipilih. Karena itu, ia harus memeriksa apakah ada cara, dan jika demikian, ubah lokasi detasemen. Sementara kami segera memindahkan skuad ke titik akhir. Dalam salah satu tutorial berikut, kita akan membuat skuad benar-benar berjalan sepanjang jalan.

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

Mari kita gunakan tombol mouse 1 (klik kanan) untuk mengirimkan perintah. Kami akan memeriksa ini jika detasemen dipilih. Jika tombol tidak ditekan, maka kami mencari jalannya.

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

Sekarang kita bisa memindahkan unit! Tetapi kadang-kadang mereka menolak untuk menemukan jalan ke beberapa sel. Secara khusus, untuk sel-sel di mana detasemen dulu. Ini terjadi karena HexUnittidak memperbarui lokasi lama ketika mengatur yang baru. Untuk memperbaikinya, kami akan menghapus tautan ke skuad di lokasi lamanya.

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

Hindari regu


Menemukan cara sekarang berfungsi dengan benar, dan unit dapat berteleportasi di peta. Meskipun mereka tidak dapat pindah ke sel yang sudah memiliki pasukan, detasemen yang berdiri di jalan diabaikan.


Unit di jalan diabaikan.

Unit dari faksi yang sama biasanya dapat bergerak melalui satu sama lain, tetapi sejauh ini kami tidak memiliki faksi. Oleh karena itu, mari kita pertimbangkan semua unit sebagai terputus satu sama lain dan memblokir jalur. Ini dapat diimplementasikan dengan melewatkan sel yang sibuk di HexGrid.Search.

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


detasemen Hindari

unitypackage

Bagian 19: Animasi Bergerak


  • Kami memindahkan unit di antara sel.
  • Visualisasikan jalan yang dilalui.
  • Kami memindahkan pasukan di sepanjang kurva.
  • Kami memaksa pasukan untuk melihat ke arah pergerakan.

Di bagian ini, kami akan memaksa unit alih-alih teleportasi untuk bergerak di sepanjang trek.


Pasukan di jalan

Gerakan di sepanjang jalan


Di bagian sebelumnya, kami menambahkan unit dan kemampuan untuk memindahkannya. Meskipun kami menggunakan pencarian jalan untuk menentukan titik akhir yang valid, setelah memberikan perintah, pasukan hanya berpindah ke sel terakhir. Untuk benar-benar mengikuti jalur yang ditemukan, kita perlu melacak jalur ini dan membuat proses animasi yang memaksa skuad untuk bergerak dari sel ke sel. Karena melihat animasi, sulit untuk memperhatikan bagaimana pasukan bergerak, kami juga memvisualisasikan jalan yang ditempuh dengan bantuan gizmos. Tetapi sebelum kita melanjutkan, kita perlu memperbaiki kesalahan.

Kesalahan saat berbelok


Karena kelalaian, kami salah menghitung jalan di mana sel akan dijangkau. Sekarang kita menentukan arah dengan membagi jarak total dengan kecepatan pasukant = d / s , dan membuang sisanya. Kesalahan terjadi ketika untuk masuk ke sel Anda harus menghabiskan semua poin gerakan yang tersisa per gerakan. Misalnya, ketika setiap langkah berharga 1, dan kecepatannya 3, maka kita dapat memindahkan tiga sel per putaran. Namun, dengan perhitungan yang ada, kita hanya bisa mengambil dua langkah di langkah pertama, karena untuk langkah ketiga

t = d / s = 3 / 3 = 1 .


Jumlah biaya untuk bergerak dengan gerakan yang salah, kecepatan 3

Untuk perhitungan gerakan yang benar, kita perlu memindahkan batas satu langkah dari sel awal. Kita bisa melakukan ini dengan mengurangi jarak dengan 1 sebelum menghitung langkah, lalu langkah untuk langkah ketiga adalaht = 2 / 3 = 0


Langkah yang benar

Kita dapat melakukan ini dengan mengubah rumus perhitungan menjadit = ( d - 1 ) / s .Kami akan membuat perubahan ini menjadi 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; } 

Kami juga mengubah tanda gerakan.

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

Perhatikan bahwa dengan pendekatan ini, jalur sel awal adalah −1. Ini normal, karena kami tidak menampilkannya, dan algoritma pencarian tetap operasional.

Mendapatkan jalan


Bergerak di sepanjang jalan adalah tugas pasukan. Agar dia melakukan ini, dia perlu tahu jalannya. Kami memiliki informasi ini HexGrid, jadi mari kita tambahkan metode untuk mendapatkan jalur saat ini dalam bentuk daftar sel. Dia bisa mengambilnya dari kumpulan daftar dan kembali jika memang ada jalan.

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

Daftar ini diisi dengan mengikuti jalur tautan dari sel terakhir ke sel awal, seperti yang dilakukan saat memvisualisasikan jalur.

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

Dalam hal ini, kita membutuhkan keseluruhan path, yang meliputi sel awal.

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

Sekarang kita memiliki jalur dalam urutan terbalik. Kita dapat bekerja dengannya, tetapi itu tidak akan sangat intuitif. Mari kita balikkan daftarnya sehingga mulai dari awal hingga akhir.

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

Permintaan gerak


Sekarang kita dapat menambahkan HexUnitmetode, memerintahkannya untuk mengikuti jalan. Awalnya, kami membiarkannya melakukan teleportasi ke sel terakhir. Kami tidak akan segera mengembalikan daftar ke kolam renang, karena akan berguna bagi kami untuk sementara waktu.

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

Untuk meminta perpindahan, kami mengubahnya HexGameUI.DoMovesehingga memanggil metode baru dengan jalur saat ini, dan tidak hanya menetapkan lokasi unit.

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

Visualisasi jalur


Sebelum kita mulai menjiwai regu, mari kita periksa apakah jalurnya benar. Kami akan melakukan ini dengan memesan untuk HexUnitmengingat jalan yang harus dilalui, sehingga dapat divisualisasikan menggunakan gizmos.

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

Tambahkan metode OnDrawGizmosuntuk menunjukkan jalur terakhir untuk pergi (jika ada). Jika unit belum bergerak, jalan harus sama null. Tetapi karena serialisasi Unity selama pengeditan setelah kompilasi ulang dalam mode Play, itu juga bisa menjadi daftar kosong.

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

Cara termudah untuk menunjukkan jalan adalah dengan menggambar bola alat untuk setiap sel jalan. Bola dengan jari-jari 2 unit cocok untuk kita.

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

Karena kita akan menunjukkan jalur untuk detasemen, kita akan dapat secara bersamaan melihat semua jalur terakhirnya.


Gizmos menampilkan jalur terakhir yang ditempuh.Untuk

menunjukkan koneksi sel yang lebih baik, kami menggambar beberapa bola dalam satu lingkaran pada garis antara sel sebelumnya dan saat ini. Untuk melakukan ini, kita perlu memulai proses dari sel kedua. Bola dapat diatur menggunakan interpolasi linier dengan kenaikan 0,1 unit, sehingga kami mendapatkan sepuluh bola per segmen.

  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); } } 


Cara yang lebih jelas

Meluncur di sepanjang jalan


Anda dapat menggunakan metode yang sama untuk memindahkan unit. Mari kita buat coroutine untuk ini. Alih-alih menggambar alat, kami akan mengatur posisi skuad. Alih-alih menambah, kami akan menggunakan 0,1 delta waktu, dan kami akan melakukan hasil untuk setiap iterasi. Dalam hal ini, skuad akan bergerak dari satu sel ke sel berikutnya dalam satu detik.

 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; } } } … } 

Mari kita mulai coroutine di akhir metode Travel. Tapi pertama-tama, kita akan menghentikan semua coroutine yang ada. Jadi kami menjamin bahwa dua coroutine tidak akan mulai pada saat yang sama, jika tidak, ini akan menghasilkan hasil yang sangat aneh.

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

Memindahkan satu sel per detik cukup lambat. Pemain selama pertandingan tidak akan mau menunggu lama. Anda dapat menjadikan kecepatan gerakan regu sebagai opsi konfigurasi, tetapi untuk sekarang, mari gunakan konstanta. Saya memberinya nilai 4 sel per detik; itu cukup cepat, tetapi mari kita perhatikan apa yang terjadi.

  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; } } } 

Sama seperti kita dapat memvisualisasikan beberapa jalur secara bersamaan, kita dapat membuat beberapa unit bepergian pada saat yang sama. Dari sudut pandang keadaan gim, gerakannya masih teleportasi, animasinya hanya visual. Unit langsung menempati sel terakhir. Anda bahkan dapat menemukan cara dan memulai langkah baru sebelum mereka tiba. Dalam hal ini, mereka diteleportasi secara visual ke awal jalur baru. Ini dapat dihindari dengan memblokir unit atau bahkan seluruh UI saat mereka bergerak, tetapi reaksi cepat semacam itu cukup nyaman ketika mengembangkan dan menguji gerakan.


Unit bergerak.

Bagaimana dengan perbedaan tinggi badan?
, . , . , . , . , Endless Legend, , . , .

Posisi setelah kompilasi


Salah satu kelemahan corutin adalah mereka tidak "bertahan" ketika dikompilasi ulang dalam mode Play. Meskipun kondisi permainan selalu benar, ini dapat menyebabkan regu terjebak di suatu tempat di jalur terakhir mereka jika kompilasi dimulai saat mereka masih bergerak. Untuk mengurangi konsekuensinya, mari kita pastikan bahwa, setelah dikompilasi ulang, unit-unit selalu berada di posisi yang benar. Ini dapat dilakukan dengan memperbarui posisi mereka di OnEnable.

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

paket unity

Gerakan halus


Gerakan dari pusat ke pusat sel terlihat terlalu mekanistik dan menciptakan perubahan arah yang tajam. Untuk banyak permainan, ini akan normal, tetapi tidak dapat diterima jika Anda membutuhkan setidaknya gerakan yang sedikit realistis. Jadi mari kita ubah gerakan agar terlihat sedikit lebih organik.

Pindah dari tulang rusuk ke tulang rusuk


Pasukan memulai perjalanannya dari pusat sel. Lewat ke tengah tepi sel, dan kemudian memasuki sel berikutnya. Alih-alih bergerak ke tengah, ia bisa langsung menuju tepi berikutnya yang harus disilangkan. Bahkan, unit akan memotong jalan ketika perlu mengubah arah. Ini dimungkinkan untuk semua sel kecuali titik akhir jalur.


Tiga cara untuk bergerak dari ujung ke ujung

Mari kita beradaptasi OnDrawGizmosuntuk menampilkan jalur yang dihasilkan dengan cara ini. Itu harus interpolasi antara tepi sel, yang dapat ditemukan dengan rata-rata posisi sel tetangga. Cukup bagi kami untuk menghitung satu sisi per iterasi, menggunakan kembali nilai dari iterasi sebelumnya. Dengan demikian, kita dapat membuat metode ini bekerja untuk sel awal, tetapi alih-alih dari tepi kita mengambil posisinya.

  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); } } } 

Untuk mencapai pusat sel akhir, kita perlu menggunakan posisi sel sebagai titik terakhir, bukan tepi. Anda dapat menambahkan verifikasi kasus ini ke loop, tetapi ini adalah kode sederhana sehingga akan lebih jelas untuk hanya menduplikasi kode dan sedikit mengubahnya.

  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); } } 




Jalur berbasis rusuk Jalur yang dihasilkan kurang seperti zig-zag, dan sudut belok maksimum dikurangi dari 120 ° hingga 90 °. Ini dapat dianggap sebagai peningkatan, jadi kami menerapkan perubahan yang sama di coroutine TravelPathuntuk melihat tampilannya di animasi.

  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; } } 


Bergerak dengan kecepatan yang berubah

Setelah memotong sudut, panjang segmen jalur menjadi tergantung pada perubahan arah. Tapi kami mengatur kecepatan dalam sel per detik. Akibatnya, kecepatan detasemen berubah secara acak.

Kurva mengikuti


Perubahan instan dalam arah dan kecepatan ketika melintasi batas sel terlihat jelek. Lebih baik gunakan perubahan arah secara bertahap. Kita dapat menambahkan dukungan untuk ini dengan memaksa pasukan mengikuti garis lengkung daripada garis lurus. Anda dapat menggunakan kurva Bezier untuk ini. Secara khusus, kita dapat mengambil kurva Bezier kuadrat di mana pusat sel akan menjadi titik kontrol tengah. Dalam hal ini, garis singgung kurva yang berdekatan akan menjadi gambar cermin satu sama lain, yaitu, seluruh jalur akan berubah menjadi kurva halus terus menerus.


Kurva dari ujung ke ujung.

Buat kelas bantu Bezierdengan metode untuk mendapatkan poin pada kurva Bezier kuadratik. Seperti dijelaskan dalam tutorial Curves and Splines , rumus ini digunakan untuk ini( 1 - t ) 2 A + 2 ( 1 - t ) t B + t 2 C dimana A , B dan C adalah titik kontrol, dan t adalah interpolator.

 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; } } 

Tidakkah GetPoint dibatasi hingga 0-1?
0-1, . . , GetPointClamped , t . , GetPointUnclamped .

Untuk menunjukkan lintasan kurva OnDrawGizmos, kita perlu melacak bukan dua, tetapi tiga titik. Poin tambahan adalah pusat sel tempat kita bekerja pada iterasi saat ini, yang memiliki indeks i - 1, karena siklus dimulai dengan 1. Setelah menerima semua poin, kita dapat menggantinya Vector3.Lerpdengan Bezier.GetPoint.

Di sel awal dan akhir, alih-alih titik akhir dan tengah, kita cukup menggunakan pusat sel.

  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); } } 


Jalur dibuat menggunakan kurva Bezier

Jalur melengkung terlihat jauh lebih baik. Kami menerapkan perubahan yang sama untuk TravelPathdan melihat bagaimana unit dianimasikan dengan pendekatan ini.

  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; } } 


Kami bergerak di sepanjang kurva.

Animasi juga menjadi halus, bahkan ketika kecepatan detasemen tidak stabil. Karena garis singgung kurva segmen yang berdekatan bertepatan, kecepatannya kontinu. Perubahan kecepatan terjadi secara bertahap dan terjadi ketika detasemen melewati sel, melambat saat mengubah arah. Jika dia lurus, maka kecepatannya tetap konstan. Selain itu, pasukan mulai dan mengakhiri perjalanannya dengan kecepatan nol. Ini meniru gerakan alami, jadi biarkan begitu saja.

Pelacakan waktu


Hingga saat ini, kami mulai mengulangi setiap segmen mulai dari 0, berlanjut hingga mencapai 1. Ini berfungsi dengan baik ketika meningkat dengan nilai konstan, tetapi iterasi kami bergantung pada delta waktu. Ketika iterasi lebih dari satu segmen selesai, kami cenderung melebihi 1 dengan jumlah tertentu, tergantung pada delta waktu. Ini tidak terlihat pada frame rate tinggi, tetapi dapat menyebabkan tersentak pada frame rate rendah.

Untuk menghindari kehilangan waktu, kami perlu mentransfer sisa waktu dari satu segmen ke segmen berikutnya. Ini dapat dilakukan dengan melacak tsepanjang jalur, dan tidak hanya di setiap segmen. Kemudian pada akhir setiap segmen kita akan mengurangi 1 dari itu.

  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; } } 

Jika kita sudah melakukan ini, mari kita pastikan bahwa waktu delta diperhitungkan di awal jalan. Ini berarti bahwa kita akan mulai bergerak segera, dan tidak akan diam untuk satu frame.

  float t = Time.deltaTime * travelSpeed; 

Selain itu, kita tidak selesai tepat pada titik waktu ketika jalan seharusnya berakhir, tetapi beberapa saat sebelumnya. Di sini, perbedaannya juga tergantung pada frame rate. Oleh karena itu, mari kita membuat pasukan menyelesaikan jalur tepat di titik akhir.

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

paket unity

Animasi orientasi


Unit-unit mulai bergerak di sepanjang kurva yang halus, tetapi mereka tidak mengubah orientasi sesuai dengan arah gerakan. Akibatnya, mereka tampak meluncur. Untuk membuat gerakan terlihat seperti gerakan nyata, kita perlu memutarnya.

Menantikan


Seperti dalam tutorial Curves and Splines , kita dapat menggunakan turunan kurva untuk menentukan orientasi unit. Rumus untuk turunan dari kurva Bezier kuadratik:2 ( ( 1 - t ) ( B - A ) + t ( C - B ) ) .Tambahkan ke Beziermetode untuk menghitungnya.

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

Vektor turunan terletak pada satu garis lurus dengan arah gerakan. Kita dapat menggunakan metode ini Quaternion.LookRotationuntuk mengubahnya menjadi giliran regu. Kami akan melaksanakannya di setiap langkah di 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; 

Apakah tidak ada kesalahan di awal jalan?
, . A dan B , . , t=0 , , Quaternion.LookRotation . , , t=0 . . , t>0 .
, t<1 .

Berbeda dengan posisi detasemen, orientasi yang tidak ideal pada ujung jalan tidak penting. Namun, kita perlu memastikan bahwa orientasinya sesuai dengan rotasi akhir. Untuk melakukan ini, setelah selesai, kami menyamakan orientasinya dengan rotasi di Y.

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

Sekarang unit-unit tersebut melihat dengan tepat ke arah gerakan, baik secara horizontal maupun vertikal. Ini berarti bahwa mereka akan bersandar ke depan dan ke belakang, turun dari lereng dan memanjat mereka. Untuk memastikan bahwa mereka selalu berdiri tegak, kami memaksa komponen Y dari vektor arah ke nol sebelum menggunakannya untuk menentukan rotasi unit.

  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); 


Menantikan saat bergerak

Kami melihat intinya


Sepanjang jalan, unit melihat ke depan, tetapi sebelum mulai bergerak, mereka dapat melihat ke arah lain. Dalam hal ini, mereka langsung mengubah orientasi mereka. Akan lebih baik jika mereka berbelok ke arah jalan sebelum dimulainya gerakan.

Melihat ke arah yang benar bisa berguna dalam situasi lain, jadi mari kita buat metode LookAtyang memaksa skuad untuk mengubah orientasi untuk melihat pada titik tertentu. Rotasi yang diperlukan dapat diatur menggunakan metode ini Transform.LookAt, pertama dengan membuat titik pada posisi vertikal yang sama dengan detasemen. Setelah itu, kita dapat mengambil orientasi skuad.

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

Agar detasemen benar-benar berubah, kita akan mengubah metodenya menjadi corutin lain yang akan memutarnya dengan kecepatan konstan. Kecepatan belok juga dapat disesuaikan, tetapi kita akan menggunakan konstanta lagi. Rotasi harus cepat, sekitar 180 ° per detik.

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

Tidak perlu mengutak-atik percepatan belokan karena tidak terlihat. Cukup bagi kita untuk menyisipkan di antara kedua orientasi. Sayangnya, ini tidak sesederhana dalam kasus dua angka, karena sudutnya melingkar. Misalnya, transisi dari 350 ° ke 10 ° akan menghasilkan rotasi 20 ° searah jarum jam, tetapi interpolasi sederhana akan memaksa rotasi 340 ° dalam arah berlawanan arah jarum jam.

Cara termudah untuk membuat rotasi yang benar adalah interpolasi antara dua angka empat menggunakan interpolasi bola. Ini akan mengarah pada belokan terpendek. Untuk melakukan ini, kita mendapatkan angka empat dari awal dan akhir, dan kemudian membuat transisi di antara mereka menggunakan 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; } 

Ini akan berhasil, tetapi interpolasi selalu berjalan dari 0 ke 1, terlepas dari sudut rotasi. Untuk memastikan kecepatan sudut yang seragam, kita perlu memperlambat interpolasi saat sudut rotasi meningkat.

  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; } 

Mengetahui sudutnya, kita dapat sepenuhnya melewati belokan jika ternyata nol.

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

Sekarang kita dapat menambahkan rotasi unit TravelPathdengan hanya melakukan hasil sebelum memindahkan LookAtposisi sel kedua. Unity akan secara otomatis meluncurkan coroutine LookAt, dan TravelPathakan menunggu penyelesaiannya.

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

Jika Anda memeriksa kode, regu teleportasi ke sel terakhir, berbelok ke sana, lalu teleport kembali ke awal jalur dan mulai bergerak dari sana. Ini terjadi karena kami memberikan nilai pada properti Locationsebelum awal coroutine TravelPath. Untuk menghilangkan teleportasi, kita dapat TravelPathmengembalikan posisi detasemen ke sel awal.

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


Putar sebelum bergerak

Sapu


Setelah menerima gerakan yang kita butuhkan, kita dapat menyingkirkan metode itu OnDrawGizmos. Hapus atau komentari kalau-kalau kita perlu melihat jalur di masa depan.

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

Karena kita tidak lagi perlu mengingat ke arah mana kita bergerak, pada akhirnya TravelPathAnda dapat membebaskan daftar sel.

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

Bagaimana dengan animasi pasukan nyata?
, . 3D- . . , . Mecanim, TravelPath .

paket unity

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


All Articles