Lapangan
- Membuat bidang ubin.
- Jalur pencarian menggunakan pencarian luas pertama.
- Terapkan dukungan untuk ubin kosong dan ujung, serta ubin dinding.
- Mengedit konten dalam mode game.
- Tampilan opsional bidang dan jalur kisi.
Ini adalah bagian pertama dari serangkaian tutorial tentang cara membuat gim
pertahanan menara sederhana. Pada bagian ini, kami akan mempertimbangkan untuk menciptakan lapangan bermain, menemukan jalan, dan menempatkan ubin dan dinding akhir.
Tutorial dibuat di Unity 2018.3.0f2.
Bidang yang siap digunakan dalam permainan ubin genre menara pertahanan.Game Tower Defense
Menara pertahanan adalah genre di mana tujuan pemain adalah untuk menghancurkan kerumunan musuh sampai mereka mencapai titik akhir mereka. Pemain memenuhi tujuannya dengan membangun menara yang menyerang musuh. Genre ini memiliki banyak variasi. Kami akan membuat game dengan bidang ubin. Musuh akan bergerak melintasi lapangan menuju titik akhir mereka, dan pemain akan membuat rintangan untuk mereka.
Saya akan berasumsi bahwa Anda telah mempelajari serangkaian tutorial tentang
mengelola objek .
Lapangan
Lapangan bermain adalah bagian terpenting dari permainan, jadi kami akan membuatnya terlebih dahulu. Ini akan menjadi objek game dengan komponen
GameBoard
sendiri, yang dapat diinisialisasi dengan mengatur ukuran dalam dua dimensi, untuk itu kita dapat menggunakan nilai
Vector2Int
. Field harus bekerja dengan ukuran apa pun, tetapi kami akan memilih ukuran di tempat lain, jadi kami akan membuat metode
Initialize
umum untuk ini.
Selain itu, kami memvisualisasikan bidang dengan satu segi empat, yang akan menunjukkan bumi. Kami tidak akan membuat objek bidang itu sendiri sebagai segi empat, tetapi menambahkan objek quad anak ke dalamnya. Setelah inisialisasi, kita akan membuat skala XY bumi sama dengan ukuran lapangan. Artinya, setiap ubin akan memiliki ukuran satu unit persegi ukuran untuk mesin.
using UnityEngine; public class GameBoard : MonoBehaviour { [SerializeField] Transform ground = default; Vector2Int size; public void Initialize (Vector2Int size) { this.size = size; ground.localScale = new Vector3(size.x, size.y, 1f); } }
Mengapa secara eksplisit menetapkan ground ke nilai default?Idenya adalah bahwa segala sesuatu yang dapat disesuaikan melalui editor Unity dapat diakses melalui bidang tersembunyi berseri. Perlu bahwa bidang-bidang ini hanya dapat diubah di inspektur. Sayangnya, editor Unity akan terus menampilkan peringatan kompiler bahwa nilainya tidak pernah ditetapkan. Kami dapat menekan peringatan ini dengan secara eksplisit menetapkan nilai default untuk bidang tersebut. Anda juga dapat menetapkan null
, tetapi saya membuatnya agar secara eksplisit menunjukkan bahwa kami hanya menggunakan nilai default, yang bukan merupakan referensi yang benar ke ground, jadi kami menggunakan default
.
Buat objek bidang dalam adegan baru dan tambahkan quad anak dengan bahan yang terlihat seperti bumi. Karena kami membuat game prototipe sederhana, bahan hijau yang seragam akan cukup. Putar quad 90 ° sepanjang sumbu X sehingga terletak pada bidang XZ.
Lapangan bermain.Mengapa tidak memposisikan game di pesawat XY?Meskipun game akan berlangsung dalam ruang 2D, kami akan membuatnya dalam 3D, dengan musuh 3D dan kamera yang dapat dipindahkan relatif ke titik tertentu. Pesawat XZ lebih nyaman untuk ini dan sesuai dengan orientasi skybox standar yang digunakan untuk pencahayaan sekitar.
Permainan
Selanjutnya, buat komponen
Game
yang akan bertanggung jawab untuk seluruh game. Pada tahap ini, ini berarti akan menginisialisasi bidang. Kami hanya membuat ukuran dapat disesuaikan melalui inspektur dan memaksa komponen untuk menginisialisasi bidang ketika bangun. Mari kita gunakan ukuran standar 11 × 11.
using UnityEngine; public class Game : MonoBehaviour { [SerializeField] Vector2Int boardSize = new Vector2Int(11, 11); [SerializeField] GameBoard board = default; void Awake () { board.Initialize(boardSize); } }
Ukuran bidang hanya bisa positif dan tidak masuk akal untuk membuat bidang dengan satu ubin. Jadi mari kita batasi minimum hingga 2 × 2. Ini dapat dilakukan dengan menambahkan metode
OnValidate
, secara paksa membatasi nilai minimum.
void OnValidate () { if (boardSize.x < 2) { boardSize.x = 2; } if (boardSize.y < 2) { boardSize.y = 2; } }
Kapan Onvalidate dipanggil?Jika ada, editor Unity memanggilnya untuk komponen setelah mengubahnya. Termasuk saat menambahkannya ke objek game, setelah memuat adegan, setelah mengkompilasi ulang, setelah mengubah editor, setelah membatalkan / mencoba ulang, dan setelah mengatur ulang komponen.
OnValidate
adalah satu-satunya tempat di kode tempat Anda dapat menetapkan nilai ke bidang konfigurasi komponen.
Objek game.Sekarang, ketika Anda memulai mode permainan, kami akan menerima bidang dengan ukuran yang benar. Selama permainan, posisikan kamera sehingga seluruh papan terlihat, salin komponen transformasinya, keluar dari mode putar dan rekatkan nilai-nilai komponen. Dalam kasus bidang 11 × 11 di titik asal, untuk mendapatkan tampilan yang nyaman dari atas, Anda dapat memposisikan kamera pada posisi (0.10.0) dan memutarnya 90 ° di sepanjang sumbu X. Kami akan membiarkan kamera dalam posisi tetap ini, tetapi dimungkinkan ubahlah di masa depan.
Kamera di atas lapangan.Bagaimana cara menyalin dan menempelkan nilai komponen?Melalui menu drop-down yang muncul ketika Anda mengklik tombol dengan roda gigi di sudut kanan atas komponen.
Kotak cetakan
Lapangan terdiri dari ubin persegi. Musuh akan dapat bergerak dari ubin ke ubin, melintasi tepi, tetapi tidak secara diagonal. Gerakan akan selalu terjadi menuju titik akhir terdekat. Mari kita secara grafis menunjukkan arah gerakan sepanjang ubin dengan panah. Anda dapat mengunduh tekstur panah di
sini .
Panah di latar belakang hitam.Tempatkan tekstur panah di proyek Anda dan aktifkan opsi
Alpha As Transparency . Kemudian buat bahan untuk panah, yang bisa menjadi bahan default untuk mode potongan yang dipilih, dan pilih panah sebagai tekstur utama.
Bahan panah.Mengapa menggunakan mode render potongan?Ini memungkinkan Anda untuk mengaburkan panah menggunakan pipa render Unity standar.
Untuk menunjukkan setiap ubin dalam game, kami akan menggunakan objek game. Masing-masing dari mereka akan memiliki quad sendiri dengan bahan panah, seperti halnya lapangan memiliki quad bumi. Kami juga akan menambahkan ubin ke komponen GameTile dengan tautan ke panah mereka.
using UnityEngine; public class GameTile : MonoBehaviour { [SerializeField] Transform arrow = default; }
Buat objek ubin dan mengubahnya menjadi cetakan. Ubin akan rata dengan tanah, jadi naikkan panah sedikit untuk menghindari masalah dengan kedalaman saat rendering. Perkecil juga sedikit, sehingga ada sedikit ruang di antara panah yang berdekatan. Y offset 0,001 dan skala 0,8 yang sama untuk semua sumbu akan dilakukan.
Kotak cetakan.Di mana hierarki pabrikan cetakan?Anda dapat membuka mode edit cetakan dengan mengklik dua kali pada aset cetakan, atau dengan memilih cetakan dan mengklik tombol Buka Cetakan di inspektur. Anda dapat keluar dari mode edit cetakan dengan mengklik tombol dengan panah di sudut kiri atas hierarki header-nya.
Perhatikan bahwa ubin itu sendiri tidak harus menjadi objek game. Mereka hanya diperlukan untuk melacak keadaan lapangan. Kita bisa menggunakan pendekatan yang sama dengan perilaku dalam rangkaian tutorial
Manajemen Objek . Namun pada tahap awal game sederhana atau prototipe objek game, kami cukup senang. Ini bisa diubah di masa depan.
Kami punya ubin
Untuk membuat ubin,
GameBoard
harus memiliki tautan ke prefab ubin.
[SerializeField] GameTile tilePrefab = default;
Tautan ke ubin cetakan.Dia kemudian dapat membuat instansnya menggunakan loop ganda di atas dua dimensi grid. Meskipun ukurannya dinyatakan sebagai X dan Y, kami akan mengatur ubin pada bidang XZ, serta bidang itu sendiri. Karena bidang dipusatkan relatif terhadap asal, kita perlu mengurangi ukuran yang sesuai dikurangi satu dibagi dua dari komponen posisi ubin. Harap dicatat bahwa ini harus menjadi divisi floating point, jika tidak, ukurannya tidak akan berlaku.
public void Initialize (Vector2Int size) { this.size = size; ground.localScale = new Vector3(size.x, size.y, 1f); Vector2 offset = new Vector2( (size.x - 1) * 0.5f, (size.y - 1) * 0.5f ); for (int y = 0; y < size.y; y++) { for (int x = 0; x < size.x; x++) { GameTile tile = Instantiate(tilePrefab); tile.transform.SetParent(transform, false); tile.transform.localPosition = new Vector3( x - offset.x, 0f, y - offset.y ); } } }
Contoh ubin yang dibuat.Nanti kita akan membutuhkan akses ke ubin ini, jadi kita akan melacaknya dalam sebuah array. Kami tidak perlu daftar, karena setelah inisialisasi, ukuran bidang tidak akan berubah.
GameTile[] tiles; public void Initialize (Vector2Int size) { … tiles = new GameTile[size.x * size.y]; for (int i = 0, y = 0; y < size.y; y++) { for (int x = 0; x < size.x; x++, i++) { GameTile tile = tiles[i] = Instantiate(tilePrefab); … } } }
Bagaimana cara kerja tugas ini?Ini adalah tugas yang ditautkan. Dalam hal ini, ini berarti bahwa kami menetapkan tautan ke turunan ubin ke elemen array dan variabel lokal. Operasi ini melakukan hal yang sama seperti kode yang ditunjukkan di bawah ini.
GameTile t = Instantiate(tilePrefab); tiles[i] = t; GameTile tile = t;
Cari cara
Pada tahap ini, setiap ubin memiliki panah, tetapi semuanya menunjuk ke arah positif sumbu Z, yang akan kita tafsirkan sebagai utara. Langkah selanjutnya adalah menentukan arah yang benar untuk ubin. Kami melakukan ini dengan menemukan jalan yang harus dilalui musuh ke titik akhir.
Tetangga Tetangga
Jalur pergi dari ubin ke ubin, di utara, timur, selatan atau barat. Untuk mempermudah pencarian, buat
GameTile
track
GameTile
ke empat tetangganya.
GameTile north, east, south, west;
Hubungan antar tetangga simetris. Jika ubin adalah tetangga timur ubin kedua, maka yang kedua adalah tetangga barat yang pertama. Tambahkan metode statis umum ke
GameTile
untuk menentukan hubungan ini antara dua ubin.
public static void MakeEastWestNeighbors (GameTile east, GameTile west) { west.east = east; east.west = west; }
Mengapa menggunakan metode statis?Kita dapat menjadikannya metode instan dengan parameter tunggal, dan dalam hal ini kita akan menyebutnya sebagai eastTile.MakeEastWestNeighbors(westTile)
atau sesuatu seperti itu. Tetapi dalam kasus-kasus di mana tidak jelas ubin mana dari metode yang harus dipanggil, lebih baik menggunakan metode statis. Contohnya adalah metode Distance
dan Dot
dari kelas Vector3
.
Setelah terhubung, seharusnya tidak pernah berubah. Jika ini terjadi, maka kami membuat kesalahan dalam kode. Anda dapat memverifikasi ini dengan membandingkan kedua tautan sebelum menetapkan nilai ke
null
, dan menampilkan kesalahan jika salah. Anda dapat menggunakan metode
Debug.Assert
untuk
Debug.Assert
.
public static void MakeEastWestNeighbors (GameTile east, GameTile west) { Debug.Assert( west.east == null && east.west == null, "Redefined neighbors!" ); west.east = east; east.west = west; }
Apa yang dilakukan dengan Debug.Assert?Jika argumen pertama false
, maka itu menampilkan kesalahan kondisi, menggunakan argumen kedua jika ditentukan. Panggilan semacam itu hanya termasuk dalam uji coba bangunan, tetapi tidak dalam uji coba rilis. Oleh karena itu, ini adalah cara yang baik untuk menambahkan pemeriksaan selama proses pengembangan yang tidak akan mempengaruhi rilis akhir.
Tambahkan metode serupa untuk menciptakan hubungan antara tetangga utara dan selatan.
public static void MakeNorthSouthNeighbors (GameTile north, GameTile south) { Debug.Assert( south.north == null && north.south == null, "Redefined neighbors!" ); south.north = north; north.south = south; }
Kami dapat menjalin hubungan ini saat membuat ubin di
GameBoard.Initialize
. Jika koordinat X lebih besar dari nol, maka kita dapat membuat hubungan timur-barat antara ubin saat ini dan sebelumnya. Jika koordinat Y lebih besar dari nol, maka kita dapat membuat hubungan utara-selatan antara ubin saat ini dan ubin dari baris sebelumnya.
for (int i = 0, y = 0; y < size.y; y++) { for (int x = 0; x < size.x; x++, i++) { … if (x > 0) { GameTile.MakeEastWestNeighbors(tile, tiles[i - 1]); } if (y > 0) { GameTile.MakeNorthSouthNeighbors(tile, tiles[i - size.x]); } } }
Perhatikan bahwa ubin di tepi lapangan tidak memiliki empat tetangga. Satu atau dua referensi tetangga akan tetap
null
.
Jarak dan arah
Kami tidak akan memaksa semua musuh untuk terus mencari jalan. Ini perlu dilakukan hanya sekali per ubin. Maka musuh akan dapat meminta dari ubin di mana mereka berada di mana harus pindah. Kami akan menyimpan informasi ini di
GameTile
dengan menambahkan tautan ke ubin jalur berikutnya. Selain itu, kami juga akan menghemat jarak ke titik akhir, dinyatakan sebagai jumlah ubin yang harus dikunjungi sebelum musuh mencapai titik akhir. Untuk musuh, informasi ini tidak berguna, tetapi kami akan menggunakannya untuk menemukan jalur terpendek.
GameTile north, east, south, west, nextOnPath; int distance;
Setiap kali kita memutuskan bahwa kita perlu mencari jalur, kita perlu menginisialisasi data jalur. Sampai jalan ditemukan, tidak ada ubin berikutnya dan jarak dapat dianggap tak terbatas. Kita bisa membayangkan ini sebagai nilai integer maksimum yang mungkin dari
int.MaxValue
. Tambahkan metode
GameTile
generik untuk mengatur ulang
GameTile
ke kondisi ini.
public void ClearPath () { distance = int.MaxValue; nextOnPath = null; }
Jalur hanya dapat dicari jika kita memiliki titik akhir. Ini berarti bahwa ubin harus menjadi titik akhir. Ubin tersebut memiliki jarak nol, dan tidak memiliki ubin terakhir, karena jalur berakhir di atasnya. Tambahkan metode generik yang mengubah ubin menjadi titik akhir.
public void BecomeDestination () { distance = 0; nextOnPath = null; }
Pada akhirnya, semua ubin harus berubah menjadi jalur, sehingga jaraknya tidak lagi sama dengan nilai
int.MaxValue
.
int.MaxValue
. Tambahkan properti pengambil yang nyaman untuk memeriksa apakah ubin saat ini memiliki jalur.
public bool HasPath => distance != int.MaxValue;
Bagaimana cara kerja properti ini?Ini adalah entri singkat untuk properti pengambil yang hanya berisi satu ekspresi. Itu tidak sama dengan kode yang ditunjukkan di bawah ini.
public bool HasPath { get { return distance != int.MaxValue; } }
Operator panah
=>
juga dapat digunakan secara individual untuk pengambil dan penyetel properti, untuk kumpulan metode, konstruktor, dan di beberapa tempat lain.
Kami tumbuh dengan cara
Jika kita memiliki ubin dengan jalan, maka kita dapat membiarkannya menumbuhkan jalan menuju salah satu tetangganya. Awalnya, satu-satunya ubin dengan jalan adalah titik akhir, jadi kita mulai dari jarak nol dan meningkatkannya dari sini, bergerak ke arah yang berlawanan dengan pergerakan musuh. Artinya, semua tetangga langsung dari titik akhir akan memiliki jarak 1, dan semua tetangga ubin ini akan memiliki jarak 2, dan seterusnya.
Tambahkan metode tersembunyi
GameTile
untuk menumbuhkan jalur ke salah satu tetangganya, ditentukan melalui parameter. Jarak ke tetangga adalah satu lebih dari ubin saat ini, dan jalur tetangga menunjukkan ubin saat ini. Metode ini seharusnya hanya dipanggil untuk ubin yang sudah memiliki jalur, jadi mari kita periksa ini dengan tegas.
void GrowPathTo (GameTile neighbor) { Debug.Assert(HasPath, "No path!"); neighbor.distance = distance + 1; neighbor.nextOnPath = this; }
Idenya adalah bahwa kita memanggil metode ini satu kali untuk masing-masing dari empat tetangga ubin. Karena beberapa tautan ini akan menjadi
null
, kami akan memeriksa ini dan menghentikan eksekusi, jika demikian. Selain itu, jika tetangga sudah memiliki jalan, maka kita tidak boleh melakukan apa-apa dan juga berhenti melakukannya.
void GrowPathTo (GameTile neighbor) { Debug.Assert(HasPath, "No path!"); if (neighbor == null || neighbor.HasPath) { return; } neighbor.distance = distance + 1; neighbor.nextOnPath = this; }
Cara
GameTile
melacak tetangganya tidak diketahui oleh kode lainnya. Karenanya,
GrowPathTo
disembunyikan. Kami akan menambahkan metode umum yang memberi tahu ubin untuk menumbuhkan jalurnya ke arah tertentu, secara tidak langsung memanggil
GrowPathTo
. Tetapi kode yang mencari di seluruh bidang harus melacak ubin yang dikunjungi. Karenanya, kami akan membuatnya mengembalikan tetangga atau
null
jika eksekusi dihentikan.
GameTile GrowPathTo (GameTile neighbor) { if (!HasPath || neighbor == null || neighbor.HasPath) { return null; } neighbor.distance = distance + 1; neighbor.nextOnPath = this; return neighbor; }
Sekarang tambahkan metode untuk menumbuhkan jalur ke arah tertentu.
public GameTile GrowPathNorth () => GrowPathTo(north); public GameTile GrowPathEast () => GrowPathTo(east); public GameTile GrowPathSouth () => GrowPathTo(south); public GameTile GrowPathWest () => GrowPathTo(west);
Pencarian luas
GameBoard
harus
GameBoard
bahwa semua ubin berisi data jalur yang benar. Kami melakukan ini dengan melakukan pencarian luas pertama. Mari kita mulai dengan ubin titik akhir, dan kemudian menumbuhkan jalur ke tetangganya, lalu ke tetangga dari ubin ini, dan seterusnya. Dengan setiap langkah, jarak bertambah satu, dan jalan tidak pernah tumbuh ke arah ubin yang sudah memiliki jalan. Ini memastikan bahwa semua petak sebagai hasilnya akan menunjuk di sepanjang jalur terpendek ke titik akhir.
Bagaimana dengan menemukan jalur menggunakan A *?Algoritma A
* adalah pengembangan evolusioner pencarian pertama. Ini berguna ketika kita mencari satu-satunya jalan terpendek. Tetapi kita membutuhkan semua jalur terpendek, sehingga A
* tidak memberikan keuntungan apa pun. Untuk contoh pencarian luas pertama dan A
* pada kisi segi enam dengan animasi, lihat seri tutorial tentang
peta segi enam .
Untuk melakukan pencarian, kita perlu melacak petak yang kita tambahkan ke jalur, tetapi dari sana kita belum mengembangkan jalur. Kumpulan ubin ini sering disebut perbatasan pencarian. Penting bahwa ubin diproses dalam urutan yang sama ketika ditambahkan ke perbatasan, jadi mari kita gunakan
Queue
. Nanti kita harus melakukan pencarian beberapa kali, jadi mari kita atur sebagai bidang
GameBoard
.
using UnityEngine; using System.Collections.Generic; public class GameBoard : MonoBehaviour { … Queue<GameTile> searchFrontier = new Queue<GameTile>(); … }
Agar keadaan lapangan selalu benar, kita harus menemukan jalur di akhir
Initialize
, tetapi menempatkan kode dalam metode
FindPaths
terpisah. Pertama-tama, Anda harus menghapus jalur semua ubin, lalu buat satu ubin titik akhir dan tambahkan ke perbatasan. Mari kita pilih ubin pertama. Karena
tiles
adalah array, kita dapat menggunakan
foreach
tanpa takut akan polusi memori. Jika nanti kita beralih dari array ke daftar, kita juga perlu mengganti
foreach
loop dengan
for
loop.
public void Initialize (Vector2Int size) { … FindPaths(); } void FindPaths () { foreach (GameTile tile in tiles) { tile.ClearPath(); } tiles[0].BecomeDestination(); searchFrontier.Enqueue(tiles[0]); }
Selanjutnya, kita perlu mengambil satu ubin dari perbatasan dan menumbuhkan jalur ke semua tetangganya, menambahkan mereka semua ke perbatasan. Pertama kita akan bergerak ke utara, lalu ke timur, selatan dan akhirnya ke barat.
public void FindPaths () { foreach (GameTile tile in tiles) { tile.ClearPath(); } tiles[0].BecomeDestination(); searchFrontier.Enqueue(tiles[0]); GameTile tile = searchFrontier.Dequeue(); searchFrontier.Enqueue(tile.GrowPathNorth()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathWest()); }
Kami ulangi tahap ini, sementara ada ubin di perbatasan.
while (searchFrontier.Count > 0) { GameTile tile = searchFrontier.Dequeue(); searchFrontier.Enqueue(tile.GrowPathNorth()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathWest()); }
Menumbuhkan jalan tidak selalu membawa kita ke ubin baru. Sebelum menambahkan ke antrian, kita perlu memeriksa nilai untuk
null
, tetapi kita dapat menunda cek untuk
null
sampai setelah output dari antrian.
GameTile tile = searchFrontier.Dequeue(); if (tile != null) { searchFrontier.Enqueue(tile.GrowPathNorth()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathWest()); }
Tampilkan jalurnya
Sekarang kami memiliki bidang yang berisi jalur yang benar, tetapi sejauh ini kami tidak melihat ini. Anda perlu mengkonfigurasi panah sehingga mereka menunjuk di sepanjang jalan melalui ubin mereka. Ini dapat dilakukan dengan memutarnya. Karena belokan ini selalu sama, kami menambahkan ke
GameTile
satu bidang
Quaternion
statis untuk masing-masing arah.
static Quaternion northRotation = Quaternion.Euler(90f, 0f, 0f), eastRotation = Quaternion.Euler(90f, 90f, 0f), southRotation = Quaternion.Euler(90f, 180f, 0f), westRotation = Quaternion.Euler(90f, 270f, 0f);
Juga tambahkan metode
ShowPath
umum. Jika jaraknya nol, maka petak adalah titik akhir dan tidak ada yang mengarah, jadi nonaktifkan panahnya. Jika tidak, aktifkan panah dan atur putarannya. Arah yang diinginkan dapat ditentukan dengan membandingkan
nextOnPath
dengan tetangganya.
public void ShowPath () { if (distance == 0) { arrow.gameObject.SetActive(false); return; } arrow.gameObject.SetActive(true); arrow.localRotation = nextOnPath == north ? northRotation : nextOnPath == east ? eastRotation : nextOnPath == south ? southRotation : westRotation; }
Panggil metode ini untuk semua ubin di akhir GameBoard.FindPaths
. public void FindPaths () { … foreach (GameTile tile in tiles) { tile.ShowPath(); } }
Menemukan cara.Mengapa kita tidak mengubah panah secara langsung menjadi GrowPathTo?Untuk memisahkan logika dan visualisasi pencarian. Nanti kita akan membuat visualisasi dinonaktifkan. Jika panah tidak muncul, kita tidak perlu memutarnya setiap kali kita memanggil FindPaths
.
Ubah prioritas pencarian
Ternyata ketika titik akhir adalah sudut barat daya, semua jalan menuju ke barat sampai mereka mencapai tepi lapangan, setelah itu mereka berbelok ke selatan. Semuanya benar di sini, karena sebenarnya tidak ada jalur yang lebih pendek ke titik akhir, karena gerakan diagonal tidak mungkin. Namun, ada banyak jalur terpendek lainnya yang mungkin terlihat lebih cantik.Untuk lebih memahami mengapa jalan seperti itu ditemukan, pindahkan titik akhir ke tengah peta. Dengan ukuran bidang yang aneh, itu hanya ubin di tengah array. tiles[tiles.Length / 2].BecomeDestination(); searchFrontier.Enqueue(tiles[tiles.Length / 2]);
Titik akhir di tengah.Hasilnya tampak logis jika Anda mengingat cara kerja pencarian. Karena kami menambahkan tetangga dalam urutan timur laut-barat daya, utara memiliki prioritas tertinggi. Karena kami melakukan pencarian dalam urutan terbalik, ini berarti bahwa arah terakhir yang kami tempuh adalah selatan. Itulah sebabnya mengapa hanya beberapa panah yang menunjuk ke selatan dan banyak yang menunjuk ke timur.Anda dapat mengubah hasilnya dengan menetapkan prioritas arah. Mari kita tukar timur dan selatan. Jadi kita harus mendapatkan simetri utara-selatan dan timur-barat. searchFrontier.Enqueue(tile.GrowPathNorth()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathWest())
Urutan pencarian adalah utara-tenggara-barat.Itu terlihat lebih cantik, tetapi lebih baik bagi jalan untuk mengubah arah, mendekati gerakan diagonal di mana ia akan terlihat alami. Kita dapat melakukan ini dengan membalik prioritas pencarian ubin tetangga dalam pola kotak-kotak.Alih-alih mencari tahu jenis ubin apa yang kami proses selama pencarian, kami menambah GameTile
properti umum yang menunjukkan apakah ubin saat ini merupakan alternatif. public bool IsAlternative { get; set; }
Kami akan mengatur properti ini di GameBoard.Initialize
. Pertama, tandai ubin sebagai alternatif jika koordinat X-nya genap. for (int i = 0, y = 0; y < size.y; y++) { for (int x = 0; x < size.x; x++, i++) { … tile.IsAlternative = (x & 1) == 0; } }
Apa operasi (x & 1) == 0 lakukan?— (AND). . 1, 1. 10101010 00001111 00001010.
. 0 1. 1, 2, 3, 4 1, 10, 11, 100. , .
AND , , . , .
Kedua, kami mengubah tanda hasil jika koordinat Y mereka genap. Jadi kita akan membuat pola catur. tile.IsAlternative = (x & 1) == 0; if ((y & 1) == 0) { tile.IsAlternative = !tile.IsAlternative; }
Seperti FindPaths
kita menjaga urutan yang sama seperti pencarian genteng alternatif, tapi untuk membuat kembali ke semua ubin lainnya. Ini akan memaksa jalan ke gerakan diagonal dan membuat zig-zag. if (tile != null) { if (tile.IsAlternative) { searchFrontier.Enqueue(tile.GrowPathNorth()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathWest()); } else { searchFrontier.Enqueue(tile.GrowPathWest()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathNorth()); } }
Urutan pencarian variabel.Mengubah Ubin
Pada titik ini, semua ubin kosong. Satu ubin digunakan sebagai titik akhir, tetapi selain tidak adanya panah yang terlihat, itu terlihat sama dengan orang lain. Kami akan menambahkan kemampuan untuk mengubah ubin dengan menempatkan objek di atasnya.Konten Ubin
Objek ubin sendiri hanyalah cara untuk melacak informasi ubin. Kami tidak memodifikasi objek ini secara langsung. Sebaliknya, tambahkan konten terpisah dan letakkan di bidang. Untuk saat ini, kita dapat membedakan antara ubin kosong dan ubin titik akhir. Untuk menunjukkan kasus-kasus ini, buat enumerasi GameTileContentType
. public enum GameTileContentType { Empty, Destination }
Selanjutnya, buat tipe komponen GameTileContent
yang memungkinkan Anda untuk mengatur jenis isinya melalui inspektur, dan aksesnya akan melalui properti pengambil yang umum. using UnityEngine; public class GameTileContent : MonoBehaviour { [SerializeField] GameTileContentType type = default; public GameTileContentType Type => type; }
Kemudian kami akan membuat cetakan untuk dua jenis konten, yang masing-masing memiliki komponen GameTileContent
dengan jenis yang ditentukan. Mari kita gunakan kubus pipih biru untuk menunjuk ubin titik akhir. Karena hampir rata, ia tidak memerlukan collider. Untuk menyiapkan konten kosong, gunakan objek game kosong.Rak pabrikan dari titik akhir dan konten kosong.Kami akan memberikan objek konten ke ubin kosong, karena semua ubin akan selalu memiliki konten, yang berarti kita tidak perlu memeriksa tautan ke konten untuk kesetaraan null
.Pabrik Konten
Untuk membuat konten dapat diedit, kami juga akan membuat pabrik untuk ini, menggunakan pendekatan yang sama seperti dalam tutorial Manajemen Objek . Ini berarti bahwa Anda GameTileContent
harus melacak pabrik asli Anda, yang harus ditetapkan hanya sekali, dan mengirim diri Anda kembali ke pabrik dengan metode ini Recycle
. GameTileContentFactory originFactory; … public GameTileContentFactory OriginFactory { get => originFactory; set { Debug.Assert(originFactory == null, "Redefined origin factory!"); originFactory = value; } } public void Recycle () { originFactory.Reclaim(this); }
Ini mengasumsikan keberadaan GameTileContentFactory
, oleh karena itu kami akan membuat jenis objek skrip untuk ini dengan metode yang diperlukan Recycle
. Pada tahap ini, kita tidak akan repot dengan penciptaan pabrik yang berfungsi penuh yang menggunakan konten, jadi kita akan membuatnya hanya menghancurkan isinya. Nantinya akan dimungkinkan untuk menambahkan penggunaan kembali objek ke pabrik tanpa mengubah sisa kode. using UnityEngine; using UnityEngine.SceneManagement; [CreateAssetMenu] public class GameTileContentFactory : ScriptableObject { public void Reclaim (GameTileContent content) { Debug.Assert(content.OriginFactory == this, "Wrong factory reclaimed!"); Destroy(content.gameObject); } }
Tambahkan metode tersembunyi Get
ke pabrik dengan prefab sebagai parameter. Di sini kita kembali melewatkan penggunaan kembali objek. Dia membuat instance objek, mengatur pabrik aslinya, memindahkannya ke adegan pabrik dan mengembalikannya. GameTileContent Get (GameTileContent prefab) { GameTileContent instance = Instantiate(prefab); instance.OriginFactory = this; MoveToFactoryScene(instance.gameObject); return instance; }
Mesin virtual telah dipindahkan ke adegan konten pabrik, yang dapat dibuat sesuai kebutuhan. Jika kita berada di editor, maka sebelum membuat adegan, kita perlu memeriksa apakah itu ada, jika kita kehilangan pandangan saat restart panas. Scene contentScene; … void MoveToFactoryScene (GameObject o) { if (!contentScene.isLoaded) { if (Application.isEditor) { contentScene = SceneManager.GetSceneByName(name); if (!contentScene.isLoaded) { contentScene = SceneManager.CreateScene(name); } } else { contentScene = SceneManager.CreateScene(name); } } SceneManager.MoveGameObjectToScene(o, contentScene); }
Kami hanya memiliki dua jenis konten, jadi tambahkan saja dua bidang konfigurasi prefab untuknya. [SerializeField] GameTileContent destinationPrefab = default; [SerializeField] GameTileContent emptyPrefab = default;
Hal terakhir yang perlu dilakukan agar pabrik dapat berfungsi adalah membuat metode umum Get
dengan parameter GameTileContentType
yang menerima instance dari cetakan yang sesuai. public GameTileContent Get (GameTileContentType type) { switch (type) { case GameTileContentType.Destination: return Get(destinationPrefab); case GameTileContentType.Empty: return Get(emptyPrefab); } Debug.Assert(false, "Unsupported type: " + type); return null; }
Apakah wajib menambahkan contoh kosong dari konten kosong ke setiap ubin?, . . , - , , , , . , . , , .
Mari kita buat aset pabrik dan konfigurasikan tautannya ke cetakan.Pabrik KontenDan kemudian meneruskan Game
tautannya ke pabrik. [SerializeField] GameTileContentFactory tileContentFactory = default;
Game dengan pabrik.Mengetuk ubin
Untuk mengubah bidang, kita harus dapat memilih ubin. Kami akan memungkinkannya dalam mode permainan. Kami akan memancarkan sinar ke adegan di tempat di mana pemain mengklik jendela permainan. Jika balok memotong dengan ubin, maka pemain menyentuhnya, itu harus diubah. Game
akan menangani input pemain, tetapi akan bertanggung jawab untuk menentukan ubin mana yang disentuh pemain GameBoard
.Tidak semua sinar berpotongan dengan ubin, jadi terkadang kita tidak akan menerima apa pun. Oleh karena itu, kami menambah GameBoard
metode GetTile
, yang selalu selalu mengembalikan pada awalnya null
(ini berarti bahwa ubin tidak ditemukan). public GameTile GetTile (Ray ray) { return null; }
Untuk menentukan apakah suatu sinar telah melewati ubin, kita perlu memanggil Physics.Raycast
dengan menentukan ray sebagai argumen. Ini mengembalikan informasi tentang apakah ada persimpangan. Jika demikian, maka kami dapat mengembalikan ubin, meskipun kami belum tahu yang mana, jadi untuk saat ini kami akan mengembalikannya null
. public GameTile TryGetTile (Ray ray) { if (Physics.Raycast(ray) { return null; } return null; }
Untuk mengetahui apakah ada persimpangan dengan ubin, kami memerlukan informasi lebih lanjut tentang persimpangan. Physics.Raycast
dapat memberikan informasi ini menggunakan parameter kedua RaycastHit
. Ini adalah parameter output, yang ditunjukkan oleh kata out
di depannya. Ini berarti bahwa pemanggilan metode dapat memberikan nilai ke variabel yang kami berikan. RaycastHit hit; if (Physics.Raycast(ray, out hit) { return null; }
Kita dapat menanamkan deklarasi variabel yang digunakan untuk parameter output, jadi mari kita lakukan. if (Physics.Raycast(ray, out RaycastHit hit) { return null; }
Kami tidak peduli dengan collider persimpangan mana yang terjadi, kami hanya menggunakan posisi persimpangan XZ untuk menentukan ubin. Kami mendapatkan koordinat ubin dengan menambahkan setengah ukuran bidang ke koordinat titik persimpangan, dan lalu mengonversi hasilnya ke nilai integer. Indeks petak akhir sebagai hasilnya adalah koordinat X ditambah koordinat Y dikalikan dengan lebar bidang. if (Physics.Raycast(ray, out RaycastHit hit)) { int x = (int)(hit.point.x + size.x * 0.5f); int y = (int)(hit.point.z + size.y * 0.5f); return tiles[x + y * size.x]; }
Tapi ini hanya mungkin ketika koordinat ubin berada di dalam bidang, jadi kami akan memeriksanya. Jika ini bukan masalahnya, maka ubin tidak akan dikembalikan. int x = (int)(hit.point.x + size.x * 0.5f); int y = (int)(hit.point.z + size.y * 0.5f); if (x >= 0 && x < size.x && y >= 0 && y < size.y) { return tiles[x + y * size.x]; }
Perubahan konten
Agar Anda dapat mengubah konten ubin, tambahkan ke GameTile
properti umum Content
. Pembuatnya hanya mengembalikan konten, dan penyetel membuang konten sebelumnya, jika ada, dan menempatkan konten baru. GameTileContent content; public GameTileContent Content { get => content; set { if (content != null) { content.Recycle(); } content = value; content.transform.localPosition = transform.localPosition; } }
Ini adalah satu-satunya tempat Anda perlu memeriksa konten null
, karena pada awalnya kami tidak memiliki konten. Untuk menjamin, kami menjalankan pernyataan agar setter tidak dipanggil dengan null
. set { Debug.Assert(value != null, "Null assigned to content!"); … }
Dan akhirnya, kita membutuhkan input pemain. Mengubah klik mouse menjadi sinar dapat dilakukan dengan memanggil ScreenPointToRay
dengan Input.mousePosition
sebagai argumen. Panggilan harus dilakukan untuk kamera utama, yang dapat diakses melalui Camera.main
. Tambahkan properti c untuk ini Game
. Ray TouchRay => Camera.main.ScreenPointToRay(Input.mousePosition);
Kemudian kami menambahkan metode Update
yang memeriksa apakah tombol mouse utama ditekan selama upgrade. Untuk melakukan ini, panggil Input.GetMouseButtonDown
dengan nol sebagai argumen. Jika tombol telah ditekan, kami memproses sentuhan pemain, yaitu, kami mengambil ubin dari bidang dan mengatur titik akhir sebagai isinya, mengambilnya dari pabrik. void Update () { if (Input.GetMouseButtonDown(0)) { HandleTouch(); } } void HandleTouch () { GameTile tile = GetTile(TouchRay); if (tile != null) { tile.Content = tileContentFactory.Get(GameTileContentType.Destination); } }
Sekarang kita dapat mengubah ubin apa pun menjadi titik akhir dengan menekan kursor.Beberapa titik akhir.Membuat bidang menjadi benar
Meskipun kami dapat mengubah ubin menjadi titik akhir, ini tidak mempengaruhi jalur sejauh ini. Selain itu, kami belum menetapkan konten kosong untuk ubin. Mempertahankan kebenaran dan integritas bidang adalah tugas GameBoard
, jadi mari kita beri dia tanggung jawab mengatur isi ubin. Untuk menerapkan ini, kami akan memberikannya tautan ke pabrik konten melalui metodenya Intialize
, dan menggunakannya untuk memberikan semua ubin contoh konten kosong. GameTileContentFactory contentFactory; public void Initialize ( Vector2Int size, GameTileContentFactory contentFactory ) { this.size = size; this.contentFactory = contentFactory; ground.localScale = new Vector3(size.x, size.y, 1f); tiles = new GameTile[size.x * size.y]; for (int i = 0, y = 0; y < size.y; y++) { for (int x = 0; x < size.x; x++, i++) { … tile.Content = contentFactory.Get(GameTileContentType.Empty); } } FindPaths(); }
Sekarang saya Game
harus memindahkan pabrik saya ke ladang. void Awake () { board.Initialize(boardSize, tileContentFactory); }
Mengapa tidak menambahkan bidang konfigurasi pabrik ke GameBoard?, , . , .
Karena sekarang kami memiliki beberapa titik akhir, kami mengubahnya GameBoard.FindPaths
sehingga ia memanggil BecomeDestination
masing-masing dan menambahkan semuanya ke perbatasan. Dan hanya itu yang diperlukan untuk mendukung banyak titik akhir. Semua ubin lainnya dibersihkan seperti biasa. Kemudian kami menghapus titik akhir hard-set di tengah. void FindPaths () { foreach (GameTile tile in tiles) { if (tile.Content.Type == GameTileContentType.Destination) { tile.BecomeDestination(); searchFrontier.Enqueue(tile); } else { tile.ClearPath(); } }
Tetapi jika kita dapat mengubah ubin menjadi titik akhir, maka kita harus dapat melakukan operasi terbalik, mengubah titik akhir menjadi ubin kosong. Tapi kemudian kita bisa mendapatkan bidang tanpa titik akhir sama sekali. Dalam hal ini, FindPaths
tidak akan dapat melakukan tugasnya. Ini terjadi ketika perbatasan kosong setelah inisialisasi jalur untuk semua sel. Kami menyatakan ini sebagai keadaan lapangan yang tidak valid, mengembalikan false
dan menyelesaikan eksekusi; jika tidak kembali pada akhirnya true
. bool FindPaths () { foreach (GameTile tile in tiles) { … } if (searchFrontier.Count == 0) { return false; } … return true; }
Cara termudah untuk mengimplementasikan dukungan untuk menghapus titik akhir, menjadikannya operasi sakelar. Dengan mengeklik ubin kosong, kami akan mengubahnya menjadi titik akhir, dan dengan mengklik titik akhir, kami akan menghapusnya. Tapi sekarang ia terlibat dalam mengubah konten GameBoard
, jadi kami akan memberikannya metode umum ToggleDestination
, yang parameternya adalah ubin. Jika ubin adalah titik akhir, maka kosongkan dan panggil FindPaths
. Kalau tidak, kita menjadikannya sebagai titik akhir dan juga menyebutnya FindPaths
. public void ToggleDestination (GameTile tile) { if (tile.Content.Type == GameTileContentType.Destination) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } else { tile.Content = contentFactory.Get(GameTileContentType.Destination); FindPaths(); } }
Menambahkan titik akhir tidak pernah dapat membuat keadaan bidang tidak valid, dan menghapus titik akhir bisa. Karenanya, kami akan memeriksa apakah berhasil mengeksekusi FindPaths
setelah kami membuat ubin kosong. Jika tidak, maka batalkan perubahan, balikkan ubin ke titik akhir, dan panggil lagi FindPaths
untuk kembali ke kondisi yang benar sebelumnya. if (tile.Content.Type == GameTileContentType.Destination) { tile.Content = contentFactory.Get(GameTileContentType.Empty); if (!FindPaths()) { tile.Content = contentFactory.Get(GameTileContentType.Destination); FindPaths(); } }
Bisakah validasi dibuat lebih efisien?, . , . , . FindPaths
, .
Sekarang pada akhirnya Initialize
kita dapat memanggil ToggleDestination
dengan ubin pusat sebagai argumen, alih-alih memanggil secara eksplisit FindPaths
. Ini adalah satu-satunya saat kami mulai dengan keadaan bidang yang tidak valid, tetapi kami dijamin akan berakhir dengan keadaan yang benar. public void Initialize ( Vector2Int size, GameTileContentFactory contentFactory ) { …
Akhirnya, kami memaksa untuk Game
menelepon ToggleDestination
alih-alih mengatur konten ubin itu sendiri. void HandleTouch () { GameTile tile = board.GetTile(TouchRay); if (tile != null) {
Banyak titik akhir dengan jalur yang benar.Bukankah kita seharusnya melarang Game untuk mengatur konten ubin secara langsung?. . , Game
. , .
Dindingnya
Tujuan pertahanan menara adalah untuk mencegah musuh mencapai titik akhir. Tujuan ini dicapai dengan dua cara. Pertama, kita membunuh mereka, dan kedua, kita memperlambat mereka sehingga ada lebih banyak waktu untuk membunuh mereka. Di bidang ubin, waktu dapat diperpanjang, meningkatkan jarak yang harus dilalui musuh. Ini bisa dicapai dengan menempatkan rintangan di lapangan. Biasanya ini adalah menara yang juga membunuh musuh, tetapi dalam tutorial ini kita akan membatasi diri kita hanya untuk tembok saja.Konten
Dinding adalah jenis konten lain, jadi mari kita tambahkan GameTileContentType
elemen ke dalamnya. public enum GameTileContentType { Empty, Destination, Wall }
Kemudian buat prefab dinding. Kali ini, buat objek game untuk konten ubin dan tambahkan kubus anak ke dalamnya, yang akan berada di atas bidang dan isi seluruh ubin. Buat setengah unit tinggi dan simpan collider, karena dinding secara visual dapat tumpang tindih bagian dari ubin di belakangnya. Karena itu, ketika seorang pemain menyentuh dinding, ia akan mempengaruhi ubin yang sesuai.Dinding Rak Pabrikan.Tambahkan prefab dinding ke pabrik, baik dalam kode dan inspektur. [SerializeField] GameTileContent wallPrefab = default; … public GameTileContent Get (GameTileContentType type) { switch (type) { case GameTileContentType.Destination: return Get(destinationPrefab); case GameTileContentType.Empty: return Get(emptyPrefab); case GameTileContentType.Wall: return Get(wallPrefab); } Debug.Assert(false, "Unsupported type: " + type); return null; }
Pabrik dengan dinding cetakan.Nyalakan dan matikan dinding
Tambahkan ke GameBoard
metode on / off dinding, seperti yang kita lakukan untuk titik akhir. Awalnya, kami tidak akan memeriksa kondisi bidang yang salah. public void ToggleWall (GameTile tile) { if (tile.Content.Type == GameTileContentType.Wall) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } else { tile.Content = contentFactory.Get(GameTileContentType.Wall); FindPaths(); } }
Kami akan memberikan dukungan untuk beralih hanya antara ubin kosong dan ubin dinding, tidak memungkinkan dinding untuk secara langsung mengganti titik akhir. Karena itu, kami hanya akan membuat dinding ketika ubin kosong. Selain itu, dinding harus memblokir pencarian jalan. Tetapi setiap ubin harus memiliki jalur ke titik akhir, jika tidak musuh akan terjebak. Untuk melakukan ini, kita lagi perlu menggunakan validasi FindPaths
, dan membuang perubahan jika mereka membuat keadaan bidang yang salah. else if (tile.Content.Type == GameTileContentType.Empty) { tile.Content = contentFactory.Get(GameTileContentType.Wall); if (!FindPaths()) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } }
Menghidupkan dan mematikan dinding akan digunakan lebih sering daripada menyalakan dan mematikan titik akhir, jadi kami akan membuat dinding-dinding penghubung dengan Game
sentuhan utama. Titik akhir dapat diaktifkan dengan sentuhan tambahan (biasanya tombol mouse kanan), yang dapat dikenali dengan memberikan Input.GetMouseButtonDown
nilai 1. void Update () { if (Input.GetMouseButtonDown(0)) { HandleTouch(); } else if (Input.GetMouseButtonDown(1)) { HandleAlternativeTouch(); } } void HandleAlternativeTouch () { GameTile tile = board.GetTile(TouchRay); if (tile != null) { board.ToggleDestination(tile); } } void HandleTouch () { GameTile tile = board.GetTile(TouchRay); if (tile != null) { board.ToggleWall(tile); } }
Sekarang kita memiliki dinding.Mengapa saya mendapatkan celah besar di antara bayang-bayang dinding yang berdekatan secara diagonal?, , , . , , far clipping plane . , far plane 20 . , MSAA, .
Mari kita juga memastikan bahwa titik akhir tidak dapat langsung menggantikan dinding. public void ToggleDestination (GameTile tile) { if (tile.Content.Type == GameTileContentType.Destination) { … } else if (tile.Content.Type == GameTileContentType.Empty) { tile.Content = contentFactory.Get(GameTileContentType.Destination); FindPaths(); } }
Path Search Lock
Agar dinding memblokir pencarian jalur, cukup bagi kami untuk tidak menambahkan ubin dengan dinding ke batas pencarian. Ini dapat dilakukan dengan memaksa GameTile.GrowPathTo
untuk tidak mengembalikan ubin dengan dinding. Tetapi jalan harus tetap tumbuh ke arah dinding, sehingga semua ubin di lapangan memiliki jalan. Hal ini diperlukan karena ada kemungkinan ubin dengan musuh tiba-tiba akan berubah menjadi dinding. GameTile GrowPathTo (GameTile neighbor) { if (!HasPath || neighbor == null || neighbor.HasPath) { return null; } neighbor.distance = distance + 1; neighbor.nextOnPath = this; return neighbor.Content.Type != GameTileContentType.Wall ? neighbor : null; }
Untuk memastikan bahwa semua ubin memiliki jalur, mereka GameBoard.FindPaths
harus memeriksa ini setelah pencarian selesai. Jika tidak demikian, maka keadaan bidang tidak benar dan perlu dikembalikan false
. Tidak perlu memperbarui visualisasi jalur untuk keadaan tidak valid, karena bidang akan kembali ke keadaan sebelumnya. bool FindPaths () { … foreach (GameTile tile in tiles) { if (!tile.HasPath) { return false; } } foreach (GameTile tile in tiles) { tile.ShowPath(); } return true; }
Dinding memengaruhi jalan.Untuk memastikan bahwa dinding benar-benar memiliki jalur yang benar, Anda harus membuat kubusnya tembus cahaya.Dinding transparan.Perhatikan bahwa persyaratan kebenaran semua jalur tidak memungkinkan dinding untuk menutup bagian dari bidang di mana tidak ada titik akhir. Kami dapat membagi peta, tetapi hanya jika ada setidaknya satu titik akhir di setiap bagian. Selain itu, setiap dinding harus bersebelahan dengan ubin kosong atau titik akhir, jika tidak maka tidak akan dapat memiliki jalur. Sebagai contoh, adalah mustahil untuk membuat blok padat 3 × 3 dinding.Sembunyikan jalannya
Visualisasi jalur memungkinkan kita untuk melihat cara kerja pencarian jalur dan memastikan bahwa itu memang benar. Tapi itu tidak perlu ditunjukkan kepada pemain, atau setidaknya belum tentu. Karena itu, mari berikan kemampuan untuk mematikan panah. Ini dapat dilakukan dengan menambahkan GameTile
metode umum HidePath
, yang hanya menonaktifkan panahnya. public void HidePath () { arrow.gameObject.SetActive(false); }
Status pemetaan jalur adalah bagian dari status bidang. Tambahkan GameBoard
bidang boolean ke default sama false
dengan melacak statusnya, serta properti umum sebagai pengambil dan penyetel. Setter harus menunjukkan atau menyembunyikan jalur di semua ubin. bool showPaths; public bool ShowPaths { get => showPaths; set { showPaths = value; if (showPaths) { foreach (GameTile tile in tiles) { tile.ShowPath(); } } else { foreach (GameTile tile in tiles) { tile.HidePath(); } } } }
Sekarang metode FindPaths
harus menunjukkan jalur yang diperbarui hanya jika rendering diaktifkan. bool FindPaths () { … if (showPaths) { foreach (GameTile tile in tiles) { tile.ShowPath(); } } return true; }
Secara default, visualisasi jalur dinonaktifkan. Matikan panah di pabrikan ubin.Panah pabrikan tidak aktif secara default.Kami membuatnya sehingga Game
mengubah status visualisasi ketika tombol ditekan. Adalah logis untuk menggunakan tombol P, tetapi juga merupakan tombol pintas untuk mengaktifkan / menonaktifkan mode permainan di editor Unity. Akibatnya, visualisasi akan beralih ketika hotkey untuk keluar dari mode permainan digunakan, yang tidak terlihat sangat bagus. Jadi mari kita gunakan tombol V (kependekan dari visualisasi).Tidak ada panah.Tampilan kotak
Ketika panah disembunyikan, menjadi sulit untuk membedakan lokasi setiap ubin. Mari kita tambahkan garis kisi. Unduh tekstur garis batas persegi dari sini yang dapat digunakan sebagai garis ubin terpisah.Tekstur jala.Kami tidak akan menambahkan tekstur ini secara individual ke setiap ubin, tetapi menerapkannya ke tanah. Tetapi kami akan membuat kisi-kisi ini opsional, serta visualisasi jalur. Oleh karena itu, kami akan menambah GameBoard
bidang konfigurasi Texture2D
dan memilih tekstur jala untuknya. [SerializeField] Texture2D gridTexture = default;
Bidang dengan tekstur jala.Tambahkan bidang Boolean lain dan properti untuk mengontrol keadaan visualisasi kisi. Dalam hal ini, setter harus mengubah materi bumi, yang dapat diimplementasikan dengan memanggil GetComponent<MeshRenderer>
bumi dan mendapatkan akses ke properti material
hasil. Jika kisi perlu ditampilkan, maka kami akan menetapkan mainTexture
tekstur kisi ke properti material. Kalau tidak, berikan itu padanya null
. Perhatikan bahwa ketika Anda mengubah tekstur material, duplikat instance material akan dibuat, sehingga menjadi independen dari aset material. bool showGrid, showPaths; public bool ShowGrid { get => showGrid; set { showGrid = value; Material m = ground.GetComponent<MeshRenderer>().material; if (showGrid) { m.mainTexture = gridTexture; } else { m.mainTexture = null; } } }
Mari kita Game
beralih visualisasi grid dengan tombol G. void Update () { … if (Input.GetKeyDown(KeyCode.G)) { board.ShowGrid = !board.ShowGrid; } }
Juga, tambahkan visualisasi mesh default ke Awake
. void Awake () { board.Initialize(boardSize, tileContentFactory); board.ShowGrid = true; }
Kisi yang tidak dihitung.Sejauh ini kami punya perbatasan di seluruh bidang. Ini cocok dengan teksturnya, tapi bukan itu yang kita butuhkan. Kita perlu skala tekstur utama bahan agar sesuai dengan ukuran kotak. Anda dapat melakukan ini dengan memanggil metode SetTextureScale
material dengan nama properti tekstur ( _MainTex ) dan ukuran dua dimensi. Kita bisa menggunakan ukuran bidang secara langsung, yang secara tidak langsung dikonversi ke nilai Vector2
. if (showGrid) { m.mainTexture = gridTexture; m.SetTextureScale("_MainTex", size); }
Grid berskala dengan visualisasi jalur dimatikan dan dihidupkan.Jadi, pada tahap ini, kami memiliki bidang yang berfungsi untuk permainan ubin genre menara pertahanan. Dalam tutorial selanjutnya kita akan menambahkan musuh.RepositoriPDF