
Dalam
artikel sebelumnya, kolega saya berbicara tentang bagaimana kami menggunakan mesin fisika dua dimensi dalam penembak multipemain seluler kami. Dan sekarang saya ingin membagikan bagaimana kami melempar semua yang kami lakukan sebelumnya dan mulai dari awal - dengan kata lain, bagaimana kami mentransfer game kami dari dunia 2D ke 3D.
Semuanya berawal dari fakta bahwa begitu seorang produser dan desainer game terkemuka datang ke departemen programmer kami dan memberi kami tantangan: penembak Top-Down PvP seluler dengan penembakan di ruang terbatas harus diubah menjadi penembak orang ketiga dengan penembakan di area terbuka. Dalam hal ini, diharapkan bahwa kartu tidak terlihat seperti ini:

Jadi:

Persyaratan teknis adalah sebagai berikut:
- ukuran peta - 100 Γ 100 meter;
- perbedaan ketinggian - 40 meter;
- dukungan untuk terowongan, jembatan;
- menembaki target pada ketinggian yang berbeda;
- tabrakan dengan geometri statis (kami tidak memiliki tabrakan dengan karakter lain dalam game);
- fisika jatuh bebas;
- granat melempar fisika.
Ke depan, saya dapat mengatakan bahwa permainan kami tidak terlihat seperti tangkapan layar terakhir: ternyata persilangan antara opsi pertama dan kedua.
Opsi Satu: Struktur Berlapis
Gagasan pertama diusulkan untuk tidak mengubah mesin fisika, tetapi hanya untuk menambahkan beberapa lapisan level "jumlah lantai". Ternyata sesuatu seperti denah di gedung:

Dengan pendekatan ini, kami tidak perlu mengulang secara radikal aplikasi klien atau server, dan secara umum tampaknya bahwa dengan cara ini tugas diselesaikan dengan cukup sederhana. Namun, ketika mencoba mengimplementasikannya, kami menemukan beberapa masalah kritis:
- Setelah mengklarifikasi detail dengan desainer level, kami sampai pada kesimpulan bahwa jumlah "lantai" dalam skema seperti itu dapat mengesankan: beberapa peta terletak di area terbuka dengan lereng dan bukit yang lembut.
- Perhitungan hit ketika memotret dari satu layer ke layer lain menjadi tugas yang tidak sepele. Contoh dari situasi masalah ditunjukkan pada gambar di bawah ini: di sini pemain 1 dapat masuk ke pemain 3, tetapi bukan pemain 2, karena jalur tembakan memblokir lapisan 2, meskipun kedua pemain 2 dan pemain 3 berada di lapisan yang sama.

Singkatnya, kami dengan cepat meninggalkan ide membagi ruang menjadi lapisan 2D - dan memutuskan bahwa kami akan bertindak dengan sepenuhnya mengganti mesin fisik.
Yang mengarahkan kami pada kebutuhan untuk memilih engine ini dan membangunnya menjadi aplikasi klien dan server yang ada.
Opsi Dua: Pilih Perpustakaan Siap
Karena permainan klien ditulis dalam Unity, kami memutuskan untuk mempertimbangkan kemungkinan menggunakan mesin fisik yang dibangun ke Unity secara default - PhysX. Secara umum, ia sepenuhnya memenuhi persyaratan dari desainer game kami untuk mendukung fisika 3D dalam game, tetapi masih ada masalah yang signifikan. Terdiri dari kenyataan bahwa aplikasi server kami ditulis dalam C # tanpa menggunakan Unity.
Ada opsi untuk menggunakan pustaka C ++ pada server - misalnya, PhysX yang sama - tetapi kami tidak serius mempertimbangkannya: karena penggunaan kode asli, ada kemungkinan server mengalami crash dengan pendekatan ini. Juga merasa malu dengan produktivitas rendah operasi Interop dan keunikan rakitan PhysX murni di bawah Unity, tidak termasuk penggunaannya di lingkungan lain.
Selain itu, dalam upaya menerapkan gagasan ini, ditemukan masalah lain:
- kurangnya dukungan untuk membangun Unity dengan IL2CPP di Linux, yang ternyata cukup kritis, karena di salah satu rilis terbaru kami beralih server game kami ke .Net Core 2.1 dan menyebarkannya di mesin Linux;
- kurangnya alat yang mudah digunakan untuk membuat profil server di Unity;
- rendahnya kinerja aplikasi Unity: kami hanya membutuhkan mesin fisik, dan tidak semua fungsi yang tersedia di Unity.
Selain itu, bersamaan dengan proyek kami, perusahaan ini mengembangkan game PvP multi-pemain prototipe lain. Pengembangnya menggunakan server Unity, dan kami mendapat cukup banyak umpan balik negatif mengenai pendekatan yang diusulkan. Secara khusus, salah satu keluhan adalah bahwa server Unity sangat "mengalir" dan harus dinyalakan kembali setiap beberapa jam.
Kombinasi dari masalah-masalah ini membuat kami meninggalkan ide ini juga. Kemudian kami memutuskan untuk meninggalkan server game di .Net Core 2.1 dan memilih daripada VolatilePhysics, yang kami gunakan sebelumnya, mesin fisik terbuka lain yang ditulis dalam C #. Yaitu, kami membutuhkan mesin C #, karena kami takut akan terjadi crash yang tidak terduga ketika menggunakan mesin yang ditulis dalam C ++.
Hasilnya, mesin berikut dipilih untuk pengujian:
Kriteria utama bagi kami adalah kinerja mesin, kemungkinan integrasinya ke dalam Unity dan dukungannya: seharusnya tidak ditinggalkan jika kami menemukan bug di dalamnya.
Jadi, kami menguji mesin Bepu Physics v1, Bepu Physics v2 dan Jitter Physics untuk kinerja, dan di antaranya Bepu Physics v2 terbukti paling produktif. Selain itu, ia adalah satu-satunya dari ketiganya yang terus berkembang aktif.
Namun, Bepu Physics v2 tidak memenuhi kriteria integrasi terakhir yang tersisa dengan Unity: perpustakaan ini menggunakan operasi dan Sistem SIMD. Angka, dan karena tidak ada dukungan SIMD dalam rakitan pada perangkat seluler dengan IL2CPP, semua manfaat dari optimasi Bepu hilang. Adegan demo dalam versi iOS di iPhone 5S sangat lambat. Kami tidak dapat menggunakan solusi ini di perangkat seluler.
Di sini harus dijelaskan mengapa kami umumnya tertarik menggunakan mesin fisik. Dalam salah satu artikel saya sebelumnya
, saya berbicara tentang bagaimana kami menerapkan bagian jaringan dari permainan dan bagaimana prediksi lokal dari aksi pemain bekerja. Singkatnya, kode yang sama dijalankan pada klien dan server - sistem ECS. Klien merespons tindakan pemain secara instan, tanpa menunggu respons dari server - apa yang disebut prediksi terjadi. Ketika respons datang dari server, klien memeriksa keadaan dunia yang diprediksi dengan yang diterima, dan jika mereka tidak cocok (misprediksi), maka berdasarkan respons dari server, rekonsiliasi dari apa yang dilihat pemain dilakukan.
Gagasan utamanya adalah kita mengeksekusi kode yang sama baik pada klien dan di server, dan situasi dengan kesalahan prediksi sangat jarang terjadi. Namun, tidak ada mesin C # fisik yang kami temukan yang memenuhi persyaratan ketika bekerja pada perangkat seluler: misalnya, tidak dapat memberikan 30 fps yang stabil pada iPhone 5S.
Opsi tiga, final: dua mesin yang berbeda
Kemudian kami memutuskan untuk bereksperimen: menggunakan dua mesin fisik yang berbeda pada klien dan server. Kami berpikir bahwa dalam kasus kami ini bisa berhasil: kami memiliki fisika tabrakan yang cukup sederhana dalam permainan kami, terlebih lagi, itu diterapkan oleh kami sebagai sistem ECS terpisah dan bukan bagian dari mesin fisik. Yang kami butuhkan dari mesin fisik adalah kemampuan untuk membuat reykast dan siaran dalam ruang 3D.
Sebagai hasilnya, kami memutuskan untuk menggunakan Unity Physics bawaan - PhysX - pada klien dan Bepu Physics v2 pada server.
Pertama-tama, kami menyoroti antarmuka untuk menggunakan mesin fisik:
Lihat kodeusing System; using System.Collections.Generic; using System.Numerics; namespace Prototype.Common.Physics { public interface IPhysicsWorld : IDisposable { bool HasBody(uint id); void SetCurrentSimulationTick(int tick); void Update(); RayCastHit RayCast(Vector3 origin, Vector3 direction, float distance, CollisionLayer layer, int ticksBehind = 0, List<uint> ignoreIds = null); RayCastHit SphereCast(Vector3 origin, Vector3 direction, float distance, float radius, CollisionLayer layer, int ticksBehind = 0, List<uint> ignoreIds = null); RayCastHit CapsuleCast(Vector3 origin, Vector3 direction, float distance, float radius, float height, CollisionLayer layer, int ticksBehind = 0, List<uint> ignoreIds = null); void CapsuleOverlap(Vector3 origin, float radius, float height, BodyMobilityField bodyMobilityField, CollisionLayer layer, List<Overlap> overlaps, int ticksBehind = 0); void RemoveOrphanedDynamicBodies(WorldState.TableSet currentWorld); void UpdateBody(uint id, Vector3 position, float angle); void CreateStaticCapsule(Vector3 origin, Quaternion rotation, float radius, float height, uint id, CollisionLayer layer); void CreateDynamicCapsule(Vector3 origin, Quaternion rotation, float radius, float height, uint id, CollisionLayer layer); void CreateStaticBox(Vector3 origin, Quaternion rotation, Vector3 size, uint id, CollisionLayer layer); void CreateDynamicBox(Vector3 origin, Quaternion rotation, Vector3 size, uint id, CollisionLayer layer); } }
Ada implementasi yang berbeda dari antarmuka ini pada klien dan server: seperti yang telah disebutkan, pada server kami menggunakan implementasi dengan Bepu, dan pada klien - Unity.
Di sini perlu disebutkan nuansa bekerja dengan fisika kita di server.
Karena kenyataan bahwa klien menerima pembaruan dunia dari server dengan penundaan (lag), pemain melihat dunia sedikit berbeda dari apa yang dilihatnya di server: ia melihat dirinya di masa kini, dan seluruh dunia di masa lalu. Karena ini, ternyata pemain menembak secara lokal pada target yang terletak di server di tempat lain. Jadi, karena kita menggunakan sistem prediksi aksi pemain lokal, kita perlu mengkompensasi kelambatan saat memotret di server.

Untuk mengkompensasi mereka, kita perlu menyimpan di server sejarah dunia selama N milidetik terakhir, dan juga dapat bekerja dengan objek dari sejarah, termasuk fisika mereka. Artinya, sistem kami harus dapat menghitung tabrakan, rakcast, dan sweepcast "di masa lalu". Sebagai aturan, mesin fisik tidak tahu bagaimana melakukan ini, dan Bepu dengan PhysX tidak terkecuali. Karena itu, kami harus mengimplementasikan fungsionalitas seperti itu sendiri.
Karena kami mensimulasikan game dengan frekuensi tetap 30 ticks per detik, kami harus menyimpan data dunia fisik untuk setiap tick. Idenya adalah untuk membuat bukan satu contoh simulasi di mesin fisik, tetapi N - untuk setiap centang yang disimpan dalam sejarah - dan menggunakan buffer siklik dari simulasi ini untuk menyimpannya dalam sejarah:
private readonly SimulationSlice[] _simulationHistory = new SimulationSlice[PhysicsConfigs.HistoryLength]; public BepupPhysicsWorld() { _currentSimulationTick = 1; for (int i = 0; i < PhysicsConfigs.HistoryLength; i++) { _simulationHistory[i] = new SimulationSlice(_bufferPool); } }
Di ECS kami, ada sejumlah sistem baca-tulis yang bekerja dengan fisika:
- InitPhysicsWorldSystem;
- SpawnPhysicsDynamicsBodiesSystem;
- DestroyPhysicsDynamicsBodiesSystem;
- UpdatePhysicsTransformsSystem;
- MovePhysicsSystem,
serta sejumlah sistem read-only, seperti sistem untuk menghitung hit dari tembakan, ledakan dari granat, dll.
Pada setiap tick dari simulasi dunia, InitPhysicsWorldSystem dijalankan terlebih dahulu, yang menetapkan nomor tick saat ini (SimulationSlice) ke mesin fisik:
public void SetCurrentSimulationTick(int tick) { var oldTick = tick - 1; var newSlice = _simulationHistory[tick % PhysicsConfigs.HistoryLength]; var oldSlice = _simulationHistory[oldTick % PhysicsConfigs.HistoryLength]; newSlice.RestoreBodiesFromPreviousTick(oldSlice); _currentSimulationTick = tick; }
Metode RestoreBodiesFromPreviousTick mengembalikan posisi objek di mesin fisik pada saat centang sebelumnya dari data yang disimpan dalam sejarah:
Lihat kode public void RestoreBodiesFromPreviousTick(SimulationSlice previous) { var oldStaticCount = previous._staticIds.Count;
Setelah itu, sistem SpawnPhysicsDynamicsBodiesSystem dan DestroyPhysicsDynamicsBodiesSystem membuat atau menghapus objek dalam mesin fisik sesuai dengan bagaimana mereka diubah dalam centang ECS ββterakhir. Kemudian UpdatePhysicsTransformsSystem memperbarui posisi semua badan dinamis sesuai dengan data dalam ECS.
Segera setelah data dalam ECS dan mesin fisika disinkronkan, kami menghitung pergerakan objek. Ketika semua operasi baca-tulis selesai, sistem baca-saja untuk menghitung logika game (tembakan, ledakan, kabut perang ...) ikut bermain.
Kode implementasi SimulationSlice Lengkap untuk Fisika Bepu:
Lihat kode using System; using System.Collections.Generic; using System.Numerics; using BepuPhysics; using BepuPhysics.Collidables; using BepuUtilities.Memory; using Quaternion = BepuUtilities.Quaternion; namespace Prototype.Physics { public partial class BepupPhysicsWorld { private unsafe partial class SimulationSlice : IDisposable { private readonly Dictionary<int, StaticBody> _staticHandlerToBody = new Dictionary<int, StaticBody>(); private readonly Dictionary<int, DynamicBody> _dynamicHandlerToBody = new Dictionary<int, DynamicBody>(); private readonly Dictionary<uint, int> _staticIdToHandler = new Dictionary<uint, int>(); private readonly Dictionary<uint, int> _dynamicIdToHandler = new Dictionary<uint, int>(); private readonly List<uint> _staticIds = new List<uint>(); private readonly List<uint> _dynamicIds = new List<uint>(); private readonly BufferPool _bufferPool; private readonly Simulation _simulation; public SimulationSlice(BufferPool bufferPool) { _bufferPool = bufferPool; _simulation = Simulation.Create(_bufferPool, new NarrowPhaseCallbacks(), new PoseIntegratorCallbacks(new Vector3(0, -9.81f, 0))); } public RayCastHit RayCast(Vector3 origin, Vector3 direction, float distance, CollisionLayer layer, List<uint> ignoreIds=null) { direction = direction.Normalized(); BepupRayCastHitHandler handler = new BepupRayCastHitHandler(_staticHandlerToBody, _dynamicHandlerToBody, layer, ignoreIds); _simulation.RayCast(origin, direction, distance, ref handler); var result = handler.RayCastHit; if (result.IsValid) { var collidableReference = handler.CollidableReference; if (handler.CollidableReference.Mobility == CollidableMobility.Static) { _simulation.Statics.GetDescription(collidableReference.Handle, out var description); result.HitEntityId = _staticHandlerToBody[collidableReference.Handle].Id; result.CollidableCenter = description.Pose.Position; } else { _simulation.Bodies.GetDescription(collidableReference.Handle, out var description); result.HitEntityId = _dynamicHandlerToBody[collidableReference.Handle].Id; result.CollidableCenter = description.Pose.Position; } } return result; } public RayCastHit SphereCast(Vector3 origin, Vector3 direction, float distance, float radius, CollisionLayer layer, List<uint> ignoreIds = null) { direction = direction.Normalized(); SweepCastHitHandler handler = new SweepCastHitHandler(_staticHandlerToBody, _dynamicHandlerToBody, layer, ignoreIds); _simulation.Sweep(new Sphere(radius), new RigidPose(origin, Quaternion.Identity), new BodyVelocity(direction.Normalized()), distance, _bufferPool, ref handler); var result = handler.RayCastHit; if (result.IsValid) { var collidableReference = handler.CollidableReference; if (handler.CollidableReference.Mobility == CollidableMobility.Static) { _simulation.Statics.GetDescription(collidableReference.Handle, out var description); result.HitEntityId = _staticHandlerToBody[collidableReference.Handle].Id; result.CollidableCenter = description.Pose.Position; } else { var reference = new BodyReference(collidableReference.Handle, _simulation.Bodies); result.HitEntityId = _dynamicHandlerToBody[collidableReference.Handle].Id; result.CollidableCenter = reference.Pose.Position; } } return result; } public RayCastHit CapsuleCast(Vector3 origin, Vector3 direction, float distance, float radius, float height, CollisionLayer layer, List<uint> ignoreIds = null) { direction = direction.Normalized(); var length = height - 2 * radius; SweepCastHitHandler handler = new SweepCastHitHandler(_staticHandlerToBody, _dynamicHandlerToBody, layer, ignoreIds); _simulation.Sweep(new Capsule(radius, length), new RigidPose(origin, Quaternion.Identity), new BodyVelocity(direction.Normalized()), distance, _bufferPool, ref handler); var result = handler.RayCastHit; if (result.IsValid) { var collidableReference = handler.CollidableReference; if (handler.CollidableReference.Mobility == CollidableMobility.Static) { _simulation.Statics.GetDescription(collidableReference.Handle, out var description); result.HitEntityId = _staticHandlerToBody[collidableReference.Handle].Id; result.CollidableCenter = description.Pose.Position; } else { var reference = new BodyReference(collidableReference.Handle, _simulation.Bodies); result.HitEntityId = _dynamicHandlerToBody[collidableReference.Handle].Id; result.CollidableCenter = reference.Pose.Position; } } return result; } public void CapsuleOverlap(Vector3 origin, float radius, float height, BodyMobilityField bodyMobilityField, CollisionLayer layer, List<Overlap> overlaps) { var length = height - 2 * radius; var handler = new BepupOverlapHitHandler( bodyMobilityField, layer, _staticHandlerToBody, _dynamicHandlerToBody, overlaps); _simulation.Sweep( new Capsule(radius, length), new RigidPose(origin, Quaternion.Identity), new BodyVelocity(Vector3.Zero), 0, _bufferPool, ref handler); } public void CreateDynamicBox(Vector3 origin, Quaternion rotation, Vector3 size, uint id, CollisionLayer layer) { var shape = new Box(size.X, size.Y, size.Z); var pose = new RigidPose() { Position = origin, Orientation = rotation }; var handler = CreateDynamic(shape, pose, false, id, layer); var body = _dynamicHandlerToBody[handler]; body.Box = shape; _dynamicHandlerToBody[handler] = body; } public void CreateStaticBox(Vector3 origin, Quaternion rotation, Vector3 size, uint id, CollisionLayer layer) { var shape = new Box(size.X, size.Y, size.Z); var pose = new RigidPose() { Position = origin, Orientation = rotation }; var handler =CreateStatic(shape, pose, false, id, layer); var body = _staticHandlerToBody[handler]; body.Box = shape; _staticHandlerToBody[handler] = body; } public void CreateStaticCapsule(Vector3 origin, Quaternion rotation, float radius, float height, uint id, CollisionLayer layer) { var length = height - 2 * radius; var shape = new Capsule(radius, length); var pose = new RigidPose() { Position = origin, Orientation = rotation }; var handler =CreateStatic(shape, pose, true, id, layer); var body = _staticHandlerToBody[handler]; body.Capsule = shape; _staticHandlerToBody[handler] = body; } public void CreateDynamicCapsule(Vector3 origin, Quaternion rotation, float radius, float height, uint id, CollisionLayer layer) { var length = height - 2 * radius; var shape = new Capsule(radius, length); var pose = new RigidPose() { Position = origin, Orientation = rotation }; var handler = CreateDynamic(shape, pose, true, id, layer); var body = _dynamicHandlerToBody[handler]; body.Capsule = shape; _dynamicHandlerToBody[handler] = body; } private int CreateDynamic<TShape>(TShape shape, RigidPose pose, bool isCapsule, uint id, CollisionLayer collisionLayer) where TShape : struct, IShape { var activity = new BodyActivityDescription() { SleepThreshold = -1 }; var collidable = new CollidableDescription() { Shape = _simulation.Shapes.Add(shape), SpeculativeMargin = 0.1f, }; var capsuleDescription = BodyDescription.CreateKinematic(pose, collidable, activity); var handler = _simulation.Bodies.Add(capsuleDescription); _dynamicIds.Add(id); _dynamicIdToHandler.Add(id, handler); _dynamicHandlerToBody.Add(handler, new DynamicBody { BodyReference = new BodyReference(handler, _simulation.Bodies), Id = id, IsCapsule = isCapsule, CollisionLayer = collisionLayer }); return handler; } private int CreateStatic<TShape>(TShape shape, RigidPose pose, bool isCapsule, uint id, CollisionLayer collisionLayer) where TShape : struct, IShape { var capsuleDescription = new StaticDescription() { Pose = pose, Collidable = new CollidableDescription() { Shape = _simulation.Shapes.Add(shape), SpeculativeMargin = 0.1f, } }; var handler = _simulation.Statics.Add(capsuleDescription); _staticIds.Add(id); _staticIdToHandler.Add(id, handler); _staticHandlerToBody.Add(handler, new StaticBody { Description = capsuleDescription, Id = id, IsCapsule = isCapsule, CollisionLayer = collisionLayer }); return handler; } public void RemoveOrphanedDynamicBodies(TableSet currentWorld) { var toDel = stackalloc uint[_dynamicIds.Count]; var toDelIndex = 0; foreach (var i in _dynamicIdToHandler) { if (currentWorld.DynamicPhysicsBody.HasCmp(i.Key)) { continue; } toDel[toDelIndex] = i.Key; toDelIndex++; } for (int i = 0; i < toDelIndex; i++) { var id = toDel[i]; var handler = _dynamicIdToHandler[id]; _simulation.Bodies.Remove(handler); _dynamicHandlerToBody.Remove(handler); _dynamicIds.Remove(id); _dynamicIdToHandler.Remove(id); } } public bool HasBody(uint id) { return _staticIdToHandler.ContainsKey(id) || _dynamicIdToHandler.ContainsKey(id); } public void RestoreBodiesFromPreviousTick(SimulationSlice previous) { var oldStaticCount = previous._staticIds.Count;
Selain itu, selain menerapkan sejarah di server, kami perlu menerapkan sejarah fisika pada klien. Klien Unity kami memiliki mode emulasi server - kami menyebutnya simulasi lokal - di mana kode server berjalan dengan klien. Kami menggunakan mode ini untuk membuat prototip cepat fitur game.
Seperti Bepu, PhysX tidak memiliki dukungan riwayat. Di sini kami menggunakan ide yang sama menggunakan beberapa simulasi fisik untuk setiap centang dalam sejarah seperti pada server. Namun, Unity memaksakan spesifiknya sendiri untuk bekerja dengan mesin fisik. Namun, perlu dicatat di sini bahwa proyek kami dikembangkan di Unity 2018.4 (LTS), dan beberapa API dapat berubah di versi yang lebih baru, sehingga tidak akan ada masalah seperti kami.
Masalahnya adalah bahwa Unity tidak memungkinkan membuat simulasi fisik yang terpisah (atau, dalam terminologi PhysX, adegan), jadi kami menerapkan setiap centang dalam sejarah fisika pada Unity sebagai adegan terpisah.
Kelas pembungkus ditulis atas adegan seperti itu - UnityPhysicsHistorySlice:
public UnityPhysicsHistorySlice(SphereCastDelegate sphereCastDelegate, OverlapSphereNonAlloc overlapSphere, CapsuleCastDelegate capsuleCast, OverlapCapsuleNonAlloc overlapCapsule, string name) { _scene = SceneManager.CreateScene(name, new CreateSceneParameters() { localPhysicsMode = LocalPhysicsMode.Physics3D }); _physicsScene = _scene.GetPhysicsScene(); _sphereCast = sphereCastDelegate; _capsuleCast = capsuleCast; _overlapSphere = overlapSphere; _overlapCapsule = overlapCapsule; _boxPool = new PhysicsSceneObjectsPool<BoxCollider>(_scene, "box", 0); _capsulePool = new PhysicsSceneObjectsPool<UnityEngine.CapsuleCollider>(_scene, "sphere", 0); }
Masalah kedua dari Unity adalah bahwa semua pekerjaan dengan fisika dilakukan melalui kelas statis Fisika, yang API-nya tidak memungkinkan Anda untuk melakukan rakecast dan sweepcast dalam adegan tertentu. API ini hanya berfungsi dengan satu adegan aktif. Namun, mesin PhysX itu sendiri memungkinkan Anda untuk bekerja dengan beberapa adegan sekaligus, Anda hanya perlu memanggil metode yang benar. Untungnya, Unity menyembunyikan metode semacam itu di belakang antarmuka kelas Physics.cs, yang tersisa hanyalah mengaksesnya. Kami melakukannya seperti ini:
Lihat kode MethodInfo raycastMethod = typeof(Physics).GetMethod("Internal_SphereCast", BindingFlags.NonPublic | BindingFlags.Static); var sphereCast = (SphereCastDelegate) Delegate.CreateDelegate(typeof(SphereCastDelegate), raycastMethod); MethodInfo overlapSphereMethod = typeof(Physics).GetMethod("OverlapSphereNonAlloc_Internal", BindingFlags.NonPublic | BindingFlags.Static); var overlapSphere = (OverlapSphereNonAlloc) Delegate.CreateDelegate(typeof(OverlapSphereNonAlloc), overlapSphereMethod); MethodInfo capsuleCastMethod = typeof(Physics).GetMethod("Internal_CapsuleCast", BindingFlags.NonPublic | BindingFlags.Static); var capsuleCast = (CapsuleCastDelegate) Delegate.CreateDelegate(typeof(CapsuleCastDelegate), capsuleCastMethod); MethodInfo overlapCapsuleMethod = typeof(Physics).GetMethod("OverlapCapsuleNonAlloc_Internal", BindingFlags.NonPublic | BindingFlags.Static); var overlapCapsule = (OverlapCapsuleNonAlloc) Delegate.CreateDelegate(typeof(OverlapCapsuleNonAlloc), overlapCapsuleMethod);
Kalau tidak, kode untuk mengimplementasikan UnityPhysicsHistorySlice tidak jauh berbeda dari apa yang ada di BepuSimulationSlice.
Jadi, kami mendapat dua implementasi fisika game: pada klien dan di server.
Langkah selanjutnya adalah pengujian.
Salah satu indikator paling penting dari "kesehatan" klien kami adalah parameter dari jumlah salah duga dengan server. Sebelum beralih ke mesin fisik yang berbeda, indikator ini bervariasi dalam 1-2% - yaitu, selama pertarungan berlangsung 9000 ticks (atau 5 menit) kami keliru dalam 90-180 tick ticks. Kami mendapatkan hasil ini selama beberapa rilis game di lounge lembut. Setelah beralih ke mesin yang berbeda, kami mengharapkan pertumbuhan yang kuat dari indikator ini - mungkin bahkan beberapa kali - setelah semua, sekarang kami mengeksekusi kode yang berbeda pada klien dan server, dan tampaknya logis bahwa kesalahan dalam perhitungan dengan algoritma yang berbeda akan cepat terakumulasi. Dalam praktiknya, ternyata parameter perbedaan hanya tumbuh 0,2-0,5% dan rata-rata 2-2,5% per pertempuran, yang sangat cocok untuk kita.
Sebagian besar mesin dan teknologi yang kami selidiki menggunakan kode yang sama pada klien dan server. Namun, hipotesis kami dengan kemungkinan menggunakan mesin fisik yang berbeda telah dikonfirmasi. Alasan utama mengapa tingkat perbedaan tumbuh sangat sedikit adalah karena kami menghitung pergerakan benda di ruang angkasa dan tabrakan oleh salah satu sistem ECS kami. Kode ini sama pada klien dan di server. Dari mesin fisik, kami membutuhkan perhitungan cepat dari rakecasts dan sweepcasts, dan hasil dari operasi ini dalam praktik untuk dua mesin kami tidak jauh berbeda.
Apa yang harus dibaca
Kesimpulannya, seperti biasa, berikut adalah beberapa tautan terkait: