Kebetulan saya sering membuat prototipe (baik untuk pekerjaan maupun proyek pribadi)
dan saya ingin berbagi pengalaman. Saya ingin menulis artikel yang akan menarik untuk dibaca sendiri, dan pertama-tama saya bertanya-tanya apa yang dipandu oleh seseorang ketika dia membuat keputusan ini atau itu dalam pelaksanaan proyek, bagaimana dia memulai proyek, sering kali paling sulit untuk memulai.
Contoh baru yang ingin saya pertimbangkan bersama Anda adalah konsep teka-teki berbasis fisika kasual.
Kadang-kadang saya akan menyebutkan hal-hal yang sangat jelas untuk memperluas topik untuk pemula.Ide terlihat seperti ini
Bagian 2Bagian 3Kami mengatur berbagai pengubah di lapangan bermain, yang mengubah arah roket, menarik, mempercepat, mengusir, dan sebagainya. Tugasnya adalah untuk membuka jalan di antara bintang-bintang ke planet berikutnya yang kita butuhkan. Menang dianggap setelah mendarat / menyentuh jejak planet ini. Lapangan bermain vertikal, beberapa layar naik, ada asumsi bahwa lintasan dapat menempati lantai layar kiri / kanan. Kalah - jika Anda melewatkan planet tujuan, bertabrakan dengan planet lain, terbang jauh di luar zona.
Dan saya harap kita semua mengerti bahwa sebelum memasuki pengembangan dan mengimplementasikan rencana kami, pertama-tama kita harus membuat prototipe - produk yang sangat kasar dan cepat yang akan memungkinkan Anda untuk dengan cepat mencoba mekanika dasar dan mendapatkan gagasan tentang gameplay. Untuk apa prototipe dibuat? Proses permainan dalam kepala kita dan dalam praktiknya adalah hal-hal yang sangat berbeda, yang tampaknya keren bagi kita, bagi orang lain itu akan menjadi horor, ditambah sering kali ada momen kontroversial dalam proyek - manajemen, aturan permainan, dinamika permainan, dll. dan sebagainya. Akan sangat bodoh untuk melewati tahap prototipe, bekerja melalui arsitektur, memberikan grafik, menyesuaikan level, dan akhirnya mengetahui bahwa permainan itu omong kosong. Dari kehidupan - dalam satu permainan ada mini-game, sekitar 10 buah, setelah membuat prototipe ternyata mereka sangat membosankan, setengah dibuang, setengah lagi diulang.
Kiat - selain mekanika dasar, pisahkan tempat kontroversial, tuliskan harapan spesifik dari prototipe. Ini dilakukan untuk memuat ulang pada saat-saat sulit. Sebagai contoh, salah satu tugas yang dihadapi prototipe ini adalah bagaimana nyaman dan dapat dimengerti bahwa lapangan bermain terdiri dari beberapa layar dan mereka harus terluka. Itu perlu untuk mengimplementasikan rencana A - svayp. Plan B - kemampuan untuk memperbesar lapangan bermain (jika perlu). Ada juga beberapa opsi untuk pengubah. Gagasan pertama terlihat di tangkapan layar - kami memaparkan pengubah dan arah pengaruhnya. Akibatnya, pengubah diganti hanya dengan bola yang mengubah arah roket ketika mereka menyentuh bola. Kami memutuskan bahwa ini akan lebih santai, tanpa lintasan, dll.
Fungsi umum yang kami terapkan:- Anda dapat mengatur lintasan awal roket, dengan tingkat penyimpangan dari tegak lurus terbatas (roket tidak dapat diputar ke samping lebih dari satu derajat)
- Seharusnya ada tombol start, di mana kita mengirim roket di jalan
- Menggulir layar saat menempatkan pengubah (untuk memulai)
- Gerakan kamera di belakang pemain (setelah start)
- Panel antarmuka dengan mana pengubah drag dan drop akan diimplementasikan di lapangan
- Prototipe harus memiliki dua pengubah - tolakan dan akselerasi
- Harus ada planet ketika Anda menyentuh yang Anda mati
- Pasti ada planet saat Anda menyentuh dan menang
Arsitektur
Poin yang sangat penting, dalam pengembangan besar secara umum diterima bahwa prototipe ditulis mengerikan karena akan lebih cepat, maka proyek tersebut ditulis dari awal. Dalam kenyataan pahit banyak proyek, kaki tumbuh dari prototipe, yang buruk untuk pengembangan besar - arsitektur kurva, kode warisan, utang teknis, waktu ekstra untuk refactoring. Nah, dan perkembangan indie secara keseluruhan lancar mengalir dari prototipe ke beta. Mengapa saya? Sangat penting untuk meletakkan arsitektur bahkan menjadi prototipe, bahkan jika itu primitif, sehingga Anda tidak menangis nanti, atau memerah di depan rekan kerja.
Sebelum memulai, kami selalu mengulangi - SOLID, KISS, DRY, YAGNI. Bahkan programmer berpengalaman melupakan Kiss dan Yagni).
Arsitektur dasar apa yang saya gunakan?Ada gameobject GameController kosong di tempat kejadian dengan tag yang sesuai, komponen / mono-sepeda tergantung di atasnya, lebih baik untuk membuatnya menjadi cetakan, kemudian tambahkan komponen yang diperlukan ke cetakan:
- GameController - (bertanggung jawab untuk keadaan permainan, langsung ke logika (menang, kalah, berapa banyak nyawa, dll)
- InputController - semua yang berhubungan dengan manajemen pemain, pelacakan tachi, klik, yang mengklik, mengontrol status, dll.
- TransformManager - dalam gim Anda seringkali perlu tahu siapa berada di mana, berbagai data terkait dengan posisi pemain / musuh. Misalnya, jika kita terbang melewati planet, maka pemain dikalahkan, pengontrol permainan bertanggung jawab untuk ini, tetapi dia harus tahu posisi pemain dari mana. Manajer transformasi adalah esensi yang tahu tentang berbagai hal
- AudioController - ini jelas, ini tentang suara
- InterfacesController - dan ini jelas, ini tentang UI
Gambaran keseluruhan muncul - untuk setiap tugas yang dapat dimengerti, Anda memiliki controller / entitas Anda sendiri yang memecahkan masalah ini, ini akan membantu untuk menghindari objek godlayk, ini memberi Anda gambaran di mana harus menggali, dari pengontrol yang kami kirim data, kami selalu dapat mengubah implementasi menerima data. Bidang publik tidak diizinkan, kami memberikan data hanya melalui properti / metode publik. Kami menghitung / mengubah data secara lokal.
Kadang-kadang terjadi bahwa GameController meningkat, karena berbagai logika dan perhitungan tertentu. Jika kita perlu memproses data - untuk ini lebih baik membuat kelas GameControllerModel yang terpisah dan melakukannya di sana.
Dan kode dimulai
Kelas dasar untuk roketusing GlobalEventAggregator; using UnityEngine; using UnityEngine.Assertions; namespace PlayerRocket { public enum RocketState { WAITFORSTART = 0, MOVE = 1, STOP = 2, COMPLETESTOP = 3, } [RequireComponent(typeof(Rigidbody))] public abstract class PlayerRocketBase : MonoBehaviour, IUseForces, IPlayerHelper { [SerializeField] protected RocketConfig config; protected Rigidbody rigidbody; protected InputController inputController; protected RocketHolder rocketHolder; protected RocketState rocketState; public Transform Transform => transform; public Rigidbody RigidbodyForForce => rigidbody; RocketState IPlayerHelper.RocketState => rocketState; protected ForceModel<IUseForces> forceModel; protected virtual void Awake() { Injections(); EventAggregator.AddListener<ButtonStartPressed>(this, StartEventReact); EventAggregator.AddListener<EndGameEvent>(this, EndGameReact); EventAggregator.AddListener<CollideWithPlanetEvent>(this, DestroyRocket); rigidbody = GetComponent<Rigidbody>(); Assert.IsNotNull(rigidbody, " " + gameObject.name); forceModel = new ForceModel<IUseForces>(this); } protected virtual void Start() { Injections(); } private void DestroyRocket(CollideWithPlanetEvent obj) { Destroy(gameObject); } private void EndGameReact(EndGameEvent obj) { Debug.Log(" "); rocketState = RocketState.STOP; } private void Injections() { EventAggregator.Invoke(new InjectEvent<InputController> { inject = (InputController obj) => inputController = obj}); EventAggregator.Invoke(new InjectEvent<RocketHolder> { inject = (RocketHolder holder) => rocketHolder = holder }); } protected abstract void StartEventReact(ButtonStartPressed buttonStartPressed); } public interface IPlayerHelper { Transform Transform { get; } RocketState RocketState { get; } } }
Mari kita pergi ke kelas:
[RequireComponent(typeof(Rigidbody))] public abstract class PlayerRocketBase : MonoBehaviour, IUseForces, IPlayerHelper
Pertama, mengapa kelas itu abstrak? Kita tidak tahu jenis roket apa yang akan kita miliki, bagaimana mereka akan bergerak, bagaimana mereka akan dianimasikan, fitur gameplay apa yang akan tersedia (misalnya, kemungkinan roket memantul ke samping). Oleh karena itu, kami membuat abstrak kelas dasar, meletakkan data standar di sana, dan meletakkan metode abstrak, yang implementasinya untuk rudal tertentu dapat bervariasi.
Dapat juga dilihat bahwa kelas sudah memiliki implementasi antarmuka dan atribut yang tergantung pada gameplay komponen yang diinginkan tanpa yang roket bukan roket.
[SerializeField] protected RocketConfig config;
Atribut ini memberi tahu kita bahwa inspektur memiliki bidang serializable tempat objek dijejalkan ke dalam, dalam sebagian besar pelajaran, termasuk Persatuan, bidang ini dibuat untuk publik, jika Anda adalah pengembang indie, jangan lakukan itu. Gunakan bidang pribadi dan atribut ini. Di sini saya ingin membahas sedikit tentang apa kelas ini dan apa fungsinya.
Konfigurasi roket using UnityEngine; namespace PlayerRocket { [CreateAssetMenu(fileName = "RocketConfig", menuName = "Configs/RocketConfigs", order = 1)] public class RocketConfig : ScriptableObject { [SerializeField] private float speed; [SerializeField] private float fuel; public float Speed => speed; public float Fuel => fuel; } }
Ini adalah ScriptableObject yang menyimpan pengaturan roket. Ini membutuhkan kumpulan data yang dibutuhkan oleh perancang game - di luar kelas. Dengan demikian, desainer game tidak perlu dipusingkan dan mengatur proyek game tertentu dengan roket tertentu, mereka hanya dapat memperbaiki konfigurasi ini, yang disimpan dalam aset / file terpisah. Mereka dapat mengkonfigurasi konfigurasi runtime dan itu akan disimpan, itu juga mungkin jika akan mungkin untuk membeli skin yang berbeda untuk roket, dan parameternya sama - konfigurasi hanya mengembara ke mana Anda perlu. Pendekatan ini berkembang dengan baik - Anda dapat menambahkan data apa pun, menulis editor khusus, dll.
protected ForceModel<IUseForces> forceModel;
Saya juga ingin memikirkan hal ini, ini adalah kelas generik untuk menerapkan pengubah ke suatu objek.
Forcemodel using System.Collections.Generic; using System.Linq; using UnityEngine; public enum TypeOfForce { Push = 0, AddSpeed = 1, } public class ForceModel<T> where T : IUseForces { readonly private T forceUser; private List<SpaceForces> forces = new List<SpaceForces>(); protected bool IsHaveAdditionalForces; public ForceModel(T user) { GlobalEventAggregator.EventAggregator.AddListener<SpaceForces>(this, ChangeModificatorsList); forceUser = user; } private void ChangeModificatorsList(SpaceForces obj) { if (obj.IsAdded) forces.Add(obj); else forces.Remove(forces.FirstOrDefault(x => x.CenterOfObject == obj.CenterOfObject)); if (forces.Count > 0) IsHaveAdditionalForces = true; else IsHaveAdditionalForces = false; } public void AddModificator() { if (!IsHaveAdditionalForces) return; foreach (var f in forces) { switch (f.TypeOfForce) { case TypeOfForce.Push: AddDirectionForce(f); break; case TypeOfForce.AddSpeed: forceUser.RigidbodyForForce.AddRelativeForce(Vector3.up*f.Force); break; } } } private void AddDirectionForce(SpaceForces spaceForces) {
Inilah yang saya tulis di atas - jika Anda perlu melakukan semacam logika komputasi / kompleks, letakkan di kelas yang terpisah. Ada logika yang sangat sederhana - ada daftar kekuatan yang berlaku untuk roket. Kami mengulangi daftar, melihat kekuatan apa itu dan menerapkan metode tertentu. Daftar ini diperbarui oleh peristiwa, peristiwa terjadi ketika memasuki / keluar di bidang pengubah. Sistem ini cukup fleksibel, pertama-tama ia bekerja dengan antarmuka (hi enkapsulasi), pengguna pengubah tidak hanya roket / pemain. Kedua, generik - Anda dapat memperluas IUseForces dengan keturunan yang berbeda untuk kebutuhan / percobaan, dan masih menggunakan kelas / model ini.
Cukup untuk bagian pertama. Pada bagian kedua, kita akan mempertimbangkan sistem peristiwa, injeksi ketergantungan, pengontrol input, kelas roket itu sendiri dan mencoba untuk meluncurkannya.