Bagaimana saya melakukan bayangan 2D di Unity

Apa yang pertama kali terlintas dalam pikiran pengembang game indie ketika dia dihadapkan dengan kebutuhan untuk menambahkan fitur yang dia tidak tahu tentang implementasinya? Tentu saja, dia akan mencari jejak orang-orang yang sudah berjalan di jalan ini dan repot-repot menuliskan pengalaman mereka. Jadi saya lakukan beberapa waktu lalu, mulai membuat bayangan di game saya. Menemukan informasi yang tepat - dalam bentuk artikel, pelajaran, dan panduan - tidaklah sulit. Namun, yang mengejutkan saya, saya menemukan bahwa tidak ada solusi yang dijelaskan hanya cocok untuk saya. Karena itu, setelah menyadari milik saya, saya memutuskan untuk memberi tahu dunia tentang hal itu.

Perlu peringatan di muka bahwa teks ini tidak berpura-pura menjadi semacam panduan ultimatum atau kelas master. Metode yang saya gunakan mungkin tidak universal, jauh dari yang paling efektif dan tidak mencakup tugas menciptakan bayangan dua dimensi secara penuh. Ini lebih merupakan cerita tentang trik apa yang harus dilakukan oleh pengembang yang tidak berpengalaman di wajah saya untuk mencapai hasil yang memuaskan persyaratannya.

Hasilnya sendiri ada di hadapan Anda:



Dan detail dari jalan menuju pencapaiannya sedang menunggu Anda di bawah potongan.

Sedikit tentang permainan itu sendiri
Dwarfinator adalah pertahanan basis / side-scroll shooter dua dimensi yang dikembangkan dengan memperhatikan segmen ponsel dan desktop. Gameplay terdiri dari penghancuran sistematis gelombang musuh dalam dua mode bergantian - pertahanan dan kejaran. Perkembangan seorang pemain melibatkan pemompaan sebuah "tank" dengan meningkatkan dan mengganti berbagai elemen itu, seperti senjata, mesin dan roda, serta meningkatkan level dan mempelajari keterampilan aktif dan pasif. Kemajuan lingkungan melibatkan peningkatan konstan dalam jumlah massa dalam gelombang, penambahan jenis musuh baru ke gelombang saat mereka bergerak melalui lokasi, dan perubahan berturut-turut dari beberapa lokasi, yang masing-masing memiliki set lawan sendiri.

Pernyataan masalah


Jadi, pada saat keputusan untuk menambahkan bayangan ke permainan, saya punya:

  • lokasi dalam bentuk dua sprite, satu untuk ditampilkan di belakang massa dan entitas lain, yang kedua untuk ditampilkan di depan mereka;



  • gerombolan dan objek statis yang dapat dirusak, terus-menerus dianimasikan dan terdiri dari sprite terpisah dalam jumlah dari beberapa hingga beberapa lusin;



  • kerang, milik dan musuh, dalam banyak kasus diwakili baik oleh satu sprite atau oleh sistem partikel, dalam kasus terakhir tidak diperlukan bayangan;



  • sebuah tangki yang terdiri dari beberapa bagian yang dirakit sesuai dengan skema yang sama dengan massa;



  • dinding dengan beberapa kondisi tetap, yang, sekali lagi, adalah satu set sprite terpisah.



Untuk semua ini, bayangan paling sederhana diperlukan, mengulangi kontur objek, dan dilemparkan dari satu sumber cahaya tetap.

Pada saat yang sama, seseorang harus memiliki sikap yang tajam terhadap produktivitas. Karena kekhasan genre dan kekhasan implementasinya, sebagian besar objek yang membuat bayangan terletak langsung di layar kapan saja. Dan jumlah totalnya bisa lebih dari seratus, jika kita berbicara tentang entitas game, dan beberapa ribu, jika kita berbicara tentang sprite individu.

Implementasi


Sebenarnya, tangkapan utamanya adalah Dwarfinator, secara kasar, adalah game 2.5D. Sebagian besar objek ada dalam ruang dua dimensi dengan sumbu X dan Y, dan sumbu Z jarang digunakan. Secara visual, dan sebagian gameplay, sumbu Y digunakan untuk menampilkan ketinggian dan kedalaman, membagi dengan cara yang sama menjadi sumbu virtual Y dan Z. Tidak mungkin menggunakan alat Unity standar dalam situasi seperti itu untuk membuat bayangan.

Tetapi pada kenyataannya, saya tidak perlu pencahayaan yang jujur, itu sudah cukup untuk dapat secara manual membuat bayangan untuk setiap objek. Oleh karena itu, hal paling sederhana yang terpikir oleh saya adalah menempatkan salinannya di belakang setiap entitas, diputar dalam ruang tiga dimensi untuk mensimulasikan lokasi di permukaan. Semua sprite dari pseudo-shadow ini di-set menjadi hitam, sedangkan struktur hierarkis dari pemilik shadow dipertahankan, yang memungkinkannya untuk dianimasikan dalam sinkronisasi dengan pemilik oleh animator yang sama.

Animasi sinkron seperti itu terlihat seperti ini:



Namun, bayangan itu membutuhkan transparansi. Solusi paling sederhana adalah mengaturnya untuk setiap bayangan sprite. Tetapi implementasi seperti itu tidak terlihat memuaskan - sprite saling tumpang tindih, membentuk area yang kurang transparan di lokasi overlay.

Tangkapan layar di bawah ini menunjukkan bagaimana bayangan beberapa segmen transparan terlihat. Parameter distorsi bayangan yang digunakan juga terlihat: rotasi sepanjang sumbu X oleh -50 derajat, rotasi sepanjang sumbu Y sebesar -140 derajat, dan skala sepanjang sumbu X, meningkat 1,3 kali relatif terhadap objek induk.



Menjadi jelas bahwa transparansi harus diterapkan pada bayangan sebagai objek yang solid. Eksperimen pertama pada topik ini tergantung pada bayangan kamera, membuat bayangan ini dalam RenderTexture, yang kemudian digunakan sebagai bahan yang melekat pada induk dari bayangan Pesawat. Dia sudah bisa mengatur transparansi tanpa masalah. Bayangan itu sendiri berada di luar bingkai untuk menghindari tumpang tindih area pengambilan kamera. Pendekatannya berhasil, tetapi ternyata sudah beberapa lusin bayangan menyebabkan masalah kinerja yang serius, terutama karena jumlah kamera di panggung. Selain itu, sejumlah animasi mengasumsikan pergerakan signifikan sprite massa individu dalam kerangka objek akarnya, karena area kamera harus ditempatkan yang secara signifikan akan melebihi ukuran gambar nyata pada titik waktu tertentu.

Solusinya ditemukan dengan cepat - jika Anda tidak dapat menggambar setiap bayangan dengan kamera terpisah - mengapa tidak menggambar semua bayangan dengan satu kamera? Yang harus dilakukan adalah menempatkan area terpisah dari pemandangan di bawah bayangan, sedikit lebih tinggi dari bidang pandang kamera utama, mengarahkan kamera tambahan ke area ini, dan menampilkan outputnya antara lokasi dan entitas lain.

Di bawah ini Anda dapat melihat contoh output dari kamera ini:



Produktivitas dari implementasi semacam itu jauh lebih sedikit berkurang, sehingga solusinya dianggap berfungsi dan diterapkan pada semua gerombolan, objek statis, dan cangkang. Ini diikuti oleh lokasi sprite. Tidak mungkin menggunakan satu sprite pada semua objek, seperti yang diterapkan sebelumnya. Menggunakan salinan objek sebagai bayangannya hanya berfungsi dengan baik selama objek tersebut benar-benar rata. Bahkan ketika membuat bayangan untuk gerombolan, terlihat bahwa titik-titik kontak dengan permukaan berjarak sepanjang koordinat ketiga melanggar kebenaran bayangan relatif terhadap titik-titik ini.

Tangkapan layar berikut menunjukkan contoh pelanggaran semacam itu. Tumit gerombolan diambil sebagai titik kontak dengan permukaan, tetapi bayangan kaki sudah melampaui kaki itu sendiri.



Dan jika dalam kasus kaki raksasa Anda masih bisa sedikit mengubah posisi bayangan dan menutupi masalahnya, maka untuk beberapa lusin batang pohon tidak ada kesempatan. Semua objek lokasi yang seharusnya membuat bayangan harus dibuat GameObject terpisah. Inilah yang saya lakukan dengan menempatkan salinan dari objek yang dapat dirusak yang sesuai pada prefab lokasi dan menonaktifkan skrip yang tidak digunakan dalam posisi ini. Pada saat yang sama, berkat ini, menjadi mungkin untuk memasukkan mereka dalam penyortiran umum objek pemandangan, dan kerang yang terbang di luar lokasi tidak lagi ditarik secara ketat di atas semua objek, tetapi terbang di antara mereka. Selain itu, menjadi mungkin untuk membuat objek itu sendiri dianimasikan.

Tapi kemudian masalah baru menungguku. Dengan bayangan dan lusinan objek baru, jumlah maksimum GameObjects bersamaan di atas panggung, dan dengan mereka komponen Animator dan SpriteRenderer, lebih dari dua kali lipat. Ketika saya merilis seluruh gelombang gerombolan ke lokasi, yang berjumlah sekitar 150 buah, Profiler dengan mencela menunjukkan kepada saya sekitar 40ms, yang hilang hanya untuk rendering dan animasi, dan frame rate umumnya bervariasi sekitar 10. Saya mati-matian mengoptimalkan skrip saya sendiri, berjuang untuk setiap milidetik, tapi itu tidak cukup.

Dalam mencari alat pengoptimalan tambahan, saya menemukan dokumentasi yang luas dan panduan untuk batch dinamis.

Sedikit tentang batching
Singkatnya, batching adalah mekanisme untuk meminimalkan jumlah panggilan draw, dan dengan itu waktu yang dihabiskan pada saat rendering frame pada interaksi antara CPU dan GPU. Saat digunakan alih-alih mengirim setiap elemen secara individual untuk dirender, elemen serupa dikelompokkan dan digabungkan secara bersamaan. Dalam kasus Unity, mesin itu sendiri mencoba untuk memanfaatkan mekanisme ini secara maksimal dan hampir tidak ada tindakan tambahan yang diperlukan dari pengembang.

Frame Debugger menunjukkan bahwa saya memiliki, paling baik, detail masing-masing objek atau massa secara terpisah. Setelah menciptakan sprite untuk yang pertama dan kedua untuk atlas, saya mencapai bayangan dengan hanya beberapa panggilan, tetapi pemilik bayangan ini dengan keras kepala menolak untuk bertarung sendiri.

Eksperimen pada adegan terpisah menunjukkan bahwa batching dinamis pecah ketika objek memiliki komponen SortingGroup, yang saya gunakan untuk mengurutkan tampilan entitas pada layar. Namun, secara teori dimungkinkan untuk melakukan tanpanya, menetapkan nilai penyortiran untuk setiap sistem sprite dan partikel dalam suatu objek secara terpisah bisa lebih mahal daripada kurangnya batching.

Tapi sesuatu menghantuiku. Objek bayangan, menjadi keturunan dari objek host di adegan nyata, secara teknis milik Grup Sorting yang sama, namun, tidak ada masalah dengan objek bayangan dinamis yang membayang. Satu-satunya perbedaan adalah bahwa objek host digambar langsung di layar oleh kamera utama, dan objek bayangan pertama kali dirender dalam RenderTexture.

Ini tangkapannya. Apa sebenarnya alasan perilaku ini tidak diketahui oleh Internet, tetapi saat merender gambar kamera di RenderTexture, SortingGroup tidak lagi memecahkan betsing. Keputusan itu kelihatannya sangat aneh, tidak logis, dan secara umum yang paling kruk. Tetapi dengan menerapkan rendering entitas menggunakan metode yang sama dengan rendering bayangan, dan dengan demikian memperoleh, selain layer shadow, layer entitas, saya telah mencapai nilai kinerja yang cukup dapat diterima.

Tangkapan layar di bawah ini menunjukkan contoh rendering lapisan entitas.



Jadi secara umum, merender entitas tertentu dalam koordinat Y terlihat seperti ini:

  1. Entitas ditempatkan pada Y - 20;
  2. Suatu entitas ditampilkan oleh kamera yang mengamati koordinat ini dalam RenderTexture untuk entitas;
  3. Bayangan entitas ditempatkan pada Y + 20;
  4. Bayangan entitas ditarik oleh kamera yang mengamati koordinat ini dalam RenderTexture untuk bayangan;
  5. Kamera utama menggambar sprite lokasi utama di layar - satu-satunya elemen yang saat ini dirender langsung ke layar;
  6. Kamera utama menggambar Plane di layar dengan bayangan RenderTexture sebagai materi;
  7. Kamera utama menggambar Plane di layar dengan RenderTexture entitas sebagai material.

Kue lapis seperti itu.

Dalam tangkapan layar di bawah ini, kamera editor diatur ke mode tiga dimensi untuk menunjukkan lokasi lapisan relatif satu sama lain.



Nuansa


Tetapi ternyata selama proses mereplikasi keputusan ke entitas lain, kasus umum tidak mencakup semua skenario yang mungkin. Sebagai contoh, ada entitas yang berada pada ketinggian relatif terhadap permukaan, khususnya, kerang dan beberapa karakter cutscene. Selain itu, cangkang juga memiliki kemampuan untuk memutar tergantung pada arah gerakan mereka di layar, karena itu, selain mengatur titik persimpangan objek dan bayangannya, perlu untuk memilih bagian yang berputar sebagai objek anak yang terpisah, untuk memperbaiki logika rotasi proyektil dan animasi mereka.

Tangkapan layar berikut menunjukkan contoh rotasi kerang dan bayangannya.



Karakter terbang, serta monster terbang yang direncanakan, juga dapat bergerak dalam koordinat Y virtual mereka, yang membutuhkan penciptaan mekanisme untuk menghitung posisi bayangan dari posisi pemiliknya pada sumbu Y virtual.

GIF di bawah ini menunjukkan contoh memindahkan objek dengan ketinggian.



Kasus lain yang keluar dari konsep umum adalah tank. Tidak seperti entitas lainnya, tangki memiliki ukuran yang sangat besar di sepanjang sumbu Z virtual, dan implementasi keseluruhan dari bayangan, seperti yang telah disebutkan, mengharuskan objek hampir datar. Cara termudah untuk menyiasatinya adalah menggambar bentuk bayangan secara manual untuk setiap bagian tangki, karena Anda dapat meletakkan apa pun di lapisan bayangan.

Untuk konstruksi bayangan yang digambar tangan dengan benar, saya harus merakit desain garis berdasarkan tangkapan layar bayangan yang ada, yang dapat dilihat pada tangkapan layar di bawah ini.



Jika Anda skala dan tempatkan struktur ini sedemikian rupa sehingga bagian atas akan berada di beberapa titik objek induk, dan bagian bawah akan berada pada titik kontak dengan permukaan, sudut kanan struktur akan menunjukkan tempat di mana titik bayangan yang sesuai seharusnya. Setelah memproyeksikan beberapa poin utama dengan cara ini, tidaklah sulit untuk membangun seluruh bayangan pada mereka.

Selain itu, masing-masing bagian tangki dapat memiliki ketinggian yang berbeda untuk memasang bagian anak, yang, seperti halnya karakter terbang dan massa, memerlukan penyesuaian posisi bayangan dari setiap bagian tertentu.

Tangkapan layar di bawah ini menunjukkan tangki, rakitan bayangannya dan juga dalam bentuk bagian yang terpisah.



Bayangan dinding ternyata menjadi rasa sakit yang terpisah. Pada saat awal pengerjaan bayangan, dindingnya memiliki sifat yang sama dengan detail tangki - satu objek dari beberapa lusin sprite terpisah. Namun, tembok itu memiliki beberapa negara yang dikendalikan oleh animator.

Berpikir keras tentang apa yang harus dilakukan dengan mereka, saya sampai pada kesimpulan bahwa konsep dinding perlu diubah. Akibatnya, dinding dibagi menjadi beberapa bagian, masing-masing memiliki set negara sendiri, animator sendiri, dan bayangannya sendiri. Ini memungkinkan untuk menggunakan pendekatan yang sama untuk membuat bayangan untuk gerombolan yang sejajar dengan sumbu X, seperti halnya dengan gerombolan, dan untuk bagian-bagian yang tidak sesuai dengan aturan ini, mereka harus membuat sesuatu sendiri. Dalam beberapa kasus, saya harus membuat animator saya sendiri untuk bagian bayangan dan secara manual mengatur posisi sprite.

Misalnya, dalam kasus bagian yang ditunjukkan pada tangkapan layar di bawah, bayangan dibuat dengan menerapkan distorsi untuk setiap log individu, bukan seluruh bagian.



Kesimpulan


Faktanya, itu saja. Terlepas dari semua nuansa di atas, tugas asli selesai secara penuh, dan sekarang proyek saya menawarkan bayangan yang cukup baik, meskipun berasal dari yang agak meragukan. Saya harap, terima kasih untuk artikel ini, untuk pengembang indie berikutnya yang menanyakan pertanyaan serupa kepada saya, Internet akan menjadi sedikit lebih berguna, jika tidak sebagai contoh untuk diikuti, maka setidaknya sebagai kesalahan orang lain untuk pembelajaran Anda sendiri.

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


All Articles