Halo semuanya! Dalam artikel ini, kita akan berbicara tentang pengalaman pribadi bekerja dengan mesin fisik untuk penembak multi-pemain dan terutama berfokus pada interaksi fisika dan
ECS : penggaruk seperti apa yang kita lewati selama bekerja, apa yang kita pelajari, mengapa kita memilih solusi spesifik.

Pertama, mari kita cari tahu mengapa mesin fisik dibutuhkan. Tidak ada jawaban universal: di setiap game itu melayani tujuannya. Beberapa game menggunakan mesin fisik untuk mensimulasikan perilaku objek di dunia dengan benar untuk mencapai efek membenamkan pemain. Dalam kasus lain, fisika adalah dasar dari permainan - seperti, misalnya, Angry Birds dan Red Faction. Ada juga "kotak pasir" di mana hukum fisik berbeda dari yang biasa dan dengan demikian membuat gameplay lebih menarik dan tidak biasa (Portal, A Slower Speed ββof Light).
Dari sudut pandang pemrograman, mesin fisik memungkinkan untuk menyederhanakan proses simulasi perilaku objek dalam game. Bahkan, itu adalah perpustakaan yang menyimpan deskripsi sifat fisik objek. Di hadapan mesin fisik, kita tidak perlu mengembangkan sistem interaksi antara tubuh dan hukum universal yang akan mengatur dunia game. Ini menghemat banyak waktu dan upaya pengembangan.
Diagram di atas menjelaskan esensi dari Player, komponen-komponennya dan data mereka, dan sistem yang bekerja dengan pemain dan komponen-komponennya. Objek utama dalam diagram adalah pemain: ia dapat bergerak di luar angkasa - Komponen Transform and Movement, MoveSystem; memiliki beberapa kesehatan dan dapat mati - komponen Kesehatan, Kerusakan, KerusakanSistem; setelah kematian muncul di titik respawn - komponen Transform untuk posisi itu, Sistem RespawnS; mungkin kebal - komponen Invincible.Apa saja fitur penerapan fisika game untuk penembak?
Tidak ada interaksi fisik yang kompleks dalam permainan kami, tetapi ada beberapa hal yang memerlukan mesin fisik. Awalnya, kami berencana menggunakannya untuk memindahkan karakter di dunia sesuai dengan hukum yang telah ditentukan. Biasanya ini dilakukan dengan memberikan tubuh impuls tertentu atau kecepatan konstan, setelah itu, menggunakan metode Simulasikan / Pembaruan perpustakaan, semua tubuh yang terdaftar di dalamnya disimulasikan tepat satu langkah ke depan.
Dalam penembakan, fisika 3D sering digunakan tidak hanya untuk mensimulasikan pergerakan karakter, tetapi juga untuk pemrosesan balistik peluru dan roket yang benar, lompatan, interaksi karakter satu sama lain dan lingkungan. Jika penembak mengaku realistis dan berusaha menyampaikan sensasi sebenarnya dari proses pemotretan, ia hanya membutuhkan mesin fisik. Ketika seorang pemain menembakkan senapan ke sasaran, ia berharap mendapatkan pengalaman dan hasil yang sedekat mungkin dengan yang sudah ia ketahui dari permainan jangka panjang para penembak - sesuatu yang secara radikal baru kemungkinan besar akan mengejutkannya.
Namun dalam kasus permainan kami, ada sejumlah batasan. Karena penembak kami mobile, itu tidak menyiratkan interaksi karakter yang kompleks satu sama lain dan dengan dunia sekitarnya, itu tidak memerlukan balistik yang indah, daya hancur, melompat pada permukaan yang tidak rata. Tetapi pada saat yang sama dan untuk alasan yang sama, ada persyaratan lalu lintas yang sangat ketat. Fisika 3D dalam hal ini akan menjadi berlebihan: ia hanya akan menggunakan sebagian kecil sumber daya komputasi dan menghasilkan data yang tidak perlu, yang dalam jaringan seluler dan sinkronisasi konstan klien dengan server melalui UDP akan memakan terlalu banyak ruang. Di sini perlu diingat bahwa dalam model jaringan kami masih ada hal-hal seperti
Prediksi dan Rekonsiliasi , yang juga melibatkan penyelesaian pada klien. Akibatnya, kami mendapatkan bahwa fisika kami harus bekerja secepat mungkin agar berhasil diluncurkan dan bekerja pada perangkat seluler, tanpa mengganggu rendering dan subsistem klien lainnya.
Jadi, fisika 3D tidak cocok untuk kita. Tetapi di sini perlu diingat bahwa meskipun permainan terlihat seperti tiga dimensi, bukan fakta bahwa fisika di dalamnya juga tiga dimensi: semuanya menentukan sifat interaksi objek satu sama lain. Seringkali efek yang tidak dapat dicakup oleh fisika 2D dikustomisasi - yaitu, logika ditulis yang terlihat seperti interaksi tiga dimensi - atau hanya diganti dengan efek visual yang tidak mempengaruhi gameplay. Dalam Heroes of the Storm, Defense of the Ancients, League of Legends, fisika dua dimensi mampu menyediakan semua fitur gameplay dari game tanpa mempengaruhi kualitas gambar atau perasaan kredibilitas yang diciptakan oleh perancang game dan seniman dunia. Jadi, misalnya, dalam permainan ini ada karakter lompat, tetapi tidak ada rasa fisik di ketinggian lompatan mereka, sehingga turun ke simulasi dua dimensi dan mengatur semacam bendera seperti _isInTheAir ketika karakter di udara - itu diperhitungkan saat menghitung logika.
Jadi diputuskan untuk menggunakan fisika 2D. Kami menulis game di Unity, tetapi server menggunakan Unity-less .net, yang tidak dimengerti Unity. Karena bagian terbesar dari kode simulasi digeledah antara klien dan server, kami mulai mencari sesuatu lintas-platform - yaitu, perpustakaan fisik yang ditulis dalam C # murni tanpa menggunakan kode asli untuk menghilangkan bahaya menabrak platform seluler. Selain itu, dengan mempertimbangkan
secara spesifik pekerjaan penembak , khususnya, rewinding konstan pada server untuk menentukan di mana pemain menembak, penting bagi kita bahwa perpustakaan dapat bekerja dengan sejarah - yaitu, kita bisa dengan murah melihat posisi tubuh N frame kembali ke masa lalu . Dan, tentu saja, proyek tidak boleh ditinggalkan: penting bahwa penulis mendukungnya dan dapat dengan cepat memperbaiki bug, jika ada yang ditemukan selama operasi.
Ternyata, saat itu sangat sedikit perpustakaan yang dapat memenuhi persyaratan kami. Bahkan, hanya satu yang cocok untuk kita -
VolatilePhysics .
Perpustakaan penting karena ia bekerja dengan kedua solusi Unity dan Unity-less, dan juga memungkinkan Anda untuk melakukan rakecast ke keadaan objek di luar kotak, mis. Cocok untuk logika penembak. Selain itu, kenyamanan pustaka terletak pada fakta bahwa mekanisme untuk mengendalikan awal simulasi Simulate () memungkinkan Anda untuk memproduksinya kapan saja ketika klien membutuhkannya. Dan fitur lain - kemampuan untuk menulis data tambahan ke tubuh fisik. Ini bisa berguna ketika menangani objek dari simulasi di hasil reykast - namun, ini sangat mengurangi kinerja.
Setelah melakukan beberapa tes dan memastikan bahwa klien dan server berinteraksi dengan baik dengan VolatilePhysics tanpa crash, kami memilih untuk itu.
Bagaimana kami memasuki perpustakaan dengan cara biasa bekerja dengan ECS dan apa yang terjadi
Langkah pertama ketika bekerja dengan VolatilePhysics adalah menciptakan dunia fisik VoltWorld. Ini adalah kelas proksi, yang dengannya pekerjaan utama berlangsung: penyetelan, mensimulasikan data tentang objek, reykast, dll. Kami membungkusnya dalam fasad khusus sehingga di masa depan kami dapat mengubah implementasi perpustakaan menjadi sesuatu yang lain. Kode fasad tampak seperti ini:
Lihat kodepublic sealed class PhysicsWorld { public const int HistoryLength = 32; private readonly VoltWorld _voltWorld; private readonly Dictionary<uint, VoltBody> _cache = new Dictionary<uint, VoltBody>(); public PhysicsWorld(float deltaTime) { _voltWorld = new VoltWorld(HistoryLength) { DeltaTime = deltaTime }; } public bool HasBody(uint tag) { return _cache.ContainsKey(tag); } public VoltBody GetBody(uint tag) { VoltBody body; _cache.TryGetValue(tag, out body); return body; } public VoltRayResult RayCast(Vector2 origin, Vector2 direction, float distance, VoltBodyFilter filter, int ticksBehind) { var ray = new VoltRayCast(origin, direction.normalized, distance); var result = new VoltRayResult(); _voltWorld.RayCast(ref ray, ref result, filter, ticksBehind); return result; } public VoltRayResult CircleCast(Vector2 origin, Vector2 direction, float distance, float radius, VoltBodyFilter filter, int ticksBehind) { var ray = new VoltRayCast(origin, direction.normalized, distance); var result = new VoltRayResult(); _voltWorld.CircleCast(ref ray, radius, ref result, filter, ticksBehind); return result; } public void Update() { _voltWorld.Update(); } public void Update(uint tag) { var body = _cache[tag]; _voltWorld.Update(body, true); } public void UpdateBody(uint tag, Vector2 position, float angle) { var body = _cache[tag]; body.Set(position, angle); } public void CreateStaticCircle(Vector2 origin, float radius, uint tag) { var shape = _voltWorld.CreateCircleWorldSpace(origin, radius, 1f, 0f, 0f); var body = _voltWorld.CreateStaticBody(origin, 0, shape); body.UserData = tag; } public void CreateDynamicCircle(Vector2 origin, float radius, uint tag) { var shape = _voltWorld.CreateCircleWorldSpace(origin, radius, 1f, 0f, 0f); var body = _voltWorld.CreateDynamicBody(origin, 0, shape); body.UserData = tag; body.CollisionFilter = StaticCollisionFilter; _cache.Add(tag, body); } public void CreateStaticSquare(Vector2 origin, float rotationAngle, Vector2 extents, uint tag) { var shape = _voltWorld.CreatePolygonBodySpace(extents.GetRectFromExtents(), 1, 0, 0); var body = _voltWorld.CreateStaticBody(origin, rotationAngle, shape); body.UserData = tag; } public void CreateDynamicSquare(Vector2 origin, float rotationAngle, Vector2 extents, uint tag) { var shape = _voltWorld.CreatePolygonBodySpace(extents.GetRectFromExtents(), 1, 0, 0); var body = _voltWorld.CreateDynamicBody(origin, rotationAngle, shape); body.UserData = tag; body.CollisionFilter = StaticCollisionFilter; _cache.Add(tag, body); } public IEnumerable<VoltBody> GetBodies() { return _voltWorld.Bodies; } private static bool StaticCollisionFilter(VoltBody a, VoltBody b) { return b.IsStatic; } }
Saat membuat dunia, besarnya sejarah ditunjukkan - jumlah negara di dunia yang akan disimpan oleh perpustakaan. Dalam kasus kami, jumlah mereka adalah 32: 30 frame per detik, kami akan membutuhkannya berdasarkan persyaratan untuk memperbarui logika dan 2 yang tambahan jika kami melampaui batas proses debugging. Kode ini juga memperhitungkan metode pengecoran luar yang menghasilkan tubuh fisik, dan berbagai jenis reykast.
Seperti yang kita ingat dari
artikel sebelumnya , dunia ECS pada dasarnya berputar di sekitar panggilan reguler ke metode Jalankan untuk semua sistem yang termasuk di dalamnya. Di tempat yang tepat dari setiap sistem, kami menggunakan panggilan ke fasad kami. Awalnya, kami tidak menulis batching untuk menantang mesin fisik, meskipun ada pemikiran seperti itu. Di dalam fasad, panggilan untuk Pembaruan () dari dunia fisik terjadi, dan perpustakaan mensimulasikan semua interaksi objek yang terjadi per bingkai.
Dengan demikian, bekerja dengan fisika bermuara pada dua komponen: gerakan seragam tubuh dalam ruang dalam satu bingkai dan banyak rakast yang diperlukan untuk pemotretan, operasi efek yang tepat, dan banyak hal lainnya. Reykasts sangat relevan dalam sejarah keadaan tubuh fisik.
Menurut hasil pengujian kami, kami dengan cepat menyadari bahwa perpustakaan bekerja sangat buruk pada kecepatan yang berbeda, dan pada kecepatan tertentu, tubuh dengan mudah mulai melewati dinding. Tidak ada pengaturan yang terkait dengan deteksi tabrakan berkelanjutan untuk menyelesaikan masalah ini di mesin kami. Tetapi tidak ada alternatif untuk solusi kami di pasar pada waktu itu, jadi saya harus membuat versi sendiri dari objek yang bergerak di seluruh dunia dan menyinkronkan data fisika dengan ECS. Jadi, misalnya, kode kami untuk sistem pergerakan adalah sebagai berikut:
Lihat kode using System; ... using Volatile; public sealed class MovePhysicsSystem : ExecutableSystem { private readonly PhysicsWorld _physicsWorld; private readonly CollisionFilter _moveFilter; private readonly VoltBodyFilter _collisionFilterDelegate; public MovePhysicsSystem(PhysicsWorld physicsWorld) { _physicsWorld = physicsWorld; _moveFilter = new CollisionFilter(true, CollisionLayer.ExplosiveBarrel); _collisionFilterDelegate = _moveFilter.Filter; } public override void Execute(GameState gs) { _moveFilter.State = gs; foreach (var pair in gs.WorldState.Movement) { ExecuteMovement(gs, pair.Key, pair.Value); } _physicsWorld.Update(); foreach (var pair in gs.WorldState.PhysicsDynamicBody) { if(pair.Value.IsAlive) { ExecutePhysicsDynamicBody(gs, pair.Key); } } } public override void Execute(GameState gs, uint avatarId) { _moveFilter.State = gs; var movement = gs.WorldState.Movement[avatarId]; if (movement != null) { ExecuteMovement(gs, avatarId, movement); _physicsWorld.Update(avatarId); var physicsDynamicBody = gs.WorldState.PhysicsDynamicBody[avatarId]; if (physicsDynamicBody != null && physicsDynamicBody.IsAlive) ExecutePhysicsDynamicBody(gs, avatarId); } } private void ExecutePhysicsDynamicBody(GameState gs, uint entityId) { var body = _physicsWorld.GetBody(entityId); if (body != null) { var transform = gs.WorldState.Transform[entityId]; transform.Position = body.Position; } } private void ExecuteMovement(GameState gs, uint entityId, Movement movement) { var body = _physicsWorld.GetBody(entityId); if (body != null) { float raycastRadius; if (CalculateRadius(gs, entityId, out raycastRadius)) { return; } body.AngularVelocity = 0; body.LinearVelocity = movement.Velocity; var movPhysicInfo = gs.WorldState.MovementPhysicInfo[entityId]; var collisionDirection = CircleRayCastSpeedCorrection(body, GameState.TickDurationSec, raycastRadius); CheckMoveInWall(movement, movPhysicInfo, collisionDirection, gs.WorldState.Transform[entityId]); } } private static bool CalculateRadius(GameState gs, uint id, out float raycastRadius) { raycastRadius = 0; var circleShape = gs.WorldState.DynamicCircleCollider[id]; if (circleShape != null) { raycastRadius = circleShape.Radius; } else { var boxShape = gs.WorldState.DynamicBoxCollider[id]; if (boxShape != null) { raycastRadius = boxShape.RaycastRadius; } else { gs.Log.Error(string.Format("Physics body {0} doesn't contains shape!", id)); return true; } } return false; } private static void CheckMoveInWall(Movement movement, MovementPhysicInfo movPhysicInfo, Vector2 collisionDirection, Transform transform) {
Idenya adalah bahwa sebelum setiap karakter bergerak, kita membuat
CircleCast ke arah gerakannya untuk menentukan apakah ada penghalang di depannya. CircleCast diperlukan karena proyeksi karakter dalam permainan mewakili lingkaran, dan kami tidak ingin mereka terjebak di sudut-sudut antara geometri yang berbeda. Kemudian kami menganggap peningkatan kecepatan dan menetapkan nilai ini ke objek dunia fisik sebagai kecepatannya dalam satu bingkai. Langkah selanjutnya adalah memanggil metode simulasi Update mesin fisik (), yang memindahkan semua objek yang kita butuhkan, secara bersamaan merekam keadaan lama dalam sejarah. Setelah simulasi di dalam mesin selesai, kami membaca data simulasi ini, menyalinnya ke komponen Transform ECS kami dan kemudian terus bekerja dengannya, khususnya, mengirimkannya melalui jaringan.
Pendekatan ini dalam memperbarui fisika dengan potongan data kecil yang dikontrol pada kecepatan gerakan karakter ternyata sangat efektif dalam menangani perbedaan dalam fisika pada klien dan server. Dan karena fisika kita tidak deterministik - yaitu, dengan data input yang sama, hasil simulasi dapat bervariasi - ada banyak diskusi tentang apakah layak digunakan sama sekali, dan apakah ada orang di industri yang melakukan sesuatu yang serupa, memiliki mesin fisik deterministik di tangan. Untungnya, kami menemukan laporan yang sangat baik dari pengembang NetherRealm Studios di Game Developers Conference tentang komponen jaringan permainan mereka dan menyadari bahwa pendekatan semacam itu benar-benar terjadi. Setelah sepenuhnya merakit sistem dan menjalankannya pada beberapa tes, kami mendapat sekitar 50 prediksi salah untuk 9000 ticks, yaitu selama pertempuran lima menit. Sejumlah kesalahan prediksi mudah diratakan oleh mekanisme Rekonsiliasi dan interpolasi visual dari posisi pemain. Kesalahan yang terjadi selama pembaruan manual fisika yang sering menggunakan data Anda sendiri tidak signifikan, oleh karena itu, interpolasi visual dapat berlangsung agak cepat - diperlukan hanya agar lompatan visual dalam model karakter tidak terjadi.
Untuk memeriksa kebetulan status klien dan server, kami menggunakan kelas tulis-sendiri dari formulir berikut:
Jika perlu, itu bisa otomatis, tetapi kami tidak melakukan ini, meskipun kami memikirkannya di masa depan.
Ubah kode perbandingan:
Lihat kode public static bool operator ==(Transform a, Transform b) { if ((object)a == null && (object)b == null) { return true; } if ((object)a == null && (object)b != null) { return false; } if ((object)a != null && (object)b == null) { return false; } if (Math.Abs(a.Angle - b.Angle) > 0.01f) { return false; } if (Math.Abs(a.Position.x - b.Position.x) > 0.01f || Math.Abs(a.Position.y - b.Position.y) > 0.01f) { return false; } return true; }
Kesulitan pertama
Tidak ada masalah dengan simulasi gerak, sementara itu dapat diproyeksikan ke pesawat 2D - fisika dalam kasus seperti itu bekerja dengan sangat baik, tetapi pada satu titik desainer game datang dan berkata: "Kami ingin granat!" Dan kami berpikir agar tidak ada yang berubah banyak , mengapa tidak mensimulasikan penerbangan 3D dari tubuh fisik dengan hanya data 2D yang ada.
Dan mereka memperkenalkan konsep ketinggian untuk beberapa objek.
Seperti apa perubahan hukum ketinggian dari waktu ke waktu untuk benda yang ditinggalkan, mereka melewati pelajaran fisika di kelas delapan, sehingga keputusan balistik ternyata sepele. Tapi solusi dengan tabrakan tidak sepele lagi. Mari kita bayangkan kasus ini: sebuah granat selama penerbangan harus bertabrakan dengan dinding, atau terbang di atasnya tergantung pada ketinggian saat ini dan ketinggian dinding. Kami akan menyelesaikan masalah hanya di dunia dua dimensi, di mana granat diwakili oleh lingkaran dan dinding dengan persegi panjang.
Tampilan geometri objek untuk menyelesaikan masalah.Pertama-tama, kami mematikan interaksi tubuh dinamis granat dengan badan statis dan dinamis lainnya. Ini perlu untuk fokus pada tujuan. Dalam tugas kami, sebuah granat harus dapat melewati benda-benda lain dan βterbang di atasβ dinding ketika proyeksi mereka pada bidang dua dimensi saling berpotongan. Dalam interaksi normal, dua objek tidak dapat saling melewati, namun dalam kasus granat dengan logika gerakan tinggi dan kustom, kami mengizinkannya melakukan ini dalam kondisi tertentu.
Kami memperkenalkan komponen terpisah dari GrenadeMovement untuk granat, di mana kami memperkenalkan konsep ketinggian:
[Component] public class GrenadeMovement { public float Height; [DontPack] public Vector2 Velocity; [DontPack] public float VerticalVelocity; public GrenadeMovement(float height, Vector2 velocity, float verticalVelocity) { } }
Sekarang granat memiliki koordinat ketinggian, tetapi informasi ini tidak memberikan apa-apa kepada dunia. Oleh karena itu, kami memutuskan untuk menipu dan menambahkan kondisi berikut: granat dapat terbang di atas dinding, tetapi hanya dengan ketinggian tertentu. Dengan demikian, seluruh definisi tumbukan turun untuk memeriksa tumbukan proyeksi dan membandingkan ketinggian dinding dengan nilai GrenadeMovement. Bidang tinggi. Jika ketinggian terbang granat kurang, itu bertabrakan dengan dinding, jika tidak ia dapat dengan tenang terus bergerak di sepanjang jalurnya, termasuk dalam ruang 2D.
Pada iterasi pertama, granat jatuh begitu saja ketika menemukan persimpangan, tapi kemudian kami menambahkan tabrakan elastis, dan itu mulai berperilaku hampir tidak dapat dibedakan dari hasil yang akan kami dapatkan dalam 3D.
Kode lengkap untuk menghitung lintasan granat dan tumbukan elastis diberikan di bawah ini:
. Apa selanjutnya
, , - , .
ECS . , , JSON, ECS. :

, «». ECS, , . β β , , ECS, ECS . , API, , , . , .
- 2D-: , . , : , opensource , - . ECS, , . , , . - , , . β - .
- , 3D-, , .
, , , . , , ECS .
Tautan yang bermanfaat
:
: