Pengembangan Unity AI: Tutorial FSM Grafis berbasis xNode
Diterbitkan: 2022-08-12Dalam “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:
- Di Unity, klik Window > Package Manager untuk membuka jendela Package Manager.
- Klik + (tanda plus) di sudut kiri atas jendela dan pilih Tambahkan paket dari git URL untuk menampilkan bidang teks.
- Ketik atau tempel
https://github.com/siccity/xNode.gitdi 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.
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.
Untuk membuat status
ChasedanPatrol:Klik kanan dan pilih State untuk membuat node baru.
Beri nama node
Chase.Kembali ke menu drop-down, pilih State lagi untuk membuat node kedua.
Beri nama node
Patrol.Seret dan lepas tindakan
ChasedanPatrolyang ada ke status terkait yang baru dibuat.
Untuk membuat transisi:
Klik kanan dan pilih Transition untuk membuat node baru.
Tetapkan objek
LineOfSightDecisionke bidangDecisiontransisi.
Untuk membuat simpul
RemainInState:- Klik kanan dan pilih RemainInState untuk membuat simpul baru.
Untuk menghubungkan grafik:
Hubungkan output
TransitionsnodePatrolke inputEntrynodeTransition.Hubungkan output
True Statedari nodeTransitionke inputEntrynodeChase.Hubungkan output
False Statedari nodeTransitionke inputEntrydari nodeRemain In State.
Grafiknya akan terlihat seperti ini:
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:
- Klik kanan dan pilih Initial Node untuk membuat node baru.
- Lampirkan output FSM Node ke input
EntrynodePatrol.
Grafik sekarang akan terlihat seperti ini:
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:
Buka aset SampleScene.
Temukan objek game
Enemykami di jendela hierarki Unity.Ganti komponen
BaseStateMachinedengan komponenBaseStateMachineGraph:Klik Tambahkan Komponen dan pilih skrip
BaseStateMachineGraphyang benar.Tetapkan grafik FSM kami,
EnemyGraph, ke bidangGraphdari komponenBaseStateMachineGraph.Hapus komponen
BaseStateMachine(karena tidak lagi diperlukan) dengan mengklik kanan dan memilih Remove Component .
Objek game Enemy akan terlihat seperti ini:
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.
