Manajemen Karakter dengan SharedEvents


Tautan ke proyek

Dalam artikel ini, saya ingin menunjukkan bagaimana Anda dapat menggunakan SharedEvents untuk mengontrol karakter orang ketiga yang menawarkan serangkaian aset standar. Saya menulis tentang SharedEvents di artikel sebelumnya ( ini dan ini ).

Selamat datang di kucing!

Hal pertama yang Anda butuhkan adalah mengambil proyek dengan SharedState / SharedEvents yang diimplementasikan dan menambahkan seperangkat aset standar



Saya membuat adegan kecil dan sangat sederhana dari membuat prototipe cetakan



Dan memanggang navigasi permukaan dengan pengaturan standar



Setelah itu, Anda perlu menambahkan prefab ThirdPersonCharacter ke adegan ini



Kemudian Anda dapat memulai dan memastikan bahwa semuanya berjalan di luar kotak. Kemudian Anda dapat melanjutkan untuk mengonfigurasi penggunaan infrastruktur SharedState / SharedEvents yang dibuat sebelumnya. Untuk melakukan ini, hapus komponen ThirdPersonUserController dari objek karakter.



karena kontrol manual menggunakan keyboard tidak diperlukan. Karakter akan dikontrol oleh agen, menunjukkan posisi di mana ia akan bergerak.

Dan untuk memungkinkan ini, Anda perlu menambahkan dan mengkonfigurasi komponen NavMeshAgent ke objek karakter



Sekarang Anda perlu membuat pengontrol sederhana yang akan mengontrol karakter
dengan mouse AgentMouseController



using UnityEngine; using UnityEngine.AI; using UnityStandardAssets.Characters.ThirdPerson; public class AgentMouseController : MonoBehaviour { public NavMeshAgent agent; public ThirdPersonCharacter character; public Camera cam; void Start() { //      agent.updateRotation = false; } void Update() { //     if (Input.GetMouseButtonDown(0)) { Ray ray = cam.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(ray, out hit)) { agent.SetDestination(hit.point); } } //    ,     if(agent.remainingDistance > agent.stoppingDistance) { character.Move(agent.desiredVelocity, false, false); } else // ,    { character.Move(Vector3.zero, false, false); } } } 

Dan tambahkan ke objek karakter, berikan tautan ke kamera, pengontrol dan agen karakter. Semua tersedia dari panggung.



Dan itu saja. Ini cukup untuk mengontrol karakter dengan memberi tahu agen tempat bergerak, menggunakan mouse (klik kiri).

Anda dapat memulai dan memastikan semuanya bekerja



Integrasi SharedEvents


Sekarang setelah adegan dasar siap, Anda dapat melanjutkan untuk mengintegrasikan kontrol karakter melalui SharedEvents . Untuk melakukan ini, Anda perlu membuat beberapa komponen. Yang pertama adalah komponen yang akan bertanggung jawab untuk menerima sinyal dari mouse dan memberitahukan semua komponen yang melacak posisi klik mouse di tempat kejadian, mereka hanya akan tertarik pada koordinat klik.

Komponen akan dipanggil, misalnya, MouseHandlerComponent



 using UnityEngine; public class MouseHandlerComponent : SharedStateComponent { public Camera cam; #region MonoBehaviour protected override void OnSharedStateChanged(SharedStateChangedEventData newState) { } protected override void OnStart() { if (cam == null) throw new MissingReferenceException("   "); } protected override void OnUpdate() { //     if (Input.GetMouseButtonDown(0)) { //           var hit = GetMouseHit(); Events.PublishAsync("poittogound", new PointOnGroundEventData { Sender = this, Point = hit.point }); } } #endregion private RaycastHit GetMouseHit() { Ray ray = cam.ScreenPointToRay(Input.mousePosition); RaycastHit hit; Physics.Raycast(ray, out hit); return hit; } } 

Komponen ini membutuhkan kelas untuk mengirim data dalam pemberitahuan. Untuk kelas yang hanya akan berisi data untuk pemberitahuan, Anda dapat membuat satu file dan beri nama DefinedEventsData



Dan tambahkan satu kelas untuk itu, untuk mengirim posisi klik dengan mouse

 using UnityEngine; public class PointOnGroundEventData : EventData { public Vector3 Point { get; set; } } 

Hal selanjutnya yang harus dilakukan adalah menambahkan komponen yang akan menjadi pembungkus atau dekorator, sesuka Anda, untuk komponen NavMeshAgent . Karena saya tidak akan mengubah komponen (pihak ketiga) yang ada, saya akan menggunakan dekorator untuk berintegrasi dengan SharedState / SharedEvents .



Komponen ini akan menerima pemberitahuan tentang klik mouse pada titik-titik tertentu di tempat kejadian dan memberi tahu agen tempat untuk pindah. Dan juga memantau posisi posisi agen di setiap frame dan membuat pemberitahuan tentang perubahannya.

Komponen ini akan tergantung pada komponen NavMeshAgent.

 using UnityEngine; using UnityEngine.AI; [RequireComponent(typeof(NavMeshAgent))] public class AgentWrapperComponent : SharedStateComponent { private NavMeshAgent agent; #region Monobehaviour protected override void OnSharedStateChanged(SharedStateChangedEventData newState) { } protected override void OnStart() { //  agent = GetComponent<NavMeshAgent>(); //      agent.updateRotation = false; Events.Subscribe<PointOnGroundEventData>("pointtoground", OnPointToGroundGot); } protected override void OnUpdate() { //     if (agent.remainingDistance > agent.stoppingDistance) { Events.Publish("agentmoved", new AgentMoveEventData { Sender = this, DesiredVelocity = agent.desiredVelocity }); } else { Events.Publish("agentmoved", new AgentMoveEventData { Sender = this, DesiredVelocity = Vector3.zero }); } } #endregion private void OnPointToGroundGot(PointOnGroundEventData eventData) { //    agent.SetDestination(eventData.Point); } } 


Untuk mengirim data, komponen ini membutuhkan kelas yang perlu ditambahkan ke file DefinedEventsData.
 public class AgentMoveEventData : EventData { public Vector3 DesiredVelocity { get; set; } } 

Ini sudah cukup bagi karakter untuk bergerak. Tapi dia akan melakukannya tanpa animasi, karena kita belum menggunakan ThirdPersonCharater . Dan untuk itu, seperti untuk NavMeshAgent, Anda perlu membuat dekorator CharacterWrapperComponent



Komponen akan mendengarkan pemberitahuan tentang perubahan posisi agen, dan memindahkan karakter ke arah yang diterima dari pemberitahuan (acara).

 using UnityEngine; using UnityStandardAssets.Characters.ThirdPerson; [RequireComponent(typeof(ThirdPersonCharacter))] public class CharacterWrapperComponent : SharedStateComponent { private ThirdPersonCharacter character; #region Monobehaviour protected override void OnSharedStateChanged(SharedStateChangedEventData newState) { } protected override void OnStart() { character = GetComponent<ThirdPersonCharacter>(); Events.Subscribe<AgentMoveEventData>("agentmoved", OnAgentMove); } protected override void OnUpdate() { } #endregion private void OnAgentMove(AgentMoveEventData eventData) { //       character.Move(eventData.DesiredVelocity, false, false); } } 

Dan itu saja. Tetap menambahkan komponen-komponen ini ke objek permainan karakter. Anda perlu membuat salinan dari yang sudah ada, menghapus komponen AgentMouseControl yang lama



Dan tambahkan MouseHandlerComponent , AgentWrapperComponent dan CharacterWrapperComponent baru .

Di MouseHandlerComponent Anda perlu mentransfer kamera dari tempat dari mana posisi klik akan dihitung.





Anda dapat memulai dan memastikan semuanya bekerja.

Itu terjadi dengan bantuan SharedEvents untuk mengontrol karakter tanpa memiliki koneksi langsung antara komponen, seperti pada contoh pertama. Ini akan memungkinkan konfigurasi komponen komposisi berbeda yang lebih fleksibel dan menyesuaikan interaksi di antara mereka.

Perilaku Asynchronous untuk SharedEvents


Cara mekanisme pemberitahuan sekarang diterapkan didasarkan pada transmisi sinkron sinyal dan prosesnya. Artinya, semakin banyak pendengar, semakin lama prosesnya. Untuk menghindari hal ini, Anda perlu menerapkan pemrosesan notifikasi sinkron. Hal pertama yang harus dilakukan adalah menambahkan versi asinkron dari metode Publikasikan

 //  data    eventName  public async Task PublishAsync<T>(string eventName, T data) where T : EventData { if (_subscribers.ContainsKey(eventName)) { var listOfDelegates = _subscribers[eventName]; var tasks = new List<Task>(); foreach (Action<T> callback in listOfDelegates) { tasks.Add(Task.Run(() => { callback(data); })); } await Task.WhenAll(tasks); } } 

Sekarang kita perlu mengubah metode OnUpdate abstrak di kelas dasar SharedStateComponent menjadi asinkron sehingga mengembalikan tugas yang dimulai di dalam implementasi metode ini dan menamainya menjadi OnUpdateAsync

 protected abstract Task[] OnUpdateAsync(); 

Anda juga akan membutuhkan mekanisme yang akan mengontrol penyelesaian tugas dari frame sebelumnya, sebelum saat ini

 private Task[] _previosFrameTasks = null; //   private async Task CompletePreviousTasks() { if (_previosFrameTasks != null && _previosFrameTasks.Length > 0) await Task.WhenAll(_previosFrameTasks); } 

Metode Pembaruan di kelas dasar perlu ditandai sebagai async dan pra-periksa pelaksanaan tugas sebelumnya

 async void Update() { await CompletePreviousTasks(); //     _previosFrameTasks = OnUpdateAsync(); } 

Setelah perubahan ini di kelas dasar, Anda dapat melanjutkan untuk mengubah implementasi metode OnUpdate lama ke OnUpdateAsync baru. Komponen pertama di mana ini akan dilakukan adalah AgentWrapperComponent . Sekarang metode ini mengharapkan kembalinya hasil. Hasil ini akan menjadi array tugas. Array karena dalam metode ini beberapa dapat diluncurkan secara paralel dan kami akan memprosesnya secara berkelompok.

 protected override Task[] OnUpdateAsync() { //     if (agent.remainingDistance > agent.stoppingDistance) { return new Task[] { Events.PublishAsync("agentmoved", new AgentMoveEventData { Sender = this, DesiredVelocity = agent.desiredVelocity }) }; } else { return new Task[] { Events.PublishAsync("agentmoved", new AgentMoveEventData { Sender = this, DesiredVelocity = Vector3.zero }) }; } } 

Kandidat berikutnya untuk perubahan metode OnUpdate adalah MouseHandlerController . Di sini prinsipnya sama

  protected override Task[] OnUpdateAsync() { //     if (Input.GetMouseButtonDown(0)) { //           var hit = GetMouseHit(); return new Task[] { Events.PublishAsync("pointtoground", new PointOnGroundEventData { Sender = this, Point = hit.point }) }; } return null; } 

Di semua implementasi lain di mana metode ini kosong, cukup untuk menggantinya

 protected override Task[] OnUpdateAsync() { return null; } 

Itu saja. Sekarang Anda dapat memulai, dan jika komponen yang memproses notifikasi secara serempak tidak mengakses komponen-komponen yang seharusnya diproses di utas utama, seperti Transform, misalnya, semuanya akan berfungsi. Jika tidak, kami akan mendapatkan kesalahan di konsol yang memberitahukan bahwa kami mengakses komponen-komponen ini bukan dari utas utama



Untuk mengatasi masalah ini, Anda perlu membuat komponen yang akan memproses kode di utas utama. Buat folder terpisah untuk skrip dan sebut Sistem, dan juga tambahkan skrip Dispatcher ke skrip tersebut.



Komponen ini akan menjadi singleton dan memiliki satu metode abstrak publik yang akan mengeksekusi kode di utas utama. Prinsip operator sangat sederhana. Kami akan menyampaikan kepadanya delegasi yang akan dieksekusi di utas utama, ia akan menempatkan mereka dalam antrian. Dan di setiap frame, jika ada sesuatu dalam antrian, jalankan di utas utama. Komponen ini akan menambahkan dirinya ke adegan dalam satu salinan, saya suka pendekatan yang sederhana dan efektif.

 using System; using System.Collections; using System.Collections.Concurrent; using UnityEngine; public class Dispatcher : MonoBehaviour { private static Dispatcher _instance; private volatile bool _queued = false; private ConcurrentQueue<Action> _queue = new ConcurrentQueue<Action>(); private static readonly object _sync_ = new object(); //     public static void RunOnMainThread(Action action) { _instance._queue.Enqueue(action); lock (_sync_) { _instance._queued = true; } } //       () [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void Initialize() { if (_instance == null) { _instance = new GameObject("Dispatcher").AddComponent<Dispatcher>(); DontDestroyOnLoad(_instance.gameObject); } } void Update() { if (_queued) //   { while (!_queue.IsEmpty) { if (_queue.TryDequeue(out Action a)) { StartCoroutine(ActionWrapper(a)); } } lock (_sync_) { _queued = false; } } } //    IEnumerator ActionWrapper(Action a) { a(); yield return null; } } 

Hal selanjutnya yang harus dilakukan adalah menerapkan dispatcher. Ada 2 tempat untuk melakukan ini. Pertama adalah dekorator karakter, di sana kami menanyakan arahnya. Dalam komponen CharacterWrapperComponent

 private void OnAgentMove(AgentMoveEventData eventData) { Dispatcher.RunOnMainThread(() => character.Move(eventData.DesiredVelocity, false, false)); } 

2 adalah dekorator agen, di sana kami menunjukkan posisi untuk agen. Dalam komponen AgentWrapperComponent

 private void OnPointToGroundGot(PointOnGroundEventData eventData) { //    Dispatcher.RunOnMainThread(() => agent.SetDestination(eventData.Point)); } 

Sekarang tidak akan ada kesalahan, kode akan berfungsi dengan benar. Anda dapat memulai dan melihat ini.

Sedikit refactoring


Setelah semuanya siap dan semuanya berfungsi, Anda dapat menyisir kode sedikit dan membuatnya sedikit lebih nyaman dan sederhana. Ini akan membutuhkan beberapa perubahan.

Agar tidak membuat array tugas dan memasukkan satu-satunya tugas secara manual, Anda dapat membuat metode ekstensi. Untuk semua metode ekstensi, Anda dapat menggunakan file yang sama untuk transmisi ke notifikasi serta untuk semua kelas. Ini akan terletak di folder System dan disebut Extensions



Di dalam, kami akan membuat metode ekstensi generik sederhana yang akan membungkus instance apa pun dalam array

 public static class Extensions { //    public static T[] WrapToArray<T>(this T source) { return new T[] { source }; } } 

Perubahan selanjutnya adalah menyembunyikan penggunaan langsung dari operator dalam komponen. Sebagai gantinya, buat metode di kelas dasar SharedStateComponent dan gunakan dispatcher dari sana.

 protected void PerformInMainThread(Action action) { Dispatcher.RunOnMainThread(action); } 

Dan sekarang Anda perlu menerapkan perubahan ini di beberapa tempat. Pertama, ubah metode di mana kami secara manual membuat array tugas dan memasukkannya ke dalam satu instance
Dalam komponen AgentWrapperComponent

 protected override Task[] OnUpdateAsync() { //     if (agent.remainingDistance > agent.stoppingDistance) { return Events.PublishAsync("agentmoved", new AgentMoveEventData { Sender = this, DesiredVelocity = agent.desiredVelocity }) .WrapToArray(); } else { return Events.PublishAsync("agentmoved", new AgentMoveEventData { Sender = this, DesiredVelocity = Vector3.zero }) .WrapToArray(); } } 

Dan di komponen MouseHandlerComponent

 protected override Task[] OnUpdateAsync() { //     if (Input.GetMouseButtonDown(0)) { //           var hit = GetMouseHit(); return Events.PublishAsync("pointtoground", new PointOnGroundEventData { Sender = this, Point = hit.point }) .WrapToArray(); } return null; } 

Sekarang kita menyingkirkan penggunaan langsung dari dispatcher di komponen dan sebaliknya kita memanggil metode PerformInMainThread di kelas dasar.

Pertama di AgentWrapperComponent

 private void OnPointToGroundGot(PointOnGroundEventData eventData) { //    PerformInMainThread(() => agent.SetDestination(eventData.Point)); } 

dan di komponen CharacterWrapperComponent

 private void OnAgentMove(AgentMoveEventData eventData) { PerformInMainThread(() => character.Move(eventData.DesiredVelocity, false, false)); } 

Itu saja. Tetap menjalankan permainan dan memastikan tidak ada yang rusak selama refactoring dan semuanya berfungsi dengan benar.

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


All Articles