
Saya ingin berbagi proses pengembangan game seluler sederhana oleh dua pengembang dan seorang artis. Artikel ini sebagian besar merupakan deskripsi implementasi teknis.
Perhatian, banyak teks!
Artikel ini bukan panduan atau pelajaran, meskipun saya berharap pembaca dapat belajar sesuatu yang bermanfaat darinya. Dirancang untuk pengembang yang akrab dengan Unity dengan beberapa pengalaman pemrograman.
Konten:
IdeGameplayPlotPengembanganInti- Elemen listrik
- Solver
- ElementsProvider
- CircuitGenerator
Kelas game- Pendekatan Pembangunan dan DI
- Konfigurasi
- Elemen listrik
- Manajemen game
- Tingkat pemuatan
- Cutscenes
- Gameplay tambahan
- Monetisasi
- Antarmuka pengguna
- Analisis
- Positioning dan Diagram Kamera
- Skema warna
Ekstensi Editor
- Generator
- Solver
Berguna- Asserthelp
- SceneObjectsHelper
- Coroutinestarter
- Gizmo
PengujianRingkasan PengembanganIde
IsiAda ide untuk membuat game mobile sederhana dalam waktu singkat.
Ketentuan:
- Mudah untuk mengimplementasikan game
- Persyaratan Seni Minimum
- Waktu pengembangan singkat (beberapa bulan)
- Dengan otomatisasi pembuatan konten yang mudah (level, lokasi, elemen game)
- Buat level dengan cepat jika game terdiri dari sejumlah level terbatas
Untuk memutuskan, tetapi apa yang sebenarnya dilakukan? Lagipula, muncul ide untuk membuat game, bukan ide game. Diputuskan untuk mencari inspirasi dari app store.
Ke item di atas ditambahkan:
- Permainan harus memiliki popularitas tertentu di antara para pemain (jumlah unduhan + peringkat)
- Toko aplikasi tidak boleh ramai dengan game serupa
Sebuah game ditemukan dengan gameplay berdasarkan gerbang logis. Tidak ada yang serupa dalam jumlah besar. Permainan ini memiliki banyak unduhan dan peringkat positif. Meskipun demikian, setelah mencoba, ada beberapa kelemahan yang dapat diperhitungkan dalam gim Anda.
Gameplay dari gim ini adalah bahwa levelnya adalah sirkuit digital dengan banyak input dan output. Pemain harus memilih kombinasi input agar outputnya logis 1. Kedengarannya tidak terlalu sulit. Gim ini juga telah menghasilkan level secara otomatis, yang menunjukkan bahwa kemampuan untuk mengotomatiskan pembuatan level, meskipun tidak terdengar sangat sederhana. Permainan ini juga bagus untuk belajar, yang sangat saya sukai.
Pro:
- Kesederhanaan teknis gameplay
- Terlihat mudah untuk diuji dengan autotest
- Kemampuan untuk menghasilkan level secara otomatis
Cons:
- Anda harus terlebih dahulu membuat level
Sekarang jelajahi kekurangan dari game yang menginspirasi.
- Tidak disesuaikan dengan rasio aspek khusus, seperti 18: 9
- Tidak ada cara untuk melewati level yang sulit atau mendapatkan petunjuk
- Dalam ulasan ada keluhan tentang sejumlah kecil level
- Ulasan tersebut mengeluhkan kurangnya variasi elemen
Kami melanjutkan ke perencanaan permainan kami:
- Kami menggunakan gerbang logika standar (AND, NAND, OR, NOR, XOR, XNOR, NOR, NOT)
- Gates ditampilkan dengan gambar alih-alih penunjukan teks, yang lebih mudah untuk dibedakan. Karena elemen memiliki notasi ANSI standar, kami menggunakannya.
- Kami membuang sakelar yang menghubungkan satu input ke salah satu output. Karena kenyataan bahwa itu mengharuskan Anda untuk mengklik pada diri sendiri dan tidak cocok dengan elemen digital nyata sedikit. Ya, dan sulit membayangkan saklar sakelar dalam sebuah chip.
- Tambahkan elemen encoder dan decoder.
- Kami memperkenalkan mode di mana pemain harus memilih elemen yang diinginkan dalam sel dengan nilai-nilai tetap pada input sirkuit.
- Kami memberikan bantuan kepada pemain: tingkat petunjuk + lewati.
- Akan menyenangkan untuk menambahkan beberapa plot.
Gameplay
IsiMode 1: Pemain menerima sirkuit dan memiliki akses untuk mengubah nilai pada input.
Mode 2: Pemain menerima sirkuit di mana ia dapat mengubah elemen tetapi tidak dapat mengubah nilai pada input.
Gameplay akan dalam bentuk level yang disiapkan sebelumnya. Setelah menyelesaikan level, pemain harus mendapatkan semacam hasil, ini akan dilakukan dalam bentuk tiga bintang tradisional, tergantung pada hasil dari bagian itu.
Apa yang bisa menjadi indikator kinerja:
Jumlah tindakan: Setiap interaksi dengan elemen permainan meningkatkan penghitung.
Jumlah perbedaan dalam status yang dihasilkan dari aslinya. Tidak memperhitungkan berapa banyak upaya yang harus diselesaikan pemain. Sayangnya, itu tidak cocok dengan rezim kedua.
Akan menyenangkan untuk menambahkan mode yang sama dengan generasi tingkat acak. Tetapi untuk sekarang, tunda dulu.
Plot
IsiSambil memikirkan gameplay dan memulai pengembangan, berbagai ide muncul untuk meningkatkan permainan. Dan ide yang cukup menarik muncul - untuk menambahkan plot.
Ini tentang seorang insinyur yang merancang sirkuit. Tidak buruk, tetapi tidak lengkap. Mungkin perlu menampilkan pembuatan chip berdasarkan apa yang pemain lakukan? Entah bagaimana rutin, tidak ada hasil yang mudah dimengerti dan sederhana.
Idenya! Seorang insinyur mengembangkan robot keren menggunakan sirkuit logikanya. Robot adalah hal yang cukup mudah dimengerti dan sangat cocok dengan gameplay.
Ingat paragraf pertama, "Persyaratan minimum untuk seni"? Sesuatu tidak cocok dengan adegan cutscene dalam plot. Kemudian seorang seniman yang akrab datang ke penyelamatan, yang setuju untuk membantu kami.
Sekarang mari kita putuskan format dan integrasi cutscene ke dalam gim.
Plot harus ditampilkan sebagai cutscene tanpa skor atau deskripsi teks yang akan menghilangkan masalah lokalisasi, menyederhanakan pemahamannya, dan banyak bermain di perangkat seluler tanpa suara. Gim ini adalah elemen sirkuit digital yang sangat nyata, artinya sangat mungkin untuk menghubungkannya dengan kenyataan.
Adegan pemotongan dan level harus merupakan adegan yang terpisah. Sebelum tingkat tertentu, adegan tertentu dimuat.
Nah, tugas sudah diatur, ada sumber daya untuk dipenuhi, pekerjaan sudah mulai mendidih.
Pengembangan
IsiSaya segera memutuskan pada platform, ini adalah Unity. Ya sedikit berlebihan, tapi toh saya kenal dia.
Selama pengembangan, kode ditulis segera dengan tes atau bahkan setelahnya. Tetapi untuk narasi holistik, pengujian ditempatkan di bagian terpisah di bawah ini. Bagian saat ini akan menjelaskan proses pengembangan secara terpisah dari pengujian.
Inti
IsiInti dari gameplay terlihat cukup sederhana dan tidak terikat pada mesin, jadi kami mulai dengan desain dalam bentuk kode C #. Tampaknya Anda dapat memilih logika inti inti yang terpisah. Bawa ke proyek terpisah.
Unity bekerja dengan solusi C # dan proyek-proyek di dalamnya agak tidak biasa untuk pengembang .Net, .sln, dan .csproj biasa dihasilkan oleh Unity sendiri dan perubahan di dalam file-file ini tidak diterima untuk dipertimbangkan di sisi Unity. Dia hanya akan menimpa mereka dan menghapus semua perubahan. Untuk membuat proyek baru, Anda harus menggunakan file
Assembly Definition .


Unity sekarang menghasilkan proyek dengan nama yang sesuai. Segala sesuatu yang terletak di folder dengan file .asmdef akan terkait dengan proyek dan perakitan ini.
Elemen listrik
IsiTugasnya adalah untuk menjelaskan dalam kode interaksi elemen logis satu sama lain.
- Suatu elemen dapat memiliki banyak input dan banyak output.
- Input elemen harus terhubung ke output elemen lain
- Elemen itu sendiri harus mengandung logikanya sendiri.
Mari kita mulai.
- Elemen tersebut berisi logika operasinya sendiri dan tautan ke inputnya. Saat meminta nilai dari suatu elemen, ia mengambil nilai dari input, menerapkan logika padanya, dan mengembalikan hasilnya. Mungkin ada beberapa output, sehingga nilai untuk output spesifik diminta, secara default 0.
- Untuk mengambil nilai pada input, akan ada konektor input p, itu menyimpan tautan ke yang lain - konektor output.
- Konektor output merujuk ke elemen tertentu dan menyimpan tautan ke elemennya, saat meminta nilai, ia meminta dari elemen.

Panah menunjukkan arah data, ketergantungan elemen dalam arah yang berlawanan.
Tentukan antarmuka konektor. Anda bisa mendapatkan nilai dari itu.
public interface IConnector { bool Value { get; } }
Bagaimana cara menghubungkannya ke konektor lain?
Tentukan lebih banyak antarmuka.
public interface IInputConnector : IConnector { IOutputConnector ConnectedOtherConnector { get; set; } }
IInputConnector adalah konektor input, ia memiliki tautan ke konektor lain.
public interface IOutputConnector : IConnector { IElectricalElement Element { set; get; } }
Konektor output merujuk ke elemennya yang akan meminta nilai.
public interface IElectricalElement { bool GetValue(byte number = 0); }
Elemen listrik harus berisi metode yang mengembalikan nilai pada output tertentu, angka adalah jumlah output.
Saya menyebutnya IElectricalElement, meskipun hanya mentransmisikan level tegangan logis, tetapi di sisi lain itu bisa menjadi elemen yang tidak menambahkan logika sama sekali, hanya menyampaikan nilai, seperti konduktor.Sekarang mari kita beralih ke implementasi
public class InputConnector : IInputConnector { public IOutputConnector ConnectedOtherConnector { get; set; } public bool Value { get { return ConnectedOtherConnector?.Value ?? false; } } }
Konektor yang masuk mungkin tidak terhubung, dalam hal ini akan kembali salah.
public class OutputConnector : IOutputConnector { private readonly byte number; public OutputConnector(byte number = 0) { this.number = number; } public IElectricalElement Element { get; set; } public bool Value => Element.GetValue(number); } }
Output harus memiliki tautan ke elemen dan jumlahnya terkait dengan elemen.
Selanjutnya, menggunakan nomor ini, ia meminta nilai dari elemen.
public abstract class ElectricalElementBase { public IInputConnector[] Input { get; set; } }
Kelas dasar untuk semua elemen, hanya berisi array input.
Contoh implementasi suatu elemen:
public class And : ElectricalElementBase, IElectricalElement { public bool GetValue(byte number = 0) { bool outputValue = false; if (Input?.Length > 0) { outputValue = Input[0].Value; foreach (var item in Input) { outputValue &= item.Value; } } return outputValue; } }
Implementasinya didasarkan sepenuhnya pada operasi logis tanpa tabel kebenaran yang keras. Mungkin tidak sejelas dengan tabel, tetapi akan fleksibel, akan bekerja pada sejumlah input.
Semua gerbang logika memiliki satu output, sehingga nilai pada output tidak akan bergantung pada nomor input.
Elemen terbalik dibuat sebagai berikut:
public class Nand : And, IElectricalElement { public new bool GetValue(byte number = 0) { return !base.GetValue(number); } }
Perlu dicatat bahwa di sini metode GetValue ditimpa, dan tidak ditimpa secara virtual. Ini dilakukan berdasarkan pada logika bahwa jika Nand menyimpan ke Dan, ia akan terus berperilaku seperti Dan. Mungkin juga untuk menerapkan komposisi, tetapi ini membutuhkan kode tambahan, yang tidak masuk akal.
Selain katup konvensional, elemen-elemen berikut telah dibuat:
Sumber - sumber nilai konstan 0 atau 1.
Konduktor - sama saja Atau konduktor, hanya memiliki aplikasi yang sedikit berbeda, lihat generasi.
AlwaysFalse - selalu mengembalikan 0, diperlukan untuk mode kedua.
Solver
IsiSelanjutnya, kelas berguna untuk secara otomatis menemukan kombinasi yang memberikan 1 pada output rangkaian.
public interface ISolver { ICollection<bool[]> GetSolutions(IElectricalElement root, params Source[] sources); } public class Solver : ISolver { public ICollection<bool[]> GetSolutions(IElectricalElement root, params Source[] sources) {
Solusinya adalah kekerasan. Untuk ini, jumlah maksimum ditentukan yang dapat dinyatakan oleh satu set bit dalam jumlah yang sama dengan jumlah sumber. Yaitu, 4 sumber = 4 bit = jumlah maksimum 15. Kami menyortir semua angka dari 0 hingga 15.
ElementsProvider
IsiUntuk kenyamanan generasi, saya memutuskan untuk menentukan angka untuk setiap elemen.Untuk melakukan ini, saya membuat kelas ElementsProvider dengan antarmuka IElementsProvider.
public interface IElementsProvider { IList<Func<IElectricalElement>> Gates { get; } IList<Func<IElectricalElement>> Conductors { get; } IList<ElectricalElementType> GateTypes { get; } IList<ElectricalElementType> ConductorTypes { get; } } public class ElementsProvider : IElementsProvider { public IList<Func<IElectricalElement>> Gates { get; } = new List<Func<IElectricalElement>> { () => new And(), () => new Nand(), () => new Or(), () => new Nor(), () => new Xor(), () => new Xnor() }; public IList<Func<IElectricalElement>> Conductors { get; } = new List<Func<IElectricalElement>> { () => new Conductor(), () => new Not() }; public IList<ElectricalElementType> GateTypes { get; } = new List<ElectricalElementType> { ElectricalElementType.And, ElectricalElementType.Nand, ElectricalElementType.Or, ElectricalElementType.Nor, ElectricalElementType.Xor, ElectricalElementType.Xnor }; public IList<ElectricalElementType> ConductorTypes { get; } = new List<ElectricalElementType> { ElectricalElementType.Conductor, ElectricalElementType.Not }; }
Dua daftar pertama adalah sesuatu seperti pabrik yang memberikan item pada jumlah yang ditentukan. Dua daftar terakhir adalah penopang yang harus digunakan karena fitur Unity. Tentang itu lebih jauh.
CircuitGenerator
IsiSekarang bagian paling sulit dari pengembangan adalah pembuatan sirkuit.
Tugasnya adalah untuk menghasilkan daftar skema dari mana Anda kemudian dapat memilih yang Anda suka di editor. Pembangkitan hanya diperlukan untuk katup sederhana.
Parameter tertentu skema ditetapkan, ini adalah: jumlah lapisan (garis horizontal elemen) dan jumlah maksimum elemen dalam lapisan. Anda juga perlu menentukan gerbang mana yang Anda butuhkan untuk menghasilkan sirkuit.
Pendekatan saya adalah membagi tugas menjadi dua bagian - membuat struktur dan pemilihan opsi.
Generator struktur menentukan posisi dan koneksi elemen logika.
Generator varian memilih kombinasi elemen dalam posisi yang valid.
Structuregener
Struktur terdiri dari lapisan elemen logika dan lapisan konduktor / inverter. Seluruh struktur tidak mengandung elemen nyata tetapi wadah untuk mereka.
Wadah adalah kelas yang diwarisi dari IElectricalElement, yang di dalamnya berisi daftar elemen yang valid dan dapat beralih di antara mereka. Setiap item memiliki nomornya sendiri dalam daftar.
ElectricalElementContainer : ElectricalElementBase, IElectricalElement
Wadah dapat mengatur "dirinya sendiri" ke salah satu elemen dari daftar. Selama inisialisasi, Anda harus memberikan daftar delegasi yang akan membuat item. Di dalam, ia memanggil setiap delegasi dan mendapatkan item. Kemudian Anda dapat mengatur tipe spesifik elemen ini, ini menghubungkan elemen internal ke input yang sama seperti dalam wadah dan output dari wadah akan diambil dari output elemen ini.

Metode untuk mengatur daftar elemen:
public void SetElements(IList<Func<IElectricalElement>> elements) { Elements = new List<IElectricalElement>(elements.Count); foreach (var item in elements) { Elements.Add(item()); } }
Selanjutnya, Anda dapat mengatur jenisnya dengan cara ini:
public void SetType(int number) { if (isInitialized == false) { throw new InvalidOperationException(UnitializedElementsExceptionMessage); } SelectedType = number; RealElement = Elements[number]; ((ElectricalElementBase) RealElement).Input = Input; }
Setelah itu akan berfungsi sebagai item yang ditentukan.
Struktur berikut dibuat untuk sirkuit:
public class CircuitStructure : ICloneable { public IDictionary<int, ElectricalElementContainer[]> Gates; public IDictionary<int, ElectricalElementContainer[]> Conductors; public Source[] Sources; public And FinalDevice; }
Kamus di sini menyimpan nomor lapisan dalam kunci dan berbagai wadah untuk lapisan ini. Berikutnya adalah array sumber dan satu FinalDevice yang semuanya terhubung.
Dengan demikian generator struktural menciptakan wadah dan menghubungkannya satu sama lain. Ini semua dibuat berlapis-lapis, dari bawah ke atas. Bagian bawah adalah yang terluas (sebagian besar elemen). Lapisan di atas mengandung elemen dua kali lebih sedikit dan seterusnya sampai kita mencapai minimum. Keluaran dari semua elemen lapisan atas terhubung ke perangkat akhir.
Lapisan elemen logika berisi wadah untuk gerbang. Di lapisan konduktor ada elemen dengan satu input dan output. Elemen dapat berupa konduktor atau elemen NO. Konduktor beralih ke output apa yang datang ke input, dan elemen NO mengembalikan nilai terbalik pada output.
Yang pertama membuat array sumber. Generasi terjadi dari bawah ke atas, lapisan konduktor dihasilkan pertama, kemudian lapisan logika, dan pada output dari itu lagi konduktor.

Tetapi skema seperti itu sangat membosankan! Kami ingin lebih menyederhanakan hidup kami dan memutuskan untuk membuat struktur yang dihasilkan lebih menarik (kompleks). Diputuskan untuk menambahkan modifikasi struktur dengan percabangan atau koneksi melalui banyak lapisan.
Nah, untuk mengatakan "disederhanakan" - ini berarti menyulitkan hidup Anda dalam hal lain.
Menghasilkan sirkuit dengan tingkat kemampuan modifikasi maksimum ternyata menjadi tugas yang melelahkan dan tidak terlalu praktis. Karena itu, tim kami memutuskan untuk melakukan apa yang memenuhi kriteria ini:
Pengembangan tugas ini tidak memakan banyak waktu.
Generasi struktur modifikasi yang kurang lebih memadai.
Tidak ada persimpangan antara konduktor.
Sebagai hasil dari pemrograman yang panjang dan sulit, solusinya ditulis pada jam 4 sore.
Mari kita lihat kode dan ̶̶̶̶̶̶̶̶̶̶.
Di sini kelas OverflowArray ditemukan. Untuk alasan historis, ditambahkan setelah generasi struktural dasar dan lebih berkaitan dengan generasi varian, oleh karena itu terletak di bawah ini. Tautan public IEnumerable<CircuitStructure> GenerateStructure(int lines, int maxElementsInLine, StructureModification modification) { var baseStructure = GenerateStructure(lines, maxElementsInLine); for (int i = 0; i < lines; i++) { int maxValue = 1; int branchingSign = 1; if (modification == StructureModification.All) { maxValue = 2; branchingSign = 2; } int lengthOverflowArray = baseStructure.Gates[(i * 2) + 1].Length; var elementArray = new OverflowArray(lengthOverflowArray, maxValue); double numberOfOption = Math.Pow(2, lengthOverflowArray); for (int k = 1; k < numberOfOption - 1; k++) { elementArray.Increase(); if (modification == StructureModification.Branching || modification == StructureModification.All) { if (!CheckOverflowArrayForAllConnection(elementArray, branchingSign, lengthOverflowArray)) { continue; } }
Setelah melihat kode ini, saya ingin memahami apa yang terjadi di dalamnya.
Jangan khawatir! Penjelasan singkat tanpa detail terburu-buru untuk Anda.
Hal pertama yang kita lakukan adalah membuat struktur (dasar) biasa.
var baseStructure = GenerateStructure(lines, maxElementsInLine);
Kemudian, sebagai hasil dari pemeriksaan sederhana, kami mengatur tanda percabangan (branchingSign) ke nilai yang sesuai. Mengapa ini perlu? Selanjutnya akan jelas.
int maxValue = 1; int branchingSign = 1; if (modification == StructureModification.All) { maxValue = 2; branchingSign = 2; }
Sekarang kita menentukan panjang OverflowArray kita dan menginisialisasi itu.
int lengthOverflowArray = baseStructure.Gates[(i * 2) + 1].Length; var elementArray = new OverflowArray(lengthOverflowArray, maxValue);
Untuk melanjutkan manipulasi kami dengan struktur, kami perlu mengetahui jumlah variasi yang mungkin dari OverflowArray kami. Untuk melakukan ini, ada rumus yang diterapkan pada baris berikutnya.
int lengthOverflowArray = baseStructure.Gates[(i * 2) + 1].Length;
Berikutnya adalah loop bersarang di mana semua "keajaiban" terjadi dan yang ada semua kata pengantar ini. Pada awalnya, kami meningkatkan nilai array kami.
elementArray.Increase();
Setelah itu, kami melihat pemeriksaan validasi, sebagai hasilnya kami melangkah lebih jauh atau iterasi berikutnya.
if (modification == StructureModification.Branching || modification == StructureModification.All) { if (!CheckOverflowArrayForAllConnection(elementArray, branchingSign, lengthOverflowArray)) { continue; } }
Jika array melewati pemeriksaan validasi, maka kami mengkloning struktur basis kami. Kloning diperlukan karena kami akan memodifikasi struktur kami untuk lebih banyak iterasi.
Dan akhirnya, kami mulai memodifikasi struktur dan membersihkannya dari elemen yang tidak perlu. Mereka menjadi tidak perlu sebagai hasil dari modifikasi struktural.
ModifyStructure(structure, elementArray, key, modification); ClearStructure(structure);
Saya tidak melihat intinya secara lebih rinci untuk menganalisis lusinan fungsi kecil yang dilakukan “di suatu tempat” di kedalaman.
Variantsgenerator
Struktur + elemen yang seharusnya ada di dalamnya disebut CircuitVariant.
public struct CircuitVariant { public CircuitStructure Structure; public IDictionary<int, int[]> Gates; public IDictionary<int, int[]> Conductors; public IList<bool[]> Solutions; }
Bidang pertama adalah tautan ke struktur. Dua kamus kedua di mana kuncinya adalah jumlah layer, dan nilainya adalah array yang berisi jumlah elemen di tempat mereka dalam struktur.
Kami melanjutkan ke pemilihan kombinasi. Kita dapat memiliki sejumlah elemen dan konduktor logika yang valid. Secara total, bisa ada 6 elemen logika dan 2 konduktor.
Anda dapat membayangkan sistem angka dengan basis 6 dan mendapatkan setiap kategori angka yang sesuai dengan elemen. Dengan demikian, dengan meningkatkan angka heksadesimal ini, Anda dapat melewati semua kombinasi elemen.
Artinya, angka heksadesimal dari tiga digit akan menjadi 3 elemen. Ini hanya layak mempertimbangkan bahwa jumlah elemen bukan 6 tetapi 4 dapat ditransmisikan.
Untuk melepaskan nomor tersebut, saya menentukan strukturnya
public struct ClampedInt { public int Value { get => value; set => this.value = Mathf.Clamp(value, 0, MaxValue); } public readonly int MaxValue; private int value; public ClampedInt(int maxValue) { MaxValue = maxValue; value = 0; } public bool TryIncrease() { if (Value + 1 <= MaxValue) { Value++; return false; }
Berikutnya adalah kelas dengan nama aneh
OverflowArray . Esensinya adalah bahwa ia menyimpan array
ClampedInt dan meningkatkan urutan tinggi jika
terjadi overflow
dalam urutan
rendah dan seterusnya hingga mencapai nilai maksimum di semua sel.
Sesuai dengan setiap ClampedInt, nilai ElectricalElementContainer yang sesuai ditetapkan. Dengan demikian, dimungkinkan untuk memilah-milah semua kombinasi yang mungkin. Perlu dicatat bahwa jika Anda ingin membuat skema dengan elemen (misalnya, Dan (0) dan Xor (4)), Anda tidak perlu memilah-milah semua opsi, termasuk elemen 1,2,3. Untuk ini, selama generasi, elemen mendapatkan nomor lokal mereka (misalnya, Dan = 0, Xor = 1), dan setelah itu mereka dikonversi kembali ke angka global.
Jadi, Anda dapat mengulangi semua kemungkinan kombinasi di semua elemen.
Setelah nilai-nilai dalam wadah diatur, rangkaian diperiksa untuk solusi untuk itu menggunakan
Solver . Jika sirkuit melewati keputusan, ia kembali.
Setelah rangkaian dihasilkan, jumlah solusi diperiksa. Seharusnya tidak melebihi batas dan seharusnya tidak memiliki keputusan yang seluruhnya terdiri dari 0 atau 1.
Banyak kode public interface IVariantsGenerator { IEnumerable<CircuitVariant> Generate(IEnumerable<CircuitStructure> structures, ICollection<int> availableGates, bool useNot, int maxSolutions = int.MaxValue); } public class VariantsGenerator : IVariantsGenerator { private readonly ISolver solver; private readonly IElementsProvider elementsProvider; public VariantsGenerator(ISolver solver, IElementsProvider elementsProvider) { this.solver = solver; this.elementsProvider = elementsProvider; } public IEnumerable<CircuitVariant> Generate(IEnumerable<CircuitStructure> structures, ICollection<int> availableGates, bool useNot, int maxSolutions = int.MaxValue) { bool manyGates = availableGates.Count > 1; var availableLeToGeneralNumber = GetDictionaryFromAllowedElements(elementsProvider.Gates, availableGates); var gatesList = GetElementsList(availableLeToGeneralNumber, elementsProvider.Gates); var availableConductorToGeneralNumber = useNot ? GetDictionaryFromAllowedElements(elementsProvider.Conductors, new[] {0, 1}) : GetDictionaryFromAllowedElements(elementsProvider.Conductors, new[] {0}); var conductorsList = GetElementsList(availableConductorToGeneralNumber, elementsProvider.Conductors); foreach (var structure in structures) { InitializeCircuitStructure(structure, gatesList, conductorsList); var gates = GetListFromLayersDictionary(structure.Gates); var conductors = GetListFromLayersDictionary(structure.Conductors); var gatesArray = new OverflowArray(gates.Count, availableGates.Count - 1); var conductorsArray = new OverflowArray(conductors.Count, useNot ? 1 : 0); do { if (useNot && conductorsArray.EqualInts) { continue; } SetContainerValuesAccordingToArray(conductors, conductorsArray); do { if (manyGates && gatesArray.Length > 1 && gatesArray.EqualInts) { continue; } SetContainerValuesAccordingToArray(gates, gatesArray); var solutions = solver.GetSolutions(structure.FinalDevice, structure.Sources); if (solutions.Any() && solutions.Count <= maxSolutions && !(solutions.Any(s => s.All(b => b)) || solutions.Any(s => s.All(b => !b)))) { var variant = new CircuitVariant { Conductors = GetElementsNumberFromLayers(structure.Conductors, availableConductorToGeneralNumber), Gates = GetElementsNumberFromLayers(structure.Gates, availableLeToGeneralNumber), Solutions = solutions, Structure = structure }; yield return variant; } } while (!gatesArray.Increase()); } while (useNot && !conductorsArray.Increase()); } } private static void InitializeCircuitStructure(CircuitStructure structure, IList<Func<IElectricalElement>> gates, IList<Func<IElectricalElement>> conductors) { var lElements = GetListFromLayersDictionary(structure.Gates); foreach (var item in lElements) { item.SetElements(gates); } var cElements = GetListFromLayersDictionary(structure.Conductors); foreach (var item in cElements) { item.SetElements(conductors); } } private static IList<Func<IElectricalElement>> GetElementsList(IDictionary<int, int> availableToGeneralGate, IReadOnlyList<Func<IElectricalElement>> elements) { var list = new List<Func<IElectricalElement>>(); foreach (var item in availableToGeneralGate) { list.Add(elements[item.Value]); } return list; } private static IDictionary<int, int> GetDictionaryFromAllowedElements(IReadOnlyCollection<Func<IElectricalElement>> allElements, IEnumerable<int> availableElements) { var enabledDic = new Dictionary<int, bool>(allElements.Count); for (int i = 0; i < allElements.Count; i++) { enabledDic.Add(i, false); } foreach (int item in availableElements) { enabledDic[item] = true; } var availableToGeneralNumber = new Dictionary<int, int>(); int index = 0; foreach (var item in enabledDic) { if (item.Value) { availableToGeneralNumber.Add(index, item.Key); index++; } } return availableToGeneralNumber; } private static void SetContainerValuesAccordingToArray(IReadOnlyList<ElectricalElementContainer> containers, IOverflowArray overflowArray) { for (int i = 0; i < containers.Count; i++) { containers[i].SetType(overflowArray[i].Value); } } private static IReadOnlyList<ElectricalElementContainer> GetListFromLayersDictionary(IDictionary<int, ElectricalElementContainer[]> layers) { var elements = new List<ElectricalElementContainer>(); foreach (var layer in layers) { elements.AddRange(layer.Value); } return elements; } private static IDictionary<int, int[]> GetElementsNumberFromLayers(IDictionary<int, ElectricalElementContainer[]> layers, IDictionary<int, int> elementIdToGlobal = null) { var dic = new Dictionary<int, int[]>(layers.Count); bool convert = elementIdToGlobal != null; foreach (var layer in layers) { var values = new int[layer.Value.Length]; for (int i = 0; i < layer.Value.Length; i++) { if (!convert) { values[i] = layer.Value[i].SelectedType; } else { values[i] = elementIdToGlobal[layer.Value[i].SelectedType]; } } dic.Add(layer.Key, values); } return dic; } }
Masing-masing generator mengembalikan varian menggunakan pernyataan hasil. Dengan demikian, CircuitGenerator menggunakan StructureGenerator dan VariantsGenerator menghasilkan IEnumerable. (Pendekatan dengan hasil banyak membantu di masa depan, lihat di bawah)
Mengikuti fakta bahwa generator opsi menerima daftar struktur. Anda dapat menghasilkan opsi untuk setiap struktur secara independen. Ini bisa diparalelkan, tetapi menambahkan AsParallel tidak berfungsi (mungkin menghasilkan interferensi). Paralel secara manual akan memakan waktu lama, karena kami membuang opsi ini.
Sebenarnya, saya mencoba melakukan generasi paralel, itu berhasil, tetapi ada beberapa kesulitan, karena tidak pergi ke repositori.Kelas game
Pendekatan Pembangunan dan DI
IsiProyek ini dibangun di bawah
Dependency Injection (DI). Ini berarti bahwa kelas-kelas dapat dengan sendirinya memerlukan semacam objek yang sesuai dengan antarmuka dan tidak terlibat dalam membuat objek ini. Apa manfaatnya:
- Tempat pembuatan dan inisialisasi objek ketergantungan didefinisikan di satu tempat dan dipisahkan dari logika kelas dependen, yang menghilangkan duplikasi kode.
- Menghilangkan kebutuhan untuk menggali seluruh pohon dependensi dan instantiate semua dependensi.
- Memungkinkan Anda mengubah implementasi antarmuka dengan mudah, yang digunakan di banyak tempat.
Sebagai wadah DI dalam proyek,
Zenject digunakan.
Zenject memiliki beberapa konteks, saya hanya menggunakan dua di antaranya:
- Konteks proyek - pendaftaran dependensi dalam seluruh aplikasi.
- Konteks adegan: pendaftaran kelas yang hanya ada dalam adegan tertentu dan masa hidupnya dibatasi oleh masa hidup adegan tersebut.
- Konteks statis adalah konteks umum untuk segala sesuatu secara umum, kekhasannya adalah bahwa ia ada di editor. Saya gunakan untuk injeksi di editor
Pendaftaran kelas disimpan di
Installer s. Saya menggunakan
ScriptableObjectInstaller untuk konteks proyek, dan
MonoInstaller untuk konteks adegan.
Sebagian besar kelas yang saya daftarkan dengan AsSingle, karena tidak mengandung status, lebih cenderung hanya sebagai wadah untuk metode. Saya menggunakan AsTransient untuk kelas di mana ada keadaan internal yang seharusnya tidak umum untuk kelas lain.Setelah itu, Anda perlu entah bagaimana membuat kelas MonoBehaviour yang akan mewakili elemen-elemen ini. Saya juga mengalokasikan kelas yang terkait dengan Unity ke proyek terpisah tergantung pada proyek Core.
Untuk kelas MonoBehaviour, saya lebih suka membuat antarmuka saya sendiri. Ini, di samping keunggulan standar antarmuka, memungkinkan Anda menyembunyikan anggota MonoBeurour yang sangat besar.Untuk kenyamanan, DI sering membuat kelas sederhana yang menjalankan semua logika, dan pembungkus MonoBehaviour untuk itu. Misalnya, kelas memiliki metode Mulai dan Pembaruan, saya membuat metode seperti itu di kelas, kemudian di kelas MonoBehaviour saya menambahkan bidang dependensi dan dalam metode yang sesuai saya sebut Mulai dan Perbarui. Ini memberikan injeksi "benar" ke konstruktor, detasemen kelas utama dari wadah DI dan kemampuan untuk dengan mudah menguji.Konfigurasi
KontenDengan konfigurasi, maksud saya data umum untuk seluruh aplikasi. Dalam kasus saya, ini adalah cetakan, pengidentifikasi untuk iklan dan pembelian, tag, nama adegan, dll. Untuk tujuan ini, saya menggunakan ScriptableObjects:- Untuk setiap grup data, kelas turunan ScriptableObject dialokasikan.
- Ini menciptakan bidang serializable yang diperlukan
- Properti baca dari bidang ini ditambahkan.
- Antarmuka dengan bidang di atas disorot
- Kelas mendaftar ke antarmuka dalam wadah DI
- Untung
public interface ITags { string FixedColor { get; } string BackgroundColor { get; } string ForegroundColor { get; } string AccentedColor { get; } } [CreateAssetMenu(fileName = nameof(Tags), menuName = "Configuration/" + nameof(Tags))] public class Tags : ScriptableObject, ITags { [SerializeField] private string fixedColor; [SerializeField] private string backgroundColor; [SerializeField] private string foregroundColor; [SerializeField] private string accentedColor; public string FixedColor => fixedColor; public string BackgroundColor => backgroundColor; public string ForegroundColor => foregroundColor; public string AccentedColor => accentedColor; private void OnEnable() { fixedColor.AssertNotEmpty(nameof(fixedColor)); backgroundColor.AssertNotEmpty(nameof(backgroundColor)); foregroundColor.AssertNotEmpty(nameof(foregroundColor)); accentedColor.AssertNotEmpty(nameof(accentedColor)); } }
Untuk konfigurasi, penginstal terpisah (disingkat kode): CreateAssetMenu(fileName = nameof(ConfigurationInstaller), menuName = "Installers/" + nameof(ConfigurationInstaller))] public class ConfigurationInstaller : ScriptableObjectInstaller<ConfigurationInstaller> { [SerializeField] private EditorElementsPrefabs editorElementsPrefabs; [SerializeField] private LevelCompletionSteps levelCompletionSteps; [SerializeField] private CommonValues commonValues; [SerializeField] private AdsConfiguration adsConfiguration; [SerializeField] private CutscenesConfiguration cutscenesConfiguration; [SerializeField] private Colors colors; [SerializeField] private Tags tags; public override void InstallBindings() { Container.Bind<IEditorElementsPrefabs>().FromInstance(editorElementsPrefabs).AsSingle(); Container.Bind<ILevelCompletionSteps>().FromInstance(levelCompletionSteps).AsSingle(); Container.Bind<ICommonValues>().FromInstance(commonValues).AsSingle(); Container.Bind<IAdsConfiguration>().FromInstance(adsConfiguration).AsSingle(); Container.Bind<ICutscenesConfiguration>().FromInstance(cutscenesConfiguration).AsSingle(); Container.Bind<IColors>().FromInstance(colors).AsSingle(); Container.Bind<ITags>().FromInstance(tags).AsSingle(); } private void OnEnable() { editorElementsPrefabs.AssertNotNull(); levelCompletionSteps.AssertNotNull(); commonValues.AssertNotNull(); adsConfiguration.AssertNotNull(); cutscenesConfiguration.AssertNotNull(); colors.AssertNOTNull(); tags.AssertNotNull(); } }
Elemen listrik
IsiSekarang Anda perlu membayangkan elemen listrik public interface IElectricalElementMb { GameObject GameObject { get; } string Name { get; set; } IElectricalElement Element { get; set; } IOutputConnectorMb[] OutputConnectorsMb { get; } IInputConnectorMb[] InputConnectorsMb { get; } Transform Transform { get; } void SetInputConnectorsMb(InputConnectorMb[] inputConnectorsMb); void SetOutputConnectorsMb(OutputConnectorMb[] outputConnectorsMb); } [DisallowMultipleComponent] public class ElectricalElementMb : MonoBehaviour, IElectricalElementMb { [SerializeField] private OutputConnectorMb[] outputConnectorsMb; [SerializeField] private InputConnectorMb[] inputConnectorsMb; public Transform Transform => transform; public GameObject GameObject => gameObject; public string Name { get => name; set => name = value; } public virtual IElectricalElement Element { get; set; } public IOutputConnectorMb[] OutputConnectorsMb => outputConnectorsMb; public IInputConnectorMb[] InputConnectorsMb => inputConnectorsMb; }
public interface IInputConnectorMb : IConnectorMb { IOutputConnectorMb OutputConnectorMb { get; set; } IInputConnector InputConnector { get; } }
public class InputConnectorMb : MonoBehaviour, IInputConnectorMb { [SerializeField] private OutputConnectorMb outputConnectorMb; public Transform Transform => transform; public IOutputConnectorMb OutputConnectorMb { get => outputConnectorMb; set => outputConnectorMb = (OutputConnectorMb) value; } public IInputConnector InputConnector { get; } = new InputConnector(); #if UNITY_EDITOR private void OnDrawGizmos() { if (outputConnectorMb != null) { Handles.DrawLine(transform.position, outputConnectorMb.Transform.position); } } #endif }
Kami memiliki elemen publik IElectricalElement Element {get; mengatur; }
Hanya di sini cara menginstal item ini?Pilihan yang baik adalah membuat generik:kelas publik ElectricalElementMb: MonoBehaviour, IElectricalElementMb di mana T: IElectricalElementTetapi yang menarik adalah bahwa Unity tidak mendukung generik di kelas MonoBehavior. Selain itu, Unity tidak mendukung serialisasi properti dan antarmuka.Namun demikian, dalam runtime sangat mungkin untuk lulus di IElectricalElement Element {get; mengatur; }
nilai yang diinginkan.Saya membuat Enum ElectricalElementType di mana akan ada semua jenis yang diperlukan. Enum diserialisasi dengan baik oleh Unity dan ditampilkan dengan baik di Inspektur sebagai daftar drop-down. Menentukan dua jenis elemen: yang dibuat dalam runtime dan yang dibuat di editor dan dapat disimpan. Dengan demikian, ada IElectricalElementMb dan IElectricalElementMbEditor, yang juga berisi bidang tipe ElectricalElementType.Tipe kedua juga perlu diinisialisasi dalam runtime. Untuk melakukan ini, ada kelas yang pada awalnya akan mem-bypass semua elemen dan menginisialisasi mereka tergantung pada jenis di bidang enum. Sebagai berikut: private static readonly Dictionary<ElectricalElementType, Func<IElectricalElement>> ElementByType = new Dictionary<ElectricalElementType, Func<IElectricalElement>> { {ElectricalElementType.And, () => new And()}, {ElectricalElementType.Or, () => new Or()}, {ElectricalElementType.Xor, () => new Xor()}, {ElectricalElementType.Nand, () => new Nand()}, {ElectricalElementType.Nor, () => new Nor()}, {ElectricalElementType.NOT, () => new NOT()}, {ElectricalElementType.Xnor, () => new Xnor()}, {ElectricalElementType.Source, () => new Source()}, {ElectricalElementType.Conductor, () => new Conductor()}, {ElectricalElementType.Placeholder, () => new AlwaysFalse()}, {ElectricalElementType.Encoder, () => new Encoder()}, {ElectricalElementType.Decoder, () => new Decoder()} };
Manajemen game
KontenSelanjutnya, muncul pertanyaan, di mana menempatkan logika permainan itu sendiri (memeriksa kondisi bagian, menghitung bacaan bagian dan membantu pemain)? .. Ada juga pertanyaan tentang lokasi logika untuk menyimpan dan memuat kemajuan, pengaturan, dan hal-hal lainnya.Untuk ini, saya membedakan kelas manajer tertentu yang bertanggung jawab untuk kelas tugas tertentu.DataManager bertanggung jawab untuk menyimpan data dari hasil melewati pengaturan pengguna dan game. Itu terdaftar oleh AsSingle dalam konteks proyek. Ini berarti bahwa ia adalah satu untuk seluruh aplikasi. Saat aplikasi sedang berjalan, data disimpan langsung di memori, di dalam DataManager.Dia menggunakan IFileStoreService , yang bertanggung jawab untuk memuat dan menyimpan data dan IFileSerializerbertanggung jawab atas serialisasi file dalam bentuk siap pakai untuk disimpan.LevelGameManager adalah manajer game dalam satu adegan.Saya mendapat sedikit GodObject, karena dia masih bertanggung jawab untuk UI, yaitu, membuka dan menutup menu, reaksi terhadap tombol. Tapi itu bisa diterima, mengingat ukuran proyek dan kurangnya kebutuhan untuk memperluasnya, sehingga urutan tindakan lebih mudah dan lebih jelas terlihat.Ada dua opsi. Inilah yang disebut LevelGameManager1 dan LevelGameManager2 masing-masing untuk mode 1 dan 2.Dalam kasus pertama, logika didasarkan pada reaksi terhadap peristiwa perubahan nilai di salah satu Sumber dan memeriksa nilai pada output rangkaian.Dalam kasus kedua, logika merespons peristiwa perubahan elemen dan juga memeriksa nilai pada output sirkuit.Ada beberapa informasi level saat ini seperti nomor level dan bantuan pemain.Data tentang level saat ini disimpan di CurrentLevelData . Nomor level disimpan di sana - properti Boolean dengan cek bantuan, bendera tawaran untuk mengevaluasi permainan dan data untuk membantu pemain. public interface ICurrentLevelData { int LevelNumber { get; } bool HelpExist { get; } bool ProposeRate { get; } } public interface ICurrentLevelDataMode1 : ICurrentLevelData { IEnumerable<SourcePositionValueHelp> PartialHelp { get; } } public interface ICurrentLevelDataMode2 : ICurrentLevelData { IEnumerable<PlaceTypeHelp> PartialHelp { get; } }
Bantuan untuk mode pertama adalah angka dan nilai sumber pada mereka. Dalam mode kedua, ini adalah jenis elemen yang perlu diatur dalam sel.Koleksi berisi struktur yang menyimpan posisi dan nilai yang harus ditetapkan pada posisi yang ditentukan. Kamus akan lebih cantik, tetapi Unity tidak bisa membuat serial kamus.Perbedaan antara adegan mode yang berbeda adalah bahwa dalam konteks adegan, LevelGameManager lain dan ICurrentLevelData lainnya diatur .Secara umum, saya memiliki pendekatan event-driven untuk komunikasi elemen. Di satu sisi, ini logis dan nyaman. Di sisi lain, ada peluang untuk mendapatkan masalah tanpa berhenti berlangganan saat diperlukan. Namun demikian, tidak ada masalah dalam proyek ini, dan skalanya tidak terlalu besar. Biasanya, langganan terjadi selama awal adegan untuk semua yang Anda butuhkan. Hampir tidak ada yang dibuat di runtime, jadi tidak ada kebingungan.Tingkat pemuatan
KontenSetiap level dalam game diwakili oleh adegan Unity, harus mengandung awalan level dan angka, misalnya, "Level23". Awalan termasuk dalam konfigurasi. Pemuatan level terjadi berdasarkan nama, yang dibentuk dari awalan. Dengan demikian, kelas LevelsManager dapat memuat level dengan angka.Cutscenes
Isi daricutscene adalah adegan kesatuan biasa dengan angka dalam judul, mirip dengan level.Animasi itu sendiri diimplementasikan menggunakan Timeline. Sayangnya, saya tidak memiliki keterampilan animasi atau kemampuan untuk bekerja dengan Timeline, jadi "jangan menembak pianis - dia bermain semampu dia."
Kebenaran ternyata bahwa satu cutscene logis harus terdiri dari adegan yang berbeda dengan objek yang berbeda. Ternyata ini diketahui agak terlambat, tetapi diputuskan sederhana: dengan menempatkan bagian-bagian adegan adegan di atas panggung di tempat yang berbeda dan langsung menggerakkan kamera.
Gameplay tambahan
KontenPermainan dievaluasi dengan jumlah tindakan per level dan penggunaan petunjuk. Semakin sedikit aksi semakin baik. Menggunakan tooltip mengurangi peringkat maksimum menjadi 2 bintang, melompati level menjadi 1 bintang. Untuk menilai bagian itu, jumlah langkah untuk melewati disimpan. Ini terdiri dari dua nilai: nilai minimum (untuk 3 bintang) dan maksimum (1 bintang).Jumlah langkah untuk melewati level tidak disimpan dalam file adegan itu sendiri, tetapi dalam file konfigurasi, karena Anda perlu menampilkan jumlah bintang untuk level yang dilewati. Ini sedikit mempersulit proses menciptakan level. Sangat menarik melihat perubahan dalam sistem kontrol versi:
Coba tebak level apa yang dimiliki. Itu mungkin untuk menyimpan kamus, tentu saja, tetapi di tempat pertama itu tidak akan diserialisasi oleh Unity, di yang kedua harus secara manual mengatur angka-angka.Jika sulit bagi pemain untuk menyelesaikan level, dia bisa mendapatkan petunjuk - nilai yang benar pada beberapa input, atau elemen yang benar dalam mode kedua. Ini juga dilakukan secara manual, meskipun bisa otomatis.Jika bantuan pemain tidak membantu, ia dapat sepenuhnya melewati level. Dalam hal kehilangan level, pemain mendapat 1 bintang untuknya.Seorang pengguna yang telah melewati level dengan sebuah petunjuk tidak dapat menjalankannya kembali untuk sementara waktu, sehingga akan sulit untuk menjalankan kembali level tersebut dengan memori baru, seolah-olah tanpa sebuah petunjuk.Monetisasi
KontenAda dua jenis monetisasi dalam game: menampilkan iklan dan menonaktifkan iklan untuk uang. Tampilan iklan termasuk menampilkan iklan antar level dan melihat iklan yang dihargai untuk melewati level.Jika pemain bersedia membayar untuk menonaktifkan iklan, maka dia bisa melakukannya. Dalam hal ini, iklan antar level dan ketika melewati level tidak akan ditampilkan.Untuk iklan, kelas yang disebut AdsService telah dibuat , dengan antarmuka public interface IAdsService { bool AdsDisabled { get; } void LoadBetweenLevelAd(); bool ShowBetweenLevelAd(int level, bool force = false); void LoadHelpAd(Action onLoaded = null); void ShowHelpAd(Action onRewarded, Action onClosed); bool HelpAdLoaded { get; } }
Di sini HelpAd adalah iklan yang dihargai karena melewatkan satu level. Awalnya, kami menyebut bantuan parsial dan bantuan penuh. Sebagian adalah petunjuk, dan penuh adalah level lewati.Kelas ini berisi batasan frekuensi menampilkan iklan berdasarkan waktu, setelah peluncuran pertama game.Implementasinya menggunakan Plugin Persatuan Iklan Seluler Google .Dengan iklan yang dihargai, saya melangkah menyapu - ternyata delegasi yang loyal dapat dipanggil di utas lain, tidak terlalu jelas mengapa. Oleh karena itu, lebih baik delegasi tersebut tidak memanggil apa pun dalam kode yang terkait dengan Unity. Jika pembelian dilakukan untuk menonaktifkan iklan, iklan tidak akan ditampilkan dan delegasi akan segera melaksanakan tampilan iklan yang berhasil.Ada antarmuka untuk berbelanja public interface IPurchaseService { bool IsAdsDisablePurchased { get; } event Action DisableAdsPurchased; void BuyDisableAds(); void RemoveDisableAd(); }
Unity IAP digunakan dalam implementasi. Ada trik untuk membeli pemutusan iklan. Google Play tampaknya tidak memberikan informasi bahwa pemain membeli pembelian. Konfirmasi hanya akan datang bahwa dia lulus sekali. Tetapi jika Anda menempatkan status produk setelah pembelian tidak Lengkap tetapi Ditunda, ini akan memungkinkan Anda untuk memeriksa properti produk hasReceipt . Jika itu benar, pembelian telah selesai.Meskipun tentu saja itu membingungkan pendekatan semacam itu, saya curiga mungkin tidak semuanya mulus.Metode RemoveDisableAd diperlukan pada saat pengujian, itu menghilangkan penghentian iklan yang dibeli.Antarmuka pengguna
KontenSemua elemen antarmuka bekerja sesuai dengan pendekatan berorientasi peristiwa. Elemen antarmuka itu sendiri biasanya tidak mengandung logika selain peristiwa yang disebut oleh metode publik yang dapat digunakan Unity. Meskipun itu juga terjadi untuk melakukan beberapa tugas yang hanya terkait dengan antarmuka. public abstract class UiElementBase : MonoBehaviour, IUiElement { public event Action ShowClick; public event Action HideCLick; public void Show() { gameObject.SetActive(true); ShowClick?.Invoke(); } public void Hide() { gameObject.SetActive(false); HideCLick?.Invoke(); } } public class PauseMenu : UiElementEscapeClose, IPauseMenu { [SerializeField] private Text levelNumberText; [SerializeField] private LocalizedText finishedText; [SerializeField] private GameObject restartButton; private int levelNumber; public event Action GoToMainMenuClick; public event Action RestartClick; public int LevelNumber { set => levelNumberText.text = $"{finishedText.Value} {value}"; } public void DisableRestartButton() { restartButton.SetActive(false); } public void GoToMainMenu() { GoToMainMenuClick?.Invoke(); } public void Restart() { RestartClick?.Invoke(); } }
Sebenarnya, ini tidak selalu terjadi. Sebaiknya tinggalkan elemen ini sebagai Tampilan aktif, buat pendengar acara darinya, sesuatu seperti pengontrol yang akan memicu tindakan yang diperlukan pada manajer.Analisis
KontenDi jalur dengan resistensi paling rendah, analitik Persatuan dipilih . Mudah diimplementasikan, meskipun terbatas untuk berlangganan gratis - tidak mungkin untuk mengekspor data sumber. Ada juga batasan jumlah acara - 100 / jam per pemain.Untuk analitik, buat kelas pembungkus AnalyticsService . Ini memiliki metode untuk setiap jenis acara, menerima parameter yang diperlukan dan menyebabkan acara dikirim menggunakan alat yang dibangun ke dalam Unity. Membuat metode untuk setiap acara tentu bukan praktik terbaik secara keseluruhan, tetapi dalam proyek yang sadar kecil itu lebih baik daripada melakukan sesuatu yang besar dan rumit.Semua acara yang digunakan adalah CustomEvent.. Mereka dibangun dari nama acara dan nama parameter kamus dan nilai. AnalyticsService mendapatkan nilai yang diperlukan dari parameter dan membuat kamus di dalamnya.Semua nama dan parameter acara ditempatkan dalam konstanta. Tidak dalam bentuk pendekatan tradisional dengan ScriptableObject, karena nilai-nilai ini seharusnya tidak pernah berubah.Contoh Metode: public void LevelComplete(int number, int stars, int actionCount, TimeSpan timeSpent, int levelMode) { CustomEvent(LevelCompleteEventName, new Dictionary<string, object> { {LevelNumber, number}, {LevelStars, stars}, {LevelActionCount, actionCount}, {LevelTimeSpent, timeSpent}, {LevelMode, levelMode} }); }
Positioning dan Diagram Kamera
IsiTugas adalah menempatkan FinalDevice di atas layar, pada jarak yang sama dari batas atas dan Sumber dari bawah juga selalu pada jarak yang sama dari batas bawah. Selain itu, layar memiliki rasio aspek yang berbeda, Anda harus menyesuaikan ukuran kamera sebelum memulai level agar sesuai dengan sirkuit dengan benar.Untuk melakukan ini, kelas CameraAlign dibuat . Algoritma Ukuran:- Temukan semua elemen yang diperlukan di atas panggung
- Temukan lebar dan tinggi minimum berdasarkan rasio aspek
- Tentukan ukuran kamera
- Atur kamera di tengah
- Pindahkan FinalDevice ke atas layar
- Pindahkan sumber ke bagian bawah layar
public class CameraAlign : ICameraAlign { private readonly ISceneObjectsHelper sceneObjectsHelper; private readonly ICommonValues commonValues; public CameraAlign(ISceneObjectsHelper sceneObjectsHelper, ICommonValues commonValues) { this.sceneObjectsHelper = sceneObjectsHelper; this.commonValues = commonValues; } public void Align(Camera camera) { var elements = sceneObjectsHelper.FindObjectsOfType<IElectricalElementMb>(); var finalDevice = sceneObjectsHelper.FindObjectOfType<IFinalDevice>(); var sources = elements.OfType<ISourceMb>().ToArray(); if (finalDevice != null && sources.Length > 0) { float leftPos = elements.Min(s => s.Transform.position.x); float rightPos = elements.Max(s => s.Transform.position.x); float width = Mathf.Abs(leftPos - rightPos); var fPos = finalDevice.Transform.position; float height = Mathf.Abs(sources.First().Transform.position.y - fPos.y) * camera.aspect; float size = Mathf.Max(width * commonValues.CameraOffset, height * commonValues.CameraOffset); camera.orthographicSize = Mathf.Clamp(size, commonValues.MinCameraSize, float.MaxValue); camera.transform.position = GetCenterPoint(elements, -1); fPos = new Vector2(fPos.x, camera.ScreenToWorldPoint(new Vector2(Screen.width, Screen.height)).y - commonValues.FinalDeviceTopOffset * camera.orthographicSize); finalDevice.Transform.position = fPos; float sourceY = camera.ScreenToWorldPoint(Vector2.zero).y + commonValues.SourcesBottomOffset; foreach (var item in sources) { item.Transform.position = new Vector2(item.Transform.position.x, sourceY); } } else { Debug.Log($"{nameof(CameraAlign)}: No final device or no sources in scene"); } } private static Vector3 GetCenterPoint(ICollection<IElectricalElementMb> elements, float z) { float top = elements.Max(e => e.Transform.position.y); float bottom = elements.Min(e => e.Transform.position.y); float left = elements.Min(e => e.Transform.position.x); float right = elements.Max(e => e.Transform.position.x); float x = left + ((right - left) / 2); float y = bottom + ((top - bottom) / 2); return new Vector3(x, y, z); } }
Metode ini dipanggil saat adegan dimulai di kelas wrapper.Skema warna
KontenKarena permainan akan memiliki antarmuka yang sangat primitif, saya memutuskan untuk membuatnya dengan dua skema warna, hitam dan putih.Untuk melakukan ini, buat antarmuka public interface IColors { Color ColorAccent { get; } Color Background { get; set; } Color Foreground { get; set; } event Action ColorsChanged; }
Warna dapat diatur secara langsung di editor Unity, ini dapat digunakan untuk pengujian. Kemudian mereka dapat diaktifkan dan memiliki dua set warna.Warna Background dan Foreground dapat berubah, satu aksen warna dalam mode apa pun.Karena pemain dapat mengatur tema non-standar, data warna harus disimpan dalam file pengaturan. Jika file pengaturan tidak berisi data berwarna, maka mereka diisi dengan nilai standar.Lalu ada beberapa kelas: CameraColorAdjustment - bertanggung jawab untuk mengatur warna latar belakang pada kamera, UiColorAdjustment - mengatur warna elemen antarmuka dan TextMeshColorAdjustment- mengatur warna angka pada sumber. UiColorAdjustment juga menggunakan tag. Di editor, Anda dapat menandai setiap elemen dengan tag yang akan menunjukkan jenis warna apa yang harus ditetapkan (Latar Belakang, Foreground, AccentColor, dan FixedColor). Ini semua diatur pada awal adegan atau dengan perubahan skema warna.Hasil:


Ekstensi Editor
IsiUntuk menyederhanakan dan mempercepat proses pengembangan, seringkali perlu untuk membuat alat yang tepat, yang tidak disediakan oleh alat editor standar. Pendekatan tradisional di Unity adalah membuat kelas keturunan EditorWindow. Ada juga pendekatan dengan UiElements, tetapi masih dalam pengembangan, jadi saya memutuskan untuk menggunakan pendekatan tradisional.Jika Anda cukup membuat kelas yang menggunakan sesuatu dari namespace UnityEditor di sebelah kelas lain untuk gim, maka proyek tidak akan dirakit, karena namespace ini tidak tersedia dalam build. Ada beberapa solusi:- Pilih proyek terpisah untuk skrip editor
- Tempatkan file di folder Aset / Editor
- Bungkus file-file ini di #jika UNITY_EDITOR
Proyek ini menggunakan pendekatan pertama dan kadang-kadang #jika UNITY_EDITOR, jika perlu, tambahkan sebagian kecil untuk editor ke kelas yang diperlukan dalam membangun.Semua kelas yang diperlukan hanya di editor yang saya tentukan di majelis, yang hanya akan tersedia di editor. Dia tidak akan pergi ke membangun permainan.
Alangkah baiknya sekarang memiliki DI di ekstensi editor Anda. Untuk ini saya menggunakan Zenject.StaticContext. Untuk mengaturnya di editor, kelas dengan atribut InitializeOnLoad digunakan, di mana ada konstruktor statis. [InitializeOnLoad] public class EditorInstaller { static EditorInstaller() { var container = StaticContext.Container; container.Bind<IElementsProvider>().To<ElementsProvider>().AsSingle(); container.Bind<ISolver>().To<Solver>().AsSingle(); .... } }
Untuk mendaftarkan kelas ScriptableObject dalam konteks statis, saya menggunakan kode berikut: BindFirstScriptableObject<ISceneNameConfiguration, SceneNameConfiguration>(container); private static void BindFirstScriptableObject<TInterface, TImplementation>(DiContainer container) where TImplementation : ScriptableObject, TInterface { var obj = GetFirstScriptableObject<TImplementation>(); container.Bind<TInterface>().FromInstance(obj).AsSingle(); } private static T GetFirstScriptableObject<T>() where T : ScriptableObject { var guids = AssetDatabase.FindAssets("t:" + typeof(T).Name); string path = AssetDatabase.GUIDToAssetPath(guids.First()); var obj = AssetDatabase.LoadAssetAtPath<T>(path); return obj; }
TImplementation hanya diperlukan untuk baris ini. AssetDatabase.LoadAssetAtPath (path)Tidak mungkin menambahkan ketergantungan ke konstruktor. Sebagai gantinya, tambahkan atribut [Suntikkan] ke bidang dependensi di kelas jendela dan panggilStaticContext.Container.Inject (ini) saat startup jendela ;Saya juga merekomendasikan menambahkan ke siklus pembaruan jendela pemeriksaan nol dari salah satu bidang dependen, dan jika bidang kosong, lakukan baris di atas. Karena setelah mengubah kode dalam proyek, Unity dapat membuat ulang jendela dan tidak memanggil Sedarlah atas itu.Generator
Isi Tampilan
awal generator.Jendelaharus menyediakan antarmuka untuk menghasilkan daftar skema dengan parameter, menampilkan daftar skema dan menempatkan skema yang dipilih pada adegan saat ini.Jendela terdiri dari tiga bagian dari kiri ke kanan:- pengaturan generasi
- daftar opsi dalam bentuk tombol
- opsi yang dipilih sebagai teks
Kolom dibuat menggunakan EditorGUILayout.BeginVertical () dan EditorGUILayout.EndVertical (). Sayangnya, itu tidak berfungsi untuk memperbaiki dan membatasi ukuran, tetapi ini tidak begitu penting.Ternyata proses pembangkitan pada sejumlah besar sirkuit tidak begitu cepat. Banyak kombinasi diperoleh dengan elemen I. Seperti yang ditunjukkan oleh profiler, bagian paling lambat adalah sirkuit itu sendiri. Paralelisasi itu bukanlah suatu opsi, semua opsi menggunakan satu skema, tetapi sulit untuk mengkloning struktur ini.Kemudian saya berpikir bahwa mungkin semua kode ekstensi editor berfungsi dalam mode Debug. Di bawah Rilis, debug tidak berfungsi dengan baik, breakpoints tidak berhenti, garis dilewati, dll. Memang, setelah mengukur kinerjanya, ternyata kecepatan generator di Unity sesuai dengan rakitan Debug yang diluncurkan dari aplikasi konsol, yang ~ 6 kali lebih lambat daripada Release. Ingatlah ini.
Atau, Anda dapat melakukan perakitan eksternal dan menambah Unity DLL dengan perakitan, tetapi ini sangat menyulitkan perakitan dan pengeditan proyek.Segera membawa proses pembuatan ke Tugas terpisah dengan kode yang berisi ini:circuitGenerator.Generate (garis, maxElementsInLine, availableLogicalElements, useNOT, modifikasi) .ToList ()Sudah lebih baik, editor tidak menggantung pada saat generasi. Tetapi masih perlu menunggu lama, selama beberapa menit (lebih dari 20 menit pada sirkuit berukuran besar). Plus, ada masalah bahwa tugas tidak dapat diselesaikan dengan mudah dan terus bekerja sampai generasi selesai.Banyak kode internal static class Ext { public static IEnumerable<CircuitVariant> OrderVariants(this IEnumerable<CircuitVariant> circuitVariants) { return circuitVariants.OrderBy(a => a.Solutions.Count()) .ThenByDescending(a => a.Solutions .Select(b => b.Sum(i => i ? 1 : -1)) .OrderByDescending(b=>b) .First()); } } public interface IEditorGenerator : IDisposable { CircuitVariant[] FilteredVariants { get; } int LastPage { get; } void FilterVariants(int page); void Start(int lines, int maxElementsInLine, ICollection<int> availableGates, bool useNOT, StructureModification? modification, int maxSolutions); void Stop(); void Fetch(); } public class EditorGenerator : IEditorGenerator { private const int PageSize = 100; private readonly ICircuitGenerator circuitGenerator; private ConcurrentBag<CircuitVariant> variants; private List<CircuitVariant> sortedVariants; private Thread generatingThread; public EditorGenerator(ICircuitGenerator circuitGenerator) { this.circuitGenerator = circuitGenerator; } public void Dispose() { generatingThread?.Abort(); } public CircuitVariant[] FilteredVariants { get; private set; } public int LastPage { get; private set; } public void FilterVariants(int page) { CheckVariants(); if (sortedVariants == null) { Fetch(); } FilteredVariants = sortedVariants.Skip(page * PageSize) .Take(PageSize) .ToArray(); int count = sortedVariants.Count; LastPage = count % PageSize == 0 ? (count / PageSize) - 1 : count / PageSize; } public void Fetch() { CheckVariants(); sortedVariants = variants.OrderVariants() .ToList(); } public void Start(int lines, int maxElementsInLine, ICollection<int> availableGates, bool useNOT, StructureModification? modification, int maxSolutions) { if (generatingThread != null) { Stop(); } variants = new ConcurrentBag<CircuitVariant>(); generatingThread = new Thread(() => { var v = circuitGenerator.Generate(lines, maxElementsInLine, availableGates, useNOT, modification, maxSolutions); foreach (var item in v) { variants.Add(item); } }); generatingThread.Start(); } public void Stop() { generatingThread?.Abort(); sortedVariants = null; variants = null; generatingThread = null; FilteredVariants = null; } private void CheckVariants() { if (variants == null) { throw new InvalidOperationException("VariantsGeneration is not started. Use Start before."); } } ~EditorGenerator() { generatingThread.Abort(); } }
Idenya adalah bahwa latar belakang harus dihasilkan, dan berdasarkan permintaan, daftar internal opsi yang diurutkan akan diperbarui. Kemudian Anda dapat halaman demi halaman untuk memilih opsi. Jadi, tidak perlu menyortir setiap waktu, yang secara signifikan mempercepat pekerjaan pada daftar besar. Skema diurutkan berdasarkan “ketertarikan”: oleh jumlah solusi, oleh peningkatan, dan oleh bagaimana berbagai nilai diperlukan untuk solusi. Artinya, sirkuit dengan solusi 1 1 1 1 kurang menarik daripada 1 0 1 1.
Jadi, ternyata, tanpa menunggu akhir generasi, untuk memilih sirkuit untuk level. Kelebihan lainnya adalah karena pagination, editor tidak melambat seperti ternak.Fitur Unity sangat mengganggu ketika Anda mengklik Play, isi jendela diatur ulang, seperti semua data yang dihasilkan. Jika mereka mudah serial, mereka bisa disimpan sebagai file. Dengan cara ini, Anda bahkan dapat men-cache hasil generasi. Namun sayangnya, membuat struktur berseri yang rumit di mana objek saling merujuk adalah sulit.Selain itu, saya menambahkan garis ke setiap gerbang, seperti if (Input.Length == 2) { return Input[0].Value && Input[1].Value; }
Yang sangat meningkatkan kinerja.Solver
IsiKetika Anda membuat sirkuit di editor, Anda harus dapat dengan cepat memahami apakah itu sedang diselesaikan dan berapa banyak solusi yang dimilikinya. Untuk melakukan ini, saya membuat jendela "solver". Ini memberikan solusi untuk skema saat ini dalam bentuk teks.
Logikanya dari "backend": public string GetSourcesLabel() { var sourcesMb = sceneObjectsHelper.FindObjectsOfType<SourceMb>().OrderBy(s => s.name); var sourcesLabelSb = new StringBuilder(); foreach (var item in sourcesMb) { sourcesLabelSb.Append($"{item.name.Replace("Source", "Src")}\t"); } return sourcesLabelSb.ToString(); } public IEnumerable<bool[]> FindSolutions() { var elementsMb = sceneObjectsHelper.FindObjectsOfType<IElectricalElementMbEditor>(); elementsConfigurator.Configure(elementsMb); var root = sceneObjectsHelper.FindObjectOfType<FinalDevice>(); if (root == null) { throw new InvalidOperationException("No final device in scene"); } var sourcesMb = sceneObjectsHelper.FindObjectsOfType<SourceMb>().OrderBy(s => s.name); var sources = sourcesMb.Select(mb => (Source) mb.Element).ToArray(); return solver.GetSolutions(root.Element, sources); }
Berguna
IsiAsserthelp
IsiUntuk memverifikasi bahwa nilai-nilai diatur dalam aset, saya menggunakan metode ekstensi yang saya panggil di OnEnable public static class AssertHelper { public static void AssertType(this IElectricalElementMbEditor elementMbEditor, ElectricalElementType expectedType) { if (elementMbEditor.Type != expectedType) { Debug.LogError($"Field for {expectedType} require element with such type, but given element is {elementMbEditor.Type}"); } } public static void AssertNOTNull<T>(this T obj, string fieldName = "") { if (obj == null) { if (string.IsNullOrEmpty(fieldName)) { fieldName = $"of type {typeof(T).Name}"; } Debug.LogError($"Field {fieldName} is not installed"); } } public static string AssertNOTEmpty(this string str, string fieldName = "") { if (string.IsNullOrWhiteSpace(str)) { Debug.LogError($"Field {fieldName} is not installed"); } return str; } public static string AssertSceneCanBeLoaded(this string name) { if (!Application.CanStreamedLevelBeLoaded(name)) { Debug.LogError($"Scene {name} can't be loaded."); } return name; } }
Memverifikasi bahwa adegan memiliki kemampuan untuk dimuat mungkin terkadang gagal, meskipun adegan mungkin dimuat. Mungkin ini adalah bug di Unity.Contoh penggunaan: mainMenuSceneName.AssertNOTEmpty(nameof(mainMenuSceneName)).AssertSceneCanBeLoaded(); levelNamePrefix.AssertNOTEmpty(nameof(levelNamePrefix)); editorElementsPrefabs.AssertNOTNull(); not.AssertType(ElectricalElementType.NOT);
SceneObjectsHelper
IsiUntuk bekerja dengan elemen adegan, kelas SceneObjectsHelper juga berguna:Banyak kode namespace Circuit.Game.Utility { public interface ISceneObjectsHelper { T[] FindObjectsOfType<T>(bool includeDisabled = false) where T : class; T FindObjectOfType<T>(bool includeDisabled = false) where T : class; T Instantiate<T>(T prefab) where T : Object; void DestroyObjectsOfType<T>(bool includeDisabled = false, bool immediate = false) where T : class; void Destroy<T>(T obj, bool immediate = false) where T : Object; void DestroyAllChildren(Transform transform); void Inject(object obj); T GetComponent<T>(GameObject obj) where T : class; } public class SceneObjectsHelper : ISceneObjectsHelper { private readonly DiContainer diContainer; public SceneObjectsHelper(DiContainer diContainer) { this.diContainer = diContainer; } public T GetComponent<T>(GameObject obj) where T : class { return obj.GetComponents<Component>().OfType<T>().FirstOrDefault(); } public T[] FindObjectsOfType<T>(bool includeDisabled = false) where T : class { if (includeDisabled) { return Resources.FindObjectsOfTypeAll(typeof(Object)).OfType<T>().ToArray(); } return Object.FindObjectsOfType<Component>().OfType<T>().ToArray(); } public void DestroyObjectsOfType<T>(bool includeDisabled = false, bool immediate = false) where T : class { var objects = includeDisabled ? Resources.FindObjectsOfTypeAll(typeof(Object)).OfType<T>().ToArray() : Object.FindObjectsOfType<Component>().OfType<T>().ToArray(); foreach (var item in objects) { if (immediate) { Object.DestroyImmediate((item as Component)?.gameObject); } else { Object.Destroy((item as Component)?.gameObject); } } } public void Destroy<T>(T obj, bool immediate = false) where T : Object { if (immediate) { Object.DestroyImmediate(obj); } else { Object.Destroy(obj); } } public void DestroyAllChildren(Transform transform) { int childCount = transform.childCount; for (int i = 0; i < childCount; i++) { Destroy(transform.GetChild(i).gameObject); } } public T FindObjectOfType<T>(bool includeDisabled = false) where T : class { if (includeDisabled) { return Resources.FindObjectsOfTypeAll(typeof(Object)).OfType<T>().FirstOrDefault(); } return Object.FindObjectsOfType<Component>().OfType<T>().FirstOrDefault(); } public void Inject(object obj) { diContainer.Inject(obj); } public T Instantiate<T>(T prefab) where T : Object { var obj = Object.Instantiate(prefab); if (obj is Component) { var components = ((Component) (object) obj).gameObject.GetComponents<Component>(); foreach (var component in components) { Inject(component); } } else { Inject(obj); } return obj; } } }
Di sini, beberapa hal mungkin tidak terlalu efektif di mana kinerja tinggi diperlukan, tetapi mereka jarang memanggil saya dan tidak membuat pengaruh apa pun. Tetapi mereka memungkinkan Anda menemukan objek melalui antarmuka, misalnya, yang terlihat cukup cantik.Coroutinestarter
IsiMeluncurkan Coroutine hanya dapat MonoBehaviour. Jadi saya membuat kelas CoroutineStarter dan mendaftarkannya dalam konteks adegan. public interface ICoroutineStarter { void BeginCoroutine(IEnumerator routine); } public class CoroutineStarter : MonoBehaviour, ICoroutineStarter { public void BeginCoroutine(IEnumerator routine) { StartCoroutine(routine); } }
Selain kenyamanan, pengenalan alat-alat semacam itu memudahkan pengujian secara otomatis. Misalnya, pelaksanaan coroutine dalam tes: coroutineStarter.When(x => x.BeginCoroutine(Arg.Any<IEnumerator>())).Do(info => { var a = (IEnumerator) info[0]; while (a.MoveNext()) { } });
Gizmo
IsiUntuk kenyamanan menampilkan elemen yang tidak terlihat, saya sarankan menggunakan gambar alat yang hanya terlihat di tempat kejadian. Mereka membuatnya mudah untuk memilih elemen yang tidak terlihat dengan klik. Koneksi elemen juga dibuat dalam bentuk garis: private void OnDrawGizmos() { if (outputConnectorMb != null) { Handles.DrawLine(transform.position, outputConnectorMb.Transform.position); } }

Pengujian
Konten yangsaya inginkan untuk mendapatkan hasil maksimal dari pengujian otomatis, karena tes digunakan sedapat mungkin dan mudah digunakan.Untuk tes Unit, merupakan kebiasaan untuk menggunakan objek tiruan alih-alih kelas yang mengimplementasikan antarmuka tempat kelas tes bergantung. Untuk ini, saya menggunakan perpustakaan NSubstitute . Apa yang sangat menyenangkan.Unity tidak mendukung NuGet, jadi saya harus mendapatkan DLL secara terpisah, kemudian assembly, karena ketergantungan ditambahkan ke file AssemblyDefinition dan digunakan tanpa masalah.
Untuk pengujian otomatis, Unity menawarkan TestRunner, yang bekerja dengan kerangka uji NUnit yang sangat populer . Dari sudut pandang TestRunner, ada dua jenis tes:- EditMode — , . Nunit . , . GameObject Monobehaviour . , EditMode .
- PlayMode — .
EditMode Dalam pengalaman saya, ada banyak ketidaknyamanan dan perilaku aneh dalam mode ini. Namun demikian, mereka nyaman untuk secara otomatis memeriksa kesehatan aplikasi secara keseluruhan. Mereka juga memberikan verifikasi jujur untuk kode dalam metode seperti Mulai, Perbarui, dan sejenisnya.Tes PlayMode dapat digambarkan sebagai tes NUnit normal, tetapi ada alternatif lain. Di PlayMode, Anda mungkin perlu menunggu beberapa saat atau sejumlah bingkai. Untuk melakukan ini, tes harus dijelaskan dengan cara yang mirip dengan Coroutine. Nilai yang dikembalikan harus IEnumerator / IEnumerable dan di dalam, untuk melewati waktu, Anda harus menggunakan, misalnya: yield return null;
atau
yield return new WaitForSeconds(1);
Ada nilai pengembalian lainnya.Tes semacam itu perlu mengatur atribut UnityTest . Ada jugaatribut UnitySetUp dan UnityTearDown yang Anda perlukan untuk menggunakan pendekatan yang sama.Saya, pada gilirannya, berbagi tes EditMode untuk Modular dan Integrasi.Tes unit hanya menguji satu kelas dalam isolasi lengkap dari kelas lain. Tes semacam itu seringkali memudahkan untuk mempersiapkan lingkungan untuk kelas yang diuji dan kesalahan, ketika lulus, memungkinkan Anda untuk melokalisasi masalah dengan lebih akurat.Dalam tes unit, saya menguji banyak kelas Core dan kelas yang dibutuhkan langsung dalam permainan.Tes elemen rangkaian sangat mirip, jadi saya membuat kelas dasar public class ElectricalElementTestsBase<TElement> where TElement : ElectricalElementBase, IElectricalElement, new() { protected TElement element; protected IInputConnector mInput1; protected IInputConnector mInput2; protected IInputConnector mInput3; protected IInputConnector mInput4; [OneTimeSetUp] public void Setup() { element = new TElement(); mInput1 = Substitute.For<IInputConnector>(); mInput2 = Substitute.For<IInputConnector>(); mInput3 = Substitute.For<IInputConnector>(); mInput4 = Substitute.For<IInputConnector>(); } protected void GetValue_3Input(bool input1, bool input2, bool input3, bool expectedOutput) {
Tes elemen lebih lanjut terlihat seperti ini: public class AndTests : ElectricalElementTestsBase<And> { [TestCase(false, false, false)] [TestCase(false, true, false)] [TestCase(true, false, false)] [TestCase(true, true, true)] public new void GetValue_2Input(bool input1, bool input2, bool output) { base.GetValue_2Input(input1, input2, output); } [TestCase(false, false)] [TestCase(true, true)] public new void GetValue_1Input(bool input, bool expectedOutput) { base.GetValue_1Input(input, expectedOutput); } }
Mungkin ini adalah komplikasi dalam hal kemudahan pemahaman, yang biasanya tidak perlu dalam tes, tetapi saya tidak ingin menyalin-menempelkan hal yang sama 11 kali.Ada juga tes GameManagers. Karena mereka memiliki banyak kesamaan, mereka juga mendapat kelas dasar tes. Manajer permainan di kedua mode harus memiliki beberapa fungsi yang identik dan beberapa yang berbeda. Hal-hal umum diuji dengan tes yang sama untuk setiap penerus dan perilaku spesifik diuji sebagai tambahan. Meskipun pendekatan acara, tidak sulit untuk menguji perilaku yang dilakukan oleh acara: [Test] public void FullHelpAgree_FinishLevel() {
Dalam tes integrasi, saya juga menguji kelas untuk editor, dan mengambilnya dari konteks statis wadah DI. Dengan demikian pengecekan termasuk injeksi yang benar, yang tidak kalah pentingnya dari pengujian unit. public class PlacerTests { [Inject] private ICircuitEditorPlacer circuitEditorPlacer; [Inject] private ICircuitGenerator circuitGenerator; [Inject] private IEditorSolver solver; [Inject] private ISceneObjectsHelper sceneObjectsHelper; [TearDown] public void TearDown() { sceneObjectsHelper.DestroyObjectsOfType<IElectricalElementMb>(immediate: true); } [OneTimeSetUp] public void Setup() { var container = StaticContext.Container; container.Inject(this); } [TestCase(1, 2)] [TestCase(2, 2)] [TestCase(3, 4)] public void PlaceSolve_And_NoModifications_AllVariantsSolved(int lines, int elementsInLine) { var variants = circuitGenerator.Generate(lines, elementsInLine, new List<int> {0}, false); foreach (var variant in variants) { circuitEditorPlacer.PlaceCircuit(variant); var solutions = solver.FindSolutions(); CollectionAssert.IsNOTEmpty(solutions); } } [TestCase(1, 2, StructureModification.Branching)] [TestCase(1, 2, StructureModification.ThroughLayer)] [TestCase(1, 2, StructureModification.All)] [TestCase(2, 2, StructureModification.Branching)] [TestCase(2, 2, StructureModification.ThroughLayer)] [TestCase(2, 2, StructureModification.All)] public void PlaceSolve_And_Modifications_AllVariantsSolved(int lines, int elementsInLine, StructureModification modification) { var variants = circuitGenerator.Generate(lines, elementsInLine, new List<int> {0}, false, modification); foreach (var variant in variants) { circuitEditorPlacer.PlaceCircuit(variant); var solutions = solver.FindSolutions(); CollectionAssert.IsNOTEmpty(solutions); } }
Tes ini menggunakan implementasi nyata dari semua dependensi dan juga menetapkan objek di atas panggung, yang sangat mungkin dalam tes EditMode. Memang benar untuk menguji bahwa itu menempatkan mereka waras - Saya punya sedikit ide tentang bagaimana, jadi saya memeriksa bahwa rangkaian yang diposting memiliki solusi.Dalam integrasi, ada juga tes untuk CircuitGenerator (StructureGenerator + VariantsGenerator) dan Solver public class CircuitGeneratorTests { private ICircuitGenerator circuitGenerator; private ISolver solver; [SetUp] public void Setup() { solver = new Solver(); var gates = new List<Func<IElectricalElement>> { () => new And(), () => new Or(), () => new Xor() }; var conductors = new List<Func<IElectricalElement>> { () => new Conductor(), () => new Not() }; var elements = Substitute.For<IElementsProvider>(); elements.Conductors.Returns(conductors); elements.Gates.Returns(gates); var structGenerator = new StructureGenerator(); var variantsGenerator = new VariantsGenerator(solver, elements); circuitGenerator = new CircuitGenerator(structGenerator, variantsGenerator); } [Test] public void Generate_2l_2max_ReturnsVariants() {
Tes PlayMode digunakan sebagai tes sistem. Mereka memeriksa cetakan, injeksi, dll. Pilihan yang baik adalah menggunakan adegan yang sudah jadi di mana tes hanya memuat dan menghasilkan beberapa interaksi. Tapi saya menggunakan adegan kosong yang sudah disiapkan untuk pengujian, di mana lingkungannya berbeda dari apa yang akan ada di dalam game. Ada upaya untuk menggunakan PlayMode untuk menguji seluruh proses permainan, seperti memasuki menu, memasuki level, dan sebagainya, tetapi pekerjaan tes ini ternyata tidak stabil, sehingga diputuskan untuk menunda itu nanti (tidak pernah).Lebih mudah menggunakan alat penilaian cakupan untuk menulis tes, tetapi sayangnya saya belum menemukan solusi yang bekerja dengan Unity.Saya menemukan masalah bahwa dengan Unity yang ditingkatkan ke 2018.3, tes mulai bekerja jauh lebih lambat, hingga 10 kali lebih lambat (dalam contoh sintetis). Proyek ini berisi 288 tes EditMode yang berjalan selama 11 detik, meskipun tidak ada yang dilakukan di sana begitu lama.Ringkasan Pengembangan
Cuplikan Layar Konten Tingkat GameLogika beberapa game dapat dirumuskan terlepas dari platform apa pun. Pada tahap awal, ini memberikan kemudahan pengembangan dan testability oleh autotests.DI nyaman. Bahkan dengan mempertimbangkan fakta bahwa Unity tidak memilikinya secara asli, sekrup di samping bekerja cukup lumayan.Unity memungkinkan Anda untuk secara otomatis menguji proyek. Benar, karena semua komponen GameObject bawaan tidak memiliki antarmuka dan hanya dapat digunakan secara langsung untuk mengejek hal-hal seperti Collider, SpriteRenderer, MeshRenderer, dll. tidak akan berhasil. Meskipun GetComponent memungkinkan Anda untuk mendapatkan komponen di antarmuka. Sebagai pilihan, tulis pembungkus Anda sendiri untuk semuanya.Menggunakan autotest menyederhanakan proses pembuatan logika awal, sementara tidak ada antarmuka pengguna ke kode. Tes beberapa kali menemukan kesalahan segera selama pengembangan. Secara alami, kesalahan muncul lebih lanjut, tetapi seringkali mungkin untuk menulis tes tambahan / memodifikasi yang sudah ada dan kemudian secara otomatis menangkapnya. Kesalahan dengan DI, cetakan, objek skrip dan sejenisnya, tes sulit untuk ditangkap, tetapi mungkin saja, karena Anda dapat menggunakan installer nyata untuk Zenject, yang akan memperketat dependensi, seperti yang terjadi pada build.Unity menghasilkan sejumlah besar kesalahan, macet. Seringkali kesalahan diselesaikan dengan me-restart editor. Menghadapi kehilangan aneh referensi ke objek di cetakan. Kadang-kadang cetakan dengan referensi menjadi hancur (ToString () mengembalikan "null"), meskipun semuanya terlihat berfungsi, cetakan tersebut diseret ke tempat kejadian dan tautannya tidak kosong. Terkadang beberapa koneksi terputus di semua adegan. Semuanya tampaknya diinstal, berfungsi, tetapi ketika beralih ke cabang lain, semua adegan rusak - tidak ada tautan di antara elemen-elemen.Untungnya, kesalahan ini sering diperbaiki dengan me-restart editor atau kadang-kadang menghapus folder Library.Secara total, sekitar setengah tahun telah berlalu dari ide menjadi publikasi di Google Play. Pengembangannya sendiri memakan waktu sekitar 3 bulan, dalam waktu luang dari pekerjaan utama.