Pengembangan Unity AI: Tutorial FSM Grafis berbasis xNode

Diterbitkan: 2022-08-12

Dalam “Unity AI Development: A Finite-state Machine Tutorial”, kami membuat game siluman sederhana—AI modular berbasis FSM. Dalam permainan, agen musuh berpatroli di ruang permainan. Saat melihat pemain, musuh mengubah statusnya dan mengikuti pemain alih-alih berpatroli.

Di babak kedua perjalanan Unity kami, kami akan membangun antarmuka pengguna grafis (GUI) untuk membuat komponen inti mesin finite-state (FSM) kami lebih cepat, dan dengan pengalaman pengembang Unity yang ditingkatkan.

Penyegaran Cepat

FSM yang dirinci dalam tutorial sebelumnya dibangun dari blok arsitektur sebagai skrip C#. Kami menambahkan tindakan dan keputusan ScriptableObject khusus sebagai kelas. Pendekatan ScriptableObject memungkinkan FSM yang mudah dipelihara dan dapat disesuaikan. Dalam tutorial ini, kami mengganti ScriptableObject s drag-and-drop FSM dengan opsi grafis.

Saya juga telah menulis skrip yang diperbarui untuk Anda yang ingin membuat permainan lebih mudah untuk menang. Untuk penerapannya, ganti saja skrip pendeteksi pemain dengan yang ini yang mempersempit bidang pandang musuh.

Memulai Dengan xNode

Kami akan membangun editor grafis kami menggunakan xNode, kerangka kerja untuk pohon perilaku berbasis simpul yang secara visual akan menampilkan aliran FSM kami. Meskipun GraphView Unity dapat menyelesaikan pekerjaan itu, API-nya bersifat eksperimental dan sedikit didokumentasikan. Antarmuka pengguna xNode memberikan pengalaman pengembang yang unggul, memfasilitasi pembuatan prototipe dan perluasan cepat FSM kami.

Mari tambahkan xNode ke proyek kita sebagai dependensi Git menggunakan Unity Package Manager:

  1. Di Unity, klik Window > Package Manager untuk membuka jendela Package Manager.
  2. Klik + (tanda plus) di sudut kiri atas jendela dan pilih Tambahkan paket dari git URL untuk menampilkan bidang teks.
  3. Ketik atau tempel https://github.com/siccity/xNode.git di kotak teks yang tidak berlabel dan klik tombol Tambah .

Sekarang kita siap untuk menyelam lebih dalam dan memahami komponen utama xNode:

Kelas Node Merupakan simpul, unit grafik yang paling mendasar. Dalam tutorial xNode ini, kami memperoleh kelas baru dari kelas Node yang mendeklarasikan node yang dilengkapi dengan fungsi dan peran khusus.
Kelas NodeGraph Mewakili kumpulan node ( Instance kelas Node ) dan tepi yang menghubungkannya. Dalam tutorial xNode ini, kita mendapatkan dari NodeGraph sebuah kelas baru yang memanipulasi dan mengevaluasi node.
Kelas NodePort Merupakan gerbang komunikasi, port input tipe atau output tipe, yang terletak di antara instance Node dalam NodeGraph . Kelas NodePort unik untuk xNode.
[Input] atribut Penambahan atribut [Input] ke port menunjuknya sebagai input, memungkinkan port untuk meneruskan nilai ke node yang menjadi bagiannya. Pikirkan atribut [Input] sebagai parameter fungsi.
[Output] atribut Penambahan atribut [Output] ke port menunjuknya sebagai output, memungkinkan port untuk melewatkan nilai dari node yang menjadi bagiannya. Pikirkan atribut [Output] sebagai nilai kembalian suatu fungsi.

Memvisualisasikan Lingkungan Bangunan xNode

Di xNode, kami bekerja dengan grafik di mana setiap State dan Transition mengambil bentuk simpul. Koneksi input dan/atau output memungkinkan node untuk berhubungan dengan salah satu atau semua node lain dalam grafik kita.

Mari kita bayangkan sebuah simpul dengan tiga nilai input: dua arbitrer dan satu boolean. Node akan menampilkan salah satu dari dua nilai input tipe arbitrer, tergantung pada apakah input boolean benar atau salah.

Node Cabang, diwakili oleh persegi panjang besar di tengah, termasuk pseudocode "Jika C == Benar A Lain B." Di sebelah kiri ada tiga persegi panjang, masing-masing memiliki panah yang menunjuk ke simpul Cabang: "A (arbitrary)," "B (arbitrary)," dan "C (boolean)." Node Cabang, akhirnya, memiliki panah yang menunjuk ke persegi panjang "Output".
Contoh Node Branch

Untuk mengonversi FSM yang ada menjadi grafik, kami memodifikasi kelas State dan Transition untuk mewarisi kelas Node alih-alih kelas ScriptableObject . Kami membuat objek grafik bertipe NodeGraph untuk memuat semua objek Status State Transition kami.

Memodifikasi BaseStateMachine untuk Digunakan Sebagai Tipe Dasar

Mulailah membangun antarmuka grafis dengan menambahkan dua metode virtual baru ke kelas BaseStateMachine yang ada:

Init Menetapkan status awal ke properti CurrentState
Execute Mengeksekusi status saat ini

Mendeklarasikan metode ini sebagai virtual memungkinkan kita untuk menimpanya, sehingga kita dapat mendefinisikan perilaku kustom kelas yang mewarisi kelas BaseStateMachine untuk inisialisasi dan eksekusi:

 using System; using System.Collections.Generic; using UnityEngine; namespace Demo.FSM { public class BaseStateMachine : MonoBehaviour { [SerializeField] private BaseState _initialState; private Dictionary<Type, Component> _cachedComponents; private void Awake() { Init(); _cachedComponents = new Dictionary<Type, Component>(); } public BaseState CurrentState { get; set; } private void Update() { Execute(); } public virtual void Init() { CurrentState = _initialState; } public virtual void Execute() { CurrentState.Execute(this); } // Allows us to execute consecutive calls of GetComponent in O(1) time public new T GetComponent<T>() where T : Component { if(_cachedComponents.ContainsKey(typeof(T))) return _cachedComponents[typeof(T)] as T; var component = base.GetComponent<T>(); if(component != null) { _cachedComponents.Add(typeof(T), component); } return component; } } }

Selanjutnya, di bawah folder FSM kita, mari kita buat:

FSMGraph Sebuah folder
BaseStateMachineGraph Kelas AC# dalam FSMGraph

Untuk saat ini, BaseStateMachineGraph hanya akan mewarisi kelas BaseStateMachine :

 using UnityEngine; namespace Demo.FSM.Graph { public class BaseStateMachineGraph : BaseStateMachine { } }

Kami tidak dapat menambahkan fungsionalitas ke BaseStateMachineGraph sampai kami membuat tipe simpul dasar kami; mari kita lakukan selanjutnya.

Menerapkan NodeGraph dan Membuat Tipe Node Dasar

Di bawah folder FSMGraph kami yang baru dibuat, kami akan membuat:

FSMGraph Kelas

Untuk saat ini, FSMGraph hanya akan mewarisi kelas NodeGraph (tanpa fungsionalitas tambahan):

 using UnityEngine; using XNode; namespace Demo.FSM.Graph { [CreateAssetMenu(menuName = "FSM/FSM Graph")] public class FSMGraph : NodeGraph { } }

Sebelum kita membuat kelas untuk node kita, mari tambahkan:

FSMNodeBase Kelas yang akan digunakan sebagai kelas dasar oleh semua node kami

Kelas FSMNodeBase akan berisi input bernama Entry dari tipe FSMNodeBase untuk memungkinkan kita menghubungkan node satu sama lain.

Kami juga akan menambahkan dua fungsi pembantu:

GetFirst Mengambil node pertama yang terhubung ke output yang diminta
GetAllOnPort Mengambil semua node yang tersisa yang terhubung ke output yang diminta
 using System.Collections.Generic; using XNode; namespace Demo.FSM.Graph { public abstract class FSMNodeBase : Node { [Input(backingValue = ShowBackingValue.Never)] public FSMNodeBase Entry; protected IEnumerable<T> GetAllOnPort<T>(string fieldName) where T : FSMNodeBase { NodePort port = GetOutputPort(fieldName); for (var portIndex = 0; portIndex < port.ConnectionCount; portIndex++) { yield return port.GetConnection(portIndex).node as T; } } protected T GetFirst<T>(string fieldName) where T : FSMNodeBase { NodePort port = GetOutputPort(fieldName); if (port.ConnectionCount > 0) return port.GetConnection(0).node as T; return null; } } }

Pada akhirnya, kita akan memiliki dua jenis node status; mari tambahkan kelas untuk mendukung ini:

BaseStateNode Kelas dasar untuk mendukung StateNode dan RemainInStateNode
 namespace Demo.FSM.Graph { public abstract class BaseStateNode : FSMNodeBase { } }

Selanjutnya, ubah kelas BaseStateMachineGraph :

 using UnityEngine; namespace Demo.FSM.Graph { public class BaseStateMachineGraph : BaseStateMachine { public new BaseStateNode CurrentState { get; set; } } }

Di sini, kami telah menyembunyikan properti CurrentState yang diwarisi dari kelas dasar dan mengubah tipenya dari BaseState menjadi BaseStateNode .

Membuat Blok Bangunan untuk Grafik FSM Kami

Selanjutnya, untuk membentuk blok bangunan utama FSM, mari tambahkan tiga kelas baru ke folder FSMGraph :

StateNode Mewakili keadaan agen. Saat dieksekusi, StateNode mengulangi TransitionNode s yang terhubung ke port output StateNode (diambil dengan metode pembantu). StateNode menanyakan masing-masing apakah akan mentransisikan node ke status yang berbeda atau membiarkan status node apa adanya.
RemainInStateNode Menunjukkan node harus tetap dalam keadaan saat ini.
TransitionNode Membuat keputusan untuk beralih ke status berbeda atau tetap berada di status yang sama.

Dalam tutorial Unity FSM sebelumnya, kelas State mengulangi daftar transisi. Di sini, di xNode, StateNode berfungsi sebagai ekuivalen State untuk beralih ke node yang diambil melalui metode helper GetAllOnPort kami.

Sekarang tambahkan atribut [Output] ke koneksi keluar (node ​​transisi) untuk menunjukkan bahwa mereka harus menjadi bagian dari GUI. Dengan desain xNode, nilai atribut berasal dari node sumber: node yang berisi bidang yang ditandai dengan atribut [Output] . Karena kami menggunakan atribut [Output] dan [Input] untuk menggambarkan hubungan dan koneksi yang akan diatur oleh GUI xNode, kami tidak dapat memperlakukan nilai-nilai ini seperti biasanya. Pertimbangkan bagaimana kami mengulangi melalui Actions versus Transitions :

 using System.Collections.Generic; namespace Demo.FSM.Graph { [CreateNodeMenu("State")] public sealed class StateNode : BaseStateNode { public List<FSMAction> Actions; [Output] public List<TransitionNode> Transitions; public void Execute(BaseStateMachineGraph baseStateMachine) { foreach (var action in Actions) action.Execute(baseStateMachine); foreach (var transition in GetAllOnPort<TransitionNode>(nameof(Transitions))) transition.Execute(baseStateMachine); } } }

Dalam hal ini, output Transitions dapat memiliki beberapa node yang melekat padanya; kita harus memanggil metode helper GetAllOnPort untuk mendapatkan daftar koneksi [Output] .

RemainInStateNode , sejauh ini, adalah kelas kami yang paling sederhana. Tidak menjalankan logika, RemainInStateNode hanya menunjukkan kepada agen kami—dalam kasus game kami, musuh—untuk tetap dalam kondisi saat ini:

 namespace Demo.FSM.Graph { [CreateNodeMenu("Remain In State")] public sealed class RemainInStateNode : BaseStateNode { } }

Pada titik ini, kelas TransitionNode masih belum lengkap dan tidak akan dikompilasi. Kesalahan terkait akan dihapus setelah kami memperbarui kelas.

Untuk membangun TransitionNode , kita perlu menyiasati persyaratan xNode bahwa nilai output berasal dari node sumber—seperti yang kita lakukan saat membangun StateNode . Perbedaan utama antara StateNode dan TransitionNode adalah bahwa output TransitionNode hanya dapat dilampirkan ke satu node. Dalam kasus kami, GetFirst akan mengambil satu node yang dilampirkan ke masing-masing port kami (satu node status untuk ditransisikan ke dalam kasus yang benar dan yang lain untuk ditransisikan ke dalam kasus yang salah):

 namespace Demo.FSM.Graph { [CreateNodeMenu("Transition")] public sealed class TransitionNode : FSMNodeBase { public Decision Decision; [Output] public BaseStateNode TrueState; [Output] public BaseStateNode FalseState; public void Execute(BaseStateMachineGraph stateMachine) { var trueState = GetFirst<BaseStateNode>(nameof(TrueState)); var falseState = GetFirst<BaseStateNode>(nameof(FalseState)); var decision = Decision.Decide(stateMachine); if (decision && !(trueState is RemainInStateNode)) { stateMachine.CurrentState = trueState; } else if(!decision && !(falseState is RemainInStateNode)) stateMachine.CurrentState = falseState; } } }

Mari kita lihat hasil grafis dari kode kita.

Membuat Grafik Visual

Dengan semua kelas FSM diurutkan, kami dapat melanjutkan untuk membuat Grafik FSM kami untuk agen musuh game. Di jendela proyek Unity, klik kanan folder EnemyAI dan pilih: Create > FSM > FSM Graph . Untuk membuat grafik kita lebih mudah diidentifikasi, beri nama EnemyGraph .

Di jendela editor xNode Graph, klik kanan untuk menampilkan daftar menu tarik-turun State , Transition , dan RemainInState . Jika jendela tidak terlihat, klik dua kali file EnemyGraph untuk membuka jendela editor xNode Graph.

  1. Untuk membuat status Chase dan Patrol :

    1. Klik kanan dan pilih State untuk membuat node baru.

    2. Beri nama node Chase .

    3. Kembali ke menu drop-down, pilih State lagi untuk membuat node kedua.

    4. Beri nama node Patrol .

    5. Seret dan lepas tindakan Chase dan Patrol yang ada ke status terkait yang baru dibuat.

  2. Untuk membuat transisi:

    1. Klik kanan dan pilih Transition untuk membuat node baru.

    2. Tetapkan objek LineOfSightDecision ke bidang Decision transisi.

  3. Untuk membuat simpul RemainInState :

    1. Klik kanan dan pilih RemainInState untuk membuat simpul baru.
  4. Untuk menghubungkan grafik:

    1. Hubungkan output Transitions node Patrol ke input Entry node Transition .

    2. Hubungkan output True State dari node Transition ke input Entry node Chase .

    3. Hubungkan output False State dari node Transition ke input Entry dari node Remain In State .

Grafiknya akan terlihat seperti ini:

Empat node direpresentasikan sebagai empat persegi panjang, masing-masing dengan lingkaran input Entry di sisi kiri atas mereka. Dari kiri ke kanan, node negara bagian Patroli menampilkan satu aksi: Aksi Patroli. Simpul status Patroli juga menyertakan lingkaran keluaran Transisi di sisi kanan bawahnya yang menghubungkan ke lingkaran Masuk dari simpul Transisi. Simpul Transition menampilkan satu keputusan: LineOfSight. Ini memiliki dua lingkaran keluaran di sisi kanan bawahnya, True State dan False State. True State terhubung ke Entry circle dari struktur ketiga kita, node Chase state. Node status Chase menampilkan satu tindakan: Tindakan Mengejar. Node status Chase memiliki lingkaran keluaran Transitions. Yang kedua dari dua lingkaran keluaran Transition, False State, terhubung ke lingkaran Entry dari struktur keempat dan terakhir kita, node RemainInState (yang muncul di bawah node Chase state).
Tampilan Awal Grafik FSM Kami

Tidak ada dalam grafik yang menunjukkan simpul mana—status Patrol atau Chase —yang merupakan simpul awal kita. Kelas BaseStateMachineGraph mendeteksi empat node tetapi, tanpa indikator, tidak dapat memilih status awal.

Untuk mengatasi masalah ini, mari kita buat:

FSMInitialNode Kelas yang output tunggalnya bertipe StateNode bernama InitialNode

Output InitialNode kami menunjukkan status awal. Selanjutnya, di FSMInitialNode , buat:

NextNode Properti untuk memungkinkan kita mengambil node yang terhubung ke output InitialNode
 using XNode; namespace Demo.FSM.Graph { [CreateNodeMenu("Initial Node"), NodeTint("#00ff52")] public class FSMInitialNode : Node { [Output] public StateNode InitialNode; public StateNode NextNode { get { var port = GetOutputPort("InitialNode"); if (port == null || port.ConnectionCount == 0) return null; return port.GetConnection(0).node as StateNode; } } } }

Sekarang setelah kita membuat kelas FSMInitialNode , kita dapat menghubungkannya ke input Entry dari status awal dan mengembalikan status awal melalui properti NextNode .

Mari kembali ke grafik kita dan tambahkan simpul awal. Di jendela editor xNode:

  1. Klik kanan dan pilih Initial Node untuk membuat node baru.
  2. Lampirkan output FSM Node ke input Entry node Patrol .

Grafik sekarang akan terlihat seperti ini:

Grafik yang sama seperti pada gambar sebelumnya, dengan satu persegi panjang hijau FSM Node ditambahkan ke kiri dari empat persegi panjang lainnya. Ini memiliki output Node Awal (diwakili oleh lingkaran biru) yang terhubung ke input "Masuk" node Patroli (diwakili oleh lingkaran merah tua).
Grafik FSM Kami Dengan Node Awal Terlampir pada Status Patroli

Untuk membuat hidup kita lebih mudah, kita akan menambahkan FSMGraph :

InitialState Sebuah properti

Saat pertama kali kami mencoba mengambil nilai properti InitialState , pengambil properti akan melintasi semua node dalam grafik kami saat mencoba menemukan FSMInitialNode . Setelah FSMInitialNode ditemukan, kami menggunakan properti NextNode untuk menemukan node status awal kami:

 using System.Linq; using UnityEngine; using XNode; namespace Demo.FSM.Graph { [CreateAssetMenu(menuName = "FSM/FSM Graph")] public sealed class FSMGraph : NodeGraph { private StateNode _initialState; public StateNode InitialState { get { if (_initialState == null) _initialState = FindInitialStateNode(); return _initialState; } } private StateNode FindInitialStateNode() { var initialNode = nodes.FirstOrDefault(x => x is FSMInitialNode); if (initialNode != null) { return (initialNode as FSMInitialNode).NextNode; } return null; } } }

Selanjutnya, di BaseStateMachineGraph , mari merujuk FSMGraph dan mengganti metode Init dan Execute BaseStateMachine . Overriding Init menetapkan CurrentState sebagai status awal grafik, dan mengesampingkan panggilan Execute Execute pada CurrentState :

 using UnityEngine; namespace Demo.FSM.Graph { public class BaseStateMachineGraph : BaseStateMachine { [SerializeField] private FSMGraph _graph; public new BaseStateNode CurrentState { get; set; } public override void Init() { CurrentState = _graph.InitialState; } public override void Execute() { ((StateNode)CurrentState).Execute(this); } } }

Sekarang, mari kita terapkan grafik ke objek Musuh, dan lihat aksinya.

Menguji Grafik FSM

Dalam persiapan untuk pengujian, di jendela Proyek Editor Unity:

  1. Buka aset SampleScene.

  2. Temukan objek game Enemy kami di jendela hierarki Unity.

  3. Ganti komponen BaseStateMachine dengan komponen BaseStateMachineGraph :

    1. Klik Tambahkan Komponen dan pilih skrip BaseStateMachineGraph yang benar.

    2. Tetapkan grafik FSM kami, EnemyGraph , ke bidang Graph dari komponen BaseStateMachineGraph .

    3. Hapus komponen BaseStateMachine (karena tidak lagi diperlukan) dengan mengklik kanan dan memilih Remove Component .

Objek game Enemy akan terlihat seperti ini:

Dari atas ke bawah, di layar Inspector, ada tanda centang di samping Enemy. "Pemain" dipilih di tarik-turun Tag, "Musuh" dipilih di tarik-turun Lapisan. Drop-down Transform menunjukkan posisi, rotasi, dan skala. Menu drop-down Capsule dikompresi, dan drop-down Mesh Renderer, Capsule Collider, dan Nav Mesh Agent muncul terkompresi dengan tanda centang di sebelah kirinya. Drop-down Enemy Sight Sensor menampilkan Script dan Abaikan Mask. Drop-down PatrolPoints menampilkan Script dan empat PatrolPoints. Ada tanda centang di samping drop-down Base State Machine Graph (Script). Skrip menunjukkan "BaseStateMachineGraph," Status Awal menunjukkan "Tidak Ada (Status Dasar), dan Grafik menunjukkan "Grafik Musuh (Grafik FSM)." Akhirnya, tarik-turun Blue Enemy (Material) dikompresi, dan tombol "Tambahkan Komponen" muncul di bawah dia.
Objek Game Enemy Kami

Itu dia! Sekarang kami memiliki FSM modular dengan editor grafis. Mengklik tombol Putar menunjukkan bahwa AI musuh yang dibuat secara grafis bekerja persis seperti musuh ScriptableObject yang kita buat sebelumnya.

Terus Maju: Mengoptimalkan FSM Kami

Peringatan: Saat Anda mengembangkan AI yang lebih canggih untuk gim Anda, jumlah status dan transisi meningkat, dan FSM menjadi membingungkan dan sulit dibaca. Editor grafis tumbuh menyerupai jaringan garis yang berasal dari beberapa status dan berakhir pada beberapa transisi—dan sebaliknya, membuat FSM sulit untuk di-debug.

Seperti pada tutorial sebelumnya, kami mengundang Anda untuk membuat kode Anda sendiri, mengoptimalkan permainan siluman Anda, dan mengatasi masalah ini. Bayangkan betapa membantunya memberi kode warna pada node status Anda untuk menunjukkan apakah sebuah node aktif atau tidak aktif, atau mengubah ukuran node RemainInState dan Initial untuk membatasi real estat layar mereka.

Peningkatan tersebut tidak hanya kosmetik. Referensi warna dan ukuran akan membantu mengidentifikasi di mana dan kapan harus melakukan debug. Grafik yang enak dilihat juga lebih mudah untuk dinilai, dianalisis, dan dipahami. Langkah selanjutnya terserah Anda—dengan dasar editor grafis kami, tidak ada batasan untuk peningkatan pengalaman pengembang yang dapat Anda lakukan.

Bacaan Lebih Lanjut di Blog Teknik Toptal:

  • 10 Kesalahan Paling Umum Yang Dilakukan Pengembang Unity
  • Unity With MVC: Cara Meningkatkan Pengembangan Game Anda
  • Menguasai Kamera 2D dalam Unity: Tutorial untuk Pengembang Game
  • Praktik Terbaik dan Kiat Unity oleh Pengembang Toptal

Blog Toptal Engineering mengucapkan terima kasih kepada Goran Lalic atas keahlian dan tinjauan teknisnya terhadap artikel ini.