Bagian 1-3: jala, warna, dan tinggi selBagian 4-7: gundukan, sungai, dan jalanBagian 8-11: air, bentang alam, dan bentengBagian 12-15: menyimpan dan memuat, tekstur, jarakBagian 16-19: menemukan jalan, regu pemain, animasiBagian 20-23: Kabut Perang, Penelitian Peta, Generasi ProseduralBagian 24-27: siklus air, erosi, bioma, peta silindrisBagian 12: simpan dan muat
- Lacak jenis medan alih-alih warna.
- Buat file.
- Kami menulis data ke file, dan kemudian membacanya.
- Kami membuat serialisasi data sel.
- Kurangi ukuran file.
Kami sudah tahu cara membuat peta yang cukup menarik. Sekarang Anda perlu belajar bagaimana cara menyimpannya.
Dimuat dari file test.map .Jenis medan
Saat menyimpan peta, kita tidak perlu menyimpan semua data yang kita lacak selama eksekusi aplikasi. Sebagai contoh, kita hanya perlu mengingat level tinggi sel. Posisi vertikal itu sendiri diambil dari data ini, jadi Anda tidak perlu menyimpannya. Sebenarnya, lebih baik jika kita tidak menyimpan metrik yang dihitung ini. Dengan demikian, data peta akan tetap benar, bahkan jika nanti kami memutuskan untuk mengubah offset ketinggian. Data terpisah dari presentasinya.
Demikian pula, kita tidak perlu menyimpan warna sel yang tepat. Anda dapat menulis bahwa selnya berwarna hijau. Namun warna hijau yang tepat bisa berubah dengan perubahan gaya visual. Untuk melakukan ini, kita dapat menyimpan indeks warna, bukan warna itu sendiri. Bahkan, mungkin cukup bagi kita untuk menyimpan indeks ini daripada warna asli dalam sel saat runtime. Ini akan memungkinkan nantinya untuk beralih ke visualisasi lega yang lebih kompleks.
Memindahkan sederetan warna
Jika sel tidak lagi memiliki data warna, maka itu harus disimpan di tempat lain. Paling nyaman menyimpannya di
HexMetrics
. Jadi mari kita tambahkan array warna ke dalamnya.
public static Color[] colors;
Seperti semua data global lainnya, seperti noise, kita dapat menginisialisasi warna-warna ini dengan
HexGrid
.
public Color[] colors; … void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexMetrics.colors = colors; … } … void OnEnable () { if (!HexMetrics.noiseSource) { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexMetrics.colors = colors; } }
Dan karena sekarang kami tidak menetapkan warna langsung ke sel, kami akan menyingkirkan warna default.
Atur warna baru agar sesuai dengan susunan umum editor peta segi enam.
Warna ditambahkan ke kisi.Refactoring sel
Hapus bidang warna dari
HexCell
. Sebaliknya, kami akan menyimpan indeks. Alih-alih indeks warna, kami menggunakan indeks jenis relief yang lebih umum.
Properti warna dapat menggunakan indeks ini hanya untuk mendapatkan warna yang sesuai. Sekarang tidak diatur secara langsung, jadi hapus bagian ini. Dalam hal ini, kami mendapatkan kesalahan kompilasi, yang akan segera kami perbaiki.
public Color Color { get { return HexMetrics.colors[terrainTypeIndex]; }
Tambahkan properti baru untuk mendapatkan dan mengatur indeks tipe ketinggian baru.
public int TerrainTypeIndex { get { return terrainTypeIndex; } set { if (terrainTypeIndex != value) { terrainTypeIndex = value; Refresh(); } } }
Editor refactoring
Di dalam
HexMapEditor
menghapus semua kode terkait warna. Ini akan memperbaiki kesalahan kompilasi.
Sekarang tambahkan bidang dan metode untuk mengontrol indeks tipe elevasi aktif.
int activeTerrainTypeIndex; … public void SetTerrainTypeIndex (int index) { activeTerrainTypeIndex = index; }
Kami menggunakan metode ini sebagai pengganti metode
SelectColor
sekarang hilang. Hubungkan widget warna di UI dengan
SetTerrainTypeIndex
, biarkan semuanya tidak berubah. Ini berarti bahwa indeks negatif masih digunakan dan berarti warnanya tidak boleh berubah.
Ubah
EditCell
sehingga indeks tipe elevasi ditetapkan ke sel yang sedang diedit.
void EditCell (HexCell cell) { if (cell) { if (activeTerrainTypeIndex >= 0) { cell.TerrainTypeIndex = activeTerrainTypeIndex; } … } }
Meskipun kami menghapus data warna dari sel, peta harus bekerja sama seperti sebelumnya. Satu-satunya perbedaan adalah bahwa warna default sekarang adalah yang pertama dalam array. Dalam kasus saya ini berwarna kuning.
Kuning adalah warna default baru.paket unityMenyimpan data ke file
Untuk mengontrol penyimpanan dan pemuatan peta, kami menggunakan
HexMapEditor
. Kami akan membuat dua metode yang akan melakukan ini, dan untuk saat ini biarkan semuanya kosong.
public void Save () { } public void Load () { }
Tambahkan dua tombol ke UI (
GameObject / UI / Button ). Hubungkan mereka ke tombol dan berikan label yang sesuai. Saya menempatkannya di bagian bawah panel kanan.
Simpan dan Muat tombol.Lokasi file
Untuk menyimpan kartu, Anda harus menyimpannya di suatu tempat. Seperti yang dilakukan di sebagian besar game, kami akan menyimpan data dalam file. Tetapi di mana harus meletakkan file ini dalam sistem file? Jawabannya tergantung pada sistem operasi mana game berjalan. Setiap OS memiliki standar sendiri untuk menyimpan file yang terkait dengan aplikasi.
Kita tidak perlu tahu standar-standar ini. Unity tahu jalan yang benar yang bisa kita dapatkan dengan
Application.persistentDataPath
. Anda dapat memeriksa bagaimana hal itu akan terjadi dengan Anda, dalam metode
Save
, menampilkannya di konsol dan menekan tombol dalam mode Play.
public void Save () { Debug.Log(Application.persistentDataPath); }
Pada sistem desktop, jalur akan berisi nama perusahaan dan produk. Jalur ini digunakan oleh editor dan majelis. Nama dapat dikonfigurasi dalam
Edit / Pengaturan Proyek / Player .
Nama perusahaan dan produk.Mengapa saya tidak dapat menemukan folder Library di Mac?Folder Perpustakaan sering disembunyikan. Cara menampilkannya tergantung pada versi OS X. Jika Anda tidak memiliki versi yang lebih lama, pilih folder beranda di Finder dan buka Show Show Options . Ada kotak centang untuk folder Perpustakaan .
Bagaimana dengan WebGL?Game WebGL tidak dapat mengakses sistem file pengguna. Sebaliknya, semua operasi file diarahkan ke sistem file yang terletak di memori. Dia transparan bagi kita. Namun, untuk menyimpan data, Anda harus memesan halaman web secara manual untuk membuang data ke dalam penyimpanan browser.
Pembuatan file
Untuk membuat file, kita perlu menggunakan kelas dari namespace
System.IO
. Oleh karena itu, kami menambahkan pernyataan
using
untuk itu di atas kelas
HexMapEditor
.
using UnityEngine; using UnityEngine.EventSystems; using System.IO; public class HexMapEditor : MonoBehaviour { … }
Pertama kita perlu membuat path lengkap ke file. Kami menggunakan
test.map sebagai
nama file. Itu harus ditambahkan ke jalur data yang disimpan. Apakah Anda perlu memasukkan maju atau backslash (garis miring atau garis miring terbalik) tergantung pada platform. Metode
Path.Combine
akan
Path.Combine
.
public void Save () { string path = Path.Combine(Application.persistentDataPath, "test.map"); }
Selanjutnya, kita perlu mengakses file di lokasi ini. Kami melakukan ini menggunakan metode
File.Open
. Karena kita ingin menulis data ke file ini, kita perlu menggunakan mode buatnya. Dalam hal ini, file baru akan dibuat pada jalur yang ditentukan, atau file yang sudah ada akan diganti.
string path = Path.Combine(Application.persistentDataPath, "test.map"); File.Open(path, FileMode.Create);
Hasil memanggil metode ini akan menjadi aliran data terbuka yang terkait dengan file ini. Kita bisa menggunakannya untuk menulis data ke file. Dan kita tidak boleh lupa untuk menutup arus ketika kita tidak lagi membutuhkannya.
string path = Path.Combine(Application.persistentDataPath, "test.map"); Stream fileStream = File.Open(path, FileMode.Create); fileStream.Close();
Pada tahap ini, ketika Anda mengklik tombol
Simpan , file
test.map akan dibuat di folder yang ditentukan sebagai jalur ke data yang disimpan. Jika Anda mempelajari file ini, itu akan kosong dan memiliki ukuran 0 byte, karena sejauh ini kami belum menulis apa pun untuknya.
Menulis ke file
Untuk menulis data ke file, kita perlu cara untuk mengalirkan data ke file. Cara termudah untuk melakukannya adalah dengan
BinaryWriter
. Objek-objek ini memungkinkan Anda untuk menulis data primitif ke aliran apa pun.
Buat objek
BinaryWriter
baru, dan aliran file kami akan menjadi argumennya. Penulis penutup menutup aliran yang digunakannya. Karenanya, kita tidak perlu lagi menyimpan tautan langsung ke aliran.
string path = Path.Combine(Application.persistentDataPath, "test.map"); BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)); writer.Close();
Untuk mentransfer data ke aliran, kita dapat menggunakan metode
BinaryWriter.Write
. Ada varian dari metode
Write
untuk semua tipe primitif, seperti integer dan float. Itu juga dapat merekam garis. Mari kita coba menulis bilangan bulat 123.
BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)); writer.Write(123); writer.Close();
Klik tombol
Simpan dan periksa
test.map lagi. Sekarang ukurannya adalah 4 byte, karena ukuran integer adalah 4 byte.
Mengapa manajer file saya menunjukkan bahwa file tersebut membutuhkan lebih banyak ruang?Karena sistem file membagi ruang menjadi blok byte. Mereka tidak melacak byte individu. Karena test.map hanya membutuhkan empat byte sejauh ini, ia membutuhkan satu blok ruang penyimpanan.
Perhatikan bahwa kami menyimpan data biner, bukan teks yang dapat dibaca manusia. Karena itu, jika kita membuka file dalam editor teks, kita akan melihat serangkaian karakter yang tidak jelas. Anda mungkin akan melihat simbol
{ diikuti oleh tidak ada atau beberapa penampung.
Anda dapat membuka file dalam hex editor. Dalam hal ini, kita akan melihat
7b 00 00 00 . Ini adalah empat byte integer kami, yang dipetakan dalam notasi heksadesimal. Dalam angka desimal biasa, ini adalah
123 0 0 0 . Dalam biner, byte pertama terlihat seperti
01111011 .
Kode ASCII untuk
{ adalah 123, sehingga karakter ini dapat ditampilkan dalam editor teks. ASCII 0 adalah karakter nol yang tidak cocok dengan karakter apa pun yang terlihat.
Tiga byte sisanya sama dengan nol, karena kami menulis angka kurang dari 256. Jika kami menulis 256, kita akan melihat
00 01 00 00 di hex editor.
Bukankah seharusnya 123 disimpan sebagai 00 00 00 7b?BinaryWriter
menggunakan format little-endian untuk menyimpan angka. Ini berarti bahwa byte paling signifikan ditulis terlebih dahulu. Format ini digunakan oleh Microsoft dalam pengembangan kerangka .Net. Itu mungkin dipilih karena CPU Intel menggunakan format little-endian.
Alternatif untuk itu adalah big-endian, di mana byte paling signifikan disimpan terlebih dahulu. Ini sesuai dengan urutan nomor yang biasa. 123 adalah seratus dua puluh tiga karena yang kami maksud adalah rekaman big-endian. Jika itu adalah endian kecil, maka itu berarti tiga ratus dua puluh satu.
Kami membuat sumber daya gratis
Adalah penting bahwa kita menutup penulis. Ketika terbuka, sistem file mengunci file, mencegah proses lain dari menulis ke sana. Jika kita lupa untuk menutupnya, kita akan memblokir diri kita juga. Jika kita menekan tombol simpan dua kali, kedua kalinya kita tidak akan dapat membuka aliran.
Alih-alih menutup penulis secara manual, kita dapat membuat blok
using
untuk ini. Ini mendefinisikan ruang lingkup di mana penulis valid. Ketika kode yang dapat dieksekusi melampaui ruang lingkup ini, penulis dihapus dan utas ditutup.
using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(123); }
Ini akan berfungsi karena kelas penulis dan aliran file mengimplementasikan antarmuka
IDisposable
. Objek-objek ini memiliki metode
Dispose
, yang secara tidak langsung dipanggil ketika mereka melampaui ruang lingkup
using
.
Keuntungan besar
using
adalah bahwa ia bekerja tidak peduli bagaimana program kehabisan ruang lingkup. Pengembalian awal, pengecualian dan kesalahan tidak mengganggunya. Selain itu, dia sangat ringkas.
Pengambilan data
Untuk membaca data yang ditulis sebelumnya, kita perlu memasukkan kode ke dalam metode
Load
. Seperti dalam kasus penyimpanan, kita perlu membuat jalur dan membuka aliran file. Perbedaannya adalah bahwa sekarang kita membuka file untuk membaca, bukan menulis. Dan alih-alih penulis, kita membutuhkan
BinaryReader
.
public void Load () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using ( BinaryReader reader = new BinaryReader(File.Open(path, FileMode.Open)) ) { } }
Dalam hal ini, kita dapat menggunakan metode
File.OpenRead
untuk membuka file untuk dibaca.
using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { }
Mengapa kita tidak bisa menggunakan File.OpenWrite saat menulis?Metode ini menciptakan aliran yang menambahkan data ke file yang ada, daripada menggantikannya.
Saat membaca, kita perlu secara eksplisit menunjukkan jenis data yang diterima. Untuk membaca integer dari stream, kita perlu menggunakan
BinaryReader.ReadInt32
. Metode ini membaca bilangan bulat 32-bit, yaitu empat byte.
using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { Debug.Log(reader.ReadInt32()); }
Perlu dicatat bahwa ketika menerima
123, itu akan cukup bagi kita untuk membaca satu byte. Tetapi pada saat yang sama, tiga byte milik integer ini akan tetap ada di aliran. Selain itu, ini tidak akan berfungsi untuk angka di luar interval 0-255. Karena itu, jangan lakukan itu.
paket unityMenulis dan membaca data peta
Saat menyimpan data, pertanyaan penting adalah apakah akan menggunakan format yang dapat dibaca manusia. Biasanya, format yang dapat dibaca manusia adalah JSON, XML, dan ASCII biasa dengan beberapa jenis struktur. File-file tersebut dapat dibuka, ditafsirkan dan diedit dalam editor teks. Selain itu, mereka menyederhanakan pertukaran data antara aplikasi yang berbeda.
Namun, format tersebut memiliki persyaratan sendiri. File akan memakan lebih banyak ruang (terkadang lebih banyak) daripada menggunakan data biner. Mereka juga dapat sangat meningkatkan biaya pengodean dan penguraian data, baik dari segi runtime dan jejak memori.
Sebaliknya, data biner kompak dan cepat. Ini penting saat merekam data dalam jumlah besar. Misalnya, saat menyimpan peta besar secara otomatis di setiap belokan game. Oleh karena itu
kita akan menggunakan format biner. Jika Anda dapat menangani ini, Anda dapat bekerja dengan format yang lebih rinci.
Bagaimana dengan serialisasi otomatis?Segera selama proses serialisasi data Unity, kita dapat langsung menulis kelas serial ke stream. Detail rekaman masing-masing bidang akan disembunyikan dari kami. Namun, kami tidak dapat membuat serialisasi sel secara langsung. Mereka adalah kelas MonoBehaviour
yang berisi data yang tidak perlu kita simpan. Oleh karena itu, kita perlu menggunakan hierarki objek yang terpisah, yang menghancurkan kesederhanaan serialisasi otomatis. Selain itu, akan lebih sulit untuk mendukung perubahan kode di masa depan. Oleh karena itu, kami akan mempertahankan kontrol penuh dengan serialisasi manual. Selain itu, itu akan membuat kita benar-benar mengerti apa yang sedang terjadi.
Untuk membuat serial peta, kita perlu menyimpan data setiap sel. Untuk menyimpan dan memuat sel tunggal, tambahkan metode
Save
dan
Load
ke
HexCell
. Karena mereka membutuhkan penulis atau pembaca untuk bekerja, kami akan menambahkannya sebagai parameter.
using UnityEngine; using System.IO; public class HexCell : MonoBehaviour { … public void Save (BinaryWriter writer) { } public void Load (BinaryReader reader) { } }
Tambahkan metode
Save
dan
Load
ke
HexGrid
. Metode ini memintas semua sel dengan memanggil metode
Load
dan
Save
.
using UnityEngine; using UnityEngine.UI; using System.IO; public class HexGrid : MonoBehaviour { … public void Save (BinaryWriter writer) { for (int i = 0; i < cells.Length; i++) { cells[i].Save(writer); } } public void Load (BinaryReader reader) { for (int i = 0; i < cells.Length; i++) { cells[i].Load(reader); } } }
Jika kita mengunduh peta, peta itu perlu diperbarui setelah data sel diubah. Untuk melakukan ini, cukup perbarui semua fragmen.
public void Load (BinaryReader reader) { for (int i = 0; i < cells.Length; i++) { cells[i].Load(reader); } for (int i = 0; i < chunks.Length; i++) { chunks[i].Refresh(); } }
Akhirnya, kami mengganti kode pengujian kami di
HexMapEditor
dengan panggilan ke metode
Save
dan
Load
kisi, yang meneruskan penulis atau pembaca dengannya.
public void Save () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { hexGrid.Save(writer); } } public void Load () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { hexGrid.Load(reader); } }
Menyimpan tipe bantuan
Pada tahap saat ini, menyimpan kembali membuat file kosong, dan mengunduh tidak menghasilkan apa-apa. Mari kita mulai secara bertahap dengan merekam dan memuat hanya indeks tipe elevasi
HexCell
.
Tetapkan nilai langsung ke bidang terrainTypeIndex. Kami tidak akan menggunakan properti. Karena kami secara eksplisit memperbarui semua fragmen, panggilan ke properti
Refresh
tidak diperlukan. Selain itu, karena kami hanya menyimpan peta yang benar, kami akan menganggap bahwa semua peta yang diunduh juga benar. Karena itu, misalnya, kami tidak akan memeriksa apakah sungai atau jalan itu diizinkan.
public void Save (BinaryWriter writer) { writer.Write(terrainTypeIndex); } public void Load (BinaryReader reader) { terrainTypeIndex = reader.ReadInt32(); }
Saat menyimpan ke file ini, satu demi satu indeks jenis bantuan semua sel akan ditulis. Karena indeks adalah bilangan bulat, ukurannya empat byte. Kartu saya berisi 300 sel, mis. Ukuran file adalah 1200 byte.
Muatan membaca indeks dalam urutan yang sama di mana mereka ditulis. Jika Anda mengubah warna sel setelah menyimpan, maka memuat peta akan mengembalikan warna ke status saat menyimpan. Karena kami tidak lagi menyimpan apa pun, sisa data sel akan tetap sama. Artinya, pemuatan akan mengubah jenis medan, tetapi tidak tinggi, ketinggian air, fitur medan, dll.
Menyimpan Semua Integer
Menyimpan indeks jenis bantuan tidak cukup bagi kami. Anda perlu menyimpan semua data lainnya. Mari kita mulai dengan semua bidang bilangan bulat. Ini adalah indeks dari jenis bantuan, tinggi sel, tingkat air, tingkat kota, tingkat pertanian, tingkat vegetasi dan indeks benda-benda khusus. Mereka harus dibaca dalam urutan yang sama ketika mereka direkam.
public void Save (BinaryWriter writer) { writer.Write(terrainTypeIndex); writer.Write(elevation); writer.Write(waterLevel); writer.Write(urbanLevel); writer.Write(farmLevel); writer.Write(plantLevel); writer.Write(specialIndex); } public void Load (BinaryReader reader) { terrainTypeIndex = reader.ReadInt32(); elevation = reader.ReadInt32(); waterLevel = reader.ReadInt32(); urbanLevel = reader.ReadInt32(); farmLevel = reader.ReadInt32(); plantLevel = reader.ReadInt32(); specialIndex = reader.ReadInt32(); }
Coba sekarang untuk menyimpan dan memuat peta, membuat perubahan di antara operasi ini. Segala sesuatu yang kami sertakan dalam data yang disimpan dipulihkan sebaik mungkin, kecuali ketinggian sel. Ini terjadi karena ketika Anda mengubah level ketinggian, Anda perlu memperbarui posisi vertikal sel. Ini dapat dilakukan dengan menugaskannya ke properti, dan bukan bidang, nilai ketinggian yang dimuat. Tetapi properti ini melakukan pekerjaan tambahan yang tidak kita butuhkan. Oleh karena itu, mari kita ekstrak kode yang memperbarui posisi sel dari setter
Elevation
dan masukkan ke dalam metode
RefreshPosition
terpisah. Satu-satunya perubahan yang perlu Anda lakukan di sini adalah mengganti
value
referensi ke bidang
elevation
.
void RefreshPosition () { Vector3 position = transform.localPosition; position.y = elevation * HexMetrics.elevationStep; position.y += (HexMetrics.SampleNoise(position).y * 2f - 1f) * HexMetrics.elevationPerturbStrength; transform.localPosition = position; Vector3 uiPosition = uiRect.localPosition; uiPosition.z = -position.y; uiRect.localPosition = uiPosition; }
Sekarang kita dapat memanggil metode saat mengatur properti, serta setelah memuat data ketinggian.
public int Elevation { … set { if (elevation == value) { return; } elevation = value; RefreshPosition(); ValidateRivers(); … } } … public void Load (BinaryReader reader) { terrainTypeIndex = reader.ReadInt32(); elevation = reader.ReadInt32(); RefreshPosition(); … }
Setelah perubahan ini, sel-sel akan dengan benar mengubah tinggi yang tampak saat memuat.
Menyimpan semua data
Keberadaan dinding dan sungai masuk / keluar dalam sel disimpan di bidang Boolean. Kita dapat menulisnya hanya sebagai integer. Selain itu, data jalan adalah larik enam nilai Boolean yang bisa kita tulis dengan satu lingkaran.
public void Save (BinaryWriter writer) { writer.Write(terrainTypeIndex); writer.Write(elevation); writer.Write(waterLevel); writer.Write(urbanLevel); writer.Write(farmLevel); writer.Write(plantLevel); writer.Write(specialIndex); writer.Write(walled); writer.Write(hasIncomingRiver); writer.Write(hasOutgoingRiver); for (int i = 0; i < roads.Length; i++) { writer.Write(roads[i]); } }
Arah sungai masuk dan keluar disimpan di bidang
HexDirection
. Tipe
HexDirection
adalah enumerasi yang disimpan secara internal sebagai beberapa nilai integer. Oleh karena itu, kami juga dapat membuat cerita bersambung sebagai integer menggunakan konversi eksplisit.
writer.Write(hasIncomingRiver); writer.Write((int)incomingRiver); writer.Write(hasOutgoingRiver); writer.Write((int)outgoingRiver);
Nilai Boolean dibaca menggunakan metode
BinaryReader.ReadBoolean
. Arah sungai adalah bilangan bulat, yang harus kita konversi kembali ke
HexDirection
.
public void Load (BinaryReader reader) { terrainTypeIndex = reader.ReadInt32(); elevation = reader.ReadInt32(); RefreshPosition(); waterLevel = reader.ReadInt32(); urbanLevel = reader.ReadInt32(); farmLevel = reader.ReadInt32(); plantLevel = reader.ReadInt32(); specialIndex = reader.ReadInt32(); walled = reader.ReadBoolean(); hasIncomingRiver = reader.ReadBoolean(); incomingRiver = (HexDirection)reader.ReadInt32(); hasOutgoingRiver = reader.ReadBoolean(); outgoingRiver = (HexDirection)reader.ReadInt32(); for (int i = 0; i < roads.Length; i++) { roads[i] = reader.ReadBoolean(); } }
Sekarang kami menyimpan semua data sel yang diperlukan untuk menyimpan dan memulihkan peta secara lengkap.
Ini membutuhkan sembilan bilangan bulat dan sembilan nilai Boolean per sel. Setiap nilai Boolean membutuhkan satu byte, jadi kami menggunakan total 45 byte per sel. Artinya, kartu dengan 300 sel membutuhkan total 13.500 byte.paket unityKurangi ukuran file
Meskipun tampaknya 13.500 byte tidak terlalu banyak untuk 300 sel, mungkin kita dapat melakukannya dengan jumlah yang lebih kecil. Pada akhirnya, kami memiliki kendali penuh atas bagaimana data diserialisasi. Mari kita lihat apakah ada cara yang lebih ringkas untuk menyimpannya.Pengurangan interval numerik
Level dan indeks sel yang berbeda disimpan sebagai integer. Namun, mereka hanya menggunakan rentang nilai yang kecil. Masing-masing dari mereka pasti akan tetap di kisaran 0-255. Ini berarti bahwa hanya byte pertama dari setiap integer yang akan digunakan. Tiga sisanya akan selalu nol. Tidak masuk akal untuk menyimpan byte kosong ini. Kita dapat membuangnya dengan menulis integer ke byte sebelum menulis ke stream. writer.Write((byte)terrainTypeIndex); writer.Write((byte)elevation); writer.Write((byte)waterLevel); writer.Write((byte)urbanLevel); writer.Write((byte)farmLevel); writer.Write((byte)plantLevel); writer.Write((byte)specialIndex); writer.Write(walled); writer.Write(hasIncomingRiver); writer.Write((byte)incomingRiver); writer.Write(hasOutgoingRiver); writer.Write((byte)outgoingRiver);
Sekarang, untuk mengembalikan angka-angka ini, kita harus menggunakan BinaryReader.ReadByte
. Konversi dari byte ke integer dilakukan secara implisit, jadi kami tidak perlu menambahkan konversi eksplisit. terrainTypeIndex = reader.ReadByte(); elevation = reader.ReadByte(); RefreshPosition(); waterLevel = reader.ReadByte(); urbanLevel = reader.ReadByte(); farmLevel = reader.ReadByte(); plantLevel = reader.ReadByte(); specialIndex = reader.ReadByte(); walled = reader.ReadBoolean(); hasIncomingRiver = reader.ReadBoolean(); incomingRiver = (HexDirection)reader.ReadByte(); hasOutgoingRiver = reader.ReadBoolean(); outgoingRiver = (HexDirection)reader.ReadByte();
Jadi kita menyingkirkan tiga byte per integer, yang menghemat 27 byte per sel. Sekarang kita menghabiskan 18 byte per sel, dan hanya 5.400 byte per 300 sel.Perlu dicatat bahwa data kartu lama menjadi tidak berarti pada tahap ini. Saat memuat penyimpanan lama, data tercampur dan kami mendapatkan sel yang bingung. Ini karena kita sekarang membaca lebih sedikit data. Jika kita membaca lebih banyak data daripada sebelumnya, kita akan mendapatkan kesalahan ketika mencoba membaca di luar akhir file.Ketidakmampuan untuk memproses data lama cocok untuk kita, karena kita sedang dalam proses menentukan format. Tetapi ketika kita memutuskan format simpan, kita perlu memastikan bahwa kode yang akan datang dapat selalu membacanya. Sekalipun kita mengubah formatnya, idealnya kita tetap bisa membaca format yang lama.Sungai Byte Union
Pada tahap ini, kami menggunakan empat byte untuk menyimpan data sungai, dua per arah. Untuk setiap arah, kami menyimpan keberadaan sungai dan arah alirannya.Tampak jelas bahwa kami tidak perlu menyimpan arah sungai jika tidak. Ini berarti bahwa sel-sel tanpa sungai membutuhkan dua byte lebih sedikit. Bahkan, satu byte ke arah sungai akan cukup bagi kita, terlepas dari keberadaannya.Kami memiliki enam kemungkinan arah, yang disimpan sebagai angka dalam interval 0–5. Tiga bit sudah cukup untuk ini, karena dalam bentuk angka biner dari 0 hingga 5 terlihat seperti 000, 001, 010, 011, 100, 101 dan 110. Artinya, satu byte lagi tetap tidak digunakan lima bit lagi. Kita dapat menggunakan salah satunya untuk menunjukkan apakah ada sungai. Sebagai contoh, Anda dapat menggunakan bit kedelapan, sesuai dengan angka 128.Untuk melakukan ini, kita akan menambahkan 128 untuknya sebelum mengubah arah menjadi byte. Artinya, jika kita memiliki sungai yang mengalir ke barat laut, kita akan menulis 133, yang dalam bentuk biner adalah 10000101. Dan jika tidak ada sungai, maka kita hanya menulis nol byte.Pada saat yang sama, empat bit lagi tetap tidak digunakan, tetapi ini normal. Kita dapat menggabungkan kedua arah sungai menjadi satu byte, tetapi ini sudah terlalu membingungkan.
Untuk mendekode data sungai, pertama-tama kita perlu membaca byte kembali. Jika nilainya tidak kurang dari 128, maka ini berarti ada sungai. Untuk mendapatkan arahannya, kurangi 128, lalu konversikan ke HexDirection
.
Hasilnya, kami mendapat 16 byte per sel. Peningkatan tampaknya tidak besar, tetapi ini adalah salah satu trik yang digunakan untuk mengurangi ukuran data biner.Simpan jalan dalam satu byte
Kita dapat menggunakan trik serupa untuk mengompres data jalan. Kami memiliki enam nilai boolean yang dapat disimpan dalam enam bit pertama byte. Artinya, setiap arah jalan diwakili oleh angka yang merupakan kekuatan dua. Ini adalah 1, 2, 4, 8, 16 dan 32, atau dalam bentuk biner 1, 10, 100, 1000, 10000 dan 100000.Untuk membuat byte jadi, kita perlu mengatur bit yang sesuai dengan arah jalan yang digunakan. Untuk mendapatkan arah yang benar untuk arah itu, kita bisa menggunakan operator <<
. Kemudian gabungkan mereka menggunakan operator bitwise OR. Misalnya, jika jalan pertama, kedua, ketiga dan keenam digunakan, maka byte yang selesai adalah 100111. int roadFlags = 0; for (int i = 0; i < roads.Length; i++) {
Bagaimana cara kerja <<?. integer . . integer . , . 1 << n
2 n , .
Untuk mendapatkan nilai Boolean dari jalan kembali, Anda perlu memeriksa apakah bit sudah diatur. Jika demikian, maka sembunyikan semua bit lainnya menggunakan operator bitwise AND dengan nomor yang sesuai. Jika hasilnya tidak sama dengan nol, maka bit diatur dan jalan ada. int roadFlags = reader.ReadByte(); for (int i = 0; i < roads.Length; i++) { roads[i] = (roadFlags & (1 << i)) != 0; }
Setelah meremas enam byte menjadi satu, kami menerima 11 byte per sel. Dengan 300 sel, ini hanya 3.300 byte. Artinya, setelah bekerja sedikit dengan byte, kami mengurangi ukuran file sebesar 75%.Bersiap untuk masa depan
Sebelum menyatakan format penyimpanan kami selesai, kami menambahkan satu lagi detail. Sebelum menyimpan data peta, kami akan dipaksa untuk HexMapEditor
menulis bilangan bulat nol. public void Save () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(0); hexGrid.Save(writer); } }
Ini akan menambahkan empat byte kosong ke awal data kami. Artinya, sebelum memuat kartu, kita harus membaca empat byte ini. public void Load () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { reader.ReadInt32(); hexGrid.Load(reader); } }
Meskipun byte ini tidak berguna sejauh ini, mereka digunakan sebagai header yang akan memberikan kompatibilitas ke belakang di masa depan. Jika kami belum menambahkan byte nol ini, maka konten beberapa byte pertama bergantung pada sel pertama peta. Oleh karena itu, di masa mendatang akan lebih sulit bagi kita untuk mengetahui versi format penyimpanan mana yang sedang kita tangani. Sekarang kita bisa memeriksa empat byte pertama. Jika mereka kosong, maka kita berhadapan dengan versi format 0. Dalam versi yang akan datang, akan dimungkinkan untuk menambahkan sesuatu yang lain di sana.Artinya, jika judulnya bukan nol, kami berhadapan dengan beberapa versi yang tidak dikenal. Karena kami tidak dapat menemukan data apa yang ada, kami harus menolak untuk mengunduh peta. using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { int header = reader.ReadInt32(); if (header == 0) { hexGrid.Load(reader); } else { Debug.LogWarning("Unknown map format " + header); } }
paket unityBagian 13: manajemen kartu
- Kami membuat kartu baru dalam mode Play.
- Tambahkan dukungan untuk berbagai ukuran kartu.
- Tambahkan ukuran peta ke data yang disimpan.
- Simpan dan muat peta yang berubah-ubah.
- Tampilkan daftar kartu.
Pada bagian ini, kami akan menambahkan dukungan untuk berbagai ukuran kartu, serta menyimpan file yang berbeda.Mulai dari bagian ini, tutorial akan dibuat di Unity 5.5.0.Awal perpustakaan peta.Buat Peta Baru
Hingga saat ini, kami hanya membuat kisi segi enam sekali - saat memuat adegan. Sekarang kita akan memungkinkan untuk memulai peta baru kapan saja. Kartu baru hanya akan menggantikan yang sekarang.Di Sedarlah HexGrid
, beberapa metrik diinisialisasi, dan kemudian jumlah sel ditentukan dan fragmen serta sel yang diperlukan dibuat. Membuat kumpulan fragmen dan sel baru, kami membuat peta baru. Mari kita bagi HexGrid.Awake
menjadi dua bagian - kode sumber inisialisasi dan metode umum CreateMap
. void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexMetrics.colors = colors; CreateMap(); } public void CreateMap () { cellCountX = chunkCountX * HexMetrics.chunkSizeX; cellCountZ = chunkCountZ * HexMetrics.chunkSizeZ; CreateChunks(); CreateCells(); }
Tambahkan tombol di UI untuk membuat peta baru. Saya membuatnya besar dan meletakkannya di bawah tombol simpan dan muat.Tombol Peta Baru.Mari kita hubungkan acara On Click tombol ini dengan metode CreateMap
objek kita HexGrid
. Artinya, kita tidak akan melalui Hex Map Editor , tetapi langsung memanggil metode objek Hex Grid .Buat peta dengan mengklik.Menghapus data lama
Sekarang, ketika Anda mengklik tombol Peta Baru , satu set fragmen dan sel baru akan dibuat. Namun, yang lama tidak dihapus secara otomatis. Karenanya, sebagai hasilnya, kami mendapatkan beberapa jerat peta yang saling bertumpukan. Untuk menghindari ini, pertama-tama kita harus menyingkirkan benda-benda tua. Ini dapat dilakukan dengan menghancurkan semua fragmen saat ini di awal CreateMap
. public void CreateMap () { if (chunks != null) { for (int i = 0; i < chunks.Length; i++) { Destroy(chunks[i].gameObject); } } … }
Bisakah kita menggunakan kembali objek yang ada?, . , . , — , .
Apakah mungkin untuk menghancurkan elemen anak seperti ini dalam satu lingkaran?Tentu saja .
Tentukan ukuran dalam sel, bukan fragmen
Sementara kita mengatur ukuran peta melalui bidang chunkCountX
dan chunkCountZ
objek HexGrid
. Tetapi akan jauh lebih nyaman untuk menunjukkan ukuran peta dalam sel. Pada saat yang sama, kita bahkan dapat mengubah ukuran fragmen di masa depan tanpa mengubah ukuran kartu. Karena itu, mari bertukar peran jumlah sel dan jumlah bidang fragmen.
Ini akan menyebabkan kesalahan kompilasi, karena HexMapCamera
menggunakan ukuran fragmen untuk membatasi posisinya . Ubah HexMapCamera.ClampPosition
sehingga dia menggunakan langsung jumlah sel yang masih dia butuhkan. Vector3 ClampPosition (Vector3 position) { float xMax = (grid.cellCountX - 0.5f) * (2f * HexMetrics.innerRadius); position.x = Mathf.Clamp(position.x, 0f, xMax); float zMax = (grid.cellCountZ - 1) * (1.5f * HexMetrics.outerRadius); position.z = Mathf.Clamp(position.z, 0f, zMax); return position; }
Sebuah fragmen berukuran 5 x 5 sel, dan peta secara default memiliki ukuran 4 x 3 fragmen. Oleh karena itu, untuk menjaga kartu tetap sama, kita harus menggunakan ukuran 20 kali 15 sel. Dan meskipun kami telah menetapkan nilai default dalam kode, objek grid masih tidak akan menggunakannya secara otomatis, karena bidang sudah ada dan default ke 0.Secara default, kartu memiliki ukuran 20 hingga 15.Ukuran kartu khusus
Langkah selanjutnya adalah dukungan untuk membuat kartu dengan ukuran berapa pun, bukan hanya ukuran standar. Untuk melakukan ini, tambahkan HexGrid.CreateMap
X dan Z ke parameter. Mereka akan mengganti jumlah sel yang ada. Di dalam, Awake
kami hanya akan memanggil mereka dengan jumlah sel saat ini. void Awake () { HexMetrics.noiseSource = noiseSource; HexMetrics.InitializeHashGrid(seed); HexMetrics.colors = colors; CreateMap(cellCountX, cellCountZ); } public void CreateMap (int x, int z) { … cellCountX = x; cellCountZ = z; chunkCountX = cellCountX / HexMetrics.chunkSizeX; chunkCountZ = cellCountZ / HexMetrics.chunkSizeZ; CreateChunks(); CreateCells(); }
Namun, ini hanya akan berfungsi dengan benar dengan jumlah sel yang merupakan kelipatan dari ukuran fragmen. Jika tidak, divisi integer akan membuat fragmen terlalu sedikit. Meskipun kami dapat menambahkan dukungan untuk fragmen yang sebagian terisi dengan sel, mari kita melarang penggunaan ukuran yang tidak sesuai dengan fragmen.Kita dapat menggunakan operator %
untuk menghitung sisa pembagian jumlah sel dengan jumlah fragmen. Jika tidak sama dengan nol, maka ada perbedaan dan kami tidak akan membuat peta baru. Dan sementara kita melakukan ini, mari kita tambahkan perlindungan terhadap ukuran nol dan negatif. public void CreateMap (int x, int z) { if ( x <= 0 || x % HexMetrics.chunkSizeX != 0 || z <= 0 || z % HexMetrics.chunkSizeZ != 0 ) { Debug.LogError("Unsupported map size."); return; } … }
Menu Kartu Baru
Pada tahap saat ini, tombol Peta Baru tidak lagi berfungsi, karena metode HexGrid.CreateMap
sekarang memiliki dua parameter. Kami tidak dapat langsung menghubungkan acara Unity ke metode tersebut. Selain itu, untuk mendukung ukuran kartu yang berbeda, kami memerlukan beberapa tombol. Alih-alih menambahkan semua tombol ini ke UI utama, mari kita buat menu popup terpisah.Tambahkan kanvas baru ke adegan ( GameObject / UI / Canvas ). Kami akan menggunakan pengaturan yang sama dengan kanvas yang ada, kecuali bahwa Urutan Urutannya harus sama dengan 1. Berkat ini, itu akan berada di atas UI editor utama. Saya membuat kanvas dan sistem acara sebagai anak dari objek UI baru sehingga hirarki adegan tetap bersih.Menu kanvas Peta Baru.Tambahkan panel ke Menu Peta Baru yang menutup seluruh layar. Diperlukan untuk menggelapkan latar belakang dan tidak membiarkan kursor berinteraksi dengan yang lainnya ketika menu terbuka. Saya memberinya warna yang seragam, membersihkan Source Image-nya , dan menetapkan (0, 0, 0, 200) sebagai Warna .Pengaturan gambar latar belakang.Tambahkan bilah menu ke tengah kanvas, mirip dengan panel Hex Map Editor . Mari kita buat label dan tombol yang jelas untuk kartu kecil, sedang dan besar. Kami juga akan menambahkan tombol batal padanya jika pemain berubah pikiran. Setelah selesai membuat desain, nonaktifkan seluruh Menu Peta Baru .Menu Peta Baru.Untuk mengelola menu, buat komponen NewMapMenu
dan tambahkan ke kanvas objek New Map Menu . Untuk membuat peta baru, kita perlu akses ke objek Hex Grid . Oleh karena itu, kami menambahkan bidang umum ke sana dan menghubungkannya. using UnityEngine; public class NewMapMenu : MonoBehaviour { public HexGrid hexGrid; }
Komponen Menu Peta Baru.Membuka dan menutup
Kita dapat membuka dan menutup menu popup hanya dengan mengaktifkan dan menonaktifkan objek kanvas. Mari kita tambahkan NewMapMenu
dua metode umum untuk melakukan ini. public void Open () { gameObject.SetActive(true); } public void Close () { gameObject.SetActive(false); }
Sekarang sambungkan tombol UI Peta Baru editor ke metode Open
di objek Menu Peta Baru .Membuka menu dengan menekan.Juga sambungkan tombol Batal ke metode Close
. Ini akan memungkinkan kita untuk membuka dan menutup menu popup.Buat Peta Baru
Untuk membuat peta baru, kita perlu memanggil metode di objek Hex GridCreateMap
. Selain itu, setelah itu kita perlu menutup menu pop-up. Tambahkan ke NewMapMenu
metode yang akan menangani ini, dengan mempertimbangkan ukuran sewenang-wenang. void CreateMap (int x, int z) { hexGrid.CreateMap(x, z); Close(); }
Metode ini seharusnya tidak bersifat umum, karena kita masih tidak dapat menghubungkannya langsung ke acara tombol. Sebagai gantinya, buat satu metode per tombol yang akan memanggil CreateMap
dengan ukuran yang ditentukan. Untuk peta kecil, saya menggunakan ukuran 20 kali 15, sesuai dengan ukuran standar peta. Untuk kartu tengah, saya memutuskan untuk menggandakan ukuran ini, mendapatkan 40 oleh 30, dan menggandakannya lagi untuk kartu besar. Hubungkan tombol dengan metode yang sesuai. public void CreateSmallMap () { CreateMap(20, 15); } public void CreateMediumMap () { CreateMap(40, 30); } public void CreateLargeMap () { CreateMap(80, 60); }
Kunci kamera
Sekarang kita dapat menggunakan menu pop-up untuk membuat peta baru dengan tiga ukuran berbeda! Semuanya bekerja dengan baik, tetapi kita perlu mengurus sedikit detail. Ketika Menu Peta Baru aktif, kita tidak dapat lagi berinteraksi dengan UI editor dan mengedit sel. Namun, kami masih dapat mengontrol kamera. Idealnya, dengan menu terbuka, kamera harus mengunci.Karena kita hanya memiliki satu kamera, solusi cepat dan pragmatis adalah dengan menambahkan properti statis padanya Locked
. Untuk penggunaan luas, solusi ini tidak terlalu cocok, tetapi untuk antarmuka sederhana kami, ini sudah cukup. Ini mengharuskan kami melacak instance statis di dalam HexMapCamera
, yang diatur ketika kamera Sedarlah. static HexMapCamera instance; … void Awake () { instance = this; swivel = transform.GetChild(0); stick = swivel.GetChild(0); }
Properti Locked
dapat menjadi properti Boolean statis sederhana hanya dengan setter. Yang dilakukannya hanyalah mematikan instance HexMapCamera
saat dikunci, dan menyalakannya saat tidak terkunci. public static bool Locked { set { instance.enabled = !value; } }
Sekarang ia NewMapMenu.Open
dapat memblokir kamera, dan NewMapMenu.Close
- membuka kuncinya. public void Open () { gameObject.SetActive(true); HexMapCamera.Locked = true; } public void Close () { gameObject.SetActive(false); HexMapCamera.Locked = false; }
Mempertahankan posisi kamera yang benar
Kemungkinan ada masalah lain dengan kamera. Saat membuat peta baru yang lebih kecil dari yang sekarang, kamera mungkin muncul di luar batas peta. Dia akan tetap di sana sampai pemain mencoba untuk memindahkan kamera. Dan hanya dengan demikian akan dibatasi oleh batas-batas peta baru.Untuk mengatasi masalah ini, kita dapat menambahkan HexMapCamera
metode statis ValidatePosition
. Memanggil metode AdjustPosition
instan dengan offset nol akan memaksa kamera untuk bergerak ke batas peta. Jika kamera sudah berada di dalam batas peta baru, maka ia akan tetap di tempatnya. public static void ValidatePosition () { instance.AdjustPosition(0f, 0f); }
Panggil metode di dalam NewMapMenu.CreateMap
setelah membuat peta baru. void CreateMap (int x, int z) { hexGrid.CreateMap(x, z); HexMapCamera.ValidatePosition(); Close(); }
paket unityMenyimpan Ukuran Peta
Meskipun kami dapat membuat kartu dengan ukuran berbeda, itu tidak diperhitungkan saat menyimpan dan memuat. Ini berarti bahwa memuat peta akan menyebabkan kesalahan atau peta yang salah jika ukuran peta saat ini tidak cocok dengan ukuran yang dimuat.Untuk mengatasi masalah ini, sebelum memuat data sel, kita perlu membuat peta baru dengan ukuran yang sesuai. Katakanlah kita memiliki peta kecil yang disimpan. Dalam hal ini, semuanya akan baik-baik saja jika kita membuat HexGrid.Load
peta 20 x 15 di awal . public void Load (BinaryReader reader) { CreateMap(20, 15); for (int i = 0; i < cells.Length; i++) { cells[i].Load(reader); } for (int i = 0; i < chunks.Length; i++) { chunks[i].Refresh(); } }
Penyimpanan ukuran kartu
Tentu saja, kita dapat menyimpan kartu dengan berbagai ukuran. Oleh karena itu, solusi umum adalah menyimpan ukuran peta di depan sel-sel ini. public void Save (BinaryWriter writer) { writer.Write(cellCountX); writer.Write(cellCountZ); for (int i = 0; i < cells.Length; i++) { cells[i].Save(writer); } }
Lalu kita bisa mendapatkan ukuran sebenarnya dan menggunakannya untuk membuat peta dengan ukuran yang benar. public void Load (BinaryReader reader) { CreateMap(reader.ReadInt32(), reader.ReadInt32()); … }
Karena sekarang kita dapat memuat peta dengan ukuran berbeda, kita kembali dihadapkan pada masalah posisi kamera. Kami akan menyelesaikannya dengan memeriksa posisinya HexMapEditor.Load
setelah memuat peta. public void Load () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { int header = reader.ReadInt32(); if (header == 0) { hexGrid.Load(reader, header); HexMapCamera.ValidatePosition(); } else { Debug.LogWarning("Unknown map format " + header); } } }
Format file baru
Meskipun pendekatan ini berfungsi dengan kartu yang akan kita simpan di masa depan, itu tidak akan bekerja dengan yang lama. Dan sebaliknya - kode dari bagian sebelumnya dari tutorial tidak akan dapat memuat file peta baru dengan benar. Untuk membedakan antara format lama dan baru, kami akan meningkatkan nilai integer dari header. Format penyimpanan lama tanpa ukuran peta memiliki versi 0. Format baru dengan ukuran peta akan memiliki versi 1. Oleh karena itu, saat merekam, HexMapEditor.Save
harus menuliskan 1 bukan 0. public void Save () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(1); hexGrid.Save(writer); } }
Mulai sekarang, kartu akan disimpan sebagai versi 1. Jika kami mencoba membukanya di tutorial dari tutorial sebelumnya, mereka akan menolak memuat dan melaporkan pada format kartu yang tidak dikenal. Faktanya, ini akan terjadi jika kita sudah mencoba memuat kartu semacam itu. Anda perlu mengubah metode HexMapEditor.Load
sehingga menerima versi baru. public void Load () { string path = Path.Combine(Application.persistentDataPath, "test.map"); using (BinaryReader reader = new BinaryReader(File.OpenRead(path))) { int header = reader.ReadInt32(); if (header == 1) { hexGrid.Load(reader); HexMapCamera.ValidatePosition(); } else { Debug.LogWarning("Unknown map format " + header); } } }
Kompatibilitas mundur
Bahkan, jika kita mau, kita masih bisa mengunduh peta versi 0, dengan asumsi mereka semua memiliki ukuran yang sama 20 kali 15. Artinya, judulnya tidak harus 1, juga bisa nol. Karena setiap versi memerlukan pendekatannya sendiri, itu HexMapEditor.Load
harus meneruskan header ke metode HexGrid.Load
. if (header <= 1) { hexGrid.Load(reader, header); HexMapCamera.ValidatePosition(); }
Tambahkan HexGrid.Load
judul ke parameter dan gunakan untuk membuat keputusan tentang tindakan selanjutnya. Jika header tidak kurang dari 1, maka Anda perlu membaca data ukuran kartu. Kalau tidak, kami menggunakan kartu ukuran tetap lama 20 dengan 15 dan lewati membaca data ukuran. public void Load (BinaryReader reader, int header) { int x = 20, z = 15; if (header >= 1) { x = reader.ReadInt32(); z = reader.ReadInt32(); } CreateMap(x, z); … }
file peta versi 0Cek Ukuran Kartu
Seperti halnya membuat peta baru, secara teori dimungkinkan bahwa kita harus memuat peta yang tidak kompatibel dengan ukuran fragmen. Ketika ini terjadi, kita harus menghentikan unduhan kartu. HexGrid.CreateMap
sudah menolak untuk membuat peta dan menampilkan kesalahan di konsol. Untuk memberitahukan hal ini kepada pemanggil metode, mari kita mengembalikan bool jitu jika peta dibuat. public bool CreateMap (int x, int z) { if ( x <= 0 || x % HexMetrics.chunkSizeX != 0 || z <= 0 || z % HexMetrics.chunkSizeZ != 0 ) { Debug.LogError("Unsupported map size."); return false; } … return true; }
Sekarang, HexGrid.Load
itu juga dapat menghentikan eksekusi ketika pembuatan peta gagal. public void Load (BinaryReader reader, int header) { int x = 20, z = 15; if (header >= 1) { x = reader.ReadInt32(); z = reader.ReadInt32(); } if (!CreateMap(x, z)) { return; } … }
Karena memuat menimpa semua data dalam sel yang ada, kita tidak perlu membuat peta baru jika peta dengan ukuran yang sama dimuat. Karena itu, langkah ini bisa dilewati. if (x != cellCountX || z != cellCountZ) { if (!CreateMap(x, z)) { return; } }
paket unityManajemen file
Kita dapat menyimpan dan memuat kartu dengan ukuran berbeda, tetapi selalu menulis dan membaca test.map . Sekarang kami akan menambahkan dukungan untuk file yang berbeda.Alih-alih langsung menyimpan atau memuat peta, kami menggunakan menu pop-up lain yang menyediakan manajemen file tingkat lanjut. Buat kanvas lain, seperti di Menu Peta Baru , tapi kali ini kami akan menyebutnya Simpan Menu . Menu ini akan menyimpan dan memuat peta, tergantung pada tombol yang ditekan untuk membukanya.Kami akan membuat desain Save Load Menu .seperti itu adalah menu simpan. Nanti kita akan secara dinamis mengubahnya menjadi menu boot. Seperti menu lain, menu harus memiliki latar belakang dan bilah menu, label menu, dan tombol batal. Kemudian tambahkan tampilan gulir ( GameObject / UI / Scroll View ) ke menu untuk menampilkan daftar file. Di bawah ini kami memasukkan bidang input ( GameObject / UI / Input Field ) untuk menunjukkan nama-nama kartu baru. Kami juga membutuhkan tombol aksi untuk menyimpan peta. Dan akhirnya. tambahkan tombol Hapus untuk menghapus kartu yang tidak perlu.Desain Simpan Menu Muat.Secara default, tampilan gulir memungkinkan pengguliran horizontal dan vertikal, tetapi kita hanya perlu daftar dengan pengguliran vertikal. Oleh karena itu, menonaktifkan bergulir horizontal dan mencabut horizontal scroll bar. Kami juga mengatur Jenis Gerakan untuk dijepit dan menonaktifkan Inersia untuk membuat daftar tampak lebih ketat.Opsi Daftar File.Kami akan menghapus anak Horizontal Scrollbar dari objek Daftar File , karena kami tidak membutuhkannya. Kemudian mengubah ukuran Scrollbar Vertical sehingga mencapai bagian bawah daftar.Teks placeholder untuk objek Input Nama dapat diubah di Placeholder anaknya . Saya menggunakan teks deskriptif, tetapi Anda bisa membiarkannya kosong dan menyingkirkan placeholder.Desain menu berubah.Kami selesai dengan desain, dan sekarang menonaktifkan menu sehingga secara default tersembunyi.Manajemen menu
Agar menu berfungsi, kita perlu skrip lain, dalam hal ini - SaveLoadMenu
. Seperti NewMapMenu
, ini membutuhkan tautan ke kisi, serta metode Open
dan Close
. using UnityEngine; public class SaveLoadMenu : MonoBehaviour { public HexGrid hexGrid; public void Open () { gameObject.SetActive(true); HexMapCamera.Locked = true; } public void Close () { gameObject.SetActive(false); HexMapCamera.Locked = false; } }
Tambahkan komponen ini ke SaveLoadMenu dan berikan tautan ke objek kisi.Komponen SaveLoadMenu.Menu akan terbuka untuk menyimpan atau memuat. Untuk menyederhanakan pekerjaan, tambahkan Open
parameter Boolean ke metode . Ini menentukan apakah menu harus dalam mode simpan. Kami akan melacak mode ini di lapangan untuk mengetahui tindakan apa yang harus dilakukan nanti. bool saveMode; public void Open (bool saveMode) { this.saveMode = saveMode; gameObject.SetActive(true); HexMapCamera.Locked = true; }
Sekarang menggabungkan tombol Simpan dan beban Obyek Hex Peta Editor dengan metode Open
dari objek Simpan Muat Menu . Periksa parameter boolean hanya untuk tombol Simpan .Membuka menu dalam mode simpan.Jika Anda belum melakukannya, hubungkan acara tombol Batal ke metode Close
. Sekarang Simpan Beban menu dapat dibuka dan ditutup.Berubah penampilan
Kami menciptakan menu sebagai menu simpan, tetapi modenya ditentukan oleh tombol yang ditekan untuk membuka. Kita perlu mengubah tampilan menu tergantung pada mode. Secara khusus, kita perlu mengubah label menu dan label tombol aksi. Ini artinya kita perlu tautan ke tag-tag ini. using UnityEngine; using UnityEngine.UI; public class SaveLoadMenu : MonoBehaviour { public Text menuLabel, actionButtonLabel; … }
Koneksi dengan tag.Saat menu terbuka dalam mode simpan, kami menggunakan label yang ada, yaitu, Simpan Peta untuk menu dan Simpan untuk tombol aksi. Kalau tidak, kita berada dalam mode pemuatan, yaitu, kami menggunakan Muat Peta dan Muat . public void Open (bool saveMode) { this.saveMode = saveMode; if (saveMode) { menuLabel.text = "Save Map"; actionButtonLabel.text = "Save"; } else { menuLabel.text = "Load Map"; actionButtonLabel.text = "Load"; } gameObject.SetActive(true); HexMapCamera.Locked = true; }
Masukkan nama kartu
Mari kita tinggalkan daftar file untuk saat ini. Pengguna dapat menentukan file yang disimpan atau diunduh dengan memasukkan nama kartu di kolom input. Untuk mendapatkan data ini, kita perlu referensi ke komponen InputField
objek Input Nama . public InputField nameInput;
Koneksi ke kolom input.Pengguna tidak perlu dipaksa untuk memasukkan path lengkap ke file peta. Hanya cukup nama kartu tanpa ekstensi .map . Mari kita tambahkan metode yang mengambil input pengguna dan menciptakan jalur yang tepat untuk itu. Ini tidak mungkin ketika input kosong, jadi dalam hal ini kami akan kembali null
. using UnityEngine; using UnityEngine.UI; using System.IO; public class SaveLoadMenu : MonoBehaviour { … string GetSelectedPath () { string mapName = nameInput.text; if (mapName.Length == 0) { return null; } return Path.Combine(Application.persistentDataPath, mapName + ".map"); } }
Apa yang terjadi jika pengguna memasukkan karakter yang tidak valid?, . , , .
Content Type . , - , . , , .
Menyimpan dan memuat
Sekarang akan terlibat dalam menyimpan dan memuat SaveLoadMenu
. Oleh karena itu, kita bergerak metode Save
dan Load
dari HexMapEditor
dalam SaveLoadMenu
. Mereka tidak lagi harus dibagikan, dan akan bekerja dengan parameter path, bukan path tetap. void Save (string path) {
Karena kita sekarang mengunggah file sewenang-wenang, alangkah baiknya untuk memverifikasi bahwa file itu benar-benar ada, dan baru kemudian mencoba membacanya. Jika tidak, maka kami melakukan kesalahan dan menghentikan operasi. void Load (string path) { if (!File.Exists(path)) { Debug.LogError("File does not exist " + path); return; } … }
Sekarang tambahkan metode umum Action
. Dimulai dengan mendapatkan jalur yang dipilih pengguna. Jika ada jalan, simpan atau muat. Kemudian tutup menu. public void Action () { string path = GetSelectedPath(); if (path == null) { return; } if (saveMode) { Save(path); } else { Load(path); } Close(); }
Dengan melampirkan acara Tombol Aksi ke metode ini , kita dapat menyimpan dan memuat menggunakan nama peta yang berubah-ubah. Karena kami tidak menyetel ulang bidang input, nama yang dipilih akan tetap sampai berikutnya menyimpan atau memuat. Ini nyaman untuk menyimpan atau memuat dari satu file beberapa kali berturut-turut, jadi kami tidak akan mengubah apa pun.Peta Daftar Item
Selanjutnya, kita akan mengisi daftar file dengan semua kartu yang ada di jalur penyimpanan data. Ketika Anda mengklik salah satu item dalam daftar, itu akan digunakan sebagai teks di Input Nama . Tambahkan SaveLoadMenu
metode umum untuk ini. public void SelectItem (string name) { nameInput.text = name; }
Kami membutuhkan sesuatu yang merupakan item daftar. Tombol biasa akan berfungsi. Buat dan kurangi ketinggian hingga 20 unit sehingga tidak memakan banyak ruang secara vertikal. Seharusnya tidak terlihat seperti sebuah tombol, sehingga membersihkan Link Sumber Gambar komponennya Gambar . Dalam hal ini, itu akan menjadi sepenuhnya putih. Selain itu, kami akan memastikan bahwa label disejajarkan ke kiri dan bahwa ada ruang antara teks dan sisi kiri tombol. Setelah selesai dengan desain tombol, kami mengubahnya menjadi cetakan.Tombol adalah item daftar.Kami tidak dapat secara langsung menghubungkan acara tombol ke Menu Peta Baru , karena ini adalah cetakan dan belum ada di tempat kejadian. Oleh karena itu, item menu memerlukan tautan ke menu sehingga dapat memanggil metode ketika diklik SelectItem
. Dia juga perlu melacak nama kartu yang diwakilinya, dan mengatur teksnya. Mari kita buat komponen kecil untuk ini SaveLoadItem
. using UnityEngine; using UnityEngine.UI; public class SaveLoadItem : MonoBehaviour { public SaveLoadMenu menu; public string MapName { get { return mapName; } set { mapName = value; transform.GetChild(0).GetComponent<Text>().text = value; } } string mapName; public void Select () { menu.SelectItem(mapName); } }
Tambahkan komponen ke item menu dan buat panggilan tombol metodenya Select
.Komponen barang.Isi Daftar
Untuk mengisi daftar, Anda SaveLoadMenu
memerlukan tautan ke Konten di dalam Viewport objek Daftar File . Dia juga membutuhkan tautan ke cetakan barang. public RectTransform listContent; public SaveLoadItem itemPrefab;
Campurkan isi daftar dan cetakan.Kami menggunakan metode baru untuk mengisi daftar ini. Langkah pertama adalah mengidentifikasi file peta yang ada. Untuk mendapatkan larik semua jalur file di dalam direktori, kita dapat menggunakan metode ini Directory.GetFiles
. Metode ini memiliki parameter kedua yang memungkinkan Anda memfilter file. Dalam kasus kami, hanya file yang cocok dengan * .map mask yang diperlukan . void FillList () { string[] paths = Directory.GetFiles(Application.persistentDataPath, "*.map"); }
Sayangnya, pesanan file tidak dijamin. Untuk menampilkannya dalam urutan abjad, kita perlu mengurutkan array System.Array.Sort
. using UnityEngine; using UnityEngine.UI; using System; using System.IO; public class SaveLoadMenu : MonoBehaviour { … void FillList () { string[] paths = Directory.GetFiles(Application.persistentDataPath, "*.map"); Array.Sort(paths); } … }
Selanjutnya, kita akan membuat instance cetakan untuk setiap elemen array. Bind item ke menu, atur nama petanya dan jadikan sebagai anak isi daftar. Array.Sort(paths); for (int i = 0; i < paths.Length; i++) { SaveLoadItem item = Instantiate(itemPrefab); item.menu = this; item.MapName = paths[i]; item.transform.SetParent(listContent, false); }
Karena Directory.GetFiles
mengembalikan path lengkap ke file, kita perlu menghapusnya. Untungnya, inilah yang membuat metode yang nyaman Path.GetFileNameWithoutExtension
. item.MapName = Path.GetFileNameWithoutExtension(paths[i]);
Sebelum menampilkan menu, kita perlu mengisi daftar. Dan karena file cenderung berubah, kita perlu melakukan ini setiap kali kita membuka menu. public void Open (bool saveMode) { … FillList(); gameObject.SetActive(true); HexMapCamera.Locked = true; }
Saat mengisi ulang daftar, kita perlu menghapus semua yang lama sebelum menambahkan item baru. void FillList () { for (int i = 0; i < listContent.childCount; i++) { Destroy(listContent.GetChild(i).gameObject); } … }
Barang tanpa pengaturan.Pengaturan poin
Sekarang daftar akan menampilkan item, tetapi mereka akan tumpang tindih dan berada di posisi yang buruk. Untuk mengubahnya menjadi daftar vertikal, tambahkan komponen Grup Tata Letak Vertikal ( Komponen / Tata Letak / Grup Tata Letak Vertikal ) ke objek Konten daftar . Agar pengaturan berfungsi dengan benar, aktifkan Lebar dari Child Control Size dan Child Force Expand . Kedua opsi Tinggi harus dinonaktifkan.Menggunakan grup tata letak vertikal.Kami punya daftar barang yang indah. Namun, ukuran isi daftar tidak menyesuaikan dengan jumlah item yang sebenarnya. Karena itu, bilah gulir tidak pernah mengubah ukuran. Kami dapat memaksa Konten untuk mengubah ukuran secara otomatis dengan menambahkan komponen Fitter Ukuran Konten ( Component / Layout / Content Size Fitter ) ke dalamnya. Mode Vertical Fit-nya harus diatur ke Preferred Size .Menggunakan bugar ukuran konten.Sekarang dengan sejumlah kecil poin, scrollbar akan hilang. Dan ketika ada terlalu banyak item dalam daftar yang tidak sesuai dengan viewport, bilah gulir muncul dan memiliki ukuran yang sesuai.Bilah gulir muncul.Penghapusan kartu
Sekarang kita dapat dengan mudah bekerja dengan banyak file peta. Namun, terkadang perlu untuk menyingkirkan beberapa kartu. Untuk melakukan ini, Anda dapat menggunakan tombol Hapus . Mari kita membuat metode untuk ini dan membuat tombol menyebutnya. Jika ada jalur yang dipilih, cukup hapus dengan File.Delete
. public void Delete () { string path = GetSelectedPath(); if (path == null) { return; } File.Delete(path); }
Di sini kita juga harus memeriksa apakah kita bekerja dengan file yang benar-benar ada. Jika ini bukan masalahnya, maka kita seharusnya tidak mencoba untuk menghapusnya, tetapi ini tidak mengarah pada kesalahan. if (File.Exists(path)) { File.Delete(path); }
Setelah mengeluarkan kartu, kita tidak perlu menutup menu. Ini membuatnya lebih mudah untuk menghapus banyak file sekaligus. Namun, setelah dihapus, kita perlu menghapus Input Nama , serta memperbarui daftar file. if (File.Exists(path)) { File.Delete(path); } nameInput.text = ""; FillList();
paket unityBagian 14: tekstur relief
- Gunakan warna titik untuk membuat peta percikan.
- Membuat aset tekstur array.
- Menambahkan indeks ketinggian ke jerat.
- Transisi antara tekstur relief.
Sampai saat ini, kami menggunakan warna solid untuk mewarnai kartu. Sekarang kita akan menerapkan tekstur.Menggambar tekstur.Campuran tiga jenis
Meskipun warna seragam jelas dapat dibedakan dan cukup sesuai dengan tugas, mereka tidak terlihat sangat menarik. Menggunakan tekstur akan secara signifikan meningkatkan daya tarik peta. Tentu saja, untuk ini kita harus mencampur tekstur, bukan hanya warna. Dalam tutorial Rendering 3, Combining Textures, saya berbicara tentang cara mencampurkan banyak tekstur menggunakan peta splat. Di peta segi enam kami, Anda dapat menggunakan pendekatan serupa.Dalam tutorial Rendering 3hanya empat tekstur yang dicampur, dan dengan satu peta percikan kami dapat mendukung hingga lima tekstur. Saat ini, kami menggunakan lima warna berbeda, jadi ini sangat cocok untuk kami. Namun, nanti kita bisa menambahkan tipe lain. Oleh karena itu, dukungan untuk sejumlah jenis bantuan diperlukan. Saat menggunakan properti tekstur yang ditetapkan secara eksplisit, ini tidak mungkin, jadi Anda harus menggunakan array tekstur. Nanti kita akan membuatnya.Saat menggunakan array tekstur, kita perlu memberi tahu shader tekstur mana yang harus dicampur. Pencampuran yang paling sulit diperlukan untuk segitiga siku-siku, yang bisa antara tiga sel dengan jenis medan mereka sendiri. Oleh karena itu, kami membutuhkan dukungan pencampuran antara tiga jenis per segitiga.Menggunakan warna titik sebagai Splat Maps
Dengan asumsi kami dapat memberi tahu Anda tekstur mana yang akan dicampurkan, kami dapat menggunakan warna titik untuk membuat peta percikan untuk setiap segitiga. Karena dalam setiap kasus maksimal tiga tekstur digunakan, kami hanya membutuhkan tiga saluran warna. Merah akan mewakili tekstur pertama, hijau - yang kedua, dan biru - yang ketiga.Peta Triangle Splat.Apakah jumlah peta splat segitiga selalu sama dengan satu?Ya . . , (1, 0, 0) , (½, ½, 0) (⅓, ⅓, ⅓) .
Jika sebuah segitiga hanya membutuhkan satu tekstur, kami hanya menggunakan saluran pertama. Artinya, warnanya akan sepenuhnya merah. Dalam hal pencampuran antara dua jenis yang berbeda, kami menggunakan saluran pertama dan kedua. Artinya, warna segitiga akan menjadi campuran merah dan hijau. Dan ketika ketiga jenis ini ditemukan, maka itu akan menjadi campuran merah, hijau dan biru.Tiga konfigurasi peta percikan.Kami akan menggunakan konfigurasi peta percikan ini terlepas dari tekstur mana yang sebenarnya tercampur. Artinya, peta percikan akan selalu sama. Hanya tekstur yang akan berubah. Cara melakukan ini, kita akan mencari tahu nanti.Kita perlu mengubah HexGridChunk
sehingga menciptakan peta percikan ini, daripada menggunakan warna sel. Karena kita akan sering menggunakan tiga warna, kita akan membuat bidang statis untuknya. static Color color1 = new Color(1f, 0f, 0f); static Color color2 = new Color(0f, 1f, 0f); static Color color3 = new Color(0f, 0f, 1f);
Pusat sel
Mari kita mulai dengan mengganti warna pusat sel secara default. Tidak ada pencampuran yang dilakukan di sini, jadi kami hanya menggunakan warna pertama, yaitu merah. void TriangulateWithoutRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { TriangulateEdgeFan(center, e, color1); … }
Pusat sel merah.Pusat sel sekarang menjadi merah. Mereka semua menggunakan yang pertama dari tiga tekstur, tidak peduli apa teksturnya. Peta percikannya sama, terlepas dari warna yang digunakan untuk mewarnai sel.Lingkungan Sungai
Kami mengubah segmen hanya di dalam sel tanpa sungai mengalir di sepanjang mereka. Kita perlu melakukan hal yang sama untuk segmen yang berdekatan dengan sungai. Dalam kasus kami, ini adalah strip tulang rusuk dan penggemar segitiga rusuk. Di sini juga, hanya merah yang cukup untuk kita. void TriangulateAdjacentToRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … TriangulateEdgeStrip(m, color1, e, color1); TriangulateEdgeFan(center, m, color1); … }
Segmen merah berdekatan dengan sungai.Sungai
Selanjutnya, kita perlu menjaga geometri sungai di dalam sel. Semuanya juga harus berubah menjadi merah. Untuk memulainya, mari kita lihat awal dan akhir sungai. void TriangulateWithRiverBeginOrEnd ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … TriangulateEdgeStrip(m, color1, e, color1); TriangulateEdgeFan(center, m, color1); … }
Dan kemudian geometri yang membentuk tepi dan dasar sungai. Saya telah mengelompokkan panggilan metode warna untuk membuat kode lebih mudah dibaca. void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … TriangulateEdgeStrip(m, color1, e, color1); terrain.AddTriangle(centerL, m.v1, m.v2);
Sungai merah di sepanjang sel.Iga
Semua tepi berbeda karena berada di antara sel yang dapat memiliki jenis medan yang berbeda. Kami menggunakan warna pertama untuk jenis sel saat ini, dan warna kedua untuk jenis tetangga. Akibatnya, peta percikan akan menjadi gradien merah-hijau, bahkan jika kedua sel memiliki tipe yang sama. Jika kedua sel menggunakan tekstur yang sama, maka itu hanya menjadi campuran dari tekstur yang sama di kedua sisi. void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { … if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(e1, cell, e2, neighbor, hasRoad); } else { TriangulateEdgeStrip(e1, color1, e2, color2, hasRoad); } … }
Iga merah-hijau, tidak termasuk tepian.Bukankah transisi yang tajam antara merah dan hijau menyebabkan masalah?, , . . splat map, . .
, .
Tepi dengan tepian sedikit lebih rumit, karena mereka memiliki simpul tambahan. Untungnya, kode interpolasi yang ada berfungsi dengan baik dengan warna peta percikan. Cukup gunakan warna pertama dan kedua, bukan warna sel-sel awal dan akhir. void TriangulateEdgeTerraces ( EdgeVertices begin, HexCell beginCell, EdgeVertices end, HexCell endCell, bool hasRoad ) { EdgeVertices e2 = EdgeVertices.TerraceLerp(begin, end, 1); Color c2 = HexMetrics.TerraceLerp(color1, color2, 1); TriangulateEdgeStrip(begin, color1, e2, c2, hasRoad); for (int i = 2; i < HexMetrics.terraceSteps; i++) { EdgeVertices e1 = e2; Color c1 = c2; e2 = EdgeVertices.TerraceLerp(begin, end, i); c2 = HexMetrics.TerraceLerp(color1, color2, i); TriangulateEdgeStrip(e1, c1, e2, c2, hasRoad); } TriangulateEdgeStrip(e2, c2, end, color2, hasRoad); }
Tulang rusuk merah-hijau.Sudut
Sudut sel adalah yang paling sulit karena mereka harus mencampur tiga tekstur yang berbeda. Kami menggunakan merah untuk puncak bawah, hijau untuk kiri dan biru untuk kanan. Mari kita mulai dengan sudut satu segitiga. void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … else { terrain.AddTriangle(bottom, left, right); terrain.AddTriangleColor(color1, color2, color3); } features.AddWall(bottom, bottomCell, left, leftCell, right, rightCell); }
Sudut merah-hijau-biru, kecuali untuk tepian.Di sini kita kembali dapat menggunakan kode interpolasi warna yang ada untuk sudut dengan tepian. Hanya interpolasi dilakukan antara tiga, bukan dua warna. Pertama, perhatikan tepian yang tidak dekat tebing. void TriangulateCornerTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { Vector3 v3 = HexMetrics.TerraceLerp(begin, left, 1); Vector3 v4 = HexMetrics.TerraceLerp(begin, right, 1); Color c3 = HexMetrics.TerraceLerp(color1, color2, 1); Color c4 = HexMetrics.TerraceLerp(color1, color3, 1); terrain.AddTriangle(begin, v3, v4); terrain.AddTriangleColor(color1, c3, c4); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v3; Vector3 v2 = v4; Color c1 = c3; Color c2 = c4; v3 = HexMetrics.TerraceLerp(begin, left, i); v4 = HexMetrics.TerraceLerp(begin, right, i); c3 = HexMetrics.TerraceLerp(color1, color2, i); c4 = HexMetrics.TerraceLerp(color1, color3, i); terrain.AddQuad(v1, v2, v3, v4); terrain.AddQuadColor(c1, c2, c3, c4); } terrain.AddQuad(v3, v4, left, right); terrain.AddQuadColor(c3, c4, color2, color3); }
Tepi sudut merah-hijau-biru, kecuali tepian di sepanjang tebing.Ketika datang ke tebing, kita perlu menggunakan metode TriangulateBoundaryTriangle
. Metode ini menerima sel awal dan kiri sebagai parameter. Namun, sekarang kita membutuhkan warna percikan yang sesuai, yang dapat bervariasi tergantung pada topologi. Karena itu, kami mengganti parameter ini dengan warna. void TriangulateBoundaryTriangle ( Vector3 begin, Color beginColor, Vector3 left, Color leftColor, Vector3 boundary, Color boundaryColor ) { Vector3 v2 = HexMetrics.Perturb(HexMetrics.TerraceLerp(begin, left, 1)); Color c2 = HexMetrics.TerraceLerp(beginColor, leftColor, 1); terrain.AddTriangleUnperturbed(HexMetrics.Perturb(begin), v2, boundary); terrain.AddTriangleColor(beginColor, c2, boundaryColor); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color c1 = c2; v2 = HexMetrics.Perturb(HexMetrics.TerraceLerp(begin, left, i)); c2 = HexMetrics.TerraceLerp(beginColor, leftColor, i); terrain.AddTriangleUnperturbed(v1, v2, boundary); terrain.AddTriangleColor(c1, c2, boundaryColor); } terrain.AddTriangleUnperturbed(v2, HexMetrics.Perturb(left), boundary); terrain.AddTriangleColor(c2, leftColor, boundaryColor); }
Ubahlah TriangulateCornerTerracesCliff
agar menggunakan warna yang benar. void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … Color boundaryColor = Color.Lerp(color1, color3, b); TriangulateBoundaryTriangle( begin, color1, left, color2, boundary, boundaryColor ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, color2, right, color3, boundary, boundaryColor ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleColor(color2, color3, boundaryColor); } }
Dan lakukan hal yang sama untuk TriangulateCornerCliffTerraces
. void TriangulateCornerCliffTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … Color boundaryColor = Color.Lerp(color1, color2, b); TriangulateBoundaryTriangle( right, color3, begin, color1, boundary, boundaryColor ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, color2, right, color3, boundary, boundaryColor ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleColor(color2, color3, boundaryColor); } }
Peta bantuan percikan penuh.paket unityArray Tekstur
Sekarang karena medan kami memiliki peta percikan, kami dapat meneruskan koleksi tekstur ke shader. Kami tidak bisa hanya menetapkan shader ke array tekstur C #, karena array harus ada dalam memori GPU sebagai satu kesatuan. Kita harus menggunakan objek khusus Texture2DArray
yang telah didukung di Unity sejak versi 5.4.Apakah semua GPU mendukung susunan tekstur?GPU , .
Unity .
- Direct3D 11/12 (Windows, Xbox One)
- OpenGL Core (Mac OS X, Linux)
- Metal (iOS, Mac OS X)
- OpenGL ES 3.0 (Android, iOS, WebGL 2.0)
- PlayStation 4
Tuan
Sayangnya, dukungan Unity untuk array tekstur di versi 5.5 sangat minim. Kami tidak bisa hanya membuat aset array tekstur dan memberikan tekstur padanya. Kita harus melakukannya secara manual. Kami dapat membuat array tekstur dalam mode Putar, atau membuat aset di editor. Mari kita membuat aset.Mengapa membuat aset?, Play . , .
, . Unity . , . , .
Untuk membuat array tekstur, kita akan merakit master kita sendiri. Buat skrip TextureArrayWizard
dan letakkan di dalam folder Editor . Sebagai gantinya, MonoBehaviour
itu harus memperluas jenis ScriptableWizard
dari namespace UnityEditor
. using UnityEditor; using UnityEngine; public class TextureArrayWizard : ScriptableWizard { }
Kita dapat membuka wizard melalui metode statis umum ScriptableWizard.DisplayWizard
. Parameternya adalah nama-nama jendela penyihir dan tombol buatnya. Kami akan memanggil metode ini dalam metode statis CreateWizard
. static void CreateWizard () { ScriptableWizard.DisplayWizard<TextureArrayWizard>( "Create Texture Array", "Create" ); }
Untuk mengakses wizard melalui editor, kita perlu menambahkan metode ini ke menu Unity. Ini dapat dilakukan dengan menambahkan atribut ke metode MenuItem
. Mari kita tambahkan ke menu Aset , dan lebih khusus lagi ke Array Aset / Buat / Tekstur . [MenuItem("Assets/Create/Texture Array")] static void CreateWizard () { … }
Wizard kustom kami.Menggunakan item menu baru, Anda dapat membuka menu pop-up dari wizard khusus kami. Itu tidak terlalu indah, tetapi cocok untuk memecahkan masalah. Namun, masih kosong. Untuk membuat larik tekstur, kita perlu larik tekstur. Tambahkan bidang umum untuk master. GUI standar dari wizard menampilkannya seperti yang dilakukan oleh inspektur standar. public Texture2D[] textures;
Kuasai dengan tekstur.Ayo buat sesuatu
Ketika Anda mengklik tombol Buat wizard, itu menghilang. Selain itu, Unity mengeluh bahwa tidak ada metode OnWizardCreate
. Ini adalah metode yang dipanggil saat tombol buat diklik, jadi kita perlu menambahkannya ke wizard. void OnWizardCreate () { }
Di sini kita akan membuat susunan tekstur kita. Setidaknya jika pengguna menambahkan tekstur ke master. Jika tidak, tidak ada yang dibuat dan pekerjaan harus dihentikan. void OnWizardCreate () { if (textures.Length == 0) { return; } }
Langkah selanjutnya adalah meminta lokasi untuk menyimpan aset array tekstur. Panel penyimpanan file dapat dibuka menggunakan metode ini EditorUtility.SaveFilePanelInProject
. Parameternya menentukan nama panel, nama file default, ekstensi file dan deskripsi. Array tekstur menggunakan ekstensi file aset umum . if (textures.Length == 0) { return; } EditorUtility.SaveFilePanelInProject( "Save Texture Array", "Texture Array", "asset", "Save Texture Array" );
SaveFilePanelInProject
mengembalikan jalur file yang dipilih pengguna. Jika pengguna mengklik batal pada panel ini, jalan akan menjadi string kosong. Karena itu, dalam hal ini, kita harus mengganggu pekerjaan. string path = EditorUtility.SaveFilePanelInProject( "Save Texture Array", "Texture Array", "asset", "Save Texture Array" ); if (path.Length == 0) { return; }
Membuat array tekstur
Jika kita memiliki jalur yang benar, maka kita dapat melanjutkan dan membuat objek baru Texture2DArray
. Metode konstruktornya memerlukan menentukan lebar dan tinggi tekstur, panjang array, format tekstur, dan kebutuhan untuk tekstur texting. Parameter ini harus sama untuk semua tekstur dalam array. Untuk mengkonfigurasi objek, kami menggunakan tekstur pertama. Pengguna harus memverifikasi bahwa semua tekstur memiliki format yang sama. if (path.Length == 0) { return; } Texture2D t = textures[0]; Texture2DArray textureArray = new Texture2DArray( t.width, t.height, textures.Length, t.format, t.mipmapCount > 1 );
Karena array tekstur adalah sumber daya GPU tunggal, ia menggunakan mode penyaringan dan lipat yang sama untuk semua tekstur. Di sini kita kembali menggunakan tekstur pertama untuk mengatur semuanya. Texture2DArray textureArray = new Texture2DArray( t.width, t.height, textures.Length, t.format, t.mipmapCount > 1 ); textureArray.anisoLevel = t.anisoLevel; textureArray.filterMode = t.filterMode; textureArray.wrapMode = t.wrapMode;
Sekarang kita bisa menyalin tekstur ke dalam array menggunakan metode ini Graphics.CopyTexture
. Metode ini menyalin data tekstur mentah, satu tingkat mip pada satu waktu. Karena itu, kita perlu memutar semua tekstur dan level mipnya. Parameter metode adalah dua set yang terdiri dari sumber daya tekstur, indeks, dan tingkat mip. Karena tekstur aslinya bukan array, indeksnya selalu nol. textureArray.wrapMode = t.wrapMode; for (int i = 0; i < textures.Length; i++) { for (int m = 0; m < t.mipmapCount; m++) { Graphics.CopyTexture(textures[i], 0, m, textureArray, i, m); } }
Pada tahap ini, kami memiliki memori dalam susunan tekstur yang benar, tetapi belum merupakan aset. Langkah terakhir adalah memanggil AssetDatabase.CreateAsset
dengan array dan path-nya. Dalam hal ini, data akan ditulis ke file di proyek kami, dan itu akan muncul di jendela proyek. for (int i = 0; i < textures.Length; i++) { … } AssetDatabase.CreateAsset(textureArray, path);
Tekstur
Untuk membuat array tekstur nyata, kita perlu tekstur asli. Berikut adalah lima tekstur yang cocok dengan warna yang kami gunakan sampai sekarang. Kuning menjadi pasir, hijau menjadi rumput, biru menjadi bumi, oranye menjadi batu, dan putih menjadi salju.Tekstur pasir, rumput, tanah, batu, dan salju.Perhatikan bahwa tekstur ini bukan foto relief ini. Ini adalah pola pseudo-acak mudah yang saya buat menggunakan NumberFlow . Saya berusaha keras untuk membuat jenis dan detail bantuan yang dapat dikenali yang tidak bertentangan dengan bantuan abstrak poligonal. Photorealism ternyata tidak cocok untuk ini. Selain itu, meskipun pola menambah variabilitas, ada beberapa fitur berbeda di dalamnya yang akan membuat pengulangan segera terlihat.Tambahkan tekstur ini ke array master, pastikan urutannya sesuai dengan warna. Yaitu, pertama pasir, kemudian rumput, tanah, batu dan akhirnya salju.Membuat array tekstur.Setelah membuat aset array tekstur, pilih dan memeriksanya di inspektur.Inspektur array tekstur.Ini adalah tampilan paling sederhana dari sepotong data array tekstur. Perhatikan bahwa ada sakelar Is Readable yang pada awalnya dihidupkan. Karena kita tidak perlu membaca data piksel dari array, matikan. Kami tidak dapat melakukan ini di wisaya karena tidak Texture2DArray
ada metode atau properti untuk mengakses parameter ini.(Dalam Unity 5.6, ada bug yang merusak array tekstur dalam rakitan pada beberapa platform. Anda dapat mengatasinya tanpa menonaktifkan Dapat Dibaca .)Perlu juga dicatat bahwa ada bidang Ruang Warnayang diberi nilai 1. Ini berarti bahwa tekstur diasumsikan berada dalam ruang gamma, yang benar. Jika mereka seharusnya berada dalam ruang linear, maka bidang tersebut harus diatur ke 0. Sebenarnya, perancang Texture2DArray
memiliki parameter tambahan untuk menentukan ruang warna, tetapi itu Texture2D
tidak menunjukkan apakah itu dalam ruang linear atau tidak, oleh karena itu, dalam hal apa pun, Anda perlu mengatur nilai secara manual.Shader
Sekarang kita memiliki serangkaian tekstur, kita perlu mengajari shader cara bekerja dengannya. Untuk saat ini, kami menggunakan shader VertexColors untuk merender medan . Karena sekarang kami akan menggunakan tekstur alih-alih warna, ubah nama menjadi Terrain . Kemudian kita mengubah parameter _MainTex menjadi array tekstur dan menetapkannya sebagai aset. Shader "Custom/Terrain" { Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Terrain Texture Array", 2DArray) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 } … }
Bahan bantuan dengan berbagai tekstur.Untuk mengaktifkan array tekstur pada semua platform yang mendukungnya, Anda perlu meningkatkan level target shader dari 3.0 menjadi 3.5. #pragma target 3.5
Karena variabel _MainTex
sekarang merujuk ke array tekstur, kita perlu mengubah tipenya. Jenisnya tergantung pada platform target dan makro akan membereskannya UNITY_DECLARE_TEX2DARRAY
.
Seperti pada shader lainnya, untuk mencicipi tekstur relief, kita memerlukan koordinat dunia XZ. Oleh karena itu, kami akan menambahkan posisi di dunia ke struktur input shader permukaan. Kami juga menghapus koordinat UV default, karena kami tidak membutuhkannya. struct Input {
Untuk sampel array tekstur, kita perlu menggunakan makro UNITY_SAMPLE_TEX2DARRAY
. Untuk mengambil sampel array, dibutuhkan tiga koordinat. Dua yang pertama adalah koordinat UV biasa. Kami akan menggunakan koordinat dunia XZ yang diskalakan hingga 0,02. Jadi kami mendapatkan resolusi tekstur yang baik pada pembesaran penuh. Tekstur akan diulang kira-kira setiap empat sel.Koordinat ketiga digunakan sebagai indeks array tekstur, seperti dalam array biasa. Karena koordinatnya mengambang, sebelum pengindeksan array GPU mengitarinya. Karena sampai kita tahu tekstur apa yang dibutuhkan, mari kita gunakan dulu. Juga, warna titik tidak akan mempengaruhi hasil akhir, karena itu adalah peta percikan. void surf (Input IN, inout SurfaceOutputStandard o) { float2 uv = IN.worldPos.xz * 0.02; fixed4 c = UNITY_SAMPLE_TEX2DARRAY(_MainTex, float3(uv, 0)); Albedo = c.rgb * _Color; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; }
Semuanya menjadi pasir.paket unityPemilihan tekstur
Kami membutuhkan peta percikan yang mencampur tiga jenis menjadi sebuah segitiga. Kami memiliki serangkaian tekstur dengan tekstur untuk setiap jenis medan. Kami memiliki shader yang sampel array tekstur. Tetapi untuk saat ini, kami tidak memiliki cara untuk memberi tahu shader tekstur mana yang harus dipilih untuk setiap segitiga.Karena setiap segitiga bercampur hingga tiga jenis, kita perlu mengasosiasikan tiga indeks dengan masing-masing segitiga. Kami tidak dapat menyimpan informasi untuk segitiga, jadi kami harus menyimpan indeks untuk simpul. Ketiga simpul segitiga hanya akan menyimpan indeks yang sama dengan warna solid.Data Jerat
Kita dapat menggunakan salah satu set UV mesh untuk menyimpan indeks. Karena tiga indeks disimpan pada setiap titik, set UV 2D yang ada tidak akan cukup. Untungnya, set UV dapat berisi hingga empat koordinat. Karena itu, kami menambah HexMesh
daftar kedua Vector3
, yang akan kami sebut sebagai jenis bantuan. public bool useCollider, useColors, useUVCoordinates, useUV2Coordinates; public bool useTerrainTypes; [NonSerialized] List<Vector3> vertices, terrainTypes;
Aktifkan jenis medan untuk anak Terrain dari pabrikan Hex Grid Chunk .Kami menggunakan jenis bantuan.Jika perlu, kami akan mengambil daftar lain Vector3
untuk jenis bantuan selama pembersihan mesh. public void Clear () { … if (useTerrainTypes) { terrainTypes = ListPool<Vector3>.Get(); } triangles = ListPool<int>.Get(); }
Dalam proses menerapkan data mesh, kami menyimpan jenis bantuan di set UV ketiga. Karena itu, mereka tidak akan bertentangan dengan dua set lainnya, jika kita memutuskan untuk menggunakannya bersama. public void Apply () { … if (useTerrainTypes) { hexMesh.SetUVs(2, terrainTypes); ListPool<Vector3>.Add(terrainTypes); } hexMesh.SetTriangles(triangles, 0); … }
Untuk mengatur jenis relief dari segitiga, kita akan gunakan Vector3
. Karena yang sama untuk seluruh segitiga, kami hanya menambahkan data yang sama tiga kali. public void AddTriangleTerrainTypes (Vector3 types) { terrainTypes.Add(types); terrainTypes.Add(types); terrainTypes.Add(types); }
Pencampuran di quad berfungsi sama. Keempat simpul adalah dari jenis yang sama. public void AddQuadTerrainTypes (Vector3 types) { terrainTypes.Add(types); terrainTypes.Add(types); terrainTypes.Add(types); terrainTypes.Add(types); }
Penggemar Segitiga Ribs
Sekarang kita perlu menambahkan tipe ke data mesh di HexGridChunk
. Mari kita mulai TriangulateEdgeFan
. Pertama, demi keterbacaan yang lebih baik, kami akan memisahkan panggilan ke metode titik dan warna. Ingatlah bahwa dengan setiap panggilan ke metode ini, kami meneruskannya kepadanya color1
, sehingga kami dapat menggunakan warna ini secara langsung, dan tidak menerapkan parameter. void TriangulateEdgeFan (Vector3 center, EdgeVertices edge, Color color) { terrain.AddTriangle(center, edge.v1, edge.v2);
Setelah warna, kami menambahkan jenis bantuan. Karena jenis dalam segitiga mungkin berbeda, ini harus menjadi parameter yang menggantikan warna. Gunakan tipe sederhana ini untuk membuat Vector3
. Hanya empat saluran pertama yang penting bagi kami, karena dalam hal ini peta percikan selalu merah. Karena ketiga komponen vektor perlu ditetapkan, mari tetapkan satu jenis. void TriangulateEdgeFan (Vector3 center, EdgeVertices edge, float type) { … Vector3 types; types.x = types.y = types.z = type; terrain.AddTriangleTerrainTypes(types); terrain.AddTriangleTerrainTypes(types); terrain.AddTriangleTerrainTypes(types); terrain.AddTriangleTerrainTypes(types); }
Sekarang kita perlu mengubah semua panggilan ke metode ini, mengganti argumen warna dengan indeks dari jenis medan sel. Kami akan membuat perubahan ini di TriangulateWithoutRiver
, TriangulateAdjacentToRiver
dan TriangulateWithRiverBeginOrEnd
.
Pada titik ini, ketika Anda memulai mode Putar, kesalahan akan muncul memberi tahu Anda bahwa set ketiga jerat UV berada di luar batas. Ini terjadi karena kami belum menambahkan tipe relief untuk setiap segitiga dan quad. Jadi mari kita terus berubah HexGridChunk
.Rib stripes
Sekarang saat membuat strip tepi, kita perlu tahu jenis medan apa yang ada di kedua sisi. Oleh karena itu, kami menambahkannya sebagai parameter, dan kemudian membuat vektor jenis yang dua salurannya ditugaskan untuk jenis ini. Saluran ketiga tidak penting, jadi samakan saja dengan yang pertama. Setelah menambahkan warna, tambahkan jenis ke quad. void TriangulateEdgeStrip ( EdgeVertices e1, Color c1, float type1, EdgeVertices e2, Color c2, float type2, bool hasRoad = false ) { terrain.AddQuad(e1.v1, e1.v2, e2.v1, e2.v2); terrain.AddQuad(e1.v2, e1.v3, e2.v2, e2.v3); terrain.AddQuad(e1.v3, e1.v4, e2.v3, e2.v4); terrain.AddQuad(e1.v4, e1.v5, e2.v4, e2.v5); terrain.AddQuadColor(c1, c2); terrain.AddQuadColor(c1, c2); terrain.AddQuadColor(c1, c2); terrain.AddQuadColor(c1, c2); Vector3 types; types.x = types.z = type1; types.y = type2; terrain.AddQuadTerrainTypes(types); terrain.AddQuadTerrainTypes(types); terrain.AddQuadTerrainTypes(types); terrain.AddQuadTerrainTypes(types); if (hasRoad) { TriangulateRoadSegment(e1.v2, e1.v3, e1.v4, e2.v2, e2.v3, e2.v4); } }
Sekarang kita perlu mengubah tantangan TriangulateEdgeStrip
. Pertama TriangulateAdjacentToRiver
, TriangulateWithRiverBeginOrEnd
dan TriangulateWithRiver
harus menggunakan tipe sel untuk kedua sisi strip rib.
Selanjutnya, case paling sederhana dari edge TriangulateConnection
harus menggunakan tipe sel untuk edge terdekat dan tipe tetangga untuk edge jauh. Mereka bisa sama atau berbeda. void TriangulateConnection ( HexDirection direction, HexCell cell, EdgeVertices e1 ) { … if (cell.GetEdgeType(direction) == HexEdgeType.Slope) { TriangulateEdgeTerraces(e1, cell, e2, neighbor, hasRoad); } else {
Hal yang sama berlaku untuk TriangulateEdgeTerraces
apa yang memicu tiga kali TriangulateEdgeStrip
. Jenis untuk tepiannya sama. void TriangulateEdgeTerraces ( EdgeVertices begin, HexCell beginCell, EdgeVertices end, HexCell endCell, bool hasRoad ) { EdgeVertices e2 = EdgeVertices.TerraceLerp(begin, end, 1); Color c2 = HexMetrics.TerraceLerp(color1, color2, 1); float t1 = beginCell.TerrainTypeIndex; float t2 = endCell.TerrainTypeIndex; TriangulateEdgeStrip(begin, color1, t1, e2, c2, t2, hasRoad); for (int i = 2; i < HexMetrics.terraceSteps; i++) { EdgeVertices e1 = e2; Color c1 = c2; e2 = EdgeVertices.TerraceLerp(begin, end, i); c2 = HexMetrics.TerraceLerp(color1, color2, i); TriangulateEdgeStrip(e1, c1, t1, e2, c2, t2, hasRoad); } TriangulateEdgeStrip(e2, c2, t1, end, color2, t2, hasRoad); }
Sudut
Kasus sudut yang paling sederhana adalah segitiga sederhana. Sel bawah mentransfer tipe pertama, yang kiri yang kedua, dan yang kanan yang ketiga. Dengan menggunakannya, buat vektor jenis dan tambahkan ke segitiga. void TriangulateCorner ( Vector3 bottom, HexCell bottomCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { … else { terrain.AddTriangle(bottom, left, right); terrain.AddTriangleColor(color1, color2, color3); Vector3 types; types.x = bottomCell.TerrainTypeIndex; types.y = leftCell.TerrainTypeIndex; types.z = rightCell.TerrainTypeIndex; terrain.AddTriangleTerrainTypes(types); } features.AddWall(bottom, bottomCell, left, leftCell, right, rightCell); }
Kami menggunakan pendekatan yang sama TriangulateCornerTerraces
, hanya di sini kami membuat grup quad-s. void TriangulateCornerTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { Vector3 v3 = HexMetrics.TerraceLerp(begin, left, 1); Vector3 v4 = HexMetrics.TerraceLerp(begin, right, 1); Color c3 = HexMetrics.TerraceLerp(color1, color2, 1); Color c4 = HexMetrics.TerraceLerp(color1, color3, 1); Vector3 types; types.x = beginCell.TerrainTypeIndex; types.y = leftCell.TerrainTypeIndex; types.z = rightCell.TerrainTypeIndex; terrain.AddTriangle(begin, v3, v4); terrain.AddTriangleColor(color1, c3, c4); terrain.AddTriangleTerrainTypes(types); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v3; Vector3 v2 = v4; Color c1 = c3; Color c2 = c4; v3 = HexMetrics.TerraceLerp(begin, left, i); v4 = HexMetrics.TerraceLerp(begin, right, i); c3 = HexMetrics.TerraceLerp(color1, color2, i); c4 = HexMetrics.TerraceLerp(color1, color3, i); terrain.AddQuad(v1, v2, v3, v4); terrain.AddQuadColor(c1, c2, c3, c4); terrain.AddQuadTerrainTypes(types); } terrain.AddQuad(v3, v4, left, right); terrain.AddQuadColor(c3, c4, color2, color3); terrain.AddQuadTerrainTypes(types); }
Saat mencampur tepian dan tebing, kita perlu menggunakannya TriangulateBoundaryTriangle
. Berikan saja parameter tipe vektor dan tambahkan ke semua segitiga. void TriangulateBoundaryTriangle ( Vector3 begin, Color beginColor, Vector3 left, Color leftColor, Vector3 boundary, Color boundaryColor, Vector3 types ) { Vector3 v2 = HexMetrics.Perturb(HexMetrics.TerraceLerp(begin, left, 1)); Color c2 = HexMetrics.TerraceLerp(beginColor, leftColor, 1); terrain.AddTriangleUnperturbed(HexMetrics.Perturb(begin), v2, boundary); terrain.AddTriangleColor(beginColor, c2, boundaryColor); terrain.AddTriangleTerrainTypes(types); for (int i = 2; i < HexMetrics.terraceSteps; i++) { Vector3 v1 = v2; Color c1 = c2; v2 = HexMetrics.Perturb(HexMetrics.TerraceLerp(begin, left, i)); c2 = HexMetrics.TerraceLerp(beginColor, leftColor, i); terrain.AddTriangleUnperturbed(v1, v2, boundary); terrain.AddTriangleColor(c1, c2, boundaryColor); terrain.AddTriangleTerrainTypes(types); } terrain.AddTriangleUnperturbed(v2, HexMetrics.Perturb(left), boundary); terrain.AddTriangleColor(c2, leftColor, boundaryColor); terrain.AddTriangleTerrainTypes(types); }
Dalam TriangulateCornerTerracesCliff
membuat vektor jenis berdasarkan sel yang ditransfer. Kemudian tambahkan ke satu segitiga dan lewati TriangulateBoundaryTriangle
. void TriangulateCornerTerracesCliff ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (rightCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } Vector3 boundary = Vector3.Lerp( HexMetrics.Perturb(begin), HexMetrics.Perturb(right), b ); Color boundaryColor = Color.Lerp(color1, color3, b); Vector3 types; types.x = beginCell.TerrainTypeIndex; types.y = leftCell.TerrainTypeIndex; types.z = rightCell.TerrainTypeIndex; TriangulateBoundaryTriangle( begin, color1, left, color2, boundary, boundaryColor, types ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, color2, right, color3, boundary, boundaryColor, types ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleColor(color2, color3, boundaryColor); terrain.AddTriangleTerrainTypes(types); } }
Hal yang sama berlaku untuk TriangulateCornerCliffTerraces
. void TriangulateCornerCliffTerraces ( Vector3 begin, HexCell beginCell, Vector3 left, HexCell leftCell, Vector3 right, HexCell rightCell ) { float b = 1f / (leftCell.Elevation - beginCell.Elevation); if (b < 0) { b = -b; } Vector3 boundary = Vector3.Lerp( HexMetrics.Perturb(begin), HexMetrics.Perturb(left), b ); Color boundaryColor = Color.Lerp(color1, color2, b); Vector3 types; types.x = beginCell.TerrainTypeIndex; types.y = leftCell.TerrainTypeIndex; types.z = rightCell.TerrainTypeIndex; TriangulateBoundaryTriangle( right, color3, begin, color1, boundary, boundaryColor, types ); if (leftCell.GetEdgeType(rightCell) == HexEdgeType.Slope) { TriangulateBoundaryTriangle( left, color2, right, color3, boundary, boundaryColor, types ); } else { terrain.AddTriangleUnperturbed( HexMetrics.Perturb(left), HexMetrics.Perturb(right), boundary ); terrain.AddTriangleColor(color2, color3, boundaryColor); terrain.AddTriangleTerrainTypes(types); } }
Sungai
Metode terakhir yang diubah adalah ini TriangulateWithRiver
. Karena di sini kita berada di tengah sel, kita hanya berurusan dengan jenis sel saat ini. Oleh karena itu, buat vektor untuk itu dan tambahkan ke segitiga dan quad-s. void TriangulateWithRiver ( HexDirection direction, HexCell cell, Vector3 center, EdgeVertices e ) { … terrain.AddTriangleColor(color1); terrain.AddQuadColor(color1); terrain.AddQuadColor(color1); terrain.AddTriangleColor(color1); Vector3 types; types.x = types.y = types.z = cell.TerrainTypeIndex; terrain.AddTriangleTerrainTypes(types); terrain.AddQuadTerrainTypes(types); terrain.AddQuadTerrainTypes(types); terrain.AddTriangleTerrainTypes(types); … }
Jenis campuran
Pada tahap ini, jerat berisi indeks elevasi yang diperlukan. Yang tersisa bagi kita adalah memaksa shader Terrain untuk menggunakannya. Agar indeks jatuh ke shader fragmen, pertama-tama kita harus melewati mereka melalui vertex shader. Kita dapat melakukan ini dalam fungsi vertex kita sendiri, seperti yang kita lakukan pada shader muara . Dalam hal ini, kami menambahkan bidang ke struktur input float3 terrain
dan menyalinnya ke dalamnya v.texcoord2.xyz
. #pragma surface surf Standard fullforwardshadows vertex:vert #pragma target 3.5 … struct Input { float4 color : COLOR; float3 worldPos; float3 terrain; }; void vert (inout appdata_full v, out Input data) { UNITY_INITIALIZE_OUTPUT(Input, data); data.terrain = v.texcoord2.xyz; }
Kita perlu sampel array tekstur tiga kali per fragmen. Oleh karena itu, mari kita membuat fungsi yang nyaman untuk membuat koordinat tekstur, mengambil sampel array dan memodulasi sampel dengan peta percikan untuk satu indeks. float4 GetTerrainColor (Input IN, int index) { float3 uvw = float3(IN.worldPos.xz * 0.02, IN.terrain[index]); float4 c = UNITY_SAMPLE_TEX2DARRAY(_MainTex, uvw); return c * IN.color[index]; } void surf (Input IN, inout SurfaceOutputStandard o) { … }
Bisakah kita bekerja dengan vektor sebagai array?Ya - color[0]
color.r
. color[1]
color.g
, .
Dengan menggunakan fungsi ini, kita cukup sampel array tekstur tiga kali dan menggabungkan hasilnya. void surf (Input IN, inout SurfaceOutputStandard o) { // float2 uv = IN.worldPos.xz * 0.02; fixed4 c = GetTerrainColor(IN, 0) + GetTerrainColor(IN, 1) + GetTerrainColor(IN, 2); o.Albedo = c.rgb * _Color; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; }
Relief bertekstur.Sekarang kita bisa melukis relief dengan tekstur. Mereka mencampur seperti warna solid. Karena kita menggunakan koordinat dunia sebagai koordinat UV, mereka tidak berubah dengan ketinggian. Akibatnya, di sepanjang tebing yang tajam, teksturnya membentang. Jika teksturnya cukup netral dan sangat bervariasi, maka hasilnya akan dapat diterima. Kalau tidak, kita mendapatkan stretch mark jelek besar. Anda dapat mencoba menyembunyikannya dengan geometri tambahan atau tekstur tebing, tetapi dalam tutorial kami tidak akan melakukan ini.Sapu
Sekarang, ketika kita menggunakan tekstur alih-alih warna, masuk akal untuk mengubah panel editor. Kita dapat membuat antarmuka yang indah yang bahkan dapat menampilkan tekstur relief, tetapi saya akan fokus pada singkatan yang sesuai dengan gaya skema yang ada.Opsi bantuan.Selain itu, HexCell
properti warna tidak lagi diperlukan, jadi hapus saja.
Anda HexGrid
juga dapat menghapus serangkaian warna dan kode terkait dari dalamnya.
Akhirnya, serangkaian warna juga tidak diperlukan di HexMetrics
.
paket unityBagian 15: jarak
- Tampilkan garis kisi.
- Beralih antara mode pengeditan dan navigasi.
- Hitung jarak antar sel.
- Kami menemukan cara mengatasi kendala.
- Kami memperhitungkan variabel biaya pemindahan.
Setelah membuat peta berkualitas tinggi, kami akan memulai navigasi.Jalur terpendek tidak selalu lurus.Tampilan kotak
Navigasi pada peta dilakukan dengan berpindah dari sel ke sel. Untuk sampai ke suatu tempat, Anda harus melalui serangkaian sel. Untuk mempermudah memperkirakan jarak, mari tambahkan opsi untuk menampilkan kisi segi enam yang menjadi dasar peta kita.Tekstur jala
Meskipun penyimpangan dari map mesh, mesh di bawahnya benar-benar rata. Kami dapat menunjukkan ini dengan memproyeksikan pola kisi ke peta. Ini dapat dicapai dengan menggunakan tekstur jala berulang.Mengulangi tekstur jala.Tekstur yang ditunjukkan di atas berisi sebagian kecil dari kisi segi enam yang meliputi 2 sel 2 sel. Area ini berbentuk persegi, bukan persegi. Karena teksturnya sendiri berbentuk bujur sangkar, polanya terlihat melar. Saat pengambilan sampel, kita perlu mengimbanginya.Proyeksi kisi
Untuk memproyeksikan pola mesh, kita perlu menambahkan properti tekstur ke shader Terrain . Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Terrain Texture Array", 2DArray) = "white" {} _GridTex ("Grid Texture", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 }
Bahan bantuan dengan tekstur jala.Cicipi tekstur menggunakan koordinat XZ dunia, lalu kalikan dengan albedo. Karena garis grid pada tekstur berwarna abu-abu, ini akan menjalin pola ke dalam relief. sampler2D _GridTex; … void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = GetTerrainColor(IN, 0) + GetTerrainColor(IN, 1) + GetTerrainColor(IN, 2); fixed4 grid = tex2D(_GridTex, IN.worldPos.xz); o.Albedo = c.rgb * grid * _Color; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = ca; }
Albedo dikalikan dengan jaring halus.Kita perlu skala pola sehingga cocok dengan sel-sel di peta. Jarak antara pusat sel tetangga adalah 15, perlu digandakan untuk naik dua sel. Yaitu, kita perlu membagi koordinat grid V dengan 30. Radius internal sel adalah 5√3, dan untuk memindahkan dua sel ke kanan, kita perlu empat kali lipat. Oleh karena itu, perlu untuk membagi koordinat grid U dengan 20√3. float2 gridUV = IN.worldPos.xz; gridUV.x *= 1 / (4 * 8.66025404); gridUV.y *= 1 / (2 * 15.0); fixed4 grid = tex2D(_GridTex, gridUV);
Ukuran jala yang benar.Sekarang garis kisi berhubungan dengan sel-sel peta. Seperti tekstur relief, mereka mengabaikan ketinggian, sehingga garis akan direntangkan di sepanjang tebing.Proyeksi pada sel dengan tinggi.Deformasi mesh biasanya tidak terlalu buruk, terutama ketika melihat peta dari jarak jauh.Jaring di kejauhan.Inklusi Kisi
Meskipun menampilkan kisi itu mudah, itu tidak selalu diperlukan. Misalnya, Anda harus mematikannya saat mengambil tangkapan layar. Selain itu, tidak semua orang lebih suka melihat grid secara konstan. Jadi mari kita membuatnya opsional. Kami akan menambahkan arahan multi_compile ke shader untuk membuat opsi dengan dan tanpa kisi. Untuk melakukan ini, kami akan menggunakan kata kunci GRID_ON
. Kompilasi shader bersyarat dijelaskan dalam tutorial Rendering 5, Multiple Lights . #pragma surface surf Standard fullforwardshadows vertex:vert #pragma target 3.5 #pragma multi_compile _ GRID_ON
Saat mendeklarasikan variabel, grid
pertama-tama kita menetapkan nilai 1. Sebagai hasilnya, kisi akan dinonaktifkan. Kemudian kami akan mencicipi tekstur grid hanya untuk varian dengan kata kunci tertentu GRID_ON
. fixed4 grid = 1; #if defined(GRID_ON) float2 gridUV = IN.worldPos.xz; gridUV.x *= 1 / (4 * 8.66025404); gridUV.y *= 1 / (2 * 15.0); grid = tex2D(_GridTex, gridUV); #endif o.Albedo = c.rgb * grid * _Color;
Karena kata kunci GRID_ON
tidak termasuk dalam shader medan, kisi akan hilang. Untuk mengaktifkannya lagi, kami akan menambahkan sakelar ke UI editor peta. Untuk memungkinkan ini, saya HexMapEditor
harus mendapatkan tautan ke materi Terrain dan metode untuk mengaktifkan atau menonaktifkan kata kunci GRID_ON
. public Material terrainMaterial; … public void ShowGrid (bool visible) { if (visible) { terrainMaterial.EnableKeyword("GRID_ON"); } else { terrainMaterial.DisableKeyword("GRID_ON"); } }
Editor March segi enam dengan mengacu pada materi.Tambahkan sakelar Kotak ke UI dan sambungkan ke metode ShowGrid
.Switch jaringan.Simpan status
Sekarang dalam mode Play, kita dapat mengganti tampilan grid. Pada pengujian pertama, kisi pada awalnya dimatikan dan menjadi terlihat ketika kita menghidupkan sakelar. Saat Anda mematikannya, kisi akan hilang lagi. Namun, jika kita keluar dari mode Putar ketika kisi terlihat, maka saat berikutnya Anda memulai mode Putar, itu akan dihidupkan lagi, meskipun sakelar dimatikan.Ini karena kami mengubah kata kunci untuk materi Terrain umum . Kami mengedit aset material, sehingga perubahan disimpan di editor Unity. Itu tidak akan disimpan di majelis.Untuk selalu memulai permainan tanpa kotak, kami akan menonaktifkan kata kunci GRID_ON
di Sedarlah HexMapEditor
. void Awake () { terrainMaterial.DisableKeyword("GRID_ON"); }
paket unityMode edit
Jika kita ingin mengontrol pergerakan di peta, maka kita perlu berinteraksi dengannya. Paling tidak, kita perlu memilih sel sebagai titik awal jalan. Tetapi ketika Anda mengklik sel, itu akan diedit. Kami dapat menonaktifkan semua opsi pengeditan secara manual, tetapi ini tidak nyaman. Selain itu, kami tidak ingin perhitungan perpindahan dilakukan selama pengeditan peta. Jadi mari kita tambahkan sakelar yang menentukan apakah kita dalam mode edit.Edit sakelar
Tambahkan ke HexMapEditor
bidang Boolean editMode
, serta metode yang mendefinisikannya. Kemudian tambahkan sakelar lain ke UI untuk mengontrolnya. Mari kita mulai dengan mode navigasi, yaitu mode pengeditan akan dinonaktifkan secara default. bool editMode; … public void SetEditMode (bool toggle) { editMode = toggle; }
Sakelar mode pengeditan.Untuk benar-benar menonaktifkan pengeditan, buat panggilan EditCells
bergantung editMode
. void HandleInput () { Ray inputRay = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(inputRay, out hit)) { HexCell currentCell = hexGrid.GetCell(hit.point); if (previousCell && previousCell != currentCell) { ValidateDrag(currentCell); } else { isDrag = false; } if (editMode) { EditCells(currentCell); } previousCell = currentCell; } else { previousCell = null; } }
Label Debugging
Sejauh ini kami tidak memiliki unit untuk bergerak di sekitar peta. Sebaliknya, kami memvisualisasikan jarak pergerakan. Untuk melakukan ini, Anda dapat menggunakan label sel yang ada. Karenanya, kami akan membuatnya terlihat saat mode pengeditan dinonaktifkan. public void SetEditMode (bool toggle) { editMode = toggle; hexGrid.ShowUI(!toggle); }
Sejak kita mulai dengan mode navigasi, label default harus diaktifkan. Saat ini HexGridChunk.Awake
menonaktifkannya, tetapi ia tidak lagi harus melakukan ini. void Awake () { gridCanvas = GetComponentInChildren<Canvas>(); cells = new HexCell[HexMetrics.chunkSizeX * HexMetrics.chunkSizeZ];
Label yang terkoordinasi.Koordinat sel sekarang menjadi terlihat segera setelah meluncurkan mode Putar. Tapi kami tidak perlu koordinat, kami menggunakan label untuk menampilkan jarak. Karena ini hanya membutuhkan satu nomor per sel, Anda dapat menambah ukuran font sehingga dapat dibaca lebih baik. Ubah cetakan Label Hex Cell sehingga menggunakan huruf tebal dengan ukuran 8.Tag dengan ukuran font tebal 8.Sekarang setelah meluncurkan mode Play, kita akan melihat tag besar. Hanya koordinat pertama sel yang terlihat, sisanya tidak ditempatkan pada label.Tag besar.Karena kami tidak lagi membutuhkan koordinat, kami akan menghapus HexGrid.CreateCell
nilai dalam penugasan label.text
. void CreateCell (int x, int z, int i) { … Text label = Instantiate<Text>(cellLabelPrefab); label.rectTransform.anchoredPosition = new Vector2(position.x, position.z);
Anda juga dapat menghapus sakelar Label dan metode yang terkait dari UI HexMapEditor.ShowUI
.
Peralihan metode tidak lebih.paket unityMenemukan jarak
Sekarang kita memiliki mode navigasi yang ditandai, kita dapat mulai menampilkan jarak. Kami akan memilih sel dan kemudian menampilkan jarak dari sel ini ke semua sel di peta.Tampilan jarak
Untuk melacak jarak ke sel, tambahkan ke HexCell
bidang bilangan bulat distance
. Ini akan menunjukkan jarak antara sel ini dan yang dipilih. Oleh karena itu, untuk sel yang dipilih itu sendiri, itu akan menjadi nol, untuk tetangga terdekatnya adalah 1, dan seterusnya. int distance;
Ketika jarak diatur, kita harus memperbarui label sel untuk menampilkan nilainya. HexCell
memiliki referensi ke RectTransform
objek UI. Kita harus memanggilnya GetComponent<Text>
untuk sampai ke sel. Pertimbangkan apa yang Text
ada di namespace UnityEngine.UI
, jadi gunakan di awal skrip. void UpdateDistanceLabel () { Text label = uiRect.GetComponent<Text>(); label.text = distance.ToString(); }
Bukankah kita seharusnya menyimpan tautan langsung ke komponen Teks?, . , , , . , .
Mari atur properti umum untuk menerima dan mengatur jarak ke sel, serta memperbarui labelnya. public int Distance { get { return distance; } set { distance = value; UpdateDistanceLabel(); } }
Tambahkan ke HexGrid
metode umum FindDistancesTo
dengan parameter sel. Untuk saat ini, kami hanya akan mengatur jarak nol ke setiap sel. public void FindDistancesTo (HexCell cell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = 0; } }
Jika mode pengeditan tidak diaktifkan, maka kami HexMapEditor.HandleInput
memanggil metode baru dengan sel saat ini. if (editMode) { EditCells(currentCell); } else { hexGrid.FindDistancesTo(currentCell); }
Jarak antar koordinat
Sekarang dalam mode navigasi, setelah menyentuh salah satunya, semua sel menampilkan nol. Tetapi, tentu saja, mereka harus menampilkan jarak sebenarnya ke sel. Untuk menghitung jarak ke mereka, kita bisa menggunakan koordinat sel. Oleh karena itu, anggaplah ia HexCoordinates
memiliki metode DistanceTo
, dan gunakan dalam HexGrid.FindDistancesTo
. public void FindDistancesTo (HexCell cell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = cell.coordinates.DistanceTo(cells[i].coordinates); } }
Sekarang tambahkan ke HexCoordinates
metode DistanceTo
. Dia harus membandingkan koordinatnya sendiri dengan koordinat set lainnya. Mari kita mulai hanya dengan mengukur X, dan kita akan mengurangi koordinat X dari satu sama lain. public int DistanceTo (HexCoordinates other) { return x - other.x; }
Akibatnya, kami mendapatkan offset sepanjang X relatif terhadap sel yang dipilih. Tetapi jarak tidak boleh negatif, jadi Anda harus mengembalikan modulo perbedaan X. return x < other.x ? other.x - x : x - other.x;
Jarak sepanjang X.Jadi kita mendapatkan jarak yang benar hanya jika kita memperhitungkan hanya satu dimensi. Tetapi ada tiga dimensi dalam kisi segi enam. Jadi mari kita tambahkan jarak untuk ketiga dimensi dan lihat apa yang memberi kita. return (x < other.x ? other.x - x : x - other.x) + (Y < other.Y ? other.Y - Y : Y - other.Y) + (z < other.z ? other.z - z : z - other.z);
Jumlah jarak XYZ.Ternyata jaraknya dua kali lipat. Artinya, untuk mendapatkan jarak yang benar, jumlah ini harus dibagi dua. return ((x < other.x ? other.x - x : x - other.x) + (Y < other.Y ? other.Y - Y : Y - other.Y) + (z < other.z ? other.z - z : z - other.z)) / 2;
Jarak nyata.Mengapa jumlah sama dengan dua kali jarak?, . , (1, −3, 2). . , . . , . .
. paket unityBekerja dengan rintangan
Jarak yang kami hitung sesuai dengan jalur terpendek dari sel yang dipilih ke setiap sel lainnya. Kami tidak dapat menemukan cara yang lebih pendek. Tetapi jalur ini dijamin benar jika rute tidak memblokir apa pun. Tebing, air, dan rintangan lain bisa membuat kita berkeliling. Mungkin beberapa sel tidak bisa dijangkau sama sekali.Untuk menemukan cara mengatasi rintangan, kita perlu menggunakan pendekatan yang berbeda daripada hanya menghitung jarak antara koordinat. Kami tidak lagi dapat memeriksa setiap sel secara individual. Kami harus mencari peta sampai kami menemukan setiap sel yang bisa dijangkau.Visualisasi pencarian
Pencarian peta adalah proses berulang. Untuk memahami apa yang kami lakukan, akan sangat membantu untuk melihat setiap tahap pencarian. Kita dapat melakukan ini dengan mengubah algoritma pencarian menjadi coroutine, yang membutuhkan ruang pencarian System.Collections
. Kecepatan refresh 60 iterasi per detik cukup kecil bagi kita untuk melihat apa yang terjadi, dan mencari di peta kecil tidak memakan terlalu banyak waktu. public void FindDistancesTo (HexCell cell) { StartCoroutine(Search(cell)); } IEnumerator Search (HexCell cell) { WaitForSeconds delay = new WaitForSeconds(1 / 60f); for (int i = 0; i < cells.Length; i++) { yield return delay; cells[i].Distance = cell.coordinates.DistanceTo(cells[i].coordinates); } }
Kami perlu memastikan bahwa hanya satu pencarian yang aktif pada waktu tertentu. Karena itu, sebelum memulai pencarian baru, kami menghentikan semua coroutine. public void FindDistancesTo (HexCell cell) { StopAllCoroutines(); StartCoroutine(Search(cell)); }
Selain itu, kami harus menyelesaikan pencarian saat memuat peta baru. public void Load (BinaryReader reader, int header) { StopAllCoroutines(); … }
Pencarian Luas Pertama
Bahkan sebelum kita memulai pencarian, kita tahu bahwa jarak ke sel yang dipilih adalah nol. Dan, tentu saja, jarak ke semua tetangganya adalah 1, jika mereka dapat dijangkau. Lalu kita bisa melihat salah satu tetangga ini. Sel ini kemungkinan besar memiliki tetangganya sendiri yang dapat dijangkau, dan yang jaraknya belum dihitung. Jika demikian, maka jarak ke tetangga ini harus 2. Kita bisa mengulangi proses ini untuk semua tetangga pada jarak 1. Setelah itu, kita ulangi untuk semua tetangga pada jarak 2. Dan seterusnya, sampai kita mencapai semua sel.Artinya, pertama-tama kita menemukan semua sel pada jarak 1, lalu kita menemukan semuanya pada jarak 2, lalu pada jarak 3, dan seterusnya, sampai kita selesai. Ini memastikan bahwa kami menemukan jarak terkecil ke setiap sel yang dapat dijangkau. Algoritma ini disebut pencarian luas pertama.Agar bisa bekerja, kita perlu tahu apakah kita sudah menentukan jarak ke sel. Seringkali untuk ini, sel ditempatkan dalam koleksi yang disebut set siap pakai atau tertutup. Tetapi kita dapat mengatur jarak ke sel int.MaxValue
untuk menunjukkan bahwa kita belum mengunjunginya. Kita perlu melakukan ini untuk semua sel sebelum melakukan pencarian. IEnumerator Search (HexCell cell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; } … }
Anda juga dapat menggunakan ini untuk menyembunyikan semua sel yang belum dikunjungi dengan mengubah HexCell.UpdateDistanceLabel
. Setelah itu, kami akan memulai setiap pencarian di peta kosong. void UpdateDistanceLabel () { Text label = uiRect.GetComponent<Text>(); label.text = distance == int.MaxValue ? "" : distance.ToString(); }
Selanjutnya, kita perlu melacak sel yang perlu dikunjungi, dan urutan kunjungannya. Koleksi seperti itu sering disebut perbatasan atau set terbuka. Kita hanya perlu memproses sel-sel dalam urutan yang sama di mana kita bertemu mereka. Untuk melakukan ini, Anda dapat menggunakan antrian Queue
, yang merupakan bagian dari namespace System.Collections.Generic
. Sel yang dipilih akan menjadi yang pertama ditempatkan dalam antrian ini dan akan memiliki jarak 0. IEnumerator Search (HexCell cell) { for (int i = 0; i < cells.Length; i++) { cells[i].Distance = int.MaxValue; } WaitForSeconds delay = new WaitForSeconds(1 / 60f); Queue<HexCell> frontier = new Queue<HexCell>(); cell.Distance = 0; frontier.Enqueue(cell);
Mulai saat ini, algoritma mengeksekusi loop sementara ada sesuatu dalam antrian. Pada setiap iterasi, sel paling depan diambil dari antrian. frontier.Enqueue(cell); while (frontier.Count > 0) { yield return delay; HexCell current = frontier.Dequeue(); }
Sekarang kita memiliki sel saat ini, yang bisa berada pada jarak berapa pun. Selanjutnya, kita perlu menambahkan semua tetangganya ke antrian satu langkah lebih jauh dari sel yang dipilih. while (frontier.Count > 0) { yield return delay; HexCell current = frontier.Dequeue(); for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if (neighbor != null) { neighbor.Distance = current.Distance + 1; frontier.Enqueue(neighbor); } } }
Tetapi kita harus menambahkan hanya sel-sel yang belum diberi jarak. if (neighbor != null && neighbor.Distance == int.MaxValue) { neighbor.Distance = current.Distance + 1; frontier.Enqueue(neighbor); }
Pencarian luas.Hindari air
Setelah memastikan bahwa pencarian pertama kali menemukan jarak yang benar pada peta monoton, kita dapat mulai menambahkan rintangan. Ini dapat dilakukan dengan menolak menambahkan sel ke antrian jika kondisi tertentu terpenuhi.Faktanya, kami telah melewati beberapa sel: sel yang tidak ada, dan sel yang telah kami tandai jaraknya. Mari kita menulis ulang kode sehingga dalam hal ini kita melewatkan tetangga secara eksplisit. for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { HexCell neighbor = current.GetNeighbor(d); if (neighbor == null || neighbor.Distance != int.MaxValue) { continue; } neighbor.Distance = current.Distance + 1; frontier.Enqueue(neighbor); }
Mari kita lewati semua sel yang ada di bawah air. Ini berarti bahwa ketika mencari jarak terdekat, kami hanya mempertimbangkan pergerakan di darat. if (neighbor == null || neighbor.Distance != int.MaxValue) { continue; } if (neighbor.IsUnderwater) { continue; }
Jarak tanpa bergerak melalui air.Algoritma masih menemukan jarak terpendek, tetapi sekarang menghindari semua air. Oleh karena itu, sel-sel bawah laut tidak pernah mendapatkan jarak, seperti area tanah yang terisolasi. Sel bawah air hanya menerima jarak jika dipilih.Hindari tebing
Juga, untuk menentukan kemungkinan mengunjungi tetangga, kita bisa menggunakan jenis iga. Misalnya, Anda dapat membuat tebing menghalangi jalan. Jika Anda membiarkan gerakan di lereng, maka sel-sel di sisi lain tebing masih dapat dicapai, hanya di jalur lain. Oleh karena itu, mereka dapat berada pada jarak yang sangat berbeda. if (neighbor.IsUnderwater) { continue; } if (current.GetEdgeType(neighbor) == HexEdgeType.Cliff) { continue; }
Jarak tanpa melintasi tebing.paket unityBiaya perjalanan
Kita dapat menghindari sel dan ujung, tetapi opsi ini adalah biner. Orang dapat membayangkan bahwa lebih mudah untuk menavigasi ke beberapa arah daripada di tempat lain. Dalam hal ini, jarak diukur dalam persalinan atau waktu.Jalan cepat
Akan logis bahwa lebih mudah dan lebih cepat untuk bepergian di jalan, jadi mari kita membuat persimpangan tepi dengan jalan lebih murah. Karena kita menggunakan nilai integer untuk mengatur jarak, kita akan meninggalkan biaya bergerak di sepanjang jalan sama dengan 1, dan biaya melintasi tepi lain kita akan meningkat menjadi 10. Ini adalah perbedaan besar yang memungkinkan kita untuk segera melihat apakah kita mendapatkan hasil yang tepat. int distance = current.Distance; if (current.HasRoadThroughEdge(d)) { distance += 1; } else { distance += 10; } neighbor.Distance = distance;
Jalan dengan jarak yang salah.Sortir Perbatasan
Sayangnya, ternyata pencarian pertama kali tidak dapat bekerja dengan biaya pemindahan variabel. Dia mengasumsikan bahwa sel ditambahkan ke perbatasan dalam urutan peningkatan jarak, dan bagi kita ini tidak lagi relevan. Kita perlu antrian prioritas, yaitu antrian yang menyortir dirinya sendiri. Tidak ada antrian prioritas standar, karena Anda tidak dapat memprogramnya sedemikian rupa sehingga sesuai dengan semua situasi.Kita dapat membuat antrian prioritas kita sendiri, tetapi mari kita optimalkan untuk tutorial selanjutnya. Untuk saat ini, kami cukup mengganti antrian dengan daftar yang akan memiliki metode Sort
. List<HexCell> frontier = new List<HexCell>(); cell.Distance = 0; frontier.Add(cell); while (frontier.Count > 0) { yield return delay; HexCell current = frontier[0]; frontier.RemoveAt(0); for (HexDirection d = HexDirection.NE; d <= HexDirection.NW; d++) { … neighbor.Distance = distance; frontier.Add(neighbor); } }
Tidak bisakah saya menggunakan ListPool <HexCell>?, , . , , .
Agar perbatasan benar, kita perlu mengurutkannya setelah menambahkan sel ke dalamnya. Bahkan, kami dapat menunda penyortiran sampai semua tetangga sel ditambahkan, tetapi, saya ulangi, sampai optimasi tidak menarik bagi kami.Kami ingin menyortir sel berdasarkan jarak. Untuk melakukan ini, kita perlu memanggil metode pengurutan daftar dengan tautan ke metode yang melakukan perbandingan ini. frontier.Add(neighbor); frontier.Sort((x, y) => x.Distance.CompareTo(y.Distance));
Bagaimana cara kerja metode Sortir ini?. , . .
frontier.Sort(CompareDistances); … static int CompareDistances (HexCell x, HexCell y) { return x.Distance.CompareTo(y.Distance); }
Batas yang diurutkan masih salah.Pembaruan perbatasan
Setelah kami mulai menyortir perbatasan, kami mulai mendapatkan hasil yang lebih baik, tetapi masih ada kesalahan. Ini karena ketika sel ditambahkan ke perbatasan, kita tidak perlu menemukan jarak terpendek ke sel ini. Ini berarti bahwa sekarang kita tidak bisa lagi melewati tetangga yang telah diberi jarak. Sebaliknya, kita perlu memeriksa apakah kita telah menemukan jalan yang lebih pendek. Jika demikian, maka kita perlu mengubah jarak ke tetangga, alih-alih menambahkannya ke perbatasan. HexCell neighbor = current.GetNeighbor(d); if (neighbor == null) { continue; } if (neighbor.IsUnderwater) { continue; } if (current.GetEdgeType(neighbor) == HexEdgeType.Cliff) { continue; } int distance = current.Distance; if (current.HasRoadThroughEdge(d)) { distance += 1; } else { distance += 10; } if (neighbor.Distance == int.MaxValue) { neighbor.Distance = distance; frontier.Add(neighbor); } else if (distance < neighbor.Distance) { neighbor.Distance = distance; } frontier.Sort((x, y) => x.Distance.CompareTo(y.Distance));
Jarak yang benar.Sekarang setelah kami memiliki jarak yang benar, kami akan mulai mempertimbangkan biaya pemindahan. Anda mungkin memperhatikan bahwa jarak ke beberapa sel pada awalnya terlalu besar, tetapi dikoreksi ketika mereka dihapus dari perbatasan. Pendekatan ini disebut algoritma Dijkstra, dinamai setelah yang pertama kali ditemukan oleh Edsger Dijkstra.Lereng
Kami tidak ingin terbatas pada perbedaan biaya hanya untuk jalan. Misalnya, Anda dapat mengurangi biaya melintasi tepi rata tanpa jalan ke 5, meninggalkan lereng tanpa jalan bernilai 10. HexEdgeType edgeType = current.GetEdgeType(neighbor); if (edgeType == HexEdgeType.Cliff) { continue; } int distance = current.Distance; if (current.HasRoadThroughEdge(d)) { distance += 1; } else { distance += edgeType == HexEdgeType.Flat ? 5 : 10; }
Untuk mengatasi lereng Anda perlu membuat lebih banyak pekerjaan, dan jalan selalu cepat.Benda bantuan
Kami dapat menambah biaya dengan adanya benda bantuan. Misalnya, dalam banyak permainan lebih sulit untuk menavigasi hutan. Dalam hal ini, kita cukup menambahkan semua level objek ke jarak. Dan di sini lagi jalan mempercepat segalanya. if (current.HasRoadThroughEdge(d)) { distance += 1; } else { distance += edgeType == HexEdgeType.Flat ? 5 : 10; distance += neighbor.UrbanLevel + neighbor.FarmLevel + neighbor.PlantLevel; }
Objek melambat jika tidak ada jalan.Dindingnya
Akhirnya, mari kita perhatikan temboknya. Dinding harus menghalangi gerakan jika jalan tidak melewatinya. 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; }
Dinding tidak membiarkan kami lewat, Anda perlu mencari gerbang.paket unity