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.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.
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
Chase
danPatrol
: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
Chase
danPatrol
yang ada ke status terkait yang baru dibuat.
Untuk membuat transisi:
Klik kanan dan pilih Transition untuk membuat node baru.
Tetapkan objek
LineOfSightDecision
ke bidangDecision
transisi.
Untuk membuat simpul
RemainInState
:- Klik kanan dan pilih RemainInState untuk membuat simpul baru.
Untuk menghubungkan grafik:
Hubungkan output
Transitions
nodePatrol
ke inputEntry
nodeTransition
.Hubungkan output
True State
dari nodeTransition
ke inputEntry
nodeChase
.Hubungkan output
False State
dari nodeTransition
ke inputEntry
dari 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
Entry
nodePatrol
.
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
Enemy
kami di jendela hierarki Unity.Ganti komponen
BaseStateMachine
dengan komponenBaseStateMachineGraph
:Klik Tambahkan Komponen dan pilih skrip
BaseStateMachineGraph
yang benar.Tetapkan grafik FSM kami,
EnemyGraph
, ke bidangGraph
dari komponenBaseStateMachineGraph
.Hapus komponen
BaseStateMachine
(karena tidak lagi diperlukan) dengan mengklik kanan dan memilih Remove Component .
Objek game Enemy
akan terlihat seperti ini:
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.