Fitur game menggunakan ECS: tambahkan kit P3K ke penembak



Mari kita beralih dari karpet ke hal-hal serius. Kami sudah bicara tentang ECS, kerangka apa yang ada untuk Unity dan mengapa mereka menulis sendiri (Anda dapat menemukan daftar di akhir artikel). Sekarang mari kita membahas contoh-contoh spesifik tentang bagaimana kita menggunakan ECS dalam penembak PvP seluler baru kita dan bagaimana kita menerapkan fitur-fitur game. Saya perhatikan bahwa kami menggunakan arsitektur ini hanya untuk mensimulasikan dunia pada server dan sistem prediksi pada klien. Visualisasi dan rendering objek diimplementasikan menggunakan pola MVP - tetapi tidak tentang itu hari ini.

Arsitektur ECS berorientasi data, semua data dunia permainan disimpan dalam apa yang disebut GameState dan merupakan daftar entitas (entitas) dengan beberapa komponen pada masing-masingnya. Seperangkat komponen menentukan perilaku suatu objek. Dan logika perilaku komponen terkonsentrasi dalam sistem.

Gamestate di ECS kami terdiri dari dua bagian: RuleBook dan WorldState. RuleBook adalah seperangkat komponen yang tidak berubah selama pertandingan. Semua data statis disimpan di sana (karakteristik senjata / karakter, komposisi tim) dan dikirim ke klien hanya sekali - selama otorisasi di server permainan.

Pertimbangkan contoh sederhana: menelurkan karakter dan pindahkan dalam ruang 2D menggunakan dua joystick. Pertama, deklarasikan komponen.

Ini menentukan pemain dan diperlukan untuk visualisasi karakter:

[Component] public class Player { } 

Komponen selanjutnya adalah permintaan untuk membuat karakter baru. Ini berisi dua bidang: waktu karakter spawn (dalam kutu) dan ID-nya:

 [Component] public class PlayerSpawnRequest { public int SpawnTime; public unit PlayerId; } 

Komponen orientasi objek dalam ruang:

 [Component] public class Transform { public Vector2 Position; public float Rotation; } 

Komponen yang menyimpan kecepatan objek saat ini:

 [Component] public class Movement { public Vector2 Velocity; public float RotateToAngle; } 

Komponen yang menyimpan input pemain (vektor joystick gerak dan vektor joystick rotasi karakter):

 [Component] public class Input { public Vector2 MoveVector; public Vector2 RotateVector; } 

Komponen dengan karakteristik statis karakter (itu akan disimpan di RuleBook, karena ini adalah karakteristik dasar dan tidak berubah selama sesi permainan):

 [Component] public class PlayerStats { public float MoveSpeed; } 

Ketika mendekomposisi fitur ke dalam sistem, kita sering dipandu oleh prinsip tanggung jawab tunggal: setiap sistem harus memenuhi satu dan hanya satu fungsi.

Fitur dapat terdiri dari beberapa sistem. Mari kita mulai dengan mendefinisikan sistem karakter spawn. Sistem melewati semua permintaan untuk membuat karakter dalam gamestate dan jika waktu dunia saat ini cocok dengan yang diperlukan, itu menciptakan entitas baru dan menempel padanya komponen yang menentukan pemain: Pemain , Transformasi , Gerakan .

 public class SpawnPlayerSystem : ExecutableSystem { public override void Execute(GameState gs) { var deleter = gs.Pools.Deferred.GetDeleter(gs.WorldState.SpawnAvatarRequest); foreach (var avatarRequest in gs.WorldState.SpawnAvatarRequest) { if (avatarRequest.Value.SpawnTime == gs.Time) { // create new entity with player ID var playerEntity = gs.WorldState.CreateEntity(avatarRequest.Value.PlayerId); // add components to determinate player behaviour playerEntity.AddPlayer(); playerEntity.AddTransform(Vector2.zero, 0); playerEntity.AddMovement(Vector2.zero, 0); // delete player spawn request deleter.Delete(avatarRequest.Key); } } } } 

Sekarang perhatikan pergerakan pemain di joystick. Kami membutuhkan sistem yang akan menangani input. Ini melewati semua komponen input, menghitung kecepatan pemain (dia berdiri atau bergerak) dan mengubah vektor joystick rotasi menjadi sudut rotasi:

 MovementControlSystem public class MovementControlSystem : ExecutableSystem { public override void Execute(GameState gs) { var playerStats = gs.RuleBook.PlayerStats[1]; foreach (var pair in gs.Input) { var movement = gs.WorldState.Movement[pair.Key]; movement.Velocity = pair.Value.MoveVector.normalized * playerStats.MoveSpeed; movement.RotateToAngle = Math.Atan2(pair.Value.RotateVector.y, pair.Value.RotateVector.x); } } } 

Berikutnya adalah sistem pergerakan:

 public class MovementSystem : ExecutableSystem { public override void Execute(GameState gs) { foreach (var pair in gs.WorldState.Movement) { var transform = gs.WorldState.Transform[pair.Key]; transform.Position += pair.Value.Velocity * GameState.TickDurationSec; } } } 

Sistem yang bertanggung jawab untuk rotasi objek:

 public class RotationSystem : ExecutableSystem { public override void Execute(GameState gs) { foreach (var pair in gs.WorldState.Movement) { var transform = gs.WorldState.Transform[pair.Key]; transform.Angle = pair.Value.RotateToAngle; } } } 

MovementSystem dan RotationSystem hanya berfungsi dengan komponen Transform dan Movement . Mereka terlepas dari esensi pemain. Jika entitas lain dengan komponen Gerakan dan Transform muncul di permainan kami, logika gerakan juga akan bekerja dengannya.

Misalnya, tambahkan kit pertolongan pertama yang akan bergerak dalam garis lurus di sepanjang bibit dan, saat memilih, mengisi kembali kesehatan karakter. Deklarasikan komponen:

 [Component] public class Health { public uint CurrentHealth; public uint MaxHealth; } [Component] public class HealthPowerUp { public uint NextChangeDirection; } [Component] public class HealthPowerUpSpawnRequest { public uint SpawnRequest; } [Component] public class HealthPowerUpStats { public float HealthRestorePercent; public float MoveSpeed; public float SecondsToChangeDirection; public float PickupRadius; public float TimeToSpawn; } 

Kami memodifikasi komponen statistik karakter dengan menambahkan jumlah maksimum kehidupan di sana:

 [Component] public class PlayerStats { public float MoveSpeed; public uint MaxHealth; } 

Sekarang kita memodifikasi sistem karakter spawn sehingga karakter muncul dengan kesehatan maksimum:

 public class SpawnPlayerSystem : ExecutableSystem { public override void Execute(GameState gs) { var deleter = gs.Pools.Deferred.GetDeleter(gs.WorldState.SpawnAvatarRequest); var playerStats = gs.RuleBook.PlayerStats[1]; foreach (var avatarRequest in gs.WorldState.SpawnAvatarRequest) { if (avatarRequest.Value.SpawnTime <= gs.Time) { // create new entity with player ID var playerEntity = gs.WorldState.CreateEntity(avatarRequest.Value.PlayerId); // add components to determinate player behaviour playerEntity.AddPlayer(); playerEntity.AddTransform(Vector2.zero, 0); playerEntity.AddMovement(Vector2.zero, 0); playerEntity.AddHealth(playerStats.MaxHealth, playerStats.MaxHealth); // delete player spawn request deleter.Delete(avatarRequest.Key); } } } } 

Lalu kami mendeklarasikan sistem spawn kit P3K kami:

 public class SpawnHealthPowerUpSystem : ExecutableSystem { public override void Execute(GameState gs) { var deleter = gs.Pools.Deferred.GetDeleter(gs.WorldState.HealthPowerUpSpawnRequest); var healthPowerUpStats = gs.RoolBook.healthPowerUpStats[1]; foreach (var spawnRequest in gs.WorldState.HealthPowerUpSpawnRequest) { // create new entity var powerUpEntity = gs.WorldState.CreateEntity(); // add components to determine healthPowerUp behaviour powerUpEntity.AddHealthPowerUp((uint)(healthPowerUpStats.SecondsToChangeDirection * GameState.Hz)); playerEntity.AddTransform(Vector2.zero, 0); playerEntity.AddMovement(healthPowerUpStats.MoveSpeed, 0); // delete player spawn request deleter.Delete(spawnRequest.Key); } } } 

Dan sistem untuk mengubah kecepatan kotak P3K. Untuk menyederhanakan, kotak P3K akan mengubah arah setiap beberapa detik:

 public class HealthPowerUpMovementSystem : ExecutableSystem { public override void Execute(GameState gs) { var healthPowerUpStats = gs.RoolBook.healthPowerUpStats[1]; foreach (var pair in gs.WorldState.HealthPowerUp) { var movement = gs.WorldState.Movement[pair.Key]; if(pair.Value.NextChangeDirection <= gs.Time) { pair.Value.NextChangeDirection = (uint) (healthPowerUpStats.SecondsToChangeDirection * GameState.Hz); movement.Velocity *= -1; } } } } 

Karena kami sudah mengumumkan MovementSystem untuk memindahkan objek dalam permainan, kami hanya perlu HealthPowerUpMovementSystem untuk mengubah vektor kecepatan, setiap N detik.

Sekarang kita menyelesaikan pemilihan kit P3K dan perhitungan karakter HP. Kita akan memerlukan komponen tambahan lain untuk menyimpan jumlah nyawa yang akan diterima karakter setelah memilih kotak P3K.

 [Component] public class HealthToAdd { public int Health; public Entity Target; } 

Dan komponen untuk menghilangkan kekuatan kita:

 [Component] public class DeleteHealthPowerUpRequest { } 

Kami menulis sistem yang memproses pemilihan kit P3K:

 public class HealthPowerUpPickUpSystem : ExecutableSystem { public override void Execute(GameState gs) { var healthPowerUpStats = gs.RoolBook.healthPowerUpStats[1]; foreach(var powerUpPair in gs.WorldState.HealthPowerUp) { var powerUpTransform = gs.WorldState.Transform[powerUpPair.Key]; foreach(var playerPair in gs.WorldState.Player) { var playerTransform = gs.WorldState.Transform[playerPair.Key]; var distance = Vector2.Distance(powerUpTransform.Position, playerTransform.Position) if(distance < healthPowerUpStats.PickupRadius) { var healthToAdd = gs.WorldState.Health[playerPair.Key].MaxHealth * healthPowerUpStats.HealthRestorePercent; var entity = gs.WorldState.CreateEntity(); entity.AddHealthToAdd(healthToAdd, gs.WorldState.Player[playerPair.Key]); var powerUpEnity = gs.WorldState[powerUpPair.Key]; powerUpEnity.AddDeleteHealthPowerUpRequest(); break; } } } } } 

Sistem melewati semua power-up aktif dan menghitung jarak ke pemain. Jika ada pemain dalam radius seleksi, sistem membuat dua komponen permintaan:

HealthToAdd - "permintaan" untuk menambahkan kehidupan ke karakter;
DeleteHealthPowerUpRequest - "request" untuk menghapus kit pertolongan pertama.

Mengapa tidak menambahkan jumlah nyawa yang tepat dalam sistem yang sama? Kami melanjutkan dari fakta bahwa pemain menerima HP tidak hanya dari kit P3K, tetapi juga dari sumber lain. Dalam hal ini, lebih baik untuk memisahkan sistem pemilihan kit pertolongan pertama dan sistem akrual kehidupan karakter. Selain itu, ini lebih konsisten dengan Prinsip Tanggung Jawab Tunggal.

Kami menerapkan sistem accruing lives dengan karakter:

 public class HealingSystem : ExecutableSystem { public override void Execute(GameState gs) { var deleter = gs.Pools.Deferred.GetDeleter(gs.WorldState.HealthToAdd); foreach(var healtToAddPair in gs.WorldState.HealthToAdd) { var healthToAdd = healtToAddPair.Value.Health; var health = healtToAddPair.Value.Target.Health; health.CurrentHealth += healthToAdd; health.CurrentHealth = Mathf.Clamp(health.CurrentHealth, 0, health.MaxHealth); deleter.Delete(healtToAddPair.Key); } } } 

Sistem melewati semua komponen HealthToAdd , menghitung jumlah nyawa yang diperlukan dalam komponen Kesehatan dari entitas Target target. Entitas ini tidak tahu apa-apa tentang sumber dan target dan cukup universal. Sistem ini dapat digunakan tidak hanya untuk menghitung kehidupan karakter, tetapi untuk objek apa pun yang melibatkan keberadaan kehidupan dan regenerasi mereka.

Untuk mengimplementasikan fitur dengan kotak P3K, ia tetap menambahkan sistem terakhir: sistem P3K setelah setelah pemilihannya.

 public class DeleteHealthPowerUpSystem : ExecutableSystem { public override void Execute(GameState gs) { var deleter = gs.Pools.Deferred.GetDeleter(gs.WorldState.DeleteHealthPowerUpReques); foreach(var healthRequest in gs.WorldState.DeleteHealthPowerUpReques) { var id = healthRequest.Key; gs.WorldState.DelHealthPowerUp(id); gs.WorldState.DelTransform(id); gs.WorldState.DelMovement(id); deleter.Delete(id); } } } 

HealthPowerUpPickUpSystem membuat permintaan untuk menghapus kit pertolongan pertama. Sistem DeleteHealthPowerUpSystem melewati semua permintaan seperti itu dan menghapus semua komponen yang merupakan inti dari kit pertolongan pertama.

Selesai Semua sistem dari contoh kami diimplementasikan. Ada satu titik bekerja dengan ECS - semua sistem dijalankan secara berurutan dan urutan ini penting.

Dalam contoh kami, urutan sistem adalah sebagai berikut:

 _systems = new List<ExecutableSystem> { new SpawnPlayerSystem(), new SpawnHealthPowerUpSystem(), new MovementControlSystem(), new HealthPowerUpMovementSystem(), new MovementSystem(), new RotationSystem(), new HealthPowerUpPickUpSystem(), new HealingSystem(), new DeleteHealthPowerUpSystem() }; 

Dalam kasus umum, sistem yang bertanggung jawab untuk membuat entitas dan komponen baru didahulukan. Kemudian sistem perawatan, dan akhirnya sistem pembuangan dan pembersihan.

Dengan dekomposisi yang tepat, ECS memiliki fleksibilitas besar. Ya, implementasi kami tidak sempurna, tetapi memungkinkan Anda untuk mengimplementasikan fitur dalam waktu singkat, dan juga memiliki kinerja yang baik pada perangkat seluler modern. Anda dapat membaca lebih lanjut tentang ECS โ€‹โ€‹di sini:

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


All Articles