Sistem pekerjaan dan jalur pencarian

Peta


Dalam artikel sebelumnya, saya melihat apa sistem Job yang baru, bagaimana cara kerjanya, cara membuat tugas, mengisinya dengan data dan melakukan perhitungan multi-threaded, dan hanya menjelaskan secara singkat di mana Anda dapat menggunakan sistem ini. Pada artikel ini, saya akan mencoba mem-parsing contoh spesifik di mana Anda dapat menggunakan sistem ini untuk mendapatkan lebih banyak kinerja.

Karena sistem ini awalnya dikembangkan dengan tujuan bekerja dengan data, itu bagus untuk menyelesaikan tugas-tugas pencarian jalur.

Unity telah memiliki pathfinder NavMesh yang baik, tetapi tidak berfungsi dalam proyek 2D, meskipun ada banyak solusi siap pakai pada aset yang sama. Baiklah, dan kami akan mencoba untuk membuat bukan hanya sistem yang akan mencari cara pada peta yang dibuat, tetapi membuat peta ini sangat dinamis, sehingga setiap kali ada perubahan, sistem akan membuat peta baru, dan semua ini tentu saja akan kami hitung menggunakan sistem tugas baru, agar tidak memuat utas utama.

Contoh pengoperasian sistem
gambar

Dalam contoh ini, kotak dibangun di peta, ada bot dan hambatan. Kotak dibangun kembali setiap kali kami mengubah properti peta apa pun, baik ukuran maupun posisinya.

Untuk pesawat, saya menggunakan SpriteRenderer sederhana, komponen ini memiliki properti batas yang luar biasa yang dengannya Anda dapat dengan mudah mengetahui ukuran peta.

Pada dasarnya itu semua hanya untuk permulaan, tetapi kami tidak akan berhenti dan segera memulai bisnis.

Mari kita mulai dengan skrip. Dan yang pertama adalah naskah penghalang Rintangan .

Rintangan
public class Obstacle : MonoBehaviour { } 


Di dalam kelas Rintangan , kita akan menangkap semua perubahan hambatan pada peta, misalnya, mengubah posisi atau ukuran suatu objek.
Selanjutnya, Anda bisa membuat kelas peta peta, di mana kotak akan dibangun, dan mewarisinya dari kelas Kendala .

Peta
 public sealed class Map : Obstacle { } 


Kelas Peta juga akan melacak semua perubahan pada peta untuk membangun kembali kisi jika perlu.

Untuk melakukan ini, isi kelas dasar Rintangan dengan semua variabel dan metode yang diperlukan untuk melacak perubahan objek.

Rintangan
 public class Obstacle : MonoBehaviour { public new SpriteRenderer renderer { get; private set;} private Vector2 tempSize; private Vector2 tempPos; protected virtual void Awake() { this.renderer = GetComponent<SpriteRenderer>(); this.tempSize = this.size; this.tempPos = this.position; } public virtual bool CheckChanges() { Vector2 newSize = this.size; float diff = (newSize - this.tempSize).sqrMagnitude; if (diff > 0.01f) { this.tempSize = newSize; return true; } Vector2 newPos = this.position; diff = (newPos - this.tempPos).sqrMagnitude; if (diff > 0.01f) { this.tempPos = newPos; return true; } return false; } public Vector2 size { get { return this.renderer.bounds.size;} } public Vector2 position { get { return this.transform.position;} } } 


Di sini, variabel renderer akan memiliki referensi ke komponen SpriteRenderer , dan variabel tempSize dan tempPos akan digunakan untuk melacak perubahan dalam ukuran dan posisi objek.

Metode virtual Sedar akan digunakan untuk menginisialisasi variabel, dan metode virtual CheckChanges akan melacak perubahan saat ini dalam ukuran dan posisi objek dan mengembalikan hasil boolean .

Untuk saat ini, mari kita tinggalkan skrip Rintangan dan beralih ke skrip peta Peta itu sendiri, di mana kita juga mengisinya dengan parameter yang diperlukan untuk pekerjaan.

Peta
 public sealed class Map : Obstacle { [Range(0.1f, 1f)] public float nodeSize = 0.5f; public Vector2 offset = new Vector2(0.5f, 0.5f); } 


Variabel nodeSize akan menunjukkan ukuran sel pada peta, di sini saya membatasi ukurannya dari 0,1 hingga 1 sehingga sel-sel pada grid tidak terlalu kecil, tetapi juga terlalu besar. Variabel offset akan digunakan untuk membuat inden peta saat membuat kisi sehingga kisi tidak membangun di sepanjang tepi peta.

Karena sekarang ada dua variabel baru di peta, ternyata perubahan mereka juga perlu dilacak. Untuk melakukan ini, tambahkan beberapa variabel dan overload metode CheckChanges di kelas Peta .

Peta
 public sealed class Map : Obstacle { [Range(0.1f, 1f)] public float nodeSize = 0.5f; public Vector2 offset = new Vector2(0.5f, 0.5f); private float tempNodeSize; private Vector2 tempOffset; protected override void Awake() { base.Awake(); this.tempNodeSize = this.nodeSize; this.tempOffset = this.offset; } public override bool CheckChanges() { float diff = Mathf.Abs(this.tempNodeSize - this.nodeSize); if (diff > 0.01f) { this.tempNodeSize = this.nodeSize; return true; } diff = (this.tempOffset - this.offset).sqrMagnitude; if (diff > 0.01f) { this.tempOffset = this.offset; return true; } return base.CheckChanges(); } } 


Selesai Sekarang Anda dapat membuat sprite peta di atas panggung dan melemparkan skrip Peta di atasnya.

gambar

Kami akan melakukan hal yang sama dengan penghalang - buat sprite sederhana di atas panggung dan lemparkan skrip Hambatan ke atasnya.

gambar

Sekarang kita memiliki objek peta dan rintangan di atas panggung.

Skrip Peta akan bertanggung jawab untuk melacak semua perubahan pada peta, di mana dalam metode Pembaruan kami akan memeriksa setiap frame untuk perubahan.

Peta
 public sealed class Map : Obstacle { /*... …*/ private bool requireRebuild; private void Update() { UpdateChanges(); } private void UpdateChanges() { if (this.requireRebuild) { print(“  ,   !”); this.requireRebuild = false; } else { this.requireRebuild = CheckChanges(); } } /*... …*/ } 


Jadi, dalam metode UpdateChanges, peta hanya akan melacak perubahannya sejauh ini. Anda bahkan dapat memulai permainan sekarang dan mencoba mengubah ukuran peta atau mengimbangi offset untuk memastikan bahwa semua perubahan dilacak.

Sekarang Anda perlu melacak perubahan hambatan itu sendiri di peta. Untuk melakukan ini, kami akan menempatkan setiap rintangan dalam daftar di peta, yang pada gilirannya akan memperbarui setiap bingkai dalam metode Pembaruan .

Di kelas Peta , buat daftar semua hambatan yang mungkin ada di peta dan beberapa metode statis untuk mendaftarkannya.

Peta
 public sealed class Map : Obstacle { /*... …*/ private static Map ObjInstance; private List<Obstacle> obstacles = new List<Obstacle>(); /*... …*/ public static bool RegisterObstacle(Obstacle obstacle) { if (obstacle == Instance) return false; else if (Instance.obstacles.Contains(obstacle) == false) { Instance.obstacles.Add(obstacle); Instance.requireRebuild = true; return true; } return false; } public static bool UnregisterObstacle(Obstacle obstacle) { if (Instance.obstacles.Remove(obstacle)) { Instance.requireRebuild = true; return true; } return false; } public static Map Instance { get { if (ObjInstance == null) ObjInstance = FindObjectOfType<Map>(); return ObjInstance; } } } 


Dalam metode RegisterObstacle statis, kami akan mendaftarkan hambatan Kendala baru di peta dan menambahkannya ke daftar, tetapi pertama-tama penting untuk mempertimbangkan bahwa peta itu sendiri juga diwarisi dari kelas Kendala dan oleh karena itu kita perlu memeriksa apakah kita mencoba mendaftarkan kartu itu sendiri sebagai hambatan.

Metode statis UnregisterObstacle , sebaliknya, menghilangkan hambatan dari peta dan menghilangkannya dari daftar ketika kita membiarkannya dihancurkan.

Pada saat yang sama, setiap kali kita menambah atau menghapus hambatan dari peta, kita perlu membuat ulang peta itu sendiri, jadi setelah menjalankan metode statis ini, setel variabel requireRebuild menjadi true .

Juga, untuk memiliki akses mudah ke skrip Peta dari skrip apa pun, saya membuat properti Instance statis yang akan mengembalikan kepada saya instance dari Peta ini .

Sekarang, mari kita kembali ke skrip Rintangan di mana kita akan mendaftarkan penghalang pada peta. Untuk melakukan ini, tambahkan beberapa metode OnEnable dan OnDisable untuk itu.

Rintangan
 public class Obstacle : MonoBehaviour { /*... …*/ protected virtual void OnEnable() { Map.RegisterObstacle(this); } protected virtual void OnDisable() { Map.UnregisterObstacle(this); } } 


Setiap kali kita membuat penghalang baru saat bermain di peta, itu akan secara otomatis mendaftar di metode OnEnable , di mana ia akan diperhitungkan ketika membangun kisi baru dan menghapus diri kita dari peta dalam metode OnDisable ketika dihancurkan atau dinonaktifkan.

Tetap hanya melacak perubahan hambatan itu sendiri dalam skrip Peta dalam metode CheckChanges yang kelebihan beban.

Peta
 public sealed class Map : Obstacle { /*... …*/ public override bool CheckChanges() { float diff = Mathf.Abs(this.tempNodeSize - this.nodeSize); if (diff > 0.01f) { this.tempNodeSize = this.nodeSize; return true; } diff = (this.tempOffset - this.offset).sqrMagnitude; if (diff > 0.01f) { this.tempOffset = this.offset; return true; } foreach(Obstacle obstacle in this.obstacles) { if (obstacle.CheckChanges()) return true; } return base.CheckChanges(); } /*... …*/ } 


Sekarang kita memiliki peta, rintangan - secara umum, semua yang Anda butuhkan untuk membangun kisi dan sekarang Anda dapat beralih ke hal yang paling penting.

Meshing


Kotak, dalam bentuknya yang paling sederhana, adalah susunan titik dua dimensi. Untuk membangunnya, Anda perlu mengetahui ukuran peta dan ukuran titik di atasnya, setelah beberapa perhitungan kami mendapatkan jumlah titik secara horizontal dan vertikal, ini adalah kisi kami.

Ada banyak cara untuk menemukan jalur di grid. Dalam artikel ini, bagaimanapun, hal utama adalah untuk memahami bagaimana cara yang benar menggunakan kemampuan sistem tugas, jadi di sini saya tidak akan mempertimbangkan opsi yang berbeda untuk menemukan jalan, kelebihan dan kekurangannya, tetapi saya akan mengambil opsi pencarian paling sederhana A * .

Dalam hal ini, semua titik pada grid harus memiliki, selain posisi, koordinat dan properti patensi.

Dengan patensi, saya pikir semuanya jelas mengapa diperlukan, tetapi koordinat akan menunjukkan urutan titik pada grid, koordinat ini tidak terikat secara khusus dengan posisi titik di ruang angkasa. Gambar di bawah ini menunjukkan grid sederhana yang menunjukkan perbedaan koordinat dari suatu posisi.

gambar
Kenapa koordinatnya?
Faktanya adalah bahwa dalam kesatuan, untuk menunjukkan posisi suatu objek di ruang angkasa, float sederhana digunakan yang sangat tidak akurat dan dapat berupa angka pecahan atau negatif, sehingga akan sulit untuk menggunakannya untuk mengimplementasikan pencarian jalur pada peta. Koordinat dibuat dalam bentuk int yang jelas yang akan selalu positif dan dengan itu lebih mudah untuk bekerja dengan ketika mencari titik tetangga.

Pertama, mari kita tentukan objek titik, ini akan menjadi struktur Node sederhana.

Node
 public struct Node { public int id; public Vector2 position; public Vector2Int coords; } 


Struktur ini akan berisi posisi posisi dalam bentuk Vector2 , di mana dengan variabel ini kita akan menggambar titik di ruang angkasa. Variabel koordinat koordinat dalam bentuk Vector2Int akan menunjukkan koordinat suatu titik di peta, dan variabel id , nomor akun numeriknya, dengan menggunakannya kita akan membandingkan titik-titik yang berbeda di grid dan memeriksa keberadaan suatu titik.

Patensi titik akan ditunjukkan dalam bentuk properti booleannya , tetapi karena kita tidak dapat menggunakan tipe data yang dapat dikonversi dalam sistem tugas, kami akan menunjukkan patennya dalam bentuk nomor int , karena ini saya menggunakan enumerasi sederhana NodeType , di mana: 0 bukan titik yang bisa dilewati, dan 1 lumayan.

NodeType dan Node
 public enum NodeType { NonWalkable = 0, Walkable = 1 } public struct Node { public int id; public Vector2 position; public Vector2Int coords; private int nodeType; public bool isWalkable { get { return this.nodeType == (int)NodeType.Walkable;} } public Node(int id, Vector2 position, Vector2Int coords, NodeType type) { this.id = id; this.position = position; this.coords = coords; this.nodeType = (int)type; } } 


Juga, untuk kenyamanan bekerja dengan suatu titik, saya akan membebani metode Persamaan untuk membuatnya lebih mudah untuk membandingkan titik dan juga melengkapi metode verifikasi untuk keberadaan suatu titik.

Node
 public struct Node { /*... …*/ public override bool Equals(object obj) { if (obj is Node) { Node other = (Node)obj; return this.id == other.id; } else return base.Equals(obj); } public static implicit operator bool(Node node) { return node.id > 0; } } 


Karena nomor id titik di grid akan mulai dengan 1 unit, saya akan memeriksa keberadaan titik sebagai syarat bahwa idnya lebih besar dari 0.

Pergi ke kelas Peta di mana kami akan menyiapkan segalanya untuk membuat peta.
Kami sudah memiliki pemeriksaan untuk mengubah parameter peta, sekarang kita perlu menentukan bagaimana proses membangun grid akan dilakukan. Untuk melakukan ini, buat satu variabel baru dan beberapa metode.

Peta
 public sealed class Map : Obstacle { /*... …*/ public bool rebuilding { get; private set; } public void Rebuild() {} private void OnRebuildStart() {} private void OnRebuildFinish() {} /*... …*/ } 


Properti pembangunan kembali akan menunjukkan apakah proses meshing sedang berlangsung. Metode Rebuild akan mengumpulkan data dan tugas untuk membangun grid, maka metode OnRebuildStart akan memulai proses pembuatan grid dan metode OnRebuildFinish akan mengumpulkan data dari tugas.

Sekarang mari kita ubah metode UpdateChanges sedikit sehingga kondisi grid diperhitungkan.

Peta
 public sealed class Map : Obstacle { /*... …*/ public bool rebuilding { get; private set; } private void UpdateChanges() { if (this.rebuilding) { print(“  ...”); } else { if (this.requireRebuild) { print(“  ,   !”); Rebuild(); } else { this.requireRebuild = CheckChanges(); } } } public void Rebuild() { if (this.rebuilding) return; print(“ !”); OnRebuildStart(); } private void OnRebuildStart() { this.rebuilding = true; } private void OnRebuildFinish() { this.rebuilding = false; } /*... …*/ } 


Seperti yang Anda lihat sekarang dalam metode UpdateChanges ada kondisi bahwa saat membangun mesh lama tidak mulai membangun yang baru, dan juga dalam metode Rebuild , tindakan pertama memeriksa apakah proses meshing sudah dalam proses.

Pemecahan masalah


Sekarang sedikit tentang proses membangun peta.
Karena kita akan menggunakan sistem tugas dan membangun grid secara paralel untuk membangun peta, saya menggunakan jenis tugas IJobParallelFor , yang akan dieksekusi beberapa kali. Agar tidak memuat proses konstruksi dengan satu tugas terpisah, kami akan menggunakan kumpulan tugas yang dikemas ke dalam satu JobHandle .

Paling sering, untuk membangun kisi, gunakan dua siklus yang saling bersarang untuk membangun, misalnya, secara horizontal dan vertikal. Dalam contoh ini, kita juga akan membangun grid terlebih dahulu secara horizontal dan kemudian secara vertikal. Untuk melakukan ini, kita menghitung jumlah titik horizontal dan vertikal dalam metode Rebuild , kemudian dalam metode Rebuild kita melalui siklus di sepanjang titik-titik vertikal, dan kita akan membangun yang horisontal secara paralel dalam tugas. Untuk lebih membayangkan proses pembangunan, lihat animasi di bawah ini.

Meshing
gambar

Jumlah titik vertikal akan menunjukkan jumlah tugas, pada gilirannya, setiap tugas akan membangun poin hanya secara horizontal, setelah menyelesaikan semua tugas, poin-poin tersebut dijumlahkan dalam satu daftar. Itulah sebabnya saya perlu menggunakan tugas seperti IJobParallelFor untuk melewati indeks titik di grid secara horizontal ke metode Execute .

Jadi kami memiliki struktur titik, sekarang Anda dapat membuat struktur tugas Pekerjaan dan mewarisinya dari antarmuka IJobParallelFor , semuanya sederhana di sini.

Ayub
 public struct Job : IJobParallelFor { public void Execute(int index) {} } 


Kami kembali ke metode Rebuild kelas peta , di mana kami akan membuat perhitungan yang diperlukan untuk pengukuran grid.

Peta
 public sealed class Map : Obstacle { /*... ...*/ public void Rebuild() { if (this.rebuilding) return; print(“ !”); Vector2 mapSize = this.size - this.offset * 2f; int horizontals = Mathf.RoundToInt(mapSize.x / this.nodeSize); int verticals = Mathf.RoundToInt(mapSize.y / this.nodeSize); if (horizontals <= 0) { OnRebuildFinish(); return; } Vector2 center = this.position; Vector2 origin = center - (mapSize / 2f); OnRebuildStart(); } /*... ...*/ } 


Dalam metode Rebuild , kami menghitung ukuran persis peta mapSize , dengan mempertimbangkan lekukan, kemudian secara vertikal kami menulis jumlah titik secara vertikal, dan dalam horizontal jumlah titik secara horizontal. Jika jumlah titik vertikal adalah 0, maka kita berhenti membangun peta dan memanggil metode OnRebuildFinish untuk menyelesaikan proses. Variabel asal akan menunjukkan tempat dari mana kita akan mulai membangun kisi - dalam contoh, ini adalah titik kiri bawah pada peta.

Sekarang Anda dapat pergi ke tugas sendiri dan mengisinya dengan data.
Selama konstruksi grid, tugas akan membutuhkan array NativeArray di mana kita akan menempatkan titik, juga karena kita memiliki hambatan pada peta, kita juga akan perlu untuk meneruskannya ke tugas, untuk ini kita akan menggunakan array NativeArray lain, maka kita perlu ukuran poin dalam masalah , posisi awal dari tempat kita akan membangun poin, serta koordinat awal seri.

Ayub
 public struct Job : IJobParallelFor { [WriteOnly] public NativeArray<Node> array; [ReadOnly] public NativeArray<Rect> bounds; public float nodeSize; public Vector2 startPos; public Vector2Int startCoords; public void Execute(int index) {} } 


Saya menandai array poin dengan atribut WriteOnly, karena dalam tugas itu hanya perlu untuk " menulis " poin yang diterima ke array, sebaliknya, array batas rintangan ditandai dengan atribut ReadOnly karena dalam tugas kita hanya akan " membaca " data dari array ini.

Nah, untuk sekarang, mari kita lanjutkan ke penghitungan poin sendiri nanti.

Sekarang kembali ke kelas Peta , di mana kami menunjukkan semua variabel yang terlibat dalam tugas.
Di sini, pertama-tama, kita perlu menangani tugas - tugas global, serangkaian hambatan dalam bentuk NativeArray , daftar tugas yang akan berisi semua poin yang diterima di grid dan Kamus dengan semua koordinat dan poin di peta, sehingga akan lebih mudah untuk mencari mereka nanti.

Peta
 public sealed class Map : Obstacle { /*... ...*/ private JobHandle handle; private NativeArray<Rect> bounds; private HashSet<NativeArray<Node>> jobs = new HashSet<NativeArray<Node>>(); private Dictionary<Vector2Int, Node> nodes = new Dictionary<Vector2Int, Node>(); /*... ...*/ } 


Sekarang lagi, kita kembali ke metode Rebuild dan terus membangun grid.
Pertama, inisialisasi array batas rintangan untuk meneruskannya ke tugas.

Bangun kembali
 public void Rebuild() { /*... ...*/ Vector2 center = this.position; Vector2 origin = center - (mapSize / 2f); int count = this.obstacles.Count; if (count > 0) { this.bounds = new NativeArray<Rect>(count, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); } OnRebuildStart(); } 


Di sini kita membuat instance NativeArray melalui konstruktor baru dengan tiga parameter. Saya memeriksa dua parameter pertama dalam artikel sebelumnya, tetapi parameter ketiga akan membantu kita menghemat sedikit waktu membuat array. Faktanya adalah bahwa kita akan menulis data ke array segera setelah pembuatannya, yang berarti kita tidak perlu memastikan bahwa itu dihapus. Parameter ini berguna untuk NativeArray yang hanya akan digunakan dalam mode baca dalam tugas.

Jadi, kemudian kita mengisi array batas dengan data.

Bangun kembali
 public void Rebuild() { /*... ...*/ Vector2 center = this.position; Vector2 origin = center - (mapSize / 2f); int count = this.obstacles.Count; if (count > 0) { this.bounds = new NativeArray<Rect>(count, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); for(int i = 0; i < count; i++) { Obstacle obs = this.obstacles[i]; Vector2 position = obs.position; Rect rect = new Rect(Vector2.zero, obs.size); rect.center = position; this.bounds[i] = rect; } } OnRebuildStart(); } 


Sekarang kita dapat beralih ke membuat tugas, untuk ini kita akan melalui siklus melalui semua baris vertikal grid.

Bangun kembali
 public void Rebuild() { /*... ...*/ Vector2 center = this.position; Vector2 origin = center - (mapSize / 2f); int count = this.obstacles.Count; if (count > 0) { this.bounds = new NativeArray<Rect>(count, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); for(int i = 0; i < count; i++) { Obstacle obs = this.obstacles[i]; Vector2 position = obs.position; Rect rect = new Rect(Vector2.zero, obs.size); rect.center = position; this.bounds[i] = rect; } } for (int i = 0; i < verticals; i++) { float xPos = origin.x; float yPos = origin.y + (i * this.nodeSize) + this.nodeSize / 2f; } OnRebuildStart(); } 


Untuk mulai dengan, di xPos dan yPos kita mendapatkan posisi horizontal awal seri.

Bangun kembali
 public void Rebuild() { /*... ...*/ Vector2 center = this.position; Vector2 origin = center - (mapSize / 2f); int count = this.obstacles.Count; if (count > 0) { this.bounds = new NativeArray<Rect>(count, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); for(int i = 0; i < count; i++) { Obstacle obs = this.obstacles[i]; Vector2 position = obs.position; Rect rect = new Rect(Vector2.zero, obs.size); rect.center = position; this.bounds[i] = rect; } } for (int i = 0; i < verticals; i++) { float xPos = origin.x; float yPos = origin.y + (i * this.nodeSize) + this.nodeSize / 2f; NativeArray<Node> array = new NativeArray<Node>(horizontals, Allocator.Persistent); Job job = new Job(); job.startCoords = new Vector2Int(i * horizontals, i); job.startPos = new Vector2(xPos, yPos); job.nodeSize = this.nodeSize; job.bounds = this.bounds; job.array = array; } OnRebuildStart(); } 


Selanjutnya, kita membuat array NativeArray sederhana di mana titik-titik dalam tugas akan ditempatkan, di sini untuk array array Anda perlu menentukan berapa banyak poin yang akan dibuat secara horizontal dan jenis alokasi Persistent , karena tugas dapat memakan waktu lebih dari satu frame.
Setelah itu, buat contoh tugas Pekerjaan itu sendiri, letakkan koordinat awal dari seri startCoords , posisi awal dari seri startPos , ukuran titik nodeSize , batas array hambatan, dan pada akhirnya array titik itu sendiri.
Tinggal menangani tugas dan daftar tugas global.

Bangun kembali
 public void Rebuild() { /*... ...*/ Vector2 center = this.position; Vector2 origin = center - (mapSize / 2f); int count = this.obstacles.Count; if (count > 0) { this.bounds = new NativeArray<Rect>(count, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); for(int i = 0; i < count; i++) { Obstacle obs = this.obstacles[i]; Vector2 position = obs.position; Rect rect = new Rect(Vector2.zero, obs.size); rect.center = position; this.bounds[i] = rect; } } for (int i = 0; i < verticals; i++) { float xPos = origin.x; float yPos = origin.y + (i * this.nodeSize) + this.nodeSize / 2f; NativeArray<Node> array = new NativeArray<Node>(horizontals, Allocator.Persistent); Job job = new Job(); job.startCoords = new Vector2Int(i * horizontals, i); job.startPos = new Vector2(xPos, yPos); job.nodeSize = this.nodeSize; job.bounds = this.bounds; job.array = array; this.handle = job.Schedule(horizontals, 3, this.handle); this.jobs.Add(array); } OnRebuildStart(); } 


Selesai Kami memiliki daftar tugas dan pegangan umum mereka, sekarang kami dapat menjalankan pegangan ini dengan memanggil metode Lengkapnya di metode OnRebuildStart .

Onrebuildstart
 private void OnRebuildStart() { this.rebuilding = true; this.handle.Complete(); } 


Karena variabel pembangunan kembali akan menunjukkan bahwa proses meshing sedang berlangsung, metode UpdateChanges itu sendiri juga harus menentukan kondisi ketika proses ini akan berakhir menggunakan pegangan dan properti IsCompleted -nya.

Pembaruan perubahan
 private void UpdateChanges() { if (this.rebuilding) { print(“  ...”); if (this.handle.IsCompleted) OnRebuildFinish(); } else { if (this.requireRebuild) { print(“  ,   !”); Rebuild(); } else { this.requireRebuild = CheckChanges(); } } } 


Setelah menyelesaikan tugas, metode OnRebuildFinish akan dipanggil di mana kami akan mengumpulkan poin yang diterima ke dalam satu daftar Kamus umum, dan yang paling penting, untuk menghapus sumber daya yang ditempati.

OnRebuildFinish
  private void OnRebuildFinish() { this.nodes.Clear(); foreach (NativeArray<Node> array in this.jobs) { foreach (Node node in array) this.nodes.Add(node.coords, node); array.Dispose(); } this.jobs.Clear(); if (this.bounds.IsCreated) this.bounds.Dispose(); this.requireRebuild = this.rebuilding = false; } 


Pertama, kita menghapus kamus node dari titik sebelumnya, kemudian menggunakan foreach loop untuk memilah-milah semua poin yang kita terima dari tugas dan menempatkannya dalam kamus node , di mana kuncinya adalah koordinat ( BUKAN posisi !) Dari titik tersebut, dan nilainya adalah titik itu sendiri. Dengan bantuan kamus ini, akan lebih mudah bagi kami untuk mencari titik tetangga di peta. Setelah mengisi, kami menghapus array array menggunakan metode Buang dan pada akhirnya kami menghapus daftar tugas pekerjaan itu sendiri .

Anda juga perlu menghapus batasan batasan jika itu sebelumnya dibuat.

Setelah semua tindakan ini, kami mendapatkan daftar semua poin di peta dan sekarang Anda dapat menggambarnya di atas panggung.

Sesuatu seperti ini
gambar

Untuk melakukan ini, di kelas Peta , buat metode OnDrawGizmos di mana kita akan menggambar poin.

Peta
 public sealed class Map : Obstacle { /*... …*/ #if UNITY_EDITOR private void OnDrawGizmos() {} #endif } 


Sekarang melalui loop kita menggambar setiap titik.

Peta
 public sealed class Map : Obstacle { /*... …*/ #if UNITY_EDITOR private void OnDrawGizmos() { foreach (Node node in this.nodes.Values) { Gizmos.DrawWireSphere(node.position, this.nodeSize / 10f); } } #endif } 


Setelah semua tindakan ini, peta kami terlihat agak membosankan, untuk benar-benar mendapatkan kisi, Anda perlu titik untuk terhubung satu sama lain.

Mesh
gambar

Untuk mencari titik tetangga, kita hanya perlu menemukan titik yang diinginkan dengan koordinatnya dalam 8 arah, jadi di kelas Peta kita akan membuat array statis arah Arah dan metode pencarian sel dengan koordinat GetNode -nya .

Peta
 public sealed class Map : Obstacle { public static readonly Vector2Int[] Directions = { Vector2Int.up, new Vector2Int(1, 1), Vector2Int.right, new Vector2Int(1, -1), Vector2Int.down, new Vector2Int(-1, -1), Vector2Int.left, new Vector2Int(-1, 1), }; /*... …*/ public Node GetNode(Vector2Int coords) { Node result = default(Node); try { result = this.nodes[coords]; } catch {} return result; } #if UNITY_EDITOR private void OnDrawGizmos() {} #endif } 


Metode GetNode akan mengembalikan titik dengan koordinat dari daftar node , tetapi Anda harus melakukan ini dengan hati-hati, karena jika koordinat Vector2Int salah, kesalahan akan terjadi, jadi di sini kami menggunakan blok pintas try catch exception , yang akan membantu memotong pengecualian dan tidak " menggantung " seluruh aplikasi dengan kesalahan.

Selanjutnya, kita akan melalui siklus di semua arah dan mencoba untuk menemukan titik-titik tetangga dalam metode OnDrawGizmos , dan yang paling penting, jangan lupa untuk mempertimbangkan paten dari titik tersebut.

Ondrawgizmos
  #if UNITY_EDITOR private void OnDrawGizmos() { Color c = Gizmos.color; foreach (Node node in this.nodes.Values) { Color newColor = Color.white; if (node.isWalkable) newColor = new Color32(153, 255, 51, 255); else newColor = Color.red; Gizmos.color = newColor; Gizmos.DrawWireSphere(node.position, this.nodeSize / 10f); newColor = Color.green; Gizmos.color = newColor; if (node.isWalkable) { for (int i = 0; i < Directions.Length; i++) { Vector2Int coords = node.coords + Directions[i]; Node connection = GetNode(coords); if (connection) { if (connection.isWalkable) Gizmos.DrawLine(node.position, connection.position); } } } } Gizmos.color = c; } #endif 


Sekarang Anda dapat dengan aman memulai permainan dan melihat apa yang terjadi.

Peta dinamis
gambar

Dalam contoh ini, kami hanya membuat grafik itu sendiri menggunakan tugas, tetapi inilah yang terjadi setelah saya mengacaukan sistem algoritma A * itu sendiri , yang juga menggunakan sistem Pekerjaan untuk menemukan jalan, sumber di akhir artikel .

Pencarian peta dan jalur
gambar

Jadi Anda dapat menggunakan sistem tugas baru untuk tujuan Anda dan membangun sistem yang menarik tanpa banyak usaha.

Seperti pada artikel sebelumnya, sistem tugas digunakan tanpa ECS , tetapi jika Anda menggunakan sistem ini bersama dengan ECS , Anda dapat mencapai hasil yang luar biasa dalam keuntungan kinerja. Good luck !

Sumber Proyek Path Finder

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


All Articles