Baru-baru ini, saya perlu menyelesaikan masalah yang cukup umum di banyak permainan papan atas: untuk menampilkan di layar sejumlah bar kesehatan musuh. Sesuatu seperti ini:
Jelas, saya ingin melakukan ini seefisien mungkin, lebih disukai dalam satu panggilan undian. Seperti biasa, sebelum mulai bekerja, saya melakukan sedikit riset online tentang keputusan orang lain, dan hasilnya sangat berbeda.
Saya tidak akan mempermalukan siapa pun atas kode tersebut, tetapi cukup untuk mengatakan bahwa beberapa solusi tidak sepenuhnya brilian, misalnya, seseorang menambahkan objek Kanvas ke setiap musuh (yang sangat tidak efisien).
Metode yang saya hasilkan sedikit berbeda dari semua yang saya lihat pada orang lain, dan sama sekali tidak menggunakan kelas UI (termasuk Canvas), jadi saya memutuskan untuk mendokumentasikannya untuk umum. Dan bagi mereka yang ingin mempelajari kode sumber, saya mempostingnya
di Github .
Mengapa tidak menggunakan Canvas?
Satu kanvas untuk setiap musuh jelas merupakan keputusan yang buruk, tetapi saya dapat menggunakan kanvas umum untuk semua musuh; kanvas tunggal juga akan menyebabkan rendering panggilan batch.
Namun, saya tidak suka jumlah pekerjaan yang dilakukan di setiap frame terkait dengan pendekatan ini. Jika Anda menggunakan Kanvas, maka di setiap bingkai Anda harus melakukan operasi berikut:
- Tentukan musuh mana yang ada di layar, dan pilih masing-masing dari strip UI pool.
- Proyeksikan posisi musuh di kamera untuk memposisikan strip.
- Ubah ukuran bagian "isi" strip, mungkin seperti Gambar.
- Kemungkinan besar mengubah ukuran strip sesuai dengan jenis musuh; misalnya, musuh besar harus memiliki strip besar sehingga tidak terlihat konyol.
Bagaimanapun, semua ini akan mencemari buffer geometri Kanvas dan mengarah ke pembangunan kembali semua data titik dalam prosesor. Saya tidak ingin semua ini dilakukan untuk elemen sederhana.
Secara singkat tentang keputusan saya
Deskripsi singkat tentang proses kerja saya:
- Kami melampirkan objek strip energi ke musuh dalam 3D.
- Ini memungkinkan Anda untuk mengatur dan memotong strip secara otomatis.
- Posisi / ukuran strip dapat disesuaikan sesuai dengan jenis musuh.
- Kami akan mengarahkan strip ke kamera dalam kode menggunakan transform, yang masih ada.
- Shader memastikan bahwa mereka selalu membuat di atas segalanya.
- Kami menggunakan Instancing untuk merender semua strip dalam satu panggilan undian.
- Kami menggunakan koordinat UV prosedural sederhana untuk menampilkan tingkat kepenuhan strip.
Sekarang mari kita lihat solusinya secara lebih rinci.
Apa itu Instancing?
Dalam bekerja dengan grafik, teknik standar telah digunakan untuk waktu yang lama: beberapa objek digabungkan bersama-sama sehingga mereka memiliki data dan materi vertex yang sama dan mereka dapat dirender dalam satu panggilan draw. Inilah yang kami butuhkan, karena setiap panggilan undian adalah beban tambahan pada CPU dan GPU. Alih-alih membuat satu panggilan undian untuk setiap objek, kami membuat semuanya sekaligus dan menggunakan shader untuk menambahkan variabilitas pada setiap salinan.
Anda dapat melakukan ini secara manual dengan menduplikasi data titik simpul X kali dalam satu buffer, di mana X adalah jumlah maksimum salinan yang dapat dirender, dan kemudian menggunakan array parameter shader untuk mengkonversi / warna / memvariasikan setiap salinan. Setiap salinan harus menyimpan pengetahuan tentang apa nomor bernomor itu, untuk menggunakan nilai ini sebagai indeks array. Kemudian kita dapat menggunakan panggilan render yang diindeks, yang memerintahkan "render only to N", di mana N adalah jumlah instance yang
sebenarnya dibutuhkan dalam frame saat ini, kurang dari jumlah maksimum X.
Sebagian besar API modern sudah memiliki kode untuk ini, jadi Anda tidak perlu melakukan ini secara manual. Operasi ini disebut "Instancing"; pada kenyataannya, itu mengotomatiskan proses yang dijelaskan di atas dengan batasan yang telah ditentukan.
Mesin Unity juga mendukung instancing , ia memiliki API sendiri dan seperangkat makro shader yang membantu dalam implementasinya. Ini menggunakan asumsi tertentu, misalnya, bahwa setiap instance memerlukan transformasi 3D penuh. Sebenarnya, untuk strip 2D tidak diperlukan sepenuhnya - kita dapat melakukannya dengan penyederhanaan, tetapi karena itu, kita akan menggunakannya. Ini akan menyederhanakan shader kami, dan juga menyediakan kemampuan untuk menggunakan indikator 3D, misalnya lingkaran atau busur.
Kelas bisa rusak
Musuh kita akan memiliki komponen yang disebut
Damageable
, memberi mereka kesehatan dan memungkinkan mereka untuk mengambil kerusakan dari tabrakan. Dalam contoh kita, ini cukup sederhana:
public class Damageable : MonoBehaviour { public int MaxHealth; public float DamageForceThreshold = 1f; public float DamageForceScale = 5f; public int CurrentHealth { get; private set; } private void Start() { CurrentHealth = MaxHealth; } private void OnCollisionEnter(Collision other) {
Obyek HealthBar: Posisi / Putar
Objek bar kesehatan sangat sederhana: pada kenyataannya, itu hanya Quad yang melekat pada musuh.

Kami menggunakan
skala objek ini untuk membuat strip panjang dan tipis, dan menempatkannya langsung di atas musuh. Jangan khawatir tentang rotasi, kami akan memperbaikinya menggunakan kode yang dilampirkan ke objek di
HealthBar.cs
:
private void AlignCamera() { if (mainCamera != null) { var camXform = mainCamera.transform; var forward = transform.position - camXform.position; forward.Normalize(); var up = Vector3.Cross(forward, camXform.right); transform.rotation = Quaternion.LookRotation(forward, up); } }
Kode ini selalu mengarahkan quad ke arah kamera. Kita
dapat melakukan pengubahan ukuran dan rotasi dalam shader, tetapi saya menerapkannya di sini karena dua alasan.
Pertama, pemasangan Unity selalu menggunakan transformasi lengkap dari setiap objek, dan karena kami tetap mentransfer semua data, Anda dapat menggunakannya. Kedua, mengatur skala / rotasi di sini memastikan bahwa jajar genjang untuk memotong strip akan selalu benar. Jika kita membuat tugas ukuran dan rotasi sebagai tanggung jawab shader, maka Unity dapat memotong strip yang seharusnya terlihat ketika mereka dekat dengan tepi layar, karena ukuran dan rotasi jajaran genjang pembatasnya tidak akan sesuai dengan apa yang akan kita render. Tentu saja, kami dapat menerapkan metode pemotongan kami sendiri, tetapi biasanya lebih baik menggunakan apa yang kami miliki jika memungkinkan (Kode Unity adalah asli dan memiliki akses ke lebih banyak data spasial daripada yang kami lakukan).
Saya akan menjelaskan bagaimana strip diberikan setelah kita melihat shader.
Shader HealthBar
Dalam versi ini, kita akan membuat strip merah-hijau klasik sederhana.
Saya menggunakan tekstur 2x1 dengan satu piksel hijau di sebelah kiri dan satu merah di kanan. Secara alami, saya mematikan pemetaan, pemfilteran, dan kompresi, dan mengatur parameter mode pengalamatan ke Clamp, yang berarti bahwa piksel di strip kami akan selalu berwarna hijau atau merah sempurna, dan tidak akan menyebar di tepinya. Ini akan memungkinkan kita untuk mengubah koordinat tekstur dalam shader untuk menggeser garis yang membagi piksel merah dan hijau ke bawah dan ke atas strip.
(Karena hanya ada dua warna di sini, saya hanya bisa menggunakan fungsi langkah dalam shader untuk kembali ke titik satu atau yang lain. Namun, metode ini nyaman karena Anda dapat menggunakan tekstur yang lebih kompleks jika diinginkan, dan ini akan bekerja sama saat transisi berada di tekstur pertengahan.)Pertama, kami akan mendeklarasikan properti yang kami butuhkan:
Shader "UI/HealthBar" { Properties { _MainTex ("Texture", 2D) = "white" {} _Fill ("Fill", float) = 0 }
_MainTex
adalah tekstur merah-hijau, dan
_Fill
adalah nilai dari 0 hingga 1, di mana 1 adalah kesehatan penuh.
Selanjutnya, kita perlu memesan strip untuk membuat dalam antrian overlay, yang berarti mengabaikan semua kedalaman dalam adegan dan membuat di atas segalanya:
SubShader { Tags { "Queue"="Overlay" } Pass { ZTest Off
Bagian selanjutnya adalah kode shader itu sendiri. Kami sedang menulis shader tanpa penerangan (tidak menyala), jadi kami tidak perlu khawatir tentang integrasi dengan berbagai shader permukaan Persatuan, ini hanya beberapa shader vertex / fragmen. Pertama, tulis bootstrap:
CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_instancing #include "UnityCG.cginc"
Untuk sebagian besar, ini adalah bootstrap standar, dengan pengecualian
#pragma multi_compile_instancing
, yang memberi tahu kompilator Unity apa yang perlu dikompilasi untuk Instancing.
Struktur vertex harus menyertakan data instan, jadi kami akan melakukan hal berikut:
struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID };
Kita juga perlu menentukan apa yang sebenarnya akan ada dalam data instance, di samping proses Unity (transform) untuk kita:
UNITY_INSTANCING_BUFFER_START(Props) UNITY_DEFINE_INSTANCED_PROP(float, _Fill) UNITY_INSTANCING_BUFFER_END(Props)
Jadi kami melaporkan bahwa Unity harus membuat buffer yang disebut "Props" untuk menyimpan data untuk setiap instance, dan di dalamnya kami akan menggunakan satu float per instance untuk properti yang disebut
_Fill
.
Anda dapat menggunakan beberapa buffer; perlu dilakukan jika Anda memiliki beberapa properti yang diperbarui pada frekuensi yang berbeda; membaginya, Anda dapat, misalnya, tidak mengubah satu buffer ketika mengubah yang lain, yang lebih efisien. Tetapi kita tidak membutuhkan ini.
Vertex shader kami hampir sepenuhnya berfungsi dengan baik, karena ukuran, posisi, dan rotasi sudah ditransfer untuk diubah. Ini diimplementasikan menggunakan
UnityObjectToClipPos
, yang secara otomatis menggunakan transformasi dari setiap instance. Orang bisa membayangkan bahwa tanpa membuat instance ini biasanya akan menjadi penggunaan sederhana dari properti matriks tunggal. tetapi ketika menggunakan instancing di dalam mesin, itu terlihat seperti array matriks, dan Unity secara mandiri memilih matriks yang cocok untuk instance ini.
Selain itu, Anda perlu mengubah UV untuk mengubah lokasi titik transisi dari merah ke hijau sesuai dengan properti
_Fill
. Berikut ini cuplikan kode yang relevan:
UNITY_SETUP_INSTANCE_ID(v); float fill = UNITY_ACCESS_INSTANCED_PROP(Props, _Fill);
UNITY_SETUP_INSTANCE_ID
dan
UNITY_ACCESS_INSTANCED_PROP
melakukan semua keajaiban dengan mengakses versi properti
_Fill
dari buffer konstan untuk instance ini.
Kita tahu bahwa dalam keadaan normal, koordinat UV segi empat mencakup seluruh interval tekstur, dan bahwa garis pemisah strip berada di tengah tekstur secara horizontal. Oleh karena itu, perhitungan matematis kecil menggeser strip secara horizontal ke kiri atau ke kanan, dan nilai Clamp dari tekstur memastikan pengisian bagian yang tersisa.
Shader fragmen tidak bisa lebih sederhana karena semua pekerjaan telah dilakukan:
return tex2D(_MainTex, i.uv);
Kode shader komentar lengkap tersedia di
repositori GitHub .
Bahan Healthbar
Maka semuanya sederhana - kita hanya perlu menetapkan materi yang digunakan shader ini pada strip kita. Hampir tidak ada lagi yang perlu dilakukan, cukup pilih shader yang diinginkan di bagian atas, tetapkan tekstur merah-hijau, dan, yang paling penting,
centang kotak "Enable GPU Instancing" .

HealthBar Isi Pembaruan Properti
Jadi, kita memiliki objek bar kesehatan, shader, dan material yang akan dirender, sekarang kita perlu mengatur properti
_Fill
untuk setiap instance. Kami melakukan ini di dalam
HealthBar.cs
sebagai berikut:
private void UpdateParams() { meshRenderer.GetPropertyBlock(matBlock); matBlock.SetFloat("_Fill", damageable.CurrentHealth / (float)damageable.MaxHealth); meshRenderer.SetPropertyBlock(matBlock); }
Kami mengubah
CurrentHealth
class menjadi nilai dari 0 hingga 1, membaginya dengan
MaxHealth
. Lalu kami meneruskannya ke properti
_Fill
menggunakan
MaterialPropertyBlock
.
Jika Anda belum pernah menggunakan
MaterialPropertyBlock
untuk mentransfer data ke shader, bahkan tanpa membuat instance, maka Anda perlu mempelajarinya. Ini tidak dijelaskan dengan baik dalam dokumentasi Unity, tetapi ini adalah cara paling efisien untuk mentransfer data dari setiap objek ke shader.
Dalam kasus kami, ketika instancing digunakan, nilai-nilai untuk semua bar kesehatan dikemas dalam buffer konstan sehingga mereka dapat ditransfer bersama-sama dan ditarik pada suatu waktu.
Hampir tidak ada apa-apa di sini kecuali boilerplate untuk mengatur variabel, dan kodenya agak membosankan; lihat
gudang GitHub untuk detailnya.
Demo
Repositori GitHub memiliki demo uji di mana sekelompok kubus biru jahat dihancurkan oleh bola merah heroik (hore!), Mengambil kerusakan yang ditampilkan oleh garis-garis yang dijelaskan dalam artikel. Demo ditulis dalam Unity 2018.3.6f1.
Efek menggunakan instancing dapat diamati dengan dua cara:
Panel Statistik
Setelah mengklik Play, klik tombol Stats di atas panel Game. Di sini Anda dapat melihat berapa banyak panggilan draw yang disimpan berkat pemasangan:

Setelah meluncurkan game, Anda dapat mengklik materi HealthBar dan
menghapus centang pada kotak "Aktifkan GPU Instancing", setelah itu jumlah panggilan yang disimpan akan dikurangi menjadi nol.
Bingkai debugger
Setelah meluncurkan game, pergi ke Window> Analysis> Frame Debugger, dan kemudian klik "Enable" di jendela yang muncul.
Di kiri bawah Anda akan melihat semua operasi rendering dilakukan. Perhatikan bahwa meskipun ada banyak tantangan terpisah untuk musuh dan peluru (jika Anda mau, Anda dapat menerapkan instancing untuk mereka juga). Jika Anda menggulir ke bawah, Anda akan melihat item "Draw Mesh (instanced) Healthbar".
Panggilan tunggal ini membuat semua strip. Jika Anda mengklik operasi ini, dan kemudian pada operasi di atasnya, Anda akan melihat bahwa semua strip menghilang, karena mereka ditarik dalam satu panggilan. Jika berada di Frame Debugger, Anda hapus centang pada kotak centang Aktifkan GPU Instancing dari materi, Anda akan melihat bahwa satu baris berubah menjadi beberapa, dan setelah mengatur kembali bendera menjadi satu.
Cara memperluas sistem ini
Seperti yang saya katakan sebelumnya, karena bilah kesehatan ini adalah benda nyata, tidak ada yang menghentikan Anda dari mengubah bilah 2D sederhana menjadi sesuatu yang lebih kompleks. Mereka bisa setengah lingkaran di bawah musuh yang berkurang dalam busur, atau memutar belah ketupat di atas kepala mereka. Dengan menggunakan pendekatan yang sama, Anda masih dapat membuat semuanya dalam satu panggilan.