Apa (tidak) yang perlu Anda ketahui untuk membuat game di Unity



Unity adalah mesin permainan dengan ambang entri jauh dari nol (dibandingkan dengan Game Maker Studio yang sama), dan dalam artikel ini saya akan memberi tahu Anda masalah apa yang saya temui ketika mulai mempelajarinya dan solusi apa yang saya temukan. Saya akan menggambarkan momen seperti itu dengan contoh permainan puzzle 2d saya untuk Android (yang saya harap akan segera dirilis di Play Market).

Saya tidak berpura-pura benar, dan saya tidak mendesak Anda untuk mengulangi setelah diri Anda sendiri, jika Anda tahu cara terbaik, saya hanya menunjukkan kepada Anda bagaimana melakukannya sendiri, dan mungkin seseorang yang baru mulai mengenal Unity akan menciptakan maha karya indie gamedev-nya dengan sedikit tenaga.

Saya seorang insinyur desain pembangkit listrik, tetapi saya selalu tertarik pada coding, dan saya akrab dengan beberapa bahasa pemrograman. Karena itu, kami setuju untuk membuat game di Unity:
  • Anda perlu tahu sedikit C # atau JavaScript (setidaknya sintaks berbentuk C).

Semua yang akan ditulis di bawah ini bukan tutorial Unity, yang sudah cukup banyak dibiakkan di jaringan tanpa saya. Di bawah ini akan dikumpulkan saat-saat sulit yang mungkin terjadi saat membuat proyek pertama Anda di Unity.

Penting untuk memperingatkan bahwa skrip yang disediakan menghilangkan sebagian besar logika permainan (mewakili "rahasia dagang"), tetapi kinerjanya sebagai contoh telah diverifikasi.

Masalah Satu - Orientasi



Kunci Orientasi
Kesulitan pertama yang muncul dalam diri saya adalah bahwa saya tidak memperhatikan mengoptimalkan antarmuka visual untuk orientasi layar. Solusinya adalah yang paling sederhana - jika Anda tidak perlu mengubah orientasi layar untuk gameplay, lebih baik untuk memblokirnya. Tidak perlu fleksibilitas yang berlebihan, Anda menulis game indie, bukan proyek di sisi lain dari satu juta dolar. Mengapa banyak transisi kondisional dan perubahan jangkar jika permainan terlihat lebih baik di Portrait (misalnya). Anda dapat mengunci orientasi layar di sini:
Edit> Pengaturan Proyek> Player


Izin yang berbeda
Penting juga untuk menguji antarmuka visual pada resolusi berbeda dalam orientasi yang dipilih, dan saat pengujian, jangan lupa tentang keberadaan perangkat dengan proporsi 4: 3 (well, atau 3: 4), sehingga kita dapat menambahkan 768x1024 (atau 1024x768) dengan aman.

Posisi yang lebih baik
Untuk menyesuaikan posisi dan skala objek game, lebih baik menggunakan Rect Transform.


Masalah Dua - KOMUNIKASI


Saya memiliki masalah yang sama karena fakta bahwa saya membuat kenalan pertama dengan game dev melalui Game Maker Studio, di mana skrip adalah bagian penuh dari objek game, dan segera memiliki akses penuh ke semua komponen objek. Unity memiliki skrip yang sama, dan hanya instance yang ditambahkan ke objek. Berbicara secara simplistik-kiasan, skrip tidak tahu secara langsung objek mana yang sedang dieksekusi. Oleh karena itu, ketika menulis skrip, Anda harus memperhitungkan inisialisasi antarmuka untuk bekerja dengan komponen suatu objek atau dengan komponen objek lainnya.

Kami melatih kucing
Di gim saya ada objek GameField, di atas panggung hanya ada satu instance, ada juga skrip dengan nama yang sama. Objek bertanggung jawab untuk menampilkan skor game dan mereproduksi seluruh suara game, jadi menurut saya itu lebih ekonomis untuk memori (secara umum, game hanya memiliki tiga Sumber Audio - satu Background Music, dua Efek Suara lainnya). Script menyelesaikan masalah menyimpan akun game, memilih AudioClip untuk memainkan suara, dan untuk beberapa logika game.

Mari kita membahas bunyi lebih rinci, karena contoh ini dengan mudah menunjukkan interaksi naskah dengan komponen-komponen objek.

Tentu saja, objek harus memiliki skrip GameField.cs itu sendiri dan komponen AudioSource, dalam kasus saya dua keseluruhan (nanti akan menjadi jelas mengapa).

Seperti yang disebutkan sebelumnya, skrip adalah "tidak tahu" bahwa objek memiliki komponen AudioSource, oleh karena itu kami mendeklarasikan dan menginisialisasi antarmuka (untuk saat ini, kami menganggap bahwa hanya ada satu AudioSource):
private AudioSource Sound; void Start(){ Sound = GetComponent<AudioSource> (); } 

Metode GetComponent <component_type> () akan mengembalikan komponen pertama dari tipe yang ditentukan dari objek.

Selain AudioSource, Anda perlu beberapa AudioClip:
 [Header ("Audio clips")] [SerializeField] private AudioClip OnStart; [SerializeField] private AudioClip OnEfScore; [SerializeField] private AudioClip OnHighScore; [SerializeField] private AudioClip OnMainTimer; [SerializeField] private AudioClip OnBubbMarker; [SerializeField] private AudioClip OnScoreUp; 

Selanjutnya, perintah dalam tanda kurung diperlukan untuk Inspektur`a, lebih detail di sini .



Sekarang skrip di Inspektur memiliki bidang baru tempat kami menyeret bunyi yang diperlukan.


Selanjutnya, buat metode SoundPlay dalam skrip yang menggunakan AudioClip:
 public void PlaySound(AudioClip Clip = null){ Sound.clip = Clip; Sound.Play (); } 

Untuk memutar suara dalam permainan, kami memanggil metode ini dengan waktu yang tepat dengan klipnya.

Ada satu minus yang signifikan dari pendekatan ini, hanya satu suara yang dapat dimainkan pada satu waktu, tetapi selama permainan mungkin diperlukan untuk memainkan dua atau lebih suara, dengan pengecualian musik latar diputar terus-menerus.

Untuk mencegah hiruk-pikuk, saya sarankan menghindari kemungkinan pemutaran simultan lebih dari 4-5 suara (lebih disukai maksimal 2-3), maksud saya suara pendek-urutan pertama permainan (melompat, koin, tembakan pemain ...), untuk kebisingan latar belakang lebih baik untuk membuat sumber Anda sendiri terdengar pada objek yang membuat suara ini (jika Anda membutuhkan suara 2d-3d) atau satu objek yang bertanggung jawab untuk semua kebisingan latar belakang (jika "volume" tidak diperlukan).

Dalam gim saya, tidak perlu memainkan lebih dari dua AudioClips secara bersamaan. Untuk menjamin pemutaran kedua suara hipotetis, saya menambahkan dua AudioSource ke objek GameField. Untuk menentukan komponen dalam skrip, kami menggunakan metode
 GetComponents<_>() 

yang mengembalikan array semua komponen dari tipe yang ditentukan dari objek.

Kode akan terlihat seperti ini:
 private AudioSource[] Sound; //    void Start(){ Sound = GetComponents<AudioSource> (); //  GetComponents } 

Sebagian besar perubahan akan memengaruhi metode PlaySound. Saya melihat dua versi dari metode ini: "universal" (untuk sejumlah AudioSource dalam suatu objek) dan "canggung" (untuk 2-3 AudioSource, bukan yang paling elegan tetapi kurang intensif sumber daya).

Opsi "canggung" untuk dua AudioSource (saya menggunakannya)
 private void PlaySound(AudioClip Clip = null){ if (!Sound [0].isPlaying) { Sound [0].clip = Clip; Sound [0].Play (); } else { Sound [1].clip = Clip; Sound [1].Play (); } } 

Anda dapat meregangkan menjadi tiga atau lebih AudioSource, tetapi jumlah kondisi akan melahap semua penghematan kinerja.

Opsi "Universal"
 private void PlaySound(AudioClip Clip = null){ foreach (AudioSource _Sound in Sound) { if (!_Sound.isPlaying) { _Sound.clip = Clip; _Sound.Play (); break; } } } 


Akses ke komponen asing
Di lapangan bermain ada beberapa contoh prefab Fishka, seperti chip game. Dibangun seperti ini:
  • Objek induk dengan SpriteRenderer-nya;
    • Objek anak dengan SpriteRenderer mereka.

Objek anak bertanggung jawab untuk menggambar tubuh chip, warnanya, elemen-elemen tambahan yang bisa berubah. Induk menggambar batas penanda di sekitar chip (chip yang aktif harus disorot dalam permainan). Script hanya pada objek induk. Jadi, untuk mengelola sprite anak, skrip induk harus menentukan sprite ini. Saya mengaturnya seperti ini - dalam skrip saya membuat antarmuka untuk mengakses anak-anak SpriteRenderer:
 [Header ("Graphic objects")] public SpriteRenderer Marker; [SerializeField] private SpriteRenderer Base; [Space] [SerializeField] private SpriteRenderer Center_Red; [SerializeField] private SpriteRenderer Center_Green; [SerializeField] private SpriteRenderer Center_Blue; 

Sekarang skrip di Inspektur memiliki bidang tambahan:


Menyeret dan menjatuhkan anak-anak ke bidang yang sesuai memberi kami akses ke mereka dalam skrip.

Contoh penggunaan:
 void OnMouseDown(){ //        Marker.enabled = !Marker.enabled; } 


Memanggil skrip orang lain
Selain memanipulasi komponen asing, Anda juga dapat mengakses skrip objek pihak ketiga, bekerja dengan variabel Publik, metode, subkelasnya.

Saya akan memberikan contoh pada objek GameField yang sudah terkenal.

Skrip GameField memiliki metode publik FishkiMarkerDisabled (), yang diperlukan untuk "menghapus" penanda dari semua chip di lapangan dan digunakan dalam proses pengaturan penanda saat mengklik pada chip, karena hanya ada satu yang aktif.

Dalam skrip Fishka.cs, ​​SpriteRenderer Marker bersifat publik, artinya dapat diakses dari skrip lain. Untuk melakukan ini, tambahkan deklarasi dan inisialisasi antarmuka untuk semua instance kelas Fishka dalam skrip GameField.cs (saat membuat skrip, kelas dengan nama yang sama dibuat di dalamnya) mirip dengan yang dilakukan untuk beberapa AudioSource:
 private Fishka[] Fishki; void Start(){ Fishki = GameObject.FindObjectsOfType (typeof(Fishka)) as Fishka[]; } public void FishkiMarkerDisabled(){ foreach (Fishka _Fishka in Fishki) { _Fishka .Marker.enabled = false; } } 

Dalam skrip Fishka.cs, ​​tambahkan deklarasi dan inisialisasi antarmuka instance kelas GameField, dan ketika kita mengklik objek, kita akan memanggil metode FishkiMarkerDisabled () dari kelas ini:
 private GameField gf; void Start(){ gf = GameObject.FindObjectOfType (typeof(GameField)) as GameField; } void OnMouseDown(){ gf.FishkiMarkerDisabled(); Marker.enabled = !Marker.enabled; } 

Dengan demikian, Anda dapat berinteraksi antara skrip (atau lebih tepatnya kelas) dari objek yang berbeda.


Masalah Tiga - KEEPERS


Penjaga akun
Begitu sesuatu seperti akun muncul di permainan, masalah langsungnya adalah penyimpanannya, baik selama pertandingan dan di luarnya, saya juga ingin menyimpan catatan untuk mendorong pemain untuk mengungguli itu.

Saya tidak akan mempertimbangkan opsi ketika seluruh game (menu, game, penarikan akun) dibangun dalam satu adegan, karena, pertama, ini bukan cara terbaik untuk membangun proyek pertama, dan kedua, menurut saya, adegan pemuatan awal harus . Karena itu, kami sepakat bahwa ada empat adegan dalam proyek:
  1. loader - adegan di mana objek musik latar diinisialisasi (akan lebih banyak nanti), dan memuat pengaturan dari save;
  2. menu - adegan dengan menu;
  3. game - game scene;
  4. score - tempat skor, catatan, papan peringkat.


Catatan: Urutan pemuatan adegan diatur dalam File> Pengaturan Bangun.

Poin yang diakumulasikan selama pertandingan disimpan dalam variabel Skor dari kelas GameField. Untuk memiliki akses ke data saat pergi ke adegan skor, buat kelas Skor publik statis, di mana kami mendeklarasikan variabel untuk menyimpan nilai dan properti untuk mendapatkan dan mengatur nilai variabel ini (metode ini dimata - matai oleh apocatastas ):
 using UnityEngine; public static class ScoreHolder{ private static int _Score = 0; public static int Score { get{ return _Score; } set{ _Score = value; } } } 

Kelas statis publik tidak perlu ditambahkan ke objek apa pun, itu segera tersedia di setiap adegan dari skrip apa pun.

Contoh penggunaan di kelas GameField dalam skor metode transisi adegan:
 using UnityEngine.SceneManagement; public class GameField : MonoBehaviour { private int Score = 0; //     ,         Scores void GotoScores(){ ScoreHolder.Score = Score; //   ScoreHolder.Score   SceneManager.LoadScene (“scores”); } } 

Dengan cara yang sama, Anda dapat menambahkan akun rekaman ke ScoreHolder selama pertandingan, tetapi itu tidak akan disimpan saat keluar.

Penjaga pengaturan
Pertimbangkan contoh menyimpan nilai variabel Boolean SoundEffectsMute, tergantung pada keadaan di mana permainan memiliki efek suara atau tidak.

Variabel itu sendiri disimpan di publik Pengaturan statis kelas:
 using UnityEngine; public static class SettingsHolder{ private static bool _SoundEffectsMute = false; public static bool SoundEffectsMute{ get{ return _SoundEffectsMute; } set{ _SoundEffectsMute = value; } } } 

Kelas ini mirip dengan ScoreHolder, Anda bahkan bisa menggabungkannya menjadi satu, tetapi menurut saya ini adalah perilaku buruk.

Seperti yang dapat Anda lihat dari skrip, secara default _SoundEffectsMute dinyatakan salah, sehingga setiap kali game dimulai, SettingsHolder.SoundEffectsMute akan mengembalikan false terlepas dari apakah pengguna telah mengubahnya sebelum atau tidak (itu diubah menggunakan tombol pada adegan menu).

Menyimpan Variabel
Yang paling optimal untuk aplikasi Android adalah menggunakan metode PlayerPrefs.SetInt untuk menyimpan (untuk lebih jelasnya lihat dokumentasi resmi ). Ada dua opsi untuk menjaga nilai SettingsHolder.SoundEffectsMute di PlayerPrefs, sebut saja "sederhana" dan "elegan".

Cara "sederhana" (untuk saya seperti itu) adalah pada metode OnMouseDown () dari kelas tombol yang disebutkan di atas. Nilai yang disimpan dimuat di kelas yang sama tetapi dalam metode Mulai ():
 using UnityEngine; public class ButtonSoundMute : MonoBehaviour { void Start(){ //    ,  PlayerPrefs    bool switch (PlayerPrefs.GetInt ("SoundEffectsMute")) { case 0: SettingsHolder.SoundEffectsMute = false; break; case 1: SettingsHolder.SoundEffectsMute = true; break; default: //    default SettingsHolder.SoundEffectsMute = true; break; } } void OnMouseDown(){ SettingsHolder.SoundEffectsMute = !SettingsHolder.SoundEffectsMute; //    ,  PlayerPrefs    bool if (SettingsHolder.SoundEffectsMute) PlayerPrefs.SetInt ("SoundEffectsMute", 1); else PlayerPrefs.SetInt ("SoundEffectsMute", 0); } } 


Metode "elegan", menurut saya, bukan yang paling benar, karena mempersulit pemeliharaan kode, tetapi ada sesuatu di dalamnya, dan saya tidak dapat membantu tetapi membagikannya. Fitur dari metode ini adalah bahwa penyetel properti SettingsHolder.SoundEffectsMute dipanggil pada waktu yang tidak memerlukan kinerja tinggi, dan dapat dimuat (oh, horor) menggunakan PlayerPrefs (baca-tulis ke file). UbahPengaturan kelas statis publik:

 using UnityEngine; public static class SettingsHolder { private static bool _SoundEffectsMute = false; public static bool SoundEffectsMute{ get{ return _SoundEffectsMute; } set{ _SoundEffectsMute = value; if (_SoundEffectsMute) PlayerPrefs.SetInt ("SoundEffectsMute", 1); else PlayerPrefs.SetInt ("SoundEffectsMute", 0); } } } 

Metode OnMouseDown dari kelas ButtonSoundMute akan menyederhanakan untuk:
 void OnMouseDown(){ SettingsHolder.SoundEffectsMute = !SettingsHolder.SoundEffectsMute; } 


Tidak layak memuat pengambil dengan membaca dari file, karena terlibat dalam proses kritis kinerja - dalam metode PlaySound () dari kelas GameField:
 private void PlaySound(AudioClip Clip = null){ if (!SettingsHolder.SoundEffectsMute) { //      “”  (. ) if (!Sound [0].isPlaying) { Sound [0].clip = Clip; Sound [0].Play (); } else { Sound [1].clip = Clip; Sound [1].Play (); } } } 


Dengan cara di atas, Anda dapat mengatur penyimpanan variabel apa pun dalam game.


Masalah Kelima - SATU UNTUK SEMUA


Musik ini akan abadi
Cepat atau lambat semua orang menghadapi masalah seperti itu, dan saya tidak terkecuali. Sesuai rencana, musik latar mulai diputar bahkan dalam adegan menu, dan jika tidak dimatikan, ia memainkan menu, permainan, dan skor pada adegan tanpa gangguan. Tetapi jika objek "memainkan" musik latar diinstal pada adegan menu, ketika Anda pergi ke adegan game, itu hancur dan suara menghilang, dan jika Anda meletakkan objek yang sama pada adegan game, maka setelah transisi musik diputar terlebih dahulu. Solusinya ternyata adalah penggunaan metode DontDestroyOnLoad (Target objek) yang ditempatkan dalam metode Mulai () dari kelas yang skripnya memiliki objek "musik". Untuk melakukan ini, buat skrip DontDestroyThis.cs:
 using UnityEngine; public class DontDestroyThis: MonoBehaviour { void Start(){ DontDestroyOnLoad(this.gameObject); } } 

Agar semuanya berfungsi, objek "musikal" harus root (pada tingkat hierarki yang sama dengan kamera utama).

Mengapa musik latar di loader
Tangkapan layar menunjukkan bahwa objek "musikal" terletak bukan pada adegan menu tetapi pada adegan loader. Ini adalah ukuran yang disebabkan oleh kenyataan bahwa adegan menu dapat dimuat lebih dari sekali (setelah adegan skor, transisi ke adegan menu), dan setiap kali dimuat, objek "musikal" lain akan dibuat, dan yang lama tidak akan dihapus. Ini dapat dilakukan seperti pada contoh dokumentasi resmi , tetapi saya memutuskan untuk mengambil keuntungan dari kenyataan bahwa adegan loader dijamin untuk memuat hanya sekali.

Mengenai hal ini, masalah utama yang saya temui ketika mengembangkan game pertama saya di Unity, sebelum mengunggah ke Play Market (saya belum mendaftarkan akun pengembang), berakhir dengan sukses.

PS
Jika informasi itu berguna, Anda dapat mendukung penulis, dan dia akhirnya akan mendaftarkan akun pengembang Android.

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


All Articles