Penembak zombie sederhana di Unity

Halo semuanya! Segera, kelas akan dimulai pada grup pertama dari kursus Pengembang Unity Games . Untuk mengantisipasi dimulainya kursus, pelajaran terbuka tentang menciptakan penembak zombie di Unity diadakan. Webinar ini diselenggarakan oleh Nikolai Zapolnov , Pengembang Game Senior dari Rovio Entertainment Corporation. Dia juga menulis artikel terperinci, yang kami sampaikan kepada Anda.



Pada artikel ini, saya ingin menunjukkan betapa mudahnya membuat game di Unity. Jika Anda memiliki pengetahuan pemrograman dasar, maka Anda dapat dengan cepat mulai bekerja dengan mesin ini dan membuat game pertama Anda.



Penafian # 1: Artikel ini untuk pemula. Jika Anda makan seekor anjing di Unity, maka itu mungkin terasa membosankan bagi Anda.

Penafian # 2: Untuk membaca artikel ini, Anda memerlukan setidaknya pengetahuan pemrograman dasar. Minimal, kata-kata "kelas" dan "metode" seharusnya tidak membuat Anda takut.

Perhatian, lalu lintas di bawah potongan!

Pengantar Persatuan


Jika Anda sudah terbiasa dengan editor Persatuan, Anda dapat melewati pengantar dan langsung ke bagian "Menciptakan dunia permainan".

Unit struktural dasar dalam Unity adalah "adegan". Adegan biasanya satu tingkat dalam permainan, meskipun dalam beberapa kasus mungkin ada beberapa level sekaligus dalam satu adegan atau, sebaliknya, satu level besar dapat dibagi menjadi beberapa adegan yang dimuat secara dinamis. Adegan diisi dengan objek game, dan mereka, pada gilirannya, diisi dengan komponen. Ini adalah komponen yang mengimplementasikan berbagai fungsi permainan: menggambar objek, animasi, fisika, dll. Model ini memungkinkan Anda untuk merakit fungsi dari blok sederhana, seperti mainan dari konstruktor Lego.

Anda dapat menulis komponen sendiri, menggunakan bahasa pemrograman C # untuk ini. Beginilah cara logika game ditulis. Di bawah ini kita akan melihat bagaimana ini dilakukan, tetapi untuk sekarang mari kita lihat mesin itu sendiri.

Saat Anda menghidupkan mesin dan membuat proyek baru, Anda akan melihat jendela di depan Anda tempat Anda dapat memilih empat elemen utama:



Di sudut kiri atas tangkapan layar adalah jendela "Hierarchy". Di sini kita bisa melihat hierarki objek game di adegan terbuka saat ini. Unity menciptakan dua objek permainan untuk kita: kamera ("Kamera Utama") yang melaluinya pemain akan melihat dunia permainan kita dan "Lampu Arah" yang akan menerangi adegan kita. Tanpanya, kita hanya akan melihat kotak hitam.

Di tengah adalah jendela pengeditan adegan ("Scene"). Di sini kita melihat level kita dan kita dapat mengeditnya secara visual - pindahkan dan putar objek dengan mouse dan lihat apa yang terjadi. Di dekatnya Anda dapat melihat tab "Game", yang saat ini tidak aktif; jika Anda beralih ke itu, Anda dapat melihat bagaimana permainan terlihat dari kamera. Dan jika Anda memulai game (menggunakan tombol dengan ikon play di toolbar), maka Unity akan beralih ke tab ini, tempat kami akan memainkan game yang diluncurkan.

Di bagian kanan atas adalah jendela "Inspektur". Di jendela ini, Unity menunjukkan parameter dari objek yang dipilih dan kita dapat mengeditnya. Secara khusus, kita dapat melihat bahwa kamera yang dipilih memiliki dua komponen - "Transform", yang menetapkan posisi kamera di dunia game, dan, pada kenyataannya, "Camera", yang mengimplementasikan fungsionalitas kamera.

By the way, komponen Transform berada dalam satu bentuk atau yang lain di semua objek game di Unity.

Dan akhirnya, di bagian bawah ada tab "Proyek", di mana kita dapat melihat semua yang disebut aset yang ada di proyek kami. Aset adalah file data seperti tekstur, sprite, model 3d, animasi, suara dan musik, file konfigurasi. Artinya, setiap data yang dapat kita gunakan untuk membuat level atau antarmuka pengguna. Unity memahami sejumlah besar format standar (misalnya, png dan jpg untuk gambar, atau fbx untuk model 3d), jadi tidak akan ada masalah memuat data ke dalam proyek. Dan jika Anda, seperti saya, tidak tahu cara menggambar, aset dapat diunduh dari Unity Asset Store, yang berisi banyak koleksi semua jenis sumber daya: baik gratis maupun dijual untuk mendapatkan uang.

Di sebelah kanan tab "Project", tab "Console" yang tidak aktif terlihat. Unity menulis peringatan dan pesan kesalahan ke konsol, jadi pastikan untuk memeriksa kembali secara berkala. Terutama jika sesuatu tidak berfungsi - kemungkinan besar, konsol akan memberi petunjuk penyebab masalahnya. Juga, konsol dapat menampilkan pesan dari kode permainan, untuk debugging.

Buat dunia game


Karena saya seorang programmer dan menggambar lebih buruk daripada cakar ayam, untuk grafik saya mengambil beberapa aset gratis dari Unity Asset Store. Anda dapat menemukan tautan ke mereka di akhir artikel ini.

Dari aset-aset ini, saya mengumpulkan tingkat sederhana yang dengannya kami akan bekerja:



Tanpa sihir, saya hanya menyeret objek yang saya suka dari jendela Project dan menggunakan mouse yang mengaturnya sesuka saya:



Omong-omong, Unity memungkinkan Anda untuk menambahkan objek standar ke adegan dengan satu klik, seperti kubus, bola, atau bidang. Untuk melakukan ini, cukup klik kanan di jendela Hierarchy dan pilih, misalnya, Object⇨Plane 3D. Jadi, aspal di tingkat saya baru saja dikumpulkan dari satu set pesawat ke mana saya "menarik" tekstur dari satu set aset.

NB Jika Anda bertanya-tanya mengapa saya menggunakan banyak pesawat, dan bukan pesawat dengan nilai skala besar, jawabannya cukup sederhana: satu pesawat dengan skala besar akan memiliki tekstur yang sangat besar, yang akan terlihat tidak alami sehubungan dengan objek lain di tempat kejadian (ini dapat diperbaiki dengan parameternya materi, tapi kami mencoba melakukan segala sesederhana mungkin, kan?)

Zombi mencari jalan


Jadi, kami memiliki level permainan, tetapi belum ada yang terjadi di dalamnya. Dalam permainan kami, zombie akan mengejar pemain dan menyerangnya, dan untuk ini mereka harus bisa bergerak ke arah pemain dan melewati rintangan.

Untuk mengimplementasikan ini, kita akan menggunakan alat "Navigasi Mesh". Berdasarkan data adegan, alat ini menghitung area tempat Anda bisa bergerak, dan menghasilkan satu set data yang dapat digunakan untuk mencari rute optimal dari titik mana saja di level ke yang lain selama pertandingan. Data ini disimpan dalam aset dan tidak dapat diubah di masa mendatang - proses ini disebut "memanggang". Jika Anda perlu mengubah rintangan secara dinamis, Anda dapat menggunakan komponen NavMeshObstacle, tetapi ini tidak diperlukan untuk permainan kami.

Poin penting: agar Persatuan mengetahui objek mana yang harus dimasukkan dalam perhitungan, di Inspektur untuk setiap objek (Anda dapat memilih semuanya sekaligus di jendela Hierarki), klik panah bawah di sebelah opsi "Statis" dan centang "Navigasi Statis":



Secara umum, poin yang tersisa juga berguna dan membantu Unity mengoptimalkan rendering adegan. Kami tidak akan memikirkannya hari ini, tetapi ketika Anda selesai mempelajari dasar-dasar mesin, saya sangat menyarankan Anda berurusan dengan parameter lain juga. Terkadang tanda centang tunggal dapat secara signifikan meningkatkan laju bingkai.

Sekarang kita akan menggunakan item menu Window⇨AI⇨Navigation dan di jendela yang terbuka, pilih tab “Bake”. Di sini, Unity akan menawarkan kita untuk mengatur parameter seperti tinggi dan jari-jari karakter, sudut kemiringan maksimum bumi tempat Anda masih bisa berjalan, ketinggian maksimum anak tangga, dan sebagainya. Kami belum akan mengubah apa pun dan cukup tekan tombol "Bake".



Unity akan membuat perhitungan yang diperlukan dan menunjukkan hasilnya:



Di sini, biru menunjukkan area di mana Anda bisa berjalan. Seperti yang Anda lihat, Unity meninggalkan sisi kecil di sekitar rintangan - lebar sisi ini tergantung pada jari-jari karakter. Dengan demikian, jika pusat karakter berada di zona biru, maka ia tidak akan "jatuh" melewati rintangan.

Setelah kisi navigasi dihitung, kita dapat menggunakan komponen NavMeshAgent untuk mencari rute pergerakan dan mengontrol pergerakan objek game di level kita.

Mari kita buat objek game "Zombie", tambahkan model 3d zombie dari aset padanya, dan juga komponen NavMeshAgent:



Jika Anda memulai permainan sekarang, maka tidak ada yang akan terjadi. Kita perlu memberi tahu komponen NavMeshAgent ke mana harus pergi. Untuk melakukan ini, kita akan membuat komponen pertama kita di C #.

Di jendela proyek, pilih direktori root (ini disebut "Aset") dan dalam daftar file, klik kanan untuk membuat direktori "Scripts". Kami akan menyimpan semua skrip kami di dalamnya sehingga proyek memiliki pesanan. Sekarang, di dalam "Script", mari kita membuat skrip "Zombie" dan menambahkannya ke objek game zombie:



Mengklik dua kali pada skrip akan membukanya di editor. Mari kita lihat apa yang telah diciptakan Unity untuk kita.

using System.Collections; using System.Collections.Generic; using UnityEngine; public class Zombie : MonoBehaviour { // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { } } 

Ini adalah komponen standar kosong. Seperti yang dapat kita lihat, Unity menghubungkan perpustakaan System.Collections dan System.Collections.Generic kepada kami (sekarang mereka tidak diperlukan, tetapi mereka sering dibutuhkan dalam kode game Unity, sehingga mereka termasuk dalam templat standar), serta perpustakaan UnityEngine, yang berisi semua API mesin inti.

Juga, Unity menciptakan kelas Zombie untuk kita (nama cocok dengan nama file; ini penting: jika mereka tidak cocok, Unity tidak akan dapat mencocokkan skrip dengan komponen di tempat kejadian). Kelas diwarisi dari MonoBehaviour - ini adalah kelas dasar untuk komponen yang dibuat pengguna.

Di dalam kelas, Unity menciptakan dua metode bagi kami: Mulai dan Perbarui. Mesin akan memanggil metode ini sendiri: Mulai - segera setelah adegan telah dimuat, dan Perbarui - setiap frame. Sebenarnya, ada banyak fungsi yang disebut oleh mesin, tetapi kebanyakan dari mereka tidak akan kita butuhkan saat ini. Daftar lengkap, serta urutan panggilan mereka, selalu dapat ditemukan dalam dokumentasi: https://docs.unity3d.com/Manual/ExecutionOrder.html

Mari kita membuat zombie bergerak di peta!

Pertama, kita perlu menghubungkan perpustakaan UnityEngine.AI. Ini berisi kelas NavMeshAgent dan kelas lain yang terkait dengan kotak navigasi. Untuk melakukan ini, tambahkan direktif menggunakan UnityEngine.AI ke awal file.

Selanjutnya, kita perlu mengakses komponen NavMeshAgent. Untuk melakukan ini, kita dapat menggunakan metode GetComponent standar. Hal ini memungkinkan Anda untuk mendapatkan tautan ke komponen apa pun dalam objek game yang sama di mana komponen dari mana kami memanggil metode ini berada (dalam kasus kami, itu adalah objek game "Zombie"). Mari kita dapatkan bidang navMeshAgent NavMeshAgent di kelas, dalam metode Mulai kita mendapatkan tautan ke NavMeshAgent dan memintanya untuk pindah ke titik (0, 0, 0). Kita harus mendapatkan skrip ini:

 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class Zombie : MonoBehaviour { NavMeshAgent navMeshAgent; // Start is called before the first frame update void Start() { navMeshAgent = GetComponent<NavMeshAgent>(); navMeshAgent.SetDestination(Vector3.zero); } // Update is called once per frame void Update() { } } 

Mulai permainan, kita akan melihat bagaimana zombie bergerak ke tengah peta:



Zombi mengejar korban


Bagus Tapi zombie kita bosan dan kesepian, mari kita tambahkan korban pemain ke gimnya.

Dengan analogi dengan zombie, kami akan membuat objek permainan "Pemain" (kali ini kami akan memilih model 3d dari seorang polisi), kami juga akan menambahkan komponen NavMeshAgent dan skrip Pemain yang baru dibuat untuknya. Kami belum menyentuh konten skrip Player, tetapi kami harus membuat perubahan pada skrip Zombie. Juga, saya sarankan mengatur nilai properti Prioritas pemain ke 10 di komponen NavMeshAgent (atau nilai lain yang kurang dari standar 50, yaitu, memberikan pemain prioritas yang lebih tinggi). Dalam hal ini, jika pemain dan zombie bertemu di peta, zombie tidak akan dapat memindahkan pemain, sementara pemain akan dapat mendorong zombie keluar.

Untuk mengejar pemain, zombie perlu mengetahui posisinya. Dan untuk ini kita perlu mendapatkan tautan di kelas Zombie kita menggunakan metode FindObjectOfType standar. Setelah mengingat tautan, kita dapat beralih ke komponen transformasi pemain dan menanyakan nilai posisi. Dan agar zombie mengejar pemain selalu, dan tidak hanya di awal permainan, kami akan menetapkan tujuan untuk NavMeshAgent dalam metode Pembaruan. Anda mendapatkan skrip berikut:

 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class Zombie : MonoBehaviour { NavMeshAgent navMeshAgent; Player player; // Start is called before the first frame update void Start() { navMeshAgent = GetComponent<NavMeshAgent>(); player = FindObjectOfType<Player>(); } // Update is called once per frame void Update() { navMeshAgent.SetDestination(player.transform.position); } } 

Jalankan game dan pastikan bahwa zombie telah menemukan korbannya:



Escape Escape


Pemain kami berdiri seperti idola. Ini jelas tidak akan membantunya bertahan di dunia yang agresif ini, jadi Anda perlu mengajarinya untuk bergerak di sekitar peta.

Untuk melakukan ini, kita perlu mendapatkan informasi tentang tombol yang ditekan dari Unity. Metode GetKey dari kelas Input standar hanya memberikan informasi seperti itu!

NB Secara umum, cara mendapatkan input ini tidak sepenuhnya kanonik. Lebih baik menggunakan Input.GetAxis dan mengikat melalui Pengaturan Proyek⇨Input Manager. Lebih baik lagi, Sistem Input Baru . Tetapi artikel ini ternyata terlalu panjang, jadi mari kita lakukan sebagai lebih sederhana.

Buka skrip Player dan ubah sebagai berikut:

 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class Player : MonoBehaviour { NavMeshAgent navMeshAgent; public float moveSpeed; // Start is called before the first frame update void Start() { navMeshAgent = GetComponent<NavMeshAgent>(); } // Update is called once per frame void Update() { Vector3 dir = Vector3.zero; if (Input.GetKey(KeyCode.LeftArrow)) dir.z = -1.0f; if (Input.GetKey(KeyCode.RightArrow)) dir.z = 1.0f; if (Input.GetKey(KeyCode.UpArrow)) dir.x = -1.0f; if (Input.GetKey(KeyCode.DownArrow)) dir.x = 1.0f; navMeshAgent.velocity = dir.normalized * moveSpeed; } } 

Seperti dalam kasus zombie, dalam metode Mulai kami mendapatkan tautan ke komponen NavMeshAgent pemain dan menyimpannya di bidang kelas. Tapi sekarang kami juga menambahkan bidang moveSpeed.
Karena kenyataan bahwa bidang ini bersifat publik, nilainya dapat diedit langsung di Inspektur di Persatuan! Jika Anda memiliki perancang permainan di tim Anda, dia akan sangat senang bahwa dia tidak perlu masuk ke kode untuk mengedit parameter pemain.

Tetapkan 10 sebagai kecepatan:



Dalam metode Pembaruan, kita akan menggunakan Input.GetKey untuk memeriksa apakah ada panah pada keyboard yang ditekan dan membentuk vektor arah untuk pemain. Perhatikan bahwa kita menggunakan koordinat X dan Z. Hal ini disebabkan oleh fakta bahwa di Unity sumbu Y melihat ke langit, dan bumi terletak di bidang XZ.

Setelah kami membentuk vektor arah untuk arah gerakan, kami menormalkannya (jika tidak, jika pemain ingin bergerak secara diagonal, vektor akan sedikit lebih panjang dari satu arah dan gerakan ini akan lebih cepat daripada bergerak langsung) dan dikalikan dengan kecepatan gerakan yang diberikan. Hasilnya diteruskan ke navMeshAgent.velocity dan agen akan melakukan sisanya.

Dengan meluncurkan game, kita akhirnya bisa mencoba melarikan diri dari zombie ke tempat yang aman:



Untuk membuat kamera bergerak dengan pemain, mari kita menulis skrip sederhana lain. Sebut saja "PlayerCamera":

 using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerCamera : MonoBehaviour { Player player; Vector3 offset; // Start is called before the first frame update void Start() { player = FindObjectOfType<Player>(); offset = transform.position - player.transform.position; } // Update is called once per frame void LateUpdate() { transform.position = player.transform.position + offset; } } 

Arti dari naskah ini harus dipahami secara luas. Dari fitur - di sini alih-alih Pembaruan, kami menggunakan LateUpdate. Metode ini mirip dengan Pembaruan, tetapi selalu dipanggil secara ketat setelah Pembaruan selesai untuk semua skrip di tempat kejadian. Dalam hal ini, kami menggunakan LateUpdate, karena penting bagi kami bahwa NavMeshAgent menghitung posisi baru pemain sebelum kami memindahkan kamera. Jika tidak, efek “menyentak” yang tidak menyenangkan dapat terjadi.

Jika sekarang Anda melampirkan komponen ini ke objek game "Kamera Utama" dan memulai permainan, karakter pemain akan selalu menjadi sorotan!

Momen animasi


Sejenak kita menyimpang dari masalah bertahan hidup dalam kondisi kiamat zombie dan berpikir tentang seni abadi. Karakter kami sekarang terlihat seperti patung animasi, digerakkan oleh kekuatan yang tidak diketahui (mungkin magnet di bawah aspal). Dan saya ingin mereka terlihat seperti orang yang nyata, hidup (dan tidak terlalu) - mereka menggerakkan tangan dan kaki mereka. Komponen Animator dan alat yang disebut Animator Controller akan membantu kita dalam hal ini.

Animator Controller adalah mesin keadaan terbatas (state machine), tempat kami mengatur keadaan tertentu (karakter berdiri, karakter aktif, karakter sekarat, dll.), Kami melampirkan animasi padanya dan menetapkan aturan untuk transisi dari satu kondisi ke kondisi lainnya. Unity akan secara otomatis beralih dari satu animasi ke animasi lainnya segera setelah aturan yang sesuai berfungsi.

Mari kita buat Pengendali Animator untuk zombie. Untuk melakukan ini, buat direktori Animasi di proyek (ingat urutan di proyek), dan di dalamnya - menggunakan tombol kanan - Animator Controller. Dan sebut saja dia "Zombie." Klik dua kali - dan editor akan muncul di hadapan kami:



Tidak ada status di sini sejauh ini, tetapi ada dua titik masuk ("Entri" dan "Semua Negara") dan satu titik keluar ("Keluar"). Seret beberapa animasi dari aset:



Seperti yang Anda lihat, segera setelah kami menyeret animasi pertama, Unity secara otomatis mengikatnya ke titik entri entri. Inilah yang disebut animasi default. Ini akan dimainkan segera setelah permulaan level.

Untuk beralih ke keadaan lain (dan memutar animasi lain), kita perlu membuat aturan transisi. Dan untuk ini, pertama-tama, kita perlu menambahkan parameter yang akan kita atur dari kode untuk mengelola animasi.

Ada dua tombol di sudut kiri atas jendela editor: "Layers" dan "Parameters". Secara default, tab "Layers" dipilih, tetapi kita perlu beralih ke "Parameters". Sekarang kita dapat menambahkan parameter tipe float baru menggunakan tombol "+". Sebut saja "kecepatan":



Sekarang kita perlu memberi tahu Unity bahwa animasi "Z_run" harus diputar ketika kecepatan lebih besar dari 0 dan "Z_idle_A" ketika kecepatan nol. Untuk melakukan ini, kita harus membuat dua transisi: satu dari "Z_idle_A" ke "Z_run", dan yang lainnya di arah yang berlawanan.

Mari kita mulai dengan transisi dari siaga ke berjalan. Klik kanan pada persegi panjang "Z_idle_A" dan pilih "Lakukan Transisi". Sebuah panah akan muncul, mengklik di mana Anda dapat mengkonfigurasi parameternya. Pertama, Anda harus menghapus centang "Memiliki Waktu Keluar". Jika ini tidak dilakukan, animasi akan beralih tidak sesuai dengan kondisi kami, tetapi ketika yang sebelumnya selesai diputar. Kami tidak membutuhkan ini sama sekali, jadi kami hapus centangnya. Kedua, di bagian bawah, dalam daftar kondisi ("Ketentuan") Anda perlu mengklik "+" dan Unity akan menambahkan ketentuan kepada kami. Nilai default dalam kasus ini persis seperti yang kita butuhkan: parameter "speed" harus lebih besar dari nol untuk beralih dari idle ke run.



Dengan analogi, kami membuat transisi ke arah yang berlawanan, tetapi sebagai kondisi kami sekarang menentukan "kecepatan" kurang dari 0,0001. Tidak ada pemeriksaan kesetaraan untuk parameter tipe float, mereka hanya dapat dibandingkan dengan lebih / kurang:



Sekarang Anda harus mengikat pengontrol ke objek game. Kami akan memilih model 3d zombie dalam adegan (ini adalah anak dari objek "Zombie") dan menyeret pengontrol dengan mouse ke bidang yang sesuai dalam komponen Animator:



Tetap menulis skrip yang akan mengontrol parameter kecepatan!

Buat skrip MovementAnimator dengan konten berikut:

 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class MovementAnimator : MonoBehaviour { NavMeshAgent navMeshAgent; Animator animator; // Start is called before the first frame update void Start() { navMeshAgent = GetComponent<NavMeshAgent>(); animator = GetComponentInChildren<Animator>(); } // Update is called once per frame void Update() { animator.SetFloat("speed", navMeshAgent.velocity.magnitude); } } 

Di sini kita, seperti pada skrip lain, dalam metode Mulai mendapatkan akses ke NavMeshAgent. Kami juga mendapatkan akses ke komponen Animator, tetapi karena kami akan melampirkan komponen "MovementAnimator" ke objek game "Zombie" dan Animator ada di objek anak, alih-alih GetComponent kita perlu menggunakan metode GetComponentInChildren standar.

Dalam metode Pembaruan, kami meminta NavMeshAgent untuk vektor kecepatannya, menghitung panjangnya dan meneruskannya ke animator sebagai parameter kecepatan. Tanpa sihir, semuanya dalam sains!

Sekarang tambahkan komponen MovementAnimator ke objek game Zombie dan, jika game dimulai, kita melihat bahwa zombie sekarang dianimasikan:



Perhatikan bahwa karena kami telah menempatkan kode kontrol animator dalam komponen MovementAnimation yang terpisah, maka dapat dengan mudah ditambahkan untuk pemain. Kami bahkan tidak perlu membuat pengontrol dari awal - Anda dapat menyalin pengontrol zombie (ini dapat dilakukan dengan memilih file "Zombie" dan menekan Ctrl + D) dan mengganti animasi di persegi panjang negara dengan "m_idle_" dan "m_run". Yang lainnya seperti zombie. Saya akan meninggalkan ini untuk Anda sebagai latihan (baik, atau unduh kode di akhir artikel).

Satu tambahan kecil yang berguna untuk dibuat adalah menambahkan baris berikut ke kelas Zombie:

Dalam metode Mulai:

 navMeshAgent.updateRotation = false; 

Dalam metode Pembaruan:

 transform.rotation = Quaternion.LookRotation(navMeshAgent.velocity.normalized); 

Baris pertama memberi tahu NavMeshAgent bahwa dia tidak boleh mengendalikan rotasi karakter, kita akan melakukannya sendiri. Baris kedua mengatur giliran karakter ke arah yang sama di mana gerakannya diarahkan. NavMeshAgent secara default menginterpolasi sudut rotasi karakter dan ini tidak terlihat bagus (zombie berputar lebih lambat daripada mengubah arah gerakan). Menambahkan baris ini menghilangkan efek ini.

NB Kami menggunakan angka empat untuk menentukan rotasi. Dalam grafik tiga dimensi, cara utama untuk menentukan rotasi suatu objek adalah sudut Euler, matriks rotasi, dan angka empat. Dua yang pertama tidak selalu nyaman digunakan, dan juga mengalami efek yang tidak menyenangkan seperti "Gimbal Lock". Kuota dicabut dari kekurangan ini dan sekarang digunakan hampir secara universal. Unity menyediakan alat yang mudah digunakan untuk bekerja dengan angka empat (serta dengan matriks dan sudut Euler), yang memungkinkan Anda untuk tidak masuk ke perincian perangkat perangkat matematika ini.

Saya melihat tujuannya


Hebat, sekarang kita bisa melarikan diri dari zombie. Tapi ini tidak cukup, cepat atau lambat zombie kedua akan muncul, lalu yang ketiga, kelima, kesepuluh ... tetapi Anda tidak bisa lari begitu saja dari kerumunan. Untuk bertahan hidup, Anda harus membunuh. Apalagi pemain sudah memiliki senjata di tangannya.

Agar pemain dapat menembak, Anda harus memberinya kesempatan untuk memilih target. Untuk melakukan ini, letakkan kursor yang dikontrol mouse di atas tanah.

Di layar, kursor mouse bergerak dalam ruang dua dimensi - permukaan monitor. Pada saat yang sama, adegan permainan kami adalah tiga dimensi. Pengamat melihat pemandangan itu melalui matanya, di mana semua sinar cahaya bertemu pada satu titik. Menggabungkan semua sinar ini, kita mendapatkan piramida visibilitas:



Mata pengamat hanya melihat apa yang jatuh ke dalam piramida ini. Selain itu, mesin secara khusus memotong piramida ini dari dua sisi: pertama, dari sisi pengamat ada layar monitor, yang disebut "pesawat dekat" (pada gambar itu dicat dengan warna kuning). Monitor tidak dapat secara fisik menampilkan objek lebih dekat daripada layar, sehingga mesin memotongnya. Kedua, karena komputer memiliki jumlah sumber daya yang terbatas, mesin tidak dapat memperpanjang sinar hingga tak terbatas (misalnya, kisaran tertentu dari nilai yang mungkin harus ditetapkan untuk penyangga kedalaman; apalagi, semakin lebar, semakin rendah akurasi), sehingga piramida terputus di belakang yang disebut "Pesawat jauh".

Karena kursor mouse bergerak di sepanjang bidang dekat, kita dapat melepaskan sinar dari titik di mana ia berada jauh ke dalam pemandangan. Objek pertama yang bersinggungan dengannya adalah objek yang ditunjuk kursor mouse dari sudut pandang pengamat.



Untuk membangun sinar seperti itu dan menemukan persimpangannya dengan objek dalam adegan, Anda dapat menggunakan metode Raycast standar dari kelas Fisika. Tetapi jika kita menggunakan metode ini, itu akan menemukan persimpangan dengan semua objek dalam adegan - bumi, dinding, zombie ... Tapi kita ingin kursor bergerak hanya di tanah, jadi kita perlu menjelaskan kepada Unity bahwa pencarian persimpangan harus dibatasi hanya seperangkat benda tertentu (dalam kasus kami, hanya bidang bumi).

Jika Anda memilih objek permainan apa pun dalam adegan, maka di bagian atas inspektur Anda dapat melihat daftar drop-down "Layer". Secara default akan ada nilai "Default". Dengan membuka daftar drop-down, Anda dapat menemukan item "Tambahkan lapisan ..." di dalamnya, yang akan membuka jendela editor lapisan. Di editor Anda perlu menambahkan layer baru (sebut saja "Ground"):



Sekarang Anda dapat memilih semua bidang tanah di tempat kejadian dan menggunakan daftar turun bawah ini untuk memberi mereka lapisan Ground. Ini akan memungkinkan kita untuk menunjukkan dalam skrip ke Fisika. Metode siaran bahwa perlu untuk memeriksa persimpangan balok hanya dengan benda-benda ini.

Sekarang mari kita seret sprite kursor dari aset ke tempat kejadian (saya menggunakan Aset Spag⇨Tekstur⇨Demo⇨white_hip⇨white_hip_14):



Saya menambahkan rotasi 90 derajat di sekitar sumbu X ke kursor sehingga letaknya horizontal di atas tanah, atur skalanya menjadi 0,25 sehingga tidak begitu besar dan atur koordinat Y ke 0,01. Yang terakhir ini penting agar tidak ada efek yang disebut "Z-fighting". Kartu video menggunakan perhitungan titik mengambang untuk menentukan objek mana yang lebih dekat ke kamera. Jika Anda menetapkan kursor ke 0 (mis., Sama dengan yang ada pada bidang dasar), maka karena kesalahan dalam perhitungan ini, untuk beberapa piksel, kartu video akan memutuskan bahwa kursor lebih dekat, dan untuk yang lain, bahwa bumi. Selain itu, dalam bingkai yang berbeda, set piksel akan berbeda, yang akan menciptakan efek yang tidak menyenangkan dari menyinari potongan kursor melalui tanah dan "berkedip" ketika bergerak. Nilai 0,01 cukup besar untuk mengimbangi kesalahan dalam perhitungan kartu video, tetapi tidak terlalu besar sehingga mata memperhatikan bahwa kursor tergantung di udara.

Sekarang ganti nama objek game menjadi Cursor dan buat skrip dengan nama yang sama dan konten berikut:

 using System.Collections; using System.Collections.Generic; using UnityEngine; public class Cursor : MonoBehaviour { SpriteRenderer spriteRenderer; int layerMask; // Start is called before the first frame update void Start() { spriteRenderer = GetComponent<SpriteRenderer>(); layerMask = LayerMask.GetMask("Ground"); } // Update is called once per frame void Update() { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (!Physics.Raycast(ray, out hit, 1000, layerMask)) spriteRenderer.enabled = false; else { transform.position = new Vector3(hit.point.x, transform.position.y, hit.point.z); spriteRenderer.enabled = true; } } } 

Karena kursor adalah sprite (gambar dua dimensi), Unity menggunakan komponen SpriteRenderer untuk merendernya. Kami mendapatkan tautan ke komponen ini dalam metode Mulai untuk dapat menyalakan / mematikannya sesuai kebutuhan.

Juga di metode Mulai, kami mengonversi nama lapisan "Tanah" yang kami buat sebelumnya menjadi bitmask. Unity menggunakan operasi bitwise untuk memfilter objek saat mencari persimpangan, dan metode LayerMask.GetMask mengembalikan bitmask yang sesuai dengan lapisan yang ditentukan.

Dalam metode Pembaruan, kami mengakses kamera utama adegan menggunakan Camera.main dan memintanya untuk menghitung ulang koordinat dua dimensi mouse (diperoleh dengan menggunakan Input.mousePosition) menjadi sinar tiga dimensi. Selanjutnya, kita meneruskan ray ini ke metode Physics.Raycast dan memeriksa apakah itu bersinggungan dengan objek apa pun di dalam adegan. Nilai 1000 adalah jarak maksimum. Dalam matematika, sinar tidak terbatas, tetapi sumber daya komputasi dan memori komputer tidak. Oleh karena itu, Unity meminta kami untuk menentukan jarak maksimum yang masuk akal.

Jika tidak ada persimpangan, maka kami mematikan SpriteRenderer dan gambar kursor menghilang dari layar. Jika persimpangan ditemukan, maka kami memindahkan kursor ke titik persimpangan.Harap dicatat bahwa kami tidak mengubah koordinat Y, karena titik persimpangan sinar dengan tanah akan memiliki Y sama dengan nol dan menugaskannya ke kursor kami, kami kembali mendapatkan efek Z-fighting, yang kami coba singkirkan di atas. Oleh karena itu, kita hanya mengambil koordinat X dan Z dari titik persimpangan, dan Y tetap sama.

Tambahkan komponen Cursor ke objek game Cursor.

Sekarang, mari selesaikan skrip Player: pertama, tambahkan bidang kursor kursor. Kemudian dalam metode Mulai, tambahkan baris berikut:

 cursor = FindObjectOfType<Cursor>(); navMeshAgent.updateRotation = false; 

Dan akhirnya, agar pemain selalu berbalik ke arah kursor, dalam metode Pembaruan, tambahkan:

 Vector3 forward = cursor.transform.position - transform.position; transform.rotation = Quaternion.LookRotation(new Vector3(forward.x, 0, forward.z)); 

Di sini kami juga tidak memperhitungkan koordinat Y.

Tembak untuk bertahan hidup


Fakta semata-mata beralih ke sisi kursor tidak akan melindungi kita dari zombie, tetapi hanya akan menyelamatkan karakter pemain dari efek kejutan - sekarang Anda tidak dapat menyelinap di belakangnya. Agar dia benar-benar dapat bertahan dalam kenyataan keras dari permainan kami, Anda perlu mengajari dia cara menembak. Dan tembakan macam apa itu jika tidak terlihat? Semua orang tahu bahwa setiap penembak terhormat selalu menembakkan peluru pelacak.

Buat objek game Shot dan tambahkan komponen LineRenderer standar ke sana. Menggunakan bidang "Lebar" di editor, berikan lebar kecil, misalnya 0,04. Seperti yang bisa kita lihat, Unity melukisnya dengan warna ungu cerah - dengan cara ini objek tanpa bahan disorot.

Bahan adalah elemen penting dari mesin tiga dimensi. Menggunakan bahan menggambarkan penampilan objek. Semua parameter pencahayaan, tekstur, shader - semua ini dijelaskan oleh materi.

Mari kita buat direktori Bahan dalam proyek dan di dalamnya materi, sebut saja Kuning. Sebagai shader, pilih Unlit / Color. Shader standar ini tidak termasuk pencahayaan, jadi peluru kami akan terlihat bahkan dalam gelap. Pilih warna kuning:



Sekarang setelah bahan dibuat, Anda dapat menetapkannya ke LineRenderer:



Buat skrip Ditembak:

 using System.Collections; using System.Collections.Generic; using UnityEngine; public class Shot : MonoBehaviour { LineRenderer lineRenderer; bool visible; // Start is called before the first frame update void Start() { lineRenderer = GetComponent<LineRenderer>(); } // Update is called once per frame void FixedUpdate() { if (visible) visible = false; else gameObject.SetActive(false); } public void Show(Vector3 from, Vector3 to) { lineRenderer.SetPositions(new Vector3[]{ from, to }); visible = true; gameObject.SetActive(true); } } 

Script ini, seperti yang mungkin sudah Anda tebak, perlu ditambahkan ke objek game Shot.

Di sini saya menggunakan trik kecil untuk menampilkan bidikan di layar untuk tepat satu frame dengan minimum kode. Pertama, saya menggunakan FixedUpdate daripada Update. Metode FixedUpdate disebut pada frekuensi yang ditentukan (secara default - 60 frame per detik), bahkan jika laju bingkai nyata tidak stabil. Kedua, saya mengatur variabel yang terlihat, yang saya setel ke true ketika saya menampilkan bidikan di layar. Di FixedUpdate berikutnya, saya mengatur ulang ke false, dan hanya di frame berikutnya saya mematikan objek permainan tembakan. Pada dasarnya, saya menggunakan variabel boolean sebagai penghitung dari 1 hingga 0.

Metode gameObject.SetActive menghidupkan atau mematikan seluruh objek game tempat komponen kami berada. Objek game yang dimatikan tidak tergambar di layar dan komponennya tidak memanggil Pembaruan, metode Pembaruan, dll. Menggunakan metode ini memungkinkan Anda untuk membuat tembakan tidak terlihat ketika pemain tidak menembak.

Ada juga metode Show publik dalam skrip, yang akan kita gunakan dalam skrip Player untuk benar-benar menampilkan peluru ketika ditembakkan.

Tetapi pertama-tama Anda harus bisa mendapatkan koordinat laras senapan agar tembakannya berasal dari lubang yang benar. Untuk melakukan ini, cari Bip001⇨Bip001 Pelvis⇨Bip001 Spine⇨Bip001 R Clavicle⇨Bip001 R UpperArm⇨Bip001 R Forearm⇨Bip001 R Hand⇨R_hand_container⇨w_handgun objek dalam model 3d pemain dan tambahkan objek anak GunBarrel ke dalamnya. Tempatkan sehingga tepat di sebelah laras senapan:



Sekarang dalam skrip Player, tambahkan bidang:

 Shot shot; public Transform gunBarrel; 


Tambahkan ke metode Mulai dari skrip Player:

 shot = FindObjectOfType<Shot>(); 

Dan dalam metode Pembaruan:

 if (Input.GetMouseButtonDown(0)) { var from = gunBarrel.position; var target = cursor.transform.position; var to = new Vector3(target.x, from.y, target.z); shot.Show(from, to); } 

Seperti yang bisa Anda tebak, bidang publik gunBarrel yang ditambahkan, seperti moveSpeed ​​sebelumnya, akan tersedia di Inspektur. Mari kita beri dia objek game nyata yang kita buat:



Jika kita sekarang memulai game, maka kita akhirnya bisa menembak zombie!



Ada yang salah di sini! Tampaknya tembakan tidak membunuh zombie, tetapi terbang saja!

Tentu saja, jika Anda melihat kode tembakan kami, kami tidak melacak dengan cara apa pun apakah tembakan kami mengenai musuh atau tidak. Cukup tarik garis ke kursor.

Ini cukup mudah untuk diperbaiki. Dalam kode untuk memproses klik mouse di kelas Player, setelah baris var ke = ... dan sebelum tembakan garis. Show (...), tambahkan baris berikut:

 var direction = (to - from).normalized; RaycastHit hit; if (Physics.Raycast(from, to - from, out hit, 100)) to = new Vector3(hit.point.x, from.y, hit.point.z); else to = from + direction * 100; 

Di sini kita menggunakan Fisika yang dikenal. Tayangan ulang untuk membiarkan sinar keluar dari laras senjata dan menentukan apakah itu bersinggungan dengan objek permainan apa pun.

Di sini, bagaimanapun, ada satu peringatan: peluru masih akan terbang melalui zombie. Faktanya adalah bahwa penulis aset menambahkan collider ke objek tingkat (bangunan, kotak, dll). Dan penulis aset dengan karakter tidak. Mari kita perbaiki kesalahpahaman yang menjengkelkan ini.

Collider adalah komponen yang dengannya mesin fisika menentukan tabrakan antar objek. Biasanya bentuk geometris sederhana digunakan sebagai colliders - kubus, bola, dll. Meskipun pendekatan ini memberikan tumbukan yang kurang akurat, rumus persimpangan antara objek tersebut cukup sederhana dan tidak memerlukan sumber daya komputasi yang besar. Tentu saja, jika Anda membutuhkan akurasi maksimum, Anda selalu dapat mengorbankan kinerja dan menggunakan MeshCollider. Tapi kita tidak membutuhkan akurasi tinggi, jadi kita akan menggunakan komponen CapsuleCollider:



Sekarang peluru tidak akan terbang melalui zombie. Namun, zombie masih abadi.

Zombies - Zombie Death!


Pertama-tama mari kita tambahkan animasi kematian ke Pengendali Animasi zombie. Untuk melakukan ini, seret animasi AssetPacks⇨ToonyTinyPeople⇨TT_demo⇨animation⇨zombie⇨Z_death_A ke dalamnya. Untuk mengaktifkannya, buat parameter baru mati dengan jenis pemicu. Tidak seperti parameter lainnya (bool, float, dll.), Pemicu tidak mengingat statusnya dan lebih mirip dengan pemanggilan fungsi: pemicu diaktifkan - transisi berfungsi, dan pemicu diatur ulang. Dan karena zombie dapat mati di negara bagian mana pun - dan jika zombie diam, dan jika sedang berjalan, kami akan menambahkan transisi dari negara bagian Any:



Tambahkan bidang berikut ke skrip Zombie:

 CapsuleCollider capsuleCollider; Animator animator; MovementAnimator movementAnimator; bool dead; 

Dalam metode Mulai kelas Zombie, masukkan:

 capsuleCollider = GetComponent<CapsuleCollider>(); animator = GetComponentInChildren<Animator>(); movementAnimator = GetComponent<MovementAnimator>(); 

Di awal metode Pembaruan, Anda perlu menambahkan tanda centang:

 if (dead) return; 

Dan akhirnya, tambahkan metode Kill public ke kelas Zombie:

 public void Kill() { if (!dead) { dead = true; Destroy(capsuleCollider); Destroy(movementAnimator); Destroy(navMeshAgent); animator.SetTrigger("died"); } } 

Tugas bidang baru, saya pikir, cukup jelas. Adapun metode Kill - di dalamnya kita (jika kita tidak mati) mengatur bendera kematian zombie dan menghapus komponen CapsuleCollider, MovementAnimator dan NavMeshAgent dari objek permainan kita, setelah itu kita mengaktifkan pemutaran animasi kematian dari pengontrol animasi.

Mengapa menghapus komponen? Sehingga begitu zombie mati, dia berhenti bergerak di sekitar peta dan tidak lagi menjadi penghalang peluru. Demi kebaikan, Anda masih perlu entah bagaimana menyingkirkan tubuh dengan cara yang indah setelah animasi kematian dimainkan. Kalau tidak, zombie mati akan terus menggerogoti sumber daya dan, ketika ada terlalu banyak mayat, permainan akan terasa melambat. Cara termudah adalah dengan menambahkan panggilan Hancurkan (gameObject, 3) di sini. Ini akan menyebabkan Unity menghapus objek game ini 3 detik setelah panggilan ini.

Agar semua ini akhirnya berhasil, sentuhan terakhir tetap ada. Di kelas Player, dalam metode Pembaruan, tempat kami memanggil Physics.Raycast, di cabang untuk kasus ketika persimpangan ditemukan, kami menambahkan tanda centang:

 if (hit.transform != null) { var zombie = hit.transform.GetComponent<Zombie>(); if (zombie != null) zombie.Kill(); } 

Physics.Raycast memanggil informasi persimpangan dalam variabel hit. Secara khusus, dalam bidang transformasi akan ada tautan ke komponen Transform dari objek permainan yang dengannya sinar berpotongan. Jika objek game ini memiliki komponen Zombie, maka itu adalah zombie dan kami membunuhnya. SD!

Nah, agar kematian musuh terlihat spektakuler, kami menambahkan sistem partikel sederhana ke zombie.

Sistem partikel memungkinkan Anda untuk mengontrol sejumlah besar objek kecil (biasanya sprite) sesuai dengan beberapa jenis hukum fisika atau rumus matematika. Misalnya, Anda dapat membuatnya terbang terpisah atau terbang lurus ke bawah dengan kecepatan tertentu. Dengan bantuan sistem partikel dalam permainan, semua jenis efek dibuat: api, asap, percikan api, hujan, salju, kotoran dari bawah roda, dll. Kami akan menggunakan sistem partikel sehingga pada saat kematian, darah menipis dari zombie.

Tambahkan sistem partikel ke objek game Zombie (klik kanan padanya dan pilih Efek⇨Partikel Sistem):

Saya menyarankan opsi berikut:
Transform:

  • Posisi: Y 0,5
  • Rotasi: X -90

Sistem partikel
  • Durasi: 0,2
  • Looping: salah
  • Mulai Seumur Hidup: 0.8
  • Ukuran Mulai: 0,5
  • Mulai warna: hijau
  • Gravity Modifier: 1
  • Mainkan saat bangun: salah
  • Emisi:
  • Rate over Time: 100
  • Bentuk:
  • Radius: 0,25

Seharusnya terlihat seperti ini:



Tetap mengaktifkannya dalam metode Bunuh dari kelas Zombie:

 GetComponentInChildren<ParticleSystem>().Play(); 

Dan sekarang masalah yang sama sekali berbeda!



Serangan zombie dalam kawanan


Faktanya, bertarung dengan satu zombie itu membosankan. Anda membunuhnya dan hanya itu. Di mana drama itu? Di mana rasa takut mati muda? Untuk menciptakan suasana kiamat dan keputusasaan yang sebenarnya, harus ada banyak zombie.

Untungnya, ini sangat sederhana. Seperti yang mungkin sudah Anda duga, kami membutuhkan skrip lain. Sebut saja EnemySpawner dan isi dengan konten berikut:

 using System.Collections; using System.Collections.Generic; using UnityEngine; public class EnemySpawner : MonoBehaviour { public float Period; public GameObject Enemy; float TimeUntilNextSpawn; // Start is called before the first frame update void Start() { TimeUntilNextSpawn = Random.Range(0, Period); } // Update is called once per frame void Update() { TimeUntilNextSpawn -= Time.deltaTime; if (TimeUntilNextSpawn <= 0.0f) { TimeUntilNextSpawn = Period; Instantiate(Enemy, transform.position, transform.rotation); } } } 

Menggunakan bidang publik Periode, perancang permainan dapat menetapkan di Inspektur seberapa sering musuh baru harus dibuat. Di bidang Musuh, kami menunjukkan musuh mana yang harus dibuat (sejauh ini kami hanya memiliki satu musuh, tetapi di masa mendatang kami dapat menambahkan lebih banyak). Nah, maka semuanya sederhana - menggunakan TimeUntilNextSpawn kita menghitung berapa banyak waktu yang tersisa sampai penampilan musuh berikutnya, dan segera setelah waktunya tiba, kita menambahkan zombie baru ke TKP menggunakan metode Instantiate standar. Oh ya, dalam metode Mulai, kami menetapkan nilai acak ke bidang TimeUntilNextSpawn, sehingga jika kami memiliki beberapa petelur di level dengan penundaan yang sama, mereka tidak menambahkan zombie pada waktu yang sama.

Masih ada satu pertanyaan - bagaimana cara menanyakan musuh di bidang Musuh? Untuk melakukan ini, kita akan menggunakan alat Unity seperti "Rak itan". Bahkan, cetakan adalah bagian dari adegan yang disimpan dalam file terpisah. Kemudian kita dapat menyisipkan file ini ke adegan lain (atau ke dalam adegan yang sama) dan kita tidak perlu mengumpulkannya lagi setiap kali. Sebagai contoh, kami mengumpulkan, dari benda-benda dinding, lantai, langit-langit, jendela dan pintu, rumah yang indah dan menyimpannya sebagai cetakan. Sekarang Anda dapat memasukkan rumah ini ke kartu lain dengan menggerakkan pergelangan tangan. Pada saat yang sama, jika Anda mengedit file cetakan (misalnya, tambahkan pintu belakang ke rumah), maka objek akan berubah di semua adegan. Terkadang sangat nyaman. Kami juga dapat menggunakan cetakan sebagai templat untuk Instantiate - dan kami akan menggunakan kesempatan ini sekarang.

Untuk membuat cetakan, cukup seret objek game dari jendela hierarki ke jendela proyek, Unity akan melakukan sisanya. Mari kita membuat cetakan dari zombie, dan kemudian menambahkan spawner musuh ke TKP:



Saya menambahkan tiga spawner dalam proyek untuk perubahan (jadi, pada akhirnya, saya memiliki 4 dari mereka). Jadi, apa yang terjadi:



Ini! Itu sudah terlihat seperti kiamat zombie!

Kesimpulan


Tentu saja, ini masih jauh dari permainan yang lengkap. Kami tidak mempertimbangkan banyak masalah, seperti membuat antarmuka pengguna, suara, kehidupan, dan kematian pemain - semua ini tidak termasuk dalam ruang lingkup artikel ini. Tetapi bagi saya tampaknya artikel ini akan menjadi pengantar yang layak bagi Unity bagi mereka yang tidak terbiasa dengan alat ini. Atau mungkin seseorang yang berpengalaman akan dapat mengambil beberapa trik darinya?

Secara umum, teman-teman, saya harap Anda menikmati artikel saya. Tulis pertanyaan Anda di komentar, saya akan coba jawab. Kode sumber proyek dapat diunduh di github: https://github.com/zapolnov/otus_zombies . Anda memerlukan Unity 2019.3.0f3 atau lebih tinggi, dapat diunduh sepenuhnya gratis dan tanpa SMS dari situs web resmi: https://store.unity.com/download .

Tautan ke aset yang digunakan dalam artikel:

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


All Articles