Blitz Engine & Battle Prime: ECS dan Kode Jaringan



Battle Prime adalah proyek pertama dari studio kami. Terlepas dari kenyataan bahwa banyak anggota tim memiliki pengalaman yang layak dalam mengembangkan game, kami secara alami menghadapi berbagai kesulitan saat mengerjakannya. Mereka muncul baik dalam proses bekerja pada mesin dan dalam proses mengembangkan game itu sendiri.

Dalam industri gamedev, sejumlah besar pengembang yang rela berbagi cerita, praktik terbaik, keputusan arsitektur - dalam satu atau lain bentuk. Pengalaman ini, diletakkan di ruang publik dalam bentuk artikel, presentasi, dan laporan, merupakan sumber ide dan inspirasi yang sangat baik. Sebagai contoh, laporan tim pengembangan Overwatch sangat berguna bagi kami ketika bekerja pada mesin. Seperti permainan itu sendiri, mereka dibuat dengan sangat berbakat, dan saya menyarankan semua orang yang tertarik untuk melihatnya. Tersedia di lemari besi GDC dan di YouTube .

Ini adalah salah satu alasan mengapa kami juga ingin berkontribusi pada tujuan bersama - dan artikel ini adalah salah satu yang pertama ditujukan untuk rincian teknis pengembangan Mesin Blitz dan memainkannya - Battle Prime.

Artikel akan dibagi menjadi dua bagian:

  • ECS: penerapan pola Entity-Component-System di dalam Blitz Engine. Bagian ini penting untuk memahami contoh kode dalam artikel, dan dengan sendirinya merupakan topik menarik yang terpisah.
  • Netcode dan gameplay: semua yang terkait dengan bagian jaringan tingkat tinggi dan penggunaannya di dalam game - arsitektur client-server, prediksi klien, replikasi. Salah satu hal terpenting dalam penembak adalah menembak, jadi lebih banyak waktu akan dikhususkan untuk itu.

Di bawah memotong banyak megabita gif!

Di dalam setiap bagian, di samping cerita tentang fungsionalitas dan penggunaannya, saya akan mencoba menggambarkan kekurangan yang ada pada dirinya sendiri - baik itu keterbatasannya, ketidaknyamanan dalam pekerjaan, atau hanya pemikiran tentang perbaikannya di masa depan.

Saya juga akan mencoba memberikan contoh kode dan beberapa statistik. Pertama, ini hanya menarik, dan kedua, ia memberikan sedikit konteks pada skala penggunaan fungsi dan proyek ini atau itu.

ECS


Di dalam mesin, kami menggunakan istilah "dunia" untuk menggambarkan adegan yang mengandung hierarki objek.

Dunia bekerja sesuai dengan templat Entity-Component-System ( deskripsi di Wikipedia ):

  • Entity - objek di dalam adegan. Ini adalah repositori untuk sekumpulan komponen. Objek dapat disarangkan, membentuk hierarki di dunia;
  • Komponen - adalah data yang diperlukan untuk pengoperasian semua mekanika, dan yang menentukan perilaku objek. Misalnya, `TransformComponent` berisi transformasi objek, dan` DynamicBodyComponent` berisi data untuk simulasi fisik. Beberapa komponen mungkin tidak memiliki data tambahan, keberadaannya yang sederhana di objek menggambarkan keadaan objek ini. Misalnya, dalam Battle Prime, `AliveComponent` dan` DeadComponent` digunakan, yang menandai karakter yang hidup dan mati, masing-masing;
  • Sistem - seperangkat fungsi yang disebut secara berkala yang mendukung solusi dari tugasnya. Dengan setiap panggilan, sistem memproses objek yang memenuhi beberapa kondisi (biasanya memiliki serangkaian komponen tertentu) dan, jika perlu, mengubahnya. Semua logika game dan sebagian besar engine diimplementasikan pada level sistem. Misalnya, di dalam mesin terdapat `LodSystem`, yang terlibat dalam menghitung indeks LOD (level detail) untuk objek berdasarkan transformasi di dunia dan data lainnya. Indeks ini yang terkandung dalam `LodComponent` kemudian digunakan oleh sistem lain untuk tugas-tugasnya.

Pendekatan ini memudahkan untuk menggabungkan mekanika yang berbeda dalam objek yang sama. Segera setelah entitas menerima data yang cukup untuk pekerjaan beberapa mekanik, sistem yang bertanggung jawab untuk mekanik ini mulai memproses objek ini.

Dalam praktiknya, menambahkan fungsional baru mengurangi ke komponen baru (atau set komponen) dan sistem baru (atau set sistem) yang mengimplementasikan fungsional ini. Dalam sebagian besar kasus, mengerjakan pola ini adalah hal yang mudah.

Refleksi


Sebelum melanjutkan ke deskripsi komponen dan sistem, saya akan membahas sedikit tentang mekanisme refleksi, karena akan sering digunakan dalam contoh kode.

Refleksi memungkinkan Anda menerima dan menggunakan informasi tentang jenis saat aplikasi sedang berjalan. Secara khusus, fitur berikut tersedia:

  • Dapatkan daftar jenis sesuai dengan kriteria tertentu (misalnya, pewaris kelas atau memiliki tag khusus),
  • Dapatkan daftar bidang kelas,
  • Dapatkan daftar metode di dalam kelas,
  • Dapatkan daftar nilai enum,
  • Panggil beberapa metode atau ubah nilai suatu bidang,
  • Dapatkan metadata bidang atau metode yang dapat digunakan untuk fungsional tertentu.

Banyak modul di dalam mesin menggunakan refleksi untuk keperluan mereka sendiri. Beberapa contoh:

  • Integrasi bahasa scripting menggunakan refleksi untuk bekerja dengan tipe yang dideklarasikan dalam kode C ++;
  • Editor menggunakan refleksi untuk mendapatkan daftar komponen yang dapat ditambahkan ke objek, serta untuk menampilkan dan mengedit bidangnya;
  • Modul jaringan menggunakan metadata bidang di dalam komponen untuk sejumlah fungsi: mereka menunjukkan parameter untuk mereplikasi bidang dari server ke klien, kuantisasi data selama replikasi, dan sebagainya;
  • Berbagai konfigurasi dideserialisasi menjadi objek dari tipe yang sesuai menggunakan refleksi.

Kami menggunakan implementasi kami sendiri, antarmuka yang tidak jauh berbeda dari solusi lain yang ada (misalnya, github.com/rttrorg/rttr ). Menggunakan contoh CapturePointComponent (yang menjelaskan titik tangkap untuk mode permainan), menambahkan refleksi ke jenisnya terlihat seperti ini:

//     class CapturePointComponent final : public Component { //            BZ_VIRTUAL_REFLECTION(Component); public: float points_to_own = 10.0f; String visible_name; // …   }; //   .cpp  BZ_VIRTUAL_REFLECTION_IMPL(CapturePointComponent) { //       ReflectionRegistrar::begin_class<CapturePointComponent>() [M<Serializable>(), M<Scriptable>(), M<DisplayName>("Capture point")] //      .field("points_to_own", &CapturePointComponent::points_to_own) [M<Serializable>(), M<DisplayName>("Points to own")] .field("visible_name", &CapturePointComponent::visible_name) [M<Serializable>(), M<DisplayName>("Name")] // …     } 

Saya ingin memberikan perhatian khusus pada metadata jenis, bidang, dan metode yang dideklarasikan menggunakan ekspresi

 M<T>() 

di mana `T` adalah jenis metadata (di dalam perintah kita cukup menggunakan istilah" meta ", di masa depan saya akan menggunakannya). Mereka digunakan oleh modul yang berbeda untuk keperluan mereka sendiri. Sebagai contoh, editor menggunakan `DisplayName` untuk menampilkan nama jenis dan bidang di dalam editor, dan modul jaringan menerima daftar semua komponen, dan di antaranya mencari kolom bertanda` Replicable` - mereka akan dikirim dari server ke klien.

Deskripsi komponen dan tambahannya ke objek


Setiap komponen adalah pewaris kelas dasar `Komponen` dan dapat menggambarkan dengan bantuan refleksi bidang yang digunakannya (jika perlu).

Beginilah cara `AvatarHitComponent` dideklarasikan dan dijelaskan di dalam game:

 /** Component that indicates avatar hit event. */ class AvatarHitComponent final : public Component { BZ_VIRTUAL_REFLECTION(Component); public: PlayerId source_id = NetConstants::INVALID_PLAYER_ID; PlayerId target_id = NetConstants::INVALID_PLAYER_ID; HitboxType hitbox_type = HitboxType::UNKNOWN; }; BZ_VIRTUAL_REFLECTION_IMPL(AvatarHitComponent) { ReflectionRegistrar::begin_class<AvatarHitComponent>() .ctor_by_pointer() .copy_ctor_by_pointer() .field("source_id", &AvatarHitComponent::source_id)[M<Replicable>()] .field("target_id", &AvatarHitComponent::target_id)[M<Replicable>()] .field("hitbox_type", &AvatarHitComponent::hitbox_type)[M<Replicable>()]; } 

Komponen ini menandai objek yang dibuat sebagai hasil dari pemain memukul pemain lain. Ini berisi informasi tentang acara ini, seperti pengidentifikasi pemain yang menyerang dan tujuannya, serta jenis kotak hit di mana serangan itu terjadi.
Sederhananya, objek ini dibuat di dalam sistem server dengan cara yang mirip:

 Entity hit_entity = world->create_entity(); auto* const avatar_hit_component = hit_entity.add<AvatarHitComponent>(); avatar_hit_component->source_id = source_player_id; avatar_hit_component->target_id = target_player_id; avatar_hit_component->hitbox_type = hitbox_type; //      //      // ... 

Objek dengan `AvatarHitComponent` kemudian digunakan oleh sistem yang berbeda: untuk memainkan suara memukul pemain, mengumpulkan statistik, melacak prestasi pemain, dan sebagainya.

Deskripsi sistem dan pekerjaannya


Suatu sistem adalah objek dengan tipe yang diwarisi dari `System`, yang berisi metode yang mengimplementasikan tugas tertentu. Sebagai aturan, satu metode sudah cukup. Beberapa metode diperlukan jika harus dilakukan pada titik waktu yang berbeda dalam kerangka yang sama.

Mirip dengan komponen yang menggambarkan bidangnya, setiap sistem menggambarkan metode yang harus dilakukan oleh dunia.

Sebagai contoh, ExplosiveSystem yang bertanggung jawab atas ledakan dinyatakan dan dijelaskan sebagai berikut:

 // System responsible for handling explosive components: // - tracking when they need to be exploded: by timer, trigger zone etc. // - destroying them on explosion and creating separate explosion entity class ExplosiveSystem final : public System { BZ_VIRTUAL_REFLECTION(System); public: ExplosiveSystem(World* world); private: void update(float dt); //    ,     // ... }; BZ_VIRTUAL_REFLECTION_IMPL(ExplosiveSystem) { ReflectionRegistrar::begin_class<ExplosiveSystem>()[M<SystemTags>("battle")] .ctor_by_pointer<World*>() .method("ExplosiveSystem::update", &ExplosiveSystem::update)[M<SystemTask>( TaskGroups::GAMEPLAY_END, ReadAccess::set< TimeSingleComponent, WeaponDescriptorComponent, BallisticComponent, ProjectileComponent, GrenadeComponent>(), WriteAccess::set<ExplosiveComponent>(), InitAccess::set<ExplosiveStatsComponent, LocalExplosionComponent, ServerExplosionComponent, EntityWasteComponent, ReplicationComponent, AbilityIdComponent, WeaponBaseStatsComponent, HitDamageStatsComponent, ClusterGrenadeStatsComponent>(), UpdateType::FIXED, Vector<TaskOrder>{ TaskOrder::before(FastName{ "ballistic_update" }) })]; } 

Data berikut ditunjukkan di dalam deskripsi sistem:

  • Tag yang menjadi milik sistem. Setiap dunia berisi satu set tag, dan pada mereka adalah sistem yang harus bekerja di dunia ini. Dalam hal ini, tag `battle` berarti dunia di mana pertempuran antara para pemain berlangsung. Contoh lain dari tag adalah `server` dan` client` (masing-masing sistem hanya berjalan di server atau klien) dan `render` (sistem hanya berjalan dalam mode GUI);
  • Grup tempat sistem ini dijalankan dan daftar komponen yang digunakan sistem ini - untuk menulis, membaca, dan membuat;
  • Jenis pembaruan - apakah sistem ini harus bekerja dalam pembaruan normal, pembaruan tetap atau lainnya;
  • Ketergantungan izin eksplisit antara sistem.

Informasi lebih lanjut tentang kelompok sistem, dependensi dan tipe pembaruan akan dijelaskan di bawah ini.

Metode yang dideklarasikan dipanggil oleh dunia pada waktu yang tepat untuk menjaga fungsionalitas sistem ini. Isi dari metode ini tergantung pada sistem, tetapi, sebagai aturan, ini adalah berjalan melalui semua objek yang memenuhi kriteria sistem ini, dan pembaruan selanjutnya. Misalnya, memperbarui `ExplosiveSystem` di dalam game adalah sebagai berikut:

 void ExplosiveSystem::update(float dt) { const auto* time_single_component = world->get<TimeSingleComponent>(); // Init new explosives for (Component* component : new_explosives_group->components) { auto* explosive_component = static_cast<ExplosiveComponent*>(component); init_explosive(explosive_component, time_single_component); } new_explosives_group->components.clear(); // Update all explosives for (ExplosiveComponent* explosive_component : explosives_group) { update_explosive(explosive_component, time_single_component, dt); } } 

Grup-grup dalam contoh di atas (`new_explosives_group` dan` explosives_group`) adalah wadah bantu yang menyederhanakan implementasi sistem. new_explosives_group adalah wadah dengan objek baru yang diperlukan untuk sistem ini dan yang belum pernah diproses, dan explosives_group adalah wadah dengan semua objek yang perlu diproses setiap frame. Dunia bertanggung jawab langsung untuk mengisi wadah-wadah ini. Penerimaan mereka oleh sistem terjadi di konstruktornya:

 ExplosiveSystem::ExplosiveSystem(World* world) : System(world) { // `explosives_group`        `ExplosiveComponent` explosives_group = world->acquire_component_group<ExplosiveComponent>(); // `new_explosives_group`        //  `ExplosiveComponent` -       new_explosives_group = explosive_group->acquire_component_group_on_add(); } 

Pembaruan dunia


Dunia, objek bertipe `Dunia`, masing-masing frame memanggil metode yang diperlukan dalam sejumlah sistem. Sistem mana yang akan dipanggil tergantung pada tipenya.

Bagian dari sistem setiap frame harus diperbarui (istilah "pembaruan normal" digunakan di dalam mesin) - tipe ini mencakup semua sistem yang memengaruhi rendering frame dan suara: animasi kerangka, partikel, UI, dan sebagainya. Bagian lain dieksekusi pada frekuensi tetap yang telah ditentukan (kami menggunakan istilah "pembaruan tetap", dan untuk jumlah pembaruan tetap per detik - FFPS) - mereka memproses sebagian besar logika gameplay dan semua yang perlu disinkronkan antara klien dan server - misalnya, bagian dari input pemain, pergerakan karakter, pemotretan, bagian dari simulasi fisik.



Frekuensi pelaksanaan pembaruan tetap harus seimbang - nilai yang terlalu kecil mengarah ke permainan yang tidak responsif (misalnya, input pemain diproses lebih jarang, dan karenanya dengan penundaan yang lebih lama), dan terlalu tinggi - untuk persyaratan kinerja yang luar biasa dari perangkat tempat aplikasi itu berjalan. Ini juga berarti bahwa semakin tinggi frekuensinya, semakin besar biaya kapasitas server (lebih sedikit pertempuran dapat bekerja secara bersamaan pada mesin yang sama).

Pada gif di bawah ini, dunia bekerja pada frekuensi 5 pembaruan tetap per detik. Anda dapat melihat penundaan antara menekan tombol W dan awal gerakan, serta penundaan antara melepaskan tombol dan menghentikan pergerakan karakter:

gambar

Di gif berikutnya, dunia bekerja pada frekuensi 30 pembaruan tetap per detik, yang memberikan kontrol yang jauh lebih responsif:

gambar

Saat ini, dalam pembaruan tetap Battle Prime dunia berjalan 31 kali per detik. Nilai "jelek" seperti itu dipilih secara khusus - ini dapat menyebabkan bug yang tidak akan ada dalam situasi lain ketika jumlah pembaruan per detik, misalnya, angka bulat atau kelipatan dari refresh rate layar.

Perintah Eksekusi Sistem


Salah satu hal yang mempersulit pekerjaan dengan ECS adalah tugas menjalankan sistem. Untuk konteks, pada saat penulisan, di klien Battle Prime selama pertempuran antara para pemain ada sistem 251 dan jumlah mereka hanya bertambah.

Sebuah sistem yang keliru dijalankan pada waktu yang salah dapat menyebabkan bug halus atau keterlambatan pengoperasian beberapa mekanik untuk satu frame (misalnya, jika sistem kerusakan bekerja di awal frame dan sistem penerbangan proyektil pada akhirnya, maka kerusakan dilakukan dengan penundaan satu frame).

Urutan eksekusi sistem dapat diatur dengan berbagai cara, misalnya:

  • Pemesanan eksplisit
  • Indikasi "prioritas" numerik sistem dan penyortiran berikutnya berdasarkan prioritas;
  • Secara otomatis membangun grafik dependensi antara sistem dan menginstalnya di tempat yang tepat dalam urutan eksekusi.

Saat ini, kami menggunakan opsi ketiga. Setiap sistem menunjukkan komponen mana yang digunakannya untuk membaca, mana untuk menulis, dan komponen mana yang dibuatnya. Kemudian, sistem secara otomatis diatur di antara mereka sendiri dalam urutan yang diperlukan:
  • Komponen pembacaan sistem A muncul setelah sistem menulis ke komponen A;
  • Sistem yang menulis atau membaca komponen B datang setelah sistem yang membuat komponen B;
  • Jika kedua sistem menulis ke komponen C, urutannya bisa apa saja (tetapi dapat ditentukan secara manual jika perlu).

Secara teori, solusi semacam itu meminimalkan kontrol atas urutan eksekusi, yang diperlukan hanyalah mengatur masker komponen untuk sistem. Dalam praktiknya, dengan pertumbuhan proyek, ini mengarah pada semakin banyak siklus antar sistem. Jika sistem-1 menulis ke komponen A, dan membaca komponen B, dan sistem-2 membaca komponen A dan menulis ke komponen B, ini merupakan siklus, dan harus diselesaikan secara manual. Seringkali, ada lebih dari dua sistem dalam satu siklus. Resolusi mereka membutuhkan waktu dan indikasi eksplisit tentang hubungan di antara mereka.

Karena itu, Blitz Engine memiliki "kelompok" sistem. Di dalam kelompok, sistem secara otomatis berbaris dalam urutan yang diinginkan (dan siklus masih diselesaikan secara manual), dan urutan kelompok diatur secara eksplisit. Keputusan ini merupakan persilangan antara pesanan yang sepenuhnya manual dan yang sepenuhnya otomatis, dan ukuran kelompok sangat memengaruhi efektivitasnya. Begitu grup menjadi terlalu besar, programmer lagi sering menemukan masalah loop di dalamnya.

Saat ini ada 10 grup di Battle Prime. Ini masih belum cukup, dan kami berencana untuk menambah jumlahnya dengan membangun urutan logis yang ketat di antara mereka, dan menggunakan konstruksi otomatis dari grafik di dalam masing-masing.

Indikasi komponen mana yang digunakan oleh sistem untuk menulis atau membaca juga akan memungkinkan di masa depan untuk secara otomatis mengelompokkan sistem menjadi "blok" yang akan dieksekusi secara paralel satu sama lain.

Di bawah ini adalah utilitas tambahan yang menampilkan daftar sistem dan dependensi di antara mereka di dalam masing-masing grup (grafik lengkap di dalam grup terlihat mengintimidasi). Warna oranye menunjukkan ketergantungan yang didefinisikan secara eksplisit antara sistem:

gambar

Komunikasi antara sistem dan konfigurasinya


Tugas-tugas yang dilakukan sistem dalam diri mereka sendiri, sampai taraf tertentu, bergantung pada hasil dari sistem lain. Sebagai contoh, suatu sistem yang memproses tabrakan dari dua objek tergantung pada simulasi fisika yang mendaftarkan tabrakan ini. Dan sistem kerusakan tergantung pada hasil sistem balistik, yang bertanggung jawab atas pergerakan kerang.

Cara paling sederhana dan paling jelas untuk berkomunikasi antar sistem adalah dengan menggunakan komponen. Satu sistem menambahkan hasil pekerjaannya ke dalam komponen, dan sistem kedua membaca hasil ini dari komponen dan memecahkan masalahnya berdasarkan pada mereka.

Pendekatan berbasis komponen mungkin tidak nyaman dalam beberapa kasus:

  • Bagaimana jika hasil dari sistem tidak terikat langsung ke beberapa objek? Misalnya, sistem yang mengumpulkan statistik pertempuran (jumlah tembakan, klik, kematian, dan sebagainya) - mengumpulkannya secara global, berdasarkan seluruh pertempuran;
  • Bagaimana jika sistem perlu dikonfigurasi dengan cara tertentu? Misalnya, sistem simulasi fisik perlu mengetahui jenis objek mana yang harus merekam tabrakan antara mereka dan yang tidak.

Untuk mengatasi masalah ini, kami menggunakan pendekatan yang kami pinjam dari tim pengembangan Overwatch - Komponen Tunggal.

Komponen tunggal adalah komponen yang ada di dunia dalam satu salinan dan diperoleh langsung dari dunia. Sistem dapat menggunakannya untuk menjumlahkan hasil pekerjaan mereka, yang kemudian digunakan oleh sistem lain, atau untuk mengkonfigurasi pekerjaan mereka.

Saat ini, proyek (modul mesin + permainan) memiliki sekitar 120 Komponen Tunggal yang digunakan untuk berbagai keperluan - mulai dari menyimpan data dunia global hingga konfigurasi sistem individual.

Pendekatan "Bersih"


Dalam bentuknya yang paling murni, pendekatan seperti itu terhadap sistem dan komponen membutuhkan ketersediaan data hanya di dalam komponen dan keberadaan logika hanya di dalam sistem. Menurut pendapat saya, dalam praktiknya, pembatasan ini jarang masuk akal untuk diamati secara ketat (meskipun perdebatan tentang hal ini masih diangkat secara berkala).

Argumen berikut yang mendukung pendekatan yang kurang "keras" dapat disorot:

  • Bagian dari kode harus dibagikan - dan dijalankan secara serempak dari sistem yang berbeda atau ketika mengatur beberapa properti komponen. Logika serupa dijelaskan secara terpisah. Sebagai bagian dari mesin, kami menggunakan istilah Utils. Misalnya, di dalam game `DamageUtils` berisi logika yang terkait dengan aplikasi kerusakan - yang dapat diterapkan dari sistem yang berbeda;
  • Tidak masuk akal untuk menyimpan data pribadi sistem di suatu tempat selain dari sistem ini sendiri - tidak ada yang akan membutuhkannya kecuali untuk itu, dan memindahkannya ke tempat lain tidak terlalu berguna. Ada pengecualian untuk aturan ini, yang terkait dengan fungsi prediksi klien - ini akan ditulis tentang bagian di bawah ini;
  • Berguna bagi komponen untuk memiliki sedikit logika - sebagian besar adalah getter dan setter pintar yang menyederhanakan bekerja dengan komponen.

Netcode


Battle Prime menggunakan arsitektur dengan server yang otoriter dan prediksi klien. Hal ini memungkinkan pemain untuk menerima umpan balik instan dari tindakan mereka bahkan pada ping tinggi dan paket kerugian, dan proyek secara keseluruhan - untuk meminimalkan kecurangan oleh pemain, karena server menentukan semua hasil simulasi di dalam pertempuran.

Semua kode di dalam proyek game dibagi menjadi tiga bagian:

  • Klien - sistem dan komponen yang hanya berfungsi pada klien. Ini termasuk hal-hal seperti UI, pemotretan otomatis dan interpolasi;
  • Server - sistem dan komponen yang hanya berfungsi di server. Misalnya, segala sesuatu yang berkaitan dengan kerusakan dan menelurkan karakter;
  • Umum - ini adalah semua yang berfungsi pada server dan klien. Secara khusus, semua sistem yang menghitung pergerakan karakter, keadaan senjata (jumlah putaran, cooldown) dan segala hal lain yang perlu diprediksi pada klien. Sebagian besar sistem yang bertanggung jawab atas efek visual juga umum - server secara opsional dapat diluncurkan dalam mode GUI (sebagian besar hanya untuk debugging).

Input pengguna (input)


Sebelum beralih ke detail replikasi dan prediksi pada klien, Anda harus memikirkan untuk bekerja dengan input di dalam mesin - detailnya akan penting di bagian di bawah ini.

Semua input dari pemain dibagi menjadi dua jenis: level rendah dan level tinggi:

  • Input tingkat rendah - ini adalah peristiwa dari perangkat input, seperti penekanan tombol, menyentuh layar, dan sebagainya. Masukan seperti itu jarang diproses oleh sistem permainan;
  • Input tingkat tinggi - adalah tindakan pengguna yang dilakukan olehnya dalam konteks permainan: tembakan, perubahan senjata, pergerakan karakter, dan sebagainya. Untuk tindakan tingkat tinggi seperti itu, kami menggunakan istilah `Aksi`. Selain itu, data tambahan dapat dikaitkan dengan tindakan - seperti arah gerakan atau indeks senjata yang dipilih. Sebagian besar sistem bekerja dengan Aksi.

Input tingkat tinggi dihasilkan berdasarkan pengikat dari input tingkat rendah, atau secara terprogram. Misalnya, aksi penembakan dapat diikat ke klik mouse, atau dapat dihasilkan oleh sistem yang bertanggung jawab untuk penembakan otomatis - segera setelah pemain membidik musuh, sistem ini menghasilkan bidikan aksi jika pengguna mengaktifkan pengaturan yang sesuai. Tindakan juga dapat dikirim oleh sistem UI: misalnya, dengan menekan tombol yang sesuai atau ketika menggerakkan joystick di layar. Sistem yang menyala tidak peduli bagaimana tindakan ini dibuat.

Tindakan yang terkait secara logis dikelompokkan bersama (objek bertipe `ActionSet`). Grup dapat terputus jika mereka tidak diperlukan dalam konteks saat ini - misalnya, di Battle Prime ada beberapa kelompok, di antaranya:

  • Tindakan untuk mengontrol pergerakan karakter,
  • Tindakan untuk menembakkan senjata otomatis,
  • Tindakan untuk menembakkan senjata semi-otomatis.

Dari dua kelompok terakhir, hanya satu yang aktif pada satu waktu, tergantung pada jenis senjata yang dipilih - mereka berbeda dalam bagaimana tindakan KEBAKARAN dihasilkan: ketika tombol ditekan (untuk senjata otomatis) atau hanya sekali ketika tombol ditekan (untuk senjata semi-otomatis )

Demikian pula, kelompok tindakan dibuat dan dikonfigurasi dalam permainan di dalam salah satu sistem:

 static const Map<FastName, ActionSet> action_sets = { { //     ControlModes::CHARACTER_MOVEMENT, ActionSet { { DigitalBinding{ ActionNames::JUMP, { { InputCode::KB_SPACE, DigitalState::just_pressed() } }, nullopt }, DigitalBinding{ ActionNames::MOVE, { { InputCode::KB_W, DigitalState::pressed() } }, ActionValue{ AnalogState{0.0f, 1.0f, 0.0f} } }, //    ... }, { AnalogBinding{ ActionNames::LOOK, InputCode::MOUSE_RELATIVE_POSITION, AnalogStateType::ABSOLUTE, AnalogStateBasis::LOGICAL, {} } //    ... } } }, { //       ControlModes::AUTOMATIC_FIRE, ActionSet { { // FIRE    ,      DigitalBinding{ ActionNames::FIRE, { { InputCode::MOUSE_LBUTTON, DigitalState::pressed() } }, nullopt }, //       ... } } }, { //       ControlModes::SEMI_AUTOMATIC_FIRE, ActionSet { { // FIRE          DigitalBinding{ ActionNames::FIRE, { { InputCode::MOUSE_LBUTTON, DigitalState::just_pressed() } }, nullopt }, //       ... } } } //   ... }; 

Battle Prime menggambarkan sekitar 40 aksi. Beberapa dari mereka hanya digunakan untuk debugging atau merekam klip.

Replikasi


Replikasi adalah proses mentransfer data dari server ke klien. Semua data ditransmisikan melalui objek di dunia:

  • Penciptaan dan penghapusan mereka,
  • Membuat dan menghapus komponen pada objek,
  • Ubah properti komponen.

Replikasi dikonfigurasikan menggunakan komponen yang sesuai. Misalnya, dengan cara yang sama, permainan mengatur replikasi senjata pemain:

 auto* replication_component = weapon_entity.add<ReplicationComponent>(); replication_component->enable_replication<WeaponDescriptorComponent>(Privacy::PUBLIC); replication_component->enable_replication<WeaponBaseStatsComponent>(Privacy::PUBLIC); replication_component->enable_replication<WeaponComponent>(Privacy::PRIVATE); replication_component->enable_replication<BallisticsStatsComponent>(Privacy::PRIVATE); // ...    

Untuk setiap komponen, privasi yang digunakan selama replikasi ditunjukkan. Komponen pribadi akan dikirim dari server hanya ke pemain yang memiliki senjata ini. Komponen publik akan dikirim ke semua orang. Dalam contoh ini, `WeaponDescriptorComponent` dan` WeaponBaseStatsComponent` bersifat publik - mereka berisi data yang diperlukan untuk tampilan yang benar dari pemain lain. Misalnya, indeks slot tempat senjata berada dan tipenya diperlukan untuk animasi. Komponen yang tersisa dikirim secara pribadi ke pemain yang memiliki senjata ini - parameter balistik cangkang, informasi tentang jumlah total ronde, mode pemotretan yang tersedia, dan sebagainya. Ada mode privasi yang lebih khusus: misalnya, Anda dapat mengirim komponen hanya ke sekutu atau hanya ke musuh.

Setiap komponen dalam uraiannya harus menunjukkan bidang mana yang harus direplikasi dalam komponen ini. Misalnya, semua bidang di dalam `WeaponComponent` ditandai sebagai` Replicable`:

 BZ_VIRTUAL_REFLECTION_IMPL(WeaponComponent) { ReflectionRegistrar::begin_class<WeaponComponent>() .ctor_by_pointer() .copy_ctor_by_pointer() .field("owner", &WeaponComponent::owner)[M<Replicable>()] .field("fire_mode", &WeaponComponent::fire_mode)[M<Replicable>()] .field("loaded_ammo", &WeaponComponent::loaded_ammo)[M<Replicable>()] .field("ammo", &WeaponComponent::ammo)[M<Replicable>()] .field("shooting_cooldown_end_ms", &WeaponComponent::shooting_cooldown_end_ms)[M<Replicable>()]; } 

Mekanisme ini sangat nyaman digunakan. Misalnya, di dalam sistem server, yang bertanggung jawab untuk "mengeluarkan" token dari lawan yang terbunuh (dalam mode permainan khusus), itu sudah cukup untuk menambah dan mengkonfigurasi `ReplicationComponent` pada token semacam itu. Ini terlihat seperti ini:

 for (const Component* component : added_dead_avatars->components) { Entity kill_token_entity = world->create_entity(); //           // ... //   auto* replication_component = kill_token_entity.add<ReplicationComponent>(); replication_component->enable_replication<TransformComponent>(Privacy::PUBLIC); replication_component->enable_replication<KillTokenComponent>(Privacy::PUBLIC); } 

Dalam contoh ini, simulasi fisik token selama kehilangan akan terjadi di server, dan transformasi akhir token akan dikirim dan diterapkan pada klien. Sistem interpolasi juga akan berfungsi pada klien, yang akan memuluskan pergerakan token ini, dengan mempertimbangkan frekuensi pembaruan, kualitas koneksi ke server, dan sebagainya. Sistem lain yang terkait dengan mode permainan ini akan menambahkan bagian visual ke objek dengan `KillTokenComponent` dan memantau pilihannya.

Satu-satunya ketidaknyamanan dari pendekatan saat ini yang ingin Anda perhatikan dan yang ingin Anda singkirkan di masa depan adalah ketidakmampuan untuk mengatur privasi untuk setiap bidang komponen. Ini tidak terlalu kritis, karena masalah yang sama dapat dengan mudah diselesaikan dengan membagi komponen menjadi beberapa: misalnya, permainan berisi `ShooterPublicComponent` dan` ShooterPrivateComponent` dengan privasi yang sesuai. Terlepas dari kenyataan bahwa mereka terikat pada satu mekanik (pemotretan), diperlukan dua komponen untuk menghemat lalu lintas - beberapa bidang sama sekali tidak diperlukan pada klien yang tidak memiliki komponen ini. Namun, ini menambah pekerjaan ke programmer.

Secara umum, objek yang direplikasi ke klien dapat memiliki status untuk frame yang berbeda. Oleh karena itu, kemampuan untuk mengelompokkan objek dengan membentuk kelompok replikasi ditambahkan. Semua komponen pada objek dalam grup yang sama selalu memiliki keadaan untuk frame yang sama pada klien - ini diperlukan agar prediksi berfungsi dengan benar (lebih lanjut tentang mereka di bawah). Misalnya, senjata dan karakter yang memilikinya berada dalam kelompok yang sama. Jika objek berada dalam kelompok yang berbeda, maka negara mereka di dunia dapat untuk bingkai yang berbeda.

Sistem replikasi mencoba untuk meminimalkan jumlah lalu lintas, khususnya dengan mengompresi data yang dikirimkan (setiap bidang di dalam komponen secara opsional dapat ditandai sesuai untuk kompresi) dan dengan mentransmisikan hanya perbedaan nilai antara dua frame.

Prediksi pelanggan


Prediksi klien (istilah prediksi sisi klien digunakan dalam bahasa Inggris) memungkinkan pemain untuk menerima umpan balik instan pada sebagian besar tindakannya dalam permainan. Pada saat yang sama, karena kata terakhir selalu berada di belakang server, jika ada kesalahan dalam simulasi (istilah mispredict digunakan dalam bahasa Inggris, di masa depan saya akan memanggil mereka hanya "salah duga") klien harus memperbaikinya. Rincian lebih lanjut tentang kesalahan prediksi dan bagaimana mereka diperbaiki akan dijelaskan di bawah ini.

Prediksi klien bekerja sesuai dengan aturan berikut:

  • Klien mensimulasikan dirinya sendiri maju dengan N frame;
  • Semua input yang dihasilkan oleh klien dikirim ke server (dalam bentuk tindakan yang dilakukan oleh pemain);
  • N tergantung pada kualitas koneksi ke server. Semakin kecil nilai ini, semakin "terkini" gambaran dunia diperuntukkan bagi klien (yaitu, jarak waktu antara pemain lokal dan pemain lain lebih kecil).

Akibatnya, server dan klien melakukan simulasi berdasarkan input klien. Server kemudian mengirimkan hasil simulasi ini ke klien. Jika klien menentukan bahwa hasilnya tidak sesuai dengan yang di server, maka ia mencoba untuk memperbaiki kesalahan - memutar dirinya kembali ke keadaan server yang terakhir diketahui, dan lagi mensimulasikan N frame di depan. Kemudian semuanya berlanjut sesuai dengan skema yang sama - klien terus mensimulasikan dirinya sendiri di masa depan sehubungan dengan server, dan server mengirimkannya hasil simulasi. Oleh karena itu, semua kode yang mempengaruhi prediksi klien harus dibagi antara klien dan server.

Juga, untuk menghemat lalu lintas, seluruh input dipadatkan berdasarkan skema yang telah ditentukan. Kemudian dikirim ke server dan segera didekompresi kembali ke klien. Pengemasan dan pembongkaran selanjutnya pada klien diperlukan untuk menghilangkan perbedaan dalam nilai yang terkait dengan input antara klien dan server. Saat membuat skema, kisaran nilai untuk tindakan ini ditunjukkan, dan jumlah bit yang harus dimasukkan. Demikian pula, pengumuman skema kemasan di Battle Prime terlihat seperti di dalam sistem umum antara klien dan server:

 auto* input_packing_sc = world->get_for_write<InputPackingSingleComponent>(); input_packing_sc->packing_schema = { { ActionNames::MOVE, AnalogStatePrecision{ 8, { -1.f, 1.f }, false } }, { ActionNames::LOOK, AnalogStatePrecision{ 16, { -PI, PI }, false } }, { ActionNames::JUMP, nullopt }, // ..    action' }; 

Kondisi kritis untuk kinerja prediksi klien untuk bekerja adalah kebutuhan input untuk memiliki waktu untuk sampai ke server pada saat simulasi frame yang input ini berhubungan. Jika input tidak berhasil mencapai frame yang diinginkan pada server (ini dapat terjadi selama, misalnya, lompatan ping yang tajam), server akan mencoba menggunakan input dari klien ini dari frame sebelumnya. Ini adalah mekanisme cadangan yang dapat membantu menghilangkan salah duga pada klien dalam beberapa situasi. Misalnya, jika klien hanya berjalan dalam satu arah dan inputnya tidak berubah untuk waktu yang relatif lama, penggunaan input untuk frame terakhir akan berhasil - server akan "menebak" itu, dan tidak akan ada perbedaan antara klien dan server. Skema serupa digunakan di Overwatch (disebutkan dalam ceramah tentang GDC:www.youtube.com/watch?v=W3aieHjyNvw ).

Saat ini, klien Battle Prime memprediksi status objek berikut:

  • Pemain avatar (posisi di dunia dan segala sesuatu yang dapat memengaruhinya, keadaan keterampilan, dll);
  • Semua senjata pemain (jumlah putaran di toko, cooldown di antara tembakan, dll.).

Menggunakan prediksi klien adalah menambah dan mengkonfigurasi `PredictionComponent` pada klien ke objek yang diinginkan. Misalnya, prediksi avatar pemain di salah satu sistem dihidupkan dengan cara yang sama:

 // `new_local_avatars`       , //      for (Entity avatar : new_local_avatars) { auto* avatar_prediction_component = avatar.add<PredictionComponent>(); avatar_prediction_component->enable_prediction<TransformComponent>(); avatar_prediction_component->enable_prediction<CharacterControllerComponent>(); avatar_prediction_component->enable_prediction<ShooterPrivateComponent>(); avatar_prediction_component->enable_prediction<ShooterPublicComponent>(); // ...      } 

Kode ini berarti bahwa bidang di dalam komponen di atas akan terus-menerus dibandingkan dengan bidang yang sama dari komponen server - jika ada ketidaksesuaian dalam nilai-nilai dalam bingkai tunggal diperhatikan, penyesuaian akan dilakukan pada klien.

Kriteria perbedaan tergantung pada jenis data. Dalam kebanyakan kasus, ini hanya panggilan ke `operator ==`, pengecualiannya adalah data berdasarkan float - bagi mereka kesalahan maksimum yang diijinkan saat ini diperbaiki dan sama dengan 0,005. Di masa depan, ada keinginan untuk menambahkan kemampuan untuk mengatur akurasi untuk setiap bidang komponen secara terpisah.

Alur kerja replikasi dan prediksi klien didasarkan pada kenyataan bahwa semua data yang diperlukan untuk simulasi terkandung dalam komponen. Di atas, pada bagian ECS, saya menulis bahwa sistem diperbolehkan untuk menyimpan sebagian data - ini bisa nyaman dalam beberapa kasus. Ini tidak berlaku untuk data apa pun yang mempengaruhi simulasi - itu harus selalu di dalam komponen, karena sistem snapshot klien dan server hanya bekerja dengan komponen.

Selain memprediksi nilai bidang dalam komponen, dimungkinkan untuk memprediksi pembuatan dan penghapusan komponen. Misalnya, jika, sebagai akibat dari menggunakan kemampuan, `SpeedModifierComponent` ditumpangkan pada karakter (yang memodifikasi kecepatan gerakan, misalnya, mempercepat pemain), maka itu harus ditambahkan ke karakter baik di server dan pada klien di frame yang sama, jika tidak, maka akan menyebabkan prediksi posisi karakter pada klien yang salah.

Memprediksi penciptaan dan penghapusan objek saat ini tidak didukung. Ini mungkin nyaman dalam beberapa situasi, tetapi juga akan menyulitkan modul jaringan. Mungkin kita akan kembali ke ini di masa depan.

Di bawah ini adalah gif di mana kontrol karakter berlangsung dengan RTT selama sekitar 1,5 detik. Seperti yang Anda lihat, karakter dikontrol secara instan, meskipun ada penundaan tinggi: bergerak, menembak, memuat ulang, melempar granat - semuanya terjadi tanpa menunggu informasi dari server. Anda juga dapat memperhatikan bahwa penangkapan suatu titik (zona yang dibatasi oleh segitiga) dimulai dengan penundaan - mekanisme ini hanya berfungsi pada server dan tidak diprediksi oleh klien.

gambar

Kesalahan prediksi dan resimulasi


Mispredict - perbedaan antara hasil simulasi server dan klien. Resimulasi adalah proses memperbaiki perbedaan ini oleh klien.

Alasan pertama untuk munculnya salah duga adalah lompatan ping yang tajam, di mana klien tidak punya waktu untuk menyesuaikan. Dalam situasi seperti itu, input dari pemain mungkin tidak punya waktu untuk sampai ke server, dan server akan menggunakan mekanisme cadangan yang dijelaskan di atas dengan duplikasi input terakhir untuk beberapa waktu, dan setelah beberapa saat ia akan berhenti menggunakannya.

Alasan kedua adalah interaksi karakter dengan objek yang sepenuhnya dikontrol server dan tidak diprediksi secara lokal oleh klien. Misalnya, tabrakan dengan pemain lain akan menyebabkan kesalahan prediksi - karena mereka, pada kenyataannya, hidup dalam dua periode waktu yang berbeda (karakter lokal di masa depan relatif terhadap pemain lain - yang posisinya berasal dari server dan diinterpolasi).

Alasan ketiga dan paling tidak menyenangkan adalah bug dalam kode. Misalnya, suatu sistem dapat secara keliru menggunakan data yang tidak direplikasi untuk mengendalikan simulasi, atau sistem bekerja dalam urutan yang salah, atau bahkan dalam urutan berbeda pada server dan klien.

Menemukan bug ini terkadang membutuhkan waktu yang cukup lama. Untuk mempermudah pencarian mereka, kami membuat beberapa alat bantu - saat aplikasi berjalan, Anda dapat melihat:

  • Komponen yang direplikasi
  • Jumlah salah duga
  • Di frame mana mereka terjadi,
  • Data apa yang ada di server dan klien di komponen yang berbeda,
  • Masukan apa yang diterapkan pada server dan pada klien untuk frame ini.




Sayangnya, bahkan dengan mereka, pencarian penyebab resimulasi masih membutuhkan waktu yang cukup lama. Tidak diragukan lagi, alat dan validasi perlu dikembangkan untuk mengurangi kemungkinan bug dan menyederhanakan pencarian mereka.

Untuk mendukung operasi resimulasi, sistem harus mewarisi dari kelas tertentu `ResimulatableSystem`. Dalam situasi ketika mispredict terjadi, dunia "memutar kembali" semua objek ke kondisi server yang terakhir diketahui, dan kemudian membuat jumlah simulasi yang diperlukan untuk memperbaiki kesalahan ini - hanya sistem yang dapat diulang yang akan berpartisipasi dalam hal ini.

Secara umum, resimulasi klien tidak boleh terlihat oleh pemain. Ketika mereka terjadi, semua bidang komponen diinterpolasi dengan lancar ke dalam nilai baru untuk secara visual memuluskan kemungkinan “berkedut”. Namun, sangat penting untuk menjaga jumlah mereka serendah mungkin.

Menembak


Kerusakan pada pemain sepenuhnya ditentukan oleh server - pelanggan tidak dapat dipercaya dalam mekanisme yang sedemikian penting untuk mengurangi kemungkinan kecurangan. Tetapi, seperti gerakan, menembak pada klien harus responsif mungkin dan tanpa penundaan - pemain harus menerima umpan balik instan dalam bentuk efek dan suara - moncong flash, jejak penerbangan proyektil, serta efek proyektil mengenai lingkungan dan pemain lain.

Oleh karena itu, seluruh keadaan karakter yang terkait dengan penembakan diprediksi oleh klien - berapa banyak putaran yang ada di toko, dispersi selama penembakan, penundaan antara tembakan, waktu tembakan terakhir, dan sebagainya. Juga pada klien adalah sistem yang sama yang bertanggung jawab untuk pergerakan cangkang seperti pada server - ini memungkinkan Anda untuk mensimulasikan tembakan pada klien tanpa menunggu hasil simulasi mereka di server.

Balistik cangkang itu sendiri tidak dapat diprediksi - karena mereka terbang dengan kecepatan yang sangat tinggi, dan, sebagai aturan, menyelesaikan gerakan mereka dalam beberapa bingkai, cangkang tersebut akan memiliki waktu untuk sampai ke suatu titik di dunia dan kehilangan efeknya sebelum kita mendapatkan hasil simulasi ini adalah proyektil dari server (atau kurangnya hasil jika, karena kesalahan prediksi, klien menembakkan proyektil karena kesalahan).

Skema kerja proyektil terbang lambat sedikit berbeda. Jika seorang pemain melempar granat, tetapi akibat salah duga ternyata granat itu tidak terlempar, itu akan dihancurkan pada klien. Demikian pula, jika klien salah memperkirakan kerusakan sebuah granat (itu sudah meledak di server, tetapi belum pada klien), maka granat klien juga akan dihancurkan. Semua informasi tentang ledakan yang ditampilkan pada klien berasal dari server untuk menghindari situasi di mana, sebagai akibat dari kesalahan klien, ledakan server terjadi di satu tempat dan pada klien di tempat lain.

Idealnya, saya ingin benar-benar memprediksi kerang terbang lambat di masa depan - tidak hanya waktu hidup, tetapi juga posisi mereka.

Kompensasi keterlambatan


Kompensasi lag adalah teknik yang memungkinkan Anda untuk meningkatkan efek keterlambatan antara server dan klien pada akurasi pengambilan gambar. Di bagian ini, saya akan menganggap bahwa penembakan selalu berasal dari senjata "hitscan" - mis. proyektil yang ditembakkan oleh senjata bergerak dengan kecepatan tak terbatas. Tetapi segala sesuatu yang dijelaskan di sini juga penting dengan jenis senjata lainnya.

Poin-poin berikut membuatnya perlu untuk mengkompensasi kelambatan saat memotret:

  • Karakter di bawah kendali pemain di masa depan relatif terhadap server (memprediksi keadaannya untuk sejumlah frame di depan);
  • Konsekuensinya, sisa pemain ada hubungannya dengan dia di masa lalu;
  • Ketika dipecat, tindakan yang sesuai dikirim oleh klien ke server dan diterapkan pada bingkai yang sama dengan yang diterapkan pada klien (jika mungkin).

Jika kami menganggap bahwa pemain membidik musuh yang berlari ke arah kepala dan menekan tombol tembakan, gambar berikut diperoleh:

  • Pada klien: penembak pada frame N1 melepaskan tembakan ke kepala musuh yang terletak di frame N0 (N0 <N1);
  • Di server: penembak pada frame N1 menembakkan tembakan ke kepala musuh, juga terletak di frame N1 (di server, semuanya ada pada waktu yang bersamaan).

Hasil ini, dengan probabilitas tinggi, adalah kehilangan selama tembakan. Karena klien menargetkan berdasarkan gambarnya tentang dunia, yang tidak sesuai dengan gambar dunia server, untuk masuk ke musuh, ia harus membidiknya bahkan ketika menggunakan senjata hitscan, dan jarak di depannya yang harus dia tembak tergantung pada kualitas koneksi dengan server. Singkatnya, ini bukan pengalaman yang baik bagi seorang penembak.

Untuk menghilangkan masalah ini, kompensasi lag digunakan. Skema karyanya adalah sebagai berikut:

  • Server memiliki sejarah snapshot ukuran terbatas di dunia;
  • Ketika ditembakkan, musuh (atau bagian dari musuh) “memutar balik” sedemikian rupa sehingga dunia pada server cocok dengan dunia yang dilihat klien dalam dirinya sendiri - klien ada di "sekarang" (saat tembakan), dan musuh di masa lalu;
  • Mekanisme deteksi hit berfungsi, hit direkam;
  • Dunia kembali ke keadaan semula.

Karena gambaran dunia pada klien juga tergantung pada pengoperasian sistem interpolasi, untuk “memutar kembali” dunia ke status klien yang paling akurat di server, klien memberinya data tambahan - perbedaan antara kerangka klien saat ini dan kerangka di mana ia melihat semua pemain lain (saat ini adalah dua byte per frame), serta waktu pembuatan input bidikan relatif terhadap awal frame.

Kompensasi lag ada pada level modul terpisah di dalam mesin dan tidak terikat pada proyek tertentu. Dari sudut pandang pengembang mekanika gameplay, penggunaannya adalah sebagai berikut:

  • `LagCompensationComponent` ditambahkan ke pemain, dan daftar hitbox yang akan disimpan dalam sejarah diisi;
  • Saat memotret (atau mekanik lain yang membutuhkan kompensasi - misalnya, dalam serangan jarak dekat), `LagCompensation :: invoke` dipanggil, di mana functor dilewatkan, yang akan dieksekusi dalam" kompensasi ", dari sudut pandang pemain tertentu, dunia. Itu harus memiliki semua deteksi hit yang diperlukan.

Kode dengan contoh penggunaan kompensasi jeda dari Batle Prime saat memindahkan proyektil balistik:

 // `targets_data`    , //   “”    , //    const auto compensated_action = [this](const Vector<LagCompensation::LagCompensationData>& targets_data) { process_projectile(projectile, elapsed_time); }; LagCompensation::invoke( observer, // ,       projectile_component->input_time_ms, // ,      compensated_entities, // ,    compensated_action // ,       ); 

Saya juga ingin mencatat bahwa kompensasi lag adalah skema yang menempatkan pengalaman penembak di atas pengalaman target yang dia tembak. Dari sudut pandang target, musuh dapat masuk ke dirinya pada saat dia sudah berada di belakang hambatan (sering keluhan di forum game). Untuk melakukan ini, kompensasi lag memiliki jumlah bingkai terbatas yang tujuannya dapat “dipompa keluar”. Saat ini, di Battle Prime, penembak dengan RTT sekitar 400 milidetik dapat dengan nyaman mengenai musuh. Jika RTT lebih tinggi, Anda harus terus maju.

Contoh menembak tanpa kompensasi - Anda harus menembak ke depan untuk terus mengenai musuh:

gambar

Dan dengan kompensasi - Anda dapat dengan nyaman mengarahkan langsung ke musuh:

gambar

Agen pembuat kami juga secara berkala menjalankan tes otomatis yang memeriksa pekerjaan berbagai mekanik. Di antara mereka, ada juga autotest untuk akurasi menembak dengan kompensasi lag diaktifkan. Dalam gif di bawah tes ini ditampilkan - karakter hanya menembak kepala musuh yang berlari melewati dan menghitung jumlah hit pada dirinya. Untuk debugging, hitbox musuh yang ada di server pada saat tembakan (putih) dan hitbox yang digunakan untuk deteksi hit di dalam dunia yang dikompensasi (berwarna biru) juga ditampilkan:

gambar

Faktor tambahan yang memengaruhi keakuratan pemotretan adalah posisi kotak hit pada karakter. Hitbox bergantung pada animasi skeletal, dan fase mereka saat ini tidak disinkronkan dengan cara apa pun, sehingga situasi mungkin di mana hitbox berbeda antara klien dan server. Konsekuensi dari ini tergantung pada animasi itu sendiri - semakin besar rentang gerakan di dalam animasi, semakin besar perbedaan potensial dalam posisi hitbox antara server dan klien. Dalam prakteknya, perbedaan seperti itu kurang terlihat bagi pemain dan lebih banyak mempengaruhi tubuh bagian bawah, yang kurang kritis dibandingkan dengan bagian atas (kepala, bagasi, lengan). Namun demikian, di masa depan saya ingin membahas secara lebih rinci masalah sinkronisasi animasi antara server dan klien.

Kesimpulan


Dalam artikel ini saya mencoba untuk menggambarkan dasar di mana Battle Prime dibangun - ini adalah penerapan pola ECS di dalam Blitz Engine, serta modul jaringan yang bertanggung jawab untuk replikasi, prediksi klien, dan mekanika terkait. Meskipun ada beberapa kekurangan (yang kami terus perbaiki), menggunakan fungsi ini sekarang sederhana dan nyaman.

Untuk menunjukkan gambaran keseluruhan dari Battle Prime, saya harus menyentuh sejumlah besar topik. Banyak dari mereka dapat dikhususkan untuk artikel yang terpisah di masa depan, di mana mereka akan dijelaskan secara lebih rinci!

Game ini sudah diuji coba di Turki dan Filipina.

Artikel kami sebelumnya dapat ditemukan di tautan berikut:

  1. habr.com/en/post/461623
  2. habr.com/en/post/465343

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


All Articles