Salute, Khabrovsk. Seperti yang sudah kami tulis, Januari kaya akan peluncuran baru dan hari ini kami mengumumkan satu set untuk kursus baru dari OTUS - "Pengembang Game untuk Persatuan" . Untuk mengantisipasi dimulainya kursus, kami membagikan terjemahan materi yang menarik kepada Anda.

Kami sedang membangun kembali inti Persatuan dengan Stack Tech Berorientasi Data kami. Seperti banyak studio game, kami juga melihat keuntungan besar dalam menggunakan Entity Component System (ECS), C # Task System (C # Job System) dan Burst Compiler. Di Unite Copenhagen, kami berkesempatan untuk mengobrol dengan Far North Entertainment dan mempelajari bagaimana mereka menerapkan fungsi DOTS ini dalam proyek-proyek tradisional Unity.
Far North Entertainment adalah studio Swedia yang dimiliki bersama oleh lima teman teknik. Sejak merilis Down to Dungeon untuk Gear VR pada awal 2018, perusahaan telah mengerjakan sebuah game yang termasuk genre klasik game PC, yaitu game pasca-apokaliptik dalam mode bertahan zombie. Apa yang membedakan proyek ini dari yang lain adalah jumlah zombie yang mengejar Anda. Visi tim dalam hal ini menarik ribuan zombie lapar mengikuti Anda dalam gerombolan besar.
Namun, mereka dengan cepat mengalami banyak masalah kinerja yang sudah pada tahap prototyping. Menciptakan, mati, memperbarui, dan menjiwai semua jumlah musuh ini tetap menjadi hambatan utama, bahkan setelah tim mencoba untuk menyelesaikan masalah dengan
penyatuan pooling dan
insting nimation .
Ini memaksa direktur teknis studio Andres Ericsson untuk mengalihkan perhatiannya ke DOTS dan mengubah pola pikir dari berorientasi objek menjadi berorientasi data. "Gagasan utama yang membantu membawa perubahan ini adalah bahwa Anda harus berhenti memikirkan objek dan hierarki objek dan mulai berpikir tentang data, bagaimana transformasi, dan bagaimana mengaksesnya," katanya . Kata-katanya berarti bahwa tidak perlu membangun arsitektur kode dengan memperhatikan objek kehidupan nyata sedemikian rupa sehingga memecahkan masalah yang paling umum dan abstrak. Dia memiliki banyak tip untuk mereka yang, seperti dia, dihadapkan dengan perubahan pandangan dunia:
“Tanyakan pada diri Anda apa masalah sebenarnya yang Anda coba selesaikan, dan data apa yang penting untuk mendapatkan solusi. Apakah Anda akan mengonversi data yang sama dengan cara yang sama berulang kali? Berapa banyak data berguna yang dapat Anda masukkan dalam satu baris cache prosesor? Jika Anda membuat perubahan pada kode yang ada, evaluasi berapa banyak data sampah yang Anda tambahkan ke baris cache. Apakah mungkin untuk membagi perhitungan menjadi beberapa utas atau apakah saya perlu menggunakan aliran perintah tunggal?Tim mulai memahami bahwa entitas dalam Sistem Komponen Persatuan hanyalah pengidentifikasi pencarian di aliran komponen. Komponen hanyalah data, sementara sistem mengandung semua logika dan menyaring entitas dengan tanda tangan tertentu, yang dikenal sebagai arketipe. “Saya pikir salah satu wawasan yang membantu kami memvisualisasikan ide-ide kami adalah memperkenalkan ECS sebagai database SQL. Setiap arketipe adalah tabel di mana setiap kolom adalah komponen, dan setiap baris adalah entitas yang unik. Intinya, Anda menggunakan sistem untuk membuat kueri untuk tabel pola dasar ini dan melakukan operasi pada entitas, ”kata Anders.
Memperkenalkan DOTS
Untuk mencapai pemahaman ini, ia mempelajari dokumentasi untuk sistem
Entity Component , contoh
ECS , dan
contoh yang kami lakukan bersama dengan Nordeus dan disajikan di Unite Austin. Informasi umum tentang arsitektur berorientasi data juga sangat membantu tim. "
Laporan Mike Acton tentang arsitektur data-sentris dengan CppCon 2014 adalah apa yang pertama kali membuka mata kita pada cara pemrograman ini."
Tim Far North menerbitkan apa yang mereka pelajari di
Blog Dev mereka, pada bulan September tahun ini mereka datang ke Kopenhagen untuk berbagi pengalaman mereka dengan transisi ke pendekatan berorientasi data di Unity.
Artikel ini didasarkan pada laporan, yang menjelaskan secara lebih rinci rincian penerapan ECS, Sistem Tugas C # dan kompiler Burst. Far North juga dengan ramah membagikan banyak contoh kode dari proyek mereka.
Organisasi Data Zombie
"Masalah yang kami hadapi adalah menginterpolasi perpindahan dan rotasi untuk ribuan objek di sisi klien," kata Anders. Pendekatan berorientasi objek awal mereka adalah untuk membuat skrip
ZombieView abstrak yang mewarisi kelas induk
EntityView generik.
EntityView adalah
MonoBehaviour yang dilampirkan ke
GameObject . Karena berfungsi sebagai representasi visual dari model game. Setiap
ZombieView bertanggung jawab untuk menangani pergerakan dan interpolasi rotasi sendiri dalam fungsi
Pembaruannya .
Ini kedengarannya normal, sampai Anda memahami bahwa setiap entitas berada dalam memori di tempat yang sewenang-wenang. Ini berarti bahwa jika Anda mengakses ribuan objek, CPU harus mengeluarkannya satu per satu, dan ini terjadi sangat lambat. Jika Anda meletakkan data Anda dalam blok-blok rapi yang disusun secara seri, prosesor dapat men-cache sejumlah besar data secara bersamaan. Sebagian besar prosesor modern dapat menerima sekitar 128 atau 256 bit dari cache dalam satu siklus.
Tim memutuskan untuk mengubah musuh menjadi DOTS dengan harapan menyelesaikan masalah kinerja sisi klien. Baris pertama adalah fungsi
Pembaruan di
ZombieView . Tim menentukan bagian mana yang harus dibagi ke dalam sistem yang berbeda dan menentukan data yang diperlukan. Hal pertama dan paling jelas adalah interpolasi posisi dan belokan, karena dunia game adalah kisi dua dimensi. Dua variabel float bertanggung jawab atas ke mana zombie pergi, dan komponen terakhir adalah posisi target, ia melacak posisi server untuk musuh.
[Serializable] public struct PositionData2D : IComponentData { public float2 Position; } [Serializable] public struct HeadingData2D : IComponentData { public float2 Heading; } [Serializable] public struct TargetPositionData : IComponentData { public float2 TargetPosition; }
Langkah selanjutnya adalah membuat pola dasar untuk musuh. Pola dasar adalah seperangkat komponen yang dimiliki entitas tertentu, dengan kata lain, itu adalah tanda tangan komponen.
Proyek ini menggunakan prefab untuk menentukan arketipe, karena musuh memerlukan lebih banyak komponen, dan beberapa dari mereka memerlukan tautan ke
GameObject . Ini berfungsi agar Anda bisa membungkus data komponen Anda di
ComponentDataProxy , yang akan mengubahnya menjadi
MonoBehaviour , yang pada gilirannya dapat dilampirkan ke cetakan. Saat Anda membuat sebuah instance menggunakan
EntityManager dan meneruskan prefab, itu membuat entitas dengan semua data komponen yang dilampirkan ke cetakan. Semua data komponen disimpan dalam potongan memori 16 kilobyte yang disebut
ArchetypeChunk .
Berikut adalah visualisasi tentang bagaimana aliran komponen akan diatur dalam potongan pola dasar kami:
“Salah satu keuntungan utama dari pola dasar potongan adalah Anda tidak perlu sering mengalokasikan banyak ketika membuat objek baru, karena memori telah dialokasikan sebelumnya. Ini berarti bahwa membuat entitas adalah menulis data ke akhir aliran komponen di dalam potongan pola dasar. Satu-satunya kasus ketika perlu untuk melakukan alokasi tumpukan lagi adalah ketika membuat entitas yang tidak sesuai dengan batas-batas potongan. Dalam hal ini, baik alokasi potongan baru dari pola dasar 16 KB dalam ukuran akan dimulai, atau jika ada fragmen kosong dari pola dasar yang sama, dapat digunakan kembali. Kemudian data untuk objek baru akan direkam dalam aliran komponen chunk baru, ” jelas Anders.
Multithreading zombie Anda
Sekarang setelah data dikemas dan ditempatkan dalam memori dengan cara yang mudah untuk caching, tim dapat dengan mudah menggunakan sistem tugas C # untuk menjalankan kode pada beberapa core CPU secara paralel.
Langkah selanjutnya adalah membuat sistem yang memfilter semua entitas dari semua blok pola dasar yang memiliki
komponen PositionData2D ,
HeadingData2D, dan
TargetPositionData .
Untuk melakukan ini, Anders dan timnya menciptakan
JobComponentSystem dan membangun permintaan mereka dalam fungsi
OnCreate . Itu terlihat seperti ini:
private EntityQuery m_Group; protected override void OnCreate() { base.OnCreate(); var query = new EntityQueryDesc { All = new [] { ComponentType.ReadWrite<PositionData2D>(), ComponentType.ReadWrite<HeadingData2D>(), ComponentType.ReadOnly<TargetPositionData>() }, }; m_Group = GetEntityQuery(query); }
Kode mengumumkan permintaan yang memfilter semua objek di dunia yang memiliki posisi, arah, dan tujuan. Selanjutnya, mereka ingin menjadwalkan tugas untuk setiap frame menggunakan sistem tugas C # untuk mendistribusikan perhitungan di beberapa alur kerja.
“Hal paling keren tentang sistem tugas C # adalah bahwa itu adalah sistem yang sama yang digunakan Unity dalam kodenya, jadi kami tidak perlu khawatir tentang thread yang dapat dieksekusi memblokir satu sama lain, membutuhkan inti prosesor yang sama dan menyebabkan masalah kinerja . ” Ucap Anders.
Tim memutuskan untuk menggunakan
IJobChunk , karena ribuan musuh menyiratkan adanya sejumlah besar potongan pola dasar yang harus sesuai dengan permintaan saat runtime.
IJobChunk mendistribusikan potongan yang benar di berbagai alur kerja.
Setiap frame, tugas
UpdatePositionAndHeadingJob baru
, bertanggung jawab untuk menangani interpolasi posisi dan putaran musuh dalam permainan.
Kode untuk tugas penjadwalan adalah sebagai berikut:
protected override JobHandle OnUpdate(JobHandle inputDeps) { var positionDataType = GetArchetypeChunkComponentType<PositionData2D>(); var headingDataType = GetArchetypeChunkComponentType<HeadingData2D>(); var targetPositionDataType = GetArchetypeChunkComponentType<TargetPositionData>(true); var updatePosAndHeadingJob = new UpdatePositionAndHeadingJob { PositionDataType = positionDataType, HeadingDataType = headingDataType, TargetPositionDataType = targetPositionDataType, DeltaTime = Time.deltaTime, RotationLerpSpeed = 2.0f, MovementLerpSpeed = 4.0f, }; return updatePosAndHeadingJob.Schedule(m_Group, inputDeps); }
Seperti inilah tugasnya:
public struct UpdatePositionAndHeadingJob : IJobChunk { public ArchetypeChunkComponentType<PositionData2D> PositionDataType; public ArchetypeChunkComponentType<HeadingData2D> HeadingDataType; [ReadOnly] public ArchetypeChunkComponentType<TargetPositionData> TargetPositionDataType; [ReadOnly] public float DeltaTime; [ReadOnly] public float RotationLerpSpeed; [ReadOnly] public float MovementLerpSpeed; }
Ketika pekerja thread mengambil tugas dari antriannya, itu memanggil inti dari tugas.
Begini inti eksekusi:
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) { var chunkPositionData = chunk.GetNativeArray(PositionDataType); var chunkHeadingData = chunk.GetNativeArray(HeadingDataType); var chunkTargetPositionData = chunk.GetNativeArray(TargetPositionDataType); for (int i = 0; i < chunk.Count; i++) { var target = chunkTargetPositionData[i]; var positionData = chunkPositionData[i]; var headingData = chunkHeadingData[i]; float2 toTarget = target.TargetPosition - positionData.Position; float distance = math.length(toTarget); headingData.Heading = math.select( headingData.Heading, math.lerp(headingData.Heading, math.normalize(toTarget), math.mul(DeltaTime, RotationLerpSpeed)), distance > 0.008 ); positionData.Position = math.select( target.TargetPosition, math.lerp( positionData.Position, target.TargetPosition, math.mul(DeltaTime, MovementLerpSpeed)), distance <= 1 ); chunkPositionData[i] = positionData; chunkHeadingData[i] = headingData; } }
“Anda mungkin memperhatikan bahwa kami menggunakan pilih daripada bercabang, ini memungkinkan kami untuk menyingkirkan efek yang disebut prediksi cabang salah. Fungsi pilih akan mengevaluasi kedua ekspresi dan memilih yang sesuai dengan kondisi, dan jika ekspresi Anda tidak begitu sulit untuk dihitung, saya akan merekomendasikan menggunakan pilih, karena seringkali lebih murah daripada menunggu CPU pulih dari prediksi cabang yang salah. " Anders.
Tingkatkan Produktivitas dengan Burst
Langkah terakhir dalam mengubah DOTS ke posisi musuh dan interpolasi saja adalah untuk mengaktifkan kompiler Burst. Tugas itu tampaknya cukup sederhana untuk Anders: "Karena data terletak di array yang berdekatan dan karena kami menggunakan perpustakaan matematika baru dari Unity, yang harus kami lakukan adalah menambahkan atribut
BurstCompile ke tugas kami."
[BurstCompile] public struct UpdatePositionAndHeadingJob : IJobChunk { public ArchetypeChunkComponentType<PositionData2D> PositionDataType; public ArchetypeChunkComponentType<HeadingData2D> HeadingDataType; [ReadOnly] public ArchetypeChunkComponentType<TargetPositionData> TargetPositionDataType; [ReadOnly] public float DeltaTime; [ReadOnly] public float RotationLerpSpeed; [ReadOnly] public float MovementLerpSpeed; }
Compiler Burst memberi kita Single Instruction Multiple Data (SIMD); instruksi mesin yang dapat bekerja dengan beberapa set data input dan membuat beberapa set data output hanya dengan satu instruksi. Ini membantu kami mengisi lebih banyak tempat di bus cache 128-bit dengan data yang benar. Kompiler Burst, dikombinasikan dengan komposisi data yang ramah terhadap cache dan sistem pekerjaan, memungkinkan tim untuk meningkatkan produktivitas secara signifikan. Ini adalah tabel yang mereka kompilasi dengan mengukur kinerja setelah setiap langkah konversi.

Ini berarti bahwa Far North sepenuhnya menyingkirkan masalah yang terkait dengan interpolasi posisi di sisi klien dan arah zombie. Data mereka sekarang disimpan dalam bentuk yang nyaman untuk caching, dan garis cache hanya diisi dengan data yang berguna. Beban didistribusikan ke semua core CPU, dan Burst compiler menghasilkan kode mesin yang sangat optimal dengan instruksi SIMD.
Far North Entertainment DOTS Tip dan Trik
- Mulai berpikir dalam hal aliran data, karena dalam ECS, entitas hanyalah indeks pencarian dalam aliran data komponen paralel.
- Bayangkan ECS sebagai basis data relasional di mana arketipe adalah tabel, komponen adalah kolom, dan entitas adalah indeks dalam tabel (baris).
- Atur data Anda menjadi array berurutan untuk menggunakan cache prosesor dan prefetch perangkat keras.
- Lupakan keinginan untuk membuat hierarki objek dan mencoba menemukan solusi bersama sebelum memahami masalah sebenarnya yang ingin Anda pecahkan.
- Pikirkan tentang pengumpulan sampah. Hindari tumpukan yang terlalu banyak di area yang kritis terhadap kinerja. Gunakan wadah asli Unity baru sebagai gantinya. Tapi hati-hati, Anda harus berurusan dengan pembersihan manual.
- Ketahuilah nilai abstraksi Anda, waspadalah terhadap overhead dalam menjalankan fungsi virtual.
- Gunakan semua core CPU dengan sistem tugas C #.
- Menganalisis tingkat perangkat keras. Apakah kompiler Burst benar-benar menghasilkan instruksi SIMD? Gunakan Burst Inspector untuk analisis.
- Berhenti membuang-buang baris cache kosong. Pikirkan pengemasan data ke dalam garis cache sebagai pengemasan data ke dalam paket UDP.
Saran utama yang ingin dibagikan Anders Ericsson adalah saran yang lebih umum untuk mereka yang proyeknya sedang dalam pengembangan:
“Cobalah untuk mengidentifikasi area spesifik dalam permainan Anda di mana Anda memiliki masalah kinerja dan lihat apakah Anda dapat menerapkan DOTS secara khusus di daerah terpencil ini. Anda tidak perlu mengubah seluruh basis kode! "Rencana masa depan
“Kami ingin menggunakan DOTS di area lain dari permainan kami, dan kami senang dengan pengumuman di Unite tentang animasi DOTS, Fisika Persatuan, dan Live Link. Kami ingin mempelajari cara mengubah lebih banyak objek game menjadi objek ECS, dan tampaknya Unity telah membuat kemajuan signifikan dalam mengimplementasikan ini, ”simpul Anders.
Jika Anda memiliki pertanyaan tambahan untuk tim Far North, kami sarankan Anda bergabung dengan
Perselisihan mereka!
Lihat daftar putar
Unite Copenhagen DOTS untuk mengetahui bagaimana studio game modern lainnya menggunakan DOTS untuk membuat game berkinerja tinggi yang hebat, dan bagaimana komponen berbasis DOTS seperti Fisika DOTS, Alur Kerja Konversi yang baru, dan kompiler Burst bekerja bersama.
Terjemahan telah berakhir, dan kami
mengundang Anda untuk menghadiri webinar gratis , di mana kami akan memberi tahu Anda cara membuat penembak zombie Anda sendiri dalam satu jam .