Unity3D ECS dan Sistem Pekerjaan

Dengan Unity3D, dengan rilis versi 2018, menjadi mungkin untuk menggunakan sistem ECS asli (untuk Unity), dibumbui dengan multi-threading dalam bentuk Sistem Pekerjaan. Tidak ada banyak materi di Internet (beberapa proyek dari Unity Technologies sendiri dan beberapa video pelatihan di YouTube). Saya mencoba untuk mewujudkan skala dan kenyamanan ECS, membuat proyek kecil tidak keluar dari kubus dan tombol. Sebelum itu, saya tidak punya pengalaman dalam merancang ECS, jadi butuh dua hari untuk mempelajari materi dan membangun kembali pemikiran dengan OOP, satu hari untuk menikmati pendekatan, dan satu atau dua hari untuk mengembangkan proyek, melawan Unity, mencabut sampel rambut dan merokok. . Artikel ini berisi sedikit teori dan contoh proyek kecil.


Arti ECS cukup sederhana - entitas ( Entity ) dengan komponennya ( Component ), yang diproses oleh sistem ( System ).

Esensi


Entitas tidak memiliki logika dan hanya menyimpan komponen (sangat mirip dengan GameObject dalam pendekatan BPK lama). Di Unity ECS, kelas Entity ada untuk ini.

Komponen


Komponen hanya menyimpan data, dan terkadang tidak mengandung apa pun dan merupakan penanda sederhana untuk diproses oleh sistem. Tetapi mereka tidak memiliki logika. Diwarisi dari ComponentDataWrapper. Itu bisa diproses oleh utas lain (tapi ada nuansa).

Sistem


Sistem bertanggung jawab untuk memproses komponen. Pada input, mereka menerima dari Unity daftar komponen yang diproses untuk jenis yang diberikan, dan dalam metode kelebihan beban (analog dari Pembaruan, Mulai, OnDestroy) keajaiban mekanika permainan terjadi. Diwarisi dari ComponentSystem atau JobComponentSystem.

Sistem pekerjaan


Mekanika sistem yang memungkinkan pemrosesan paralel komponen. Dalam sistem OnUpdate, struktur Pekerjaan dibuat dan ditambahkan ke pemrosesan. Dalam kebosanan dan sumber daya gratis, Unity akan memproses dan menerapkan hasilnya pada komponen.

Multithreading dan Persatuan 2018


Semua pekerjaan Sistem Pekerjaan berlangsung di utas lainnya, dan komponen standar (Transform, Rigidbody, dll.) Tidak dapat diubah di utas apa pun kecuali yang utama. Oleh karena itu, dalam paket standar terdapat komponen "penggantian" yang kompatibel - Komponen Posisi, Komponen Rotasi, Komponen Pemberi Instansi Mesh.

Hal yang sama berlaku untuk struktur standar seperti Vector3 atau Quaternion. Komponen untuk paralelisasi hanya menggunakan tipe data yang paling sederhana (float3, float4, itu saja, programmer grafis akan senang), ditambahkan dalam namespace Unity.Mathematics, ada juga kelas matematika untuk memprosesnya. Tanpa string, tanpa tipe referensi, hanya hardcore.

"Tunjukkan kodenya"


Jadi, saatnya memindahkan sesuatu!

Buat komponen yang menyimpan nilai kecepatan, dan juga merupakan salah satu penanda untuk sistem yang memindahkan objek. Atribut Serializable memungkinkan Anda untuk mengatur dan melacak nilai di inspektur.

Penentu kecepatan
[Serializable] public struct SpeedData : IComponentData { public int Value; } public class SpeedComponent : ComponentDataWrapper<SpeedData> {} 


Menggunakan atribut Inject, sistem memperoleh struktur yang mengandung komponen hanya dari entitas-entitas di mana ketiga komponen hadir. Jadi, jika beberapa entitas memiliki komponen PositionComponent dan SpeedComponent, tetapi tidak RotationComponent, maka entitas ini tidak akan ditambahkan ke struktur yang memasuki sistem. Dengan demikian, dimungkinkan untuk menyaring entitas dengan kehadiran komponen.

Sistem Gerakan
 public class MovementSystem : ComponentSystem { public struct ShipsPositions { public int Length; public ComponentDataArray<Position> Positions; public ComponentDataArray<Rotation> Rotations; public ComponentDataArray<SpeedData> Speeds; } [Inject] ShipsPositions _shipsMovementData; protected override void OnUpdate() { for(int i = 0; i < _shipsMovementData.Length; i++) { _shipsMovementData.Positions[i] = new Position(_shipsMovementData.Positions[i].Value + math.forward(_shipsMovementData.Rotations[i].Value) * Time.deltaTime * _shipsMovementData.Speeds[i].Value); } } } 


Sekarang semua benda yang mengandung ketiga komponen ini akan bergerak maju dengan kecepatan yang diberikan.

Wiiiii


Itu mudah. Meskipun butuh satu hari untuk memikirkan ECS.

Tapi berhentilah. Di mana Sistem Pekerjaan di sini?

Faktanya adalah bahwa tidak ada yang cukup rusak untuk menggunakan multithreading. Waktunya istirahat!

Saya menarik dari sampel sistem yang menimbulkan cetakan. Dari menarik - di sini adalah sepotong kode:

Spawner
 EntityManager.Instantiate(prefab, entities); for (int i = 0; i < count; i++) { var position = new Position { Value = spawnPositions[i] }; EntityManager.SetComponentData(entities[i], position); EntityManager.SetComponentData(entities[i], new SpeedData { Value = Random.Range(15, 25) }); } 


Jadi, mari kita letakkan 1000 objek. Masih terlalu bagus untuk instantiating jerat pada GPU. 5000 - juga sekitar Saya akan menunjukkan apa yang terjadi dengan 50.000 objek.

Entity Debugger telah muncul di Unity, menunjukkan berapa banyak ms yang diambil setiap sistem. Sistem dapat dihidupkan / dimatikan tepat di runtime, untuk melihat objek apa yang mereka proses, secara umum, hal yang tidak tergantikan.

Dapatkan bola seperti pesawat ruang angkasa


Alat ini merekam dengan kecepatan 15 fps, jadi seluruh titiknya ada dalam angka dalam daftar sistem. Milik kami, MovementSystem, mencoba untuk memindahkan semua 50.000 objek di setiap frame, dan melakukannya rata-rata dalam 60 ms. Jadi, sekarang gim ini cukup rusak untuk optimasi.
Kami kencangkan JobSystem ke sistem pergerakan.

Sistem Gerakan yang Dimodifikasi
 public class MovementSystem : JobComponentSystem { [ComputeJobOptimization] struct MoveShipJob : IJobProcessComponentData<Position, Rotation, SpeedData> { public float dt; public void Execute(ref Position position, ref Rotation rotation, ref SpeedData speed) { position.Value += math.forward(rotation.Value) * dt * speed.Value; } } protected override JobHandle OnUpdate(JobHandle inputDeps) { var job = new MoveShipJob { dt = Time.deltaTime }; return job.Schedule(this, 1, inputDeps); } } 


Sekarang sistem mewarisi dari JobComponentSystem dan di setiap frame menciptakan handler khusus dimana Unity mentransfer 3 komponen dan deltaTime yang sama dari sistem.

Luncurkan pesawat ruang angkasa lagi


0,15 ms (0,4 di puncak, ya) versus 50-70! 50 ribu benda! Saya memasukkan angka-angka ini dalam kalkulator, sebagai tanggapan dia menunjukkan wajah bahagia.

Manajemen


Anda bisa melihat bola terbang tanpa henti, atau Anda bisa terbang di antara kapal-kapal.
Butuh sistem taxi.

Komponen Rotation sudah ada di prefab, buat komponen untuk menyimpan kontrol.

Controlcompponent
 [Serializable] public struct RotationControlData : IComponentData { public float roll; public float pitch; public float yaw; } public class ControlComponent : ComponentDataWrapper<RotationControlData>{} 


Kami juga membutuhkan komponen pemain (meskipun itu tidak masalah untuk mengarahkan semua 50k kapal sekaligus)

Komponen Player
 public struct PlayerData : IComponentData { } public class PlayerComponent : ComponentDataWrapper<PlayerData> { } 


Dan segera, pembaca input pengguna.

Sistem UserControlSystem
 public class UserControlSystem : ComponentSystem { public struct InputPlayerData { public int Length; [ReadOnly] public ComponentDataArray<PlayerData> Data; public ComponentDataArray<RotationControlData> Controls; } [Inject] InputPlayerData _playerData; protected override void OnUpdate() { for (int i = 0; i < _playerData.Length; i++) { _playerData.Controls[i] = new RotationControlData { roll = Input.GetAxis("Horizontal"), pitch = Input.GetAxis("Vertical"), yaw = Input.GetKey(KeyCode.Q) ? -1 : Input.GetKey(KeyCode.E) ? 1 : 0 }; } } } 


Alih-alih Input standar, mungkin ada sepeda atau AI favorit.

Dan akhirnya, memproses kontrol dan gilirannya itu sendiri. Saya dihadapkan dengan fakta bahwa math.euler belum diimplementasikan, jadi serangan cepat di Wikipedia menyelamatkan saya dari konversi dari sudut Euler ke angka empat.

ProcessRotationInputSystem
 public class ProcessRotationInputSystem : JobComponentSystem { struct LocalRotationSpeedGroup { public ComponentDataArray<Rotation> rotations; [ReadOnly] public ComponentDataArray<RotationSpeedData> rotationSpeeds; [ReadOnly] public ComponentDataArray<RotationControlData> controlData; public int Length; } [Inject] private LocalRotationSpeedGroup _rotationGroup; [ComputeJobOptimization] struct RotateJob : IJobParallelFor { public ComponentDataArray<Rotation> rotations; [ReadOnly] public ComponentDataArray<RotationSpeedData> rotationSpeeds; [ReadOnly] public ComponentDataArray<RotationControlData> controlData; public float dt; public void Execute(int i) { var speed = rotationSpeeds[i].Value; if (speed > 0.0f) { quaternion nRotation = math.normalize(rotations[i].Value); float yaw = controlData[i].yaw * speed * dt; float pitch = controlData[i].pitch * speed * dt; float roll = -controlData[i].roll * speed * dt; quaternion result = math.mul(nRotation, Euler(pitch, roll, yaw)); rotations[i] = new Rotation { Value = result }; } } quaternion Euler(float roll, float yaw, float pitch) { float cy = math.cos(yaw * 0.5f); float sy = math.sin(yaw * 0.5f); float cr = math.cos(roll * 0.5f); float sr = math.sin(roll * 0.5f); float cp = math.cos(pitch * 0.5f); float sp = math.sin(pitch * 0.5f); float qw = cy * cr * cp + sy * sr * sp; float qx = cy * sr * cp - sy * cr * sp; float qy = cy * cr * sp + sy * sr * cp; float qz = sy * cr * cp - cy * sr * sp; return new quaternion(qx, qy, qz, qw); } } protected override JobHandle OnUpdate(JobHandle inputDeps) { var job = new RotateJob { rotations = _rotationGroup.rotations, rotationSpeeds = _rotationGroup.rotationSpeeds, controlData = _rotationGroup.controlData, dt = Time.deltaTime }; return job.Schedule(_rotationGroup.Length, 64, inputDeps); } } 


Anda mungkin akan bertanya mengapa Anda tidak bisa hanya mengirimkan 3 komponen sekaligus ke Job, seperti di MovementSystem? Karena Saya berjuang dengan ini untuk waktu yang lama, tetapi saya tidak tahu mengapa itu tidak berhasil seperti itu. Dalam sampel, belokan diimplementasikan melalui ComponentDataArray, tetapi kami tidak akan mundur dari kanon.

Kami melempar cetakan ke atas panggung, menggantung komponen, mengikat kamera, mengatur wallpaper membosankan, dan pergi!



Kesimpulan


Orang-orang dari Unity Technologies telah bergerak ke arah yang benar dari multithreading. Sistem Pekerjaan itu sendiri masih lembab (versi alfa adalah semua), tetapi cukup dapat digunakan dan mempercepat sekarang. Sayangnya, komponen standar tidak kompatibel dengan Sistem Pekerjaan (tetapi tidak dengan ECS secara terpisah!), Jadi Anda harus memahat kruk untuk menyiasatinya. Sebagai contoh, satu orang dari forum Unity mengimplementasikan sistem fisiknya untuk GPU, dan, seperti, membuat kemajuan.
ECS with Unity pernah digunakan sebelumnya, ada beberapa analog yang makmur, misalnya artikel dengan ikhtisar yang paling terkenal. Ini juga menjelaskan pro dan kontra dari pendekatan arsitektur ini.

Dari diri saya sendiri, saya dapat menambahkan nilai tambah sebagai kemurnian kode. Saya mulai dengan mencoba mengimplementasikan gerakan dalam satu sistem. Jumlah komponen ketergantungan tumbuh dengan cepat, dan saya harus membagi kode menjadi sistem yang kecil dan nyaman. Dan mereka dapat dengan mudah digunakan kembali di proyek lain.

Kode proyek ada di sini: GitHub

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


All Articles