Pengembangan Unity AI: Tutorial Mesin kondisi-terbatas
Diterbitkan: 2022-03-11Dalam dunia game yang kompetitif, pengembang berusaha untuk menawarkan pengalaman pengguna yang menghibur bagi mereka yang berinteraksi dengan karakter non-pemain (NPC) yang kami buat. Pengembang dapat memberikan interaktivitas ini dengan menggunakan mesin finite-state (FSM) untuk menciptakan solusi AI yang mensimulasikan kecerdasan di NPC kami.
Tren AI telah bergeser ke pohon perilaku, tetapi FSM tetap relevan. Mereka tergabung—dalam satu kapasitas atau lainnya—ke dalam hampir setiap game elektronik.
Anatomi FSM
FSM adalah model komputasi di mana hanya satu dari sejumlah keadaan hipotetis yang dapat aktif pada satu waktu. Sebuah transisi FSM dari satu negara ke negara lain, menanggapi kondisi atau masukan. Komponen intinya meliputi:
| Komponen | Keterangan |
|---|---|
| Negara | Salah satu dari serangkaian opsi terbatas yang menunjukkan kondisi keseluruhan FSM saat ini; setiap keadaan tertentu mencakup serangkaian tindakan yang terkait |
| Tindakan | Apa yang dilakukan negara saat FSM menanyakannya |
| Keputusan | Logika yang terbentuk ketika transisi terjadi |
| Transisi | Proses perubahan status |
Sementara kami akan fokus pada FSM dari perspektif implementasi AI, konsep seperti mesin keadaan animasi dan keadaan permainan umum juga berada di bawah payung FSM.
Memvisualisasikan FSM
Mari kita lihat contoh game arcade klasik Pac-Man. Dalam keadaan awal permainan (keadaan "pengejaran"), NPC adalah hantu berwarna-warni yang mengejar dan akhirnya melampaui pemain. Hantu bertransisi ke keadaan menghindar setiap kali pemain memakan pelet kekuatan dan mengalami peningkatan kekuatan, mendapatkan kemampuan untuk memakan hantu. Hantu, sekarang berwarna biru, menghindari pemain sampai waktu power-up habis dan hantu bertransisi kembali ke status pengejaran, di mana perilaku dan warna asli mereka dipulihkan.
Hantu Pac-Man selalu berada di salah satu dari dua keadaan: mengejar atau menghindar. Secara alami, kita harus menyediakan dua transisi—satu dari chase ke evade, yang lain dari evade ke chase:
Mesin keadaan-terbatas, secara desain, menanyakan keadaan saat ini, yang menanyakan keputusan dan tindakan dari keadaan itu. Diagram berikut mewakili contoh Pac-Man kami dan menunjukkan keputusan yang memeriksa status power-up pemain. Jika power-up telah dimulai, NPC bertransisi dari chase ke evade. Jika power-up telah berakhir, transisi NPC dari menghindar ke mengejar. Akhirnya, jika tidak ada perubahan power-up, tidak ada transisi yang terjadi.
Skalabilitas
FSM membebaskan kami untuk membangun AI modular. Misalnya, hanya dengan satu tindakan baru, kita dapat membuat NPC dengan perilaku baru. Dengan demikian, kita dapat menganggap tindakan baru—memakan pelet kekuatan—dengan salah satu hantu Pac-Man kita, memberinya kemampuan untuk memakan pelet kekuatan sambil menghindari pemain. Kami dapat menggunakan kembali tindakan, keputusan, dan transisi yang ada untuk mendukung perilaku ini.
Karena sumber daya yang dibutuhkan untuk mengembangkan NPC unik sangat minim, kami berada di posisi yang tepat untuk memenuhi persyaratan proyek yang berkembang dari beberapa NPC unik. Di sisi lain, jumlah status dan transisi yang berlebihan dapat membuat kita terjerat dalam mesin kondisi spageti — FSM yang koneksinya terlalu banyak sehingga sulit untuk di-debug dan dipelihara.
Menerapkan FSM di Unity
Untuk mendemonstrasikan cara mengimplementasikan mesin keadaan-terbatas di Unity, mari buat game siluman sederhana. Arsitektur kami akan menggabungkan ScriptableObject s, yang merupakan wadah data yang dapat menyimpan dan berbagi informasi di seluruh aplikasi, sehingga kami tidak perlu mereproduksinya. ScriptableObject s mampu memproses terbatas, seperti memanggil tindakan dan membuat kueri keputusan. Selain dokumentasi resmi Unity, pembahasan Arsitektur Game dengan Objek Skrip yang lebih lama tetap menjadi sumber yang bagus jika Anda ingin menyelam lebih dalam.
Sebelum kami menambahkan AI ke proyek awal yang siap dikompilasi ini, pertimbangkan arsitektur yang diusulkan:
Dalam contoh permainan kami, musuh (NPC yang diwakili oleh kapsul biru) berpatroli. Ketika musuh melihat pemain (diwakili oleh kapsul abu-abu), musuh mulai mengikuti pemain:
Berbeda dengan Pac-Man, musuh dalam game kita tidak akan kembali ke status default ("patroli") setelah mengikuti pemain.
Membuat Kelas
Mari kita mulai dengan membuat kelas kita. Di folder scripts baru, kami akan menambahkan semua blok bangunan arsitektur yang diusulkan sebagai skrip C#.
Menerapkan Kelas BaseStateMachine
Kelas BaseStateMachine adalah satu-satunya MonoBehavior yang akan kami tambahkan untuk mengakses NPC kami yang mendukung AI. Demi kesederhanaan, BaseStateMachine kami akan sangat sederhana. Namun, jika kami mau, kami dapat menambahkan FSM kustom yang diwarisi yang menyimpan parameter dan referensi tambahan ke komponen tambahan. Perhatikan bahwa kode tidak akan dikompilasi dengan benar sampai kita menambahkan kelas BaseState kita, yang akan kita lakukan nanti dalam tutorial kita.
Kode untuk BaseStateMachine merujuk dan mengeksekusi status saat ini untuk melakukan tindakan dan melihat apakah transisi diperlukan:
using UnityEngine; namespace Demo.FSM { public class BaseStateMachine : MonoBehaviour { [SerializeField] private BaseState _initialState; private void Awake() { CurrentState = _initialState; } public BaseState CurrentState { get; set; } private void Update() { CurrentState.Execute(this); } } } Menerapkan Kelas BaseState
Status kami adalah tipe BaseState , yang kami peroleh dari ScriptableObject . BaseState menyertakan metode tunggal, Execute , mengambil BaseStateMachine sebagai argumennya dan meneruskan tindakan dan transisinya. Beginilah tampilan BaseState :
using UnityEngine; namespace Demo.FSM { public class BaseState : ScriptableObject { public virtual void Execute(BaseStateMachine machine) { } } } Menerapkan Kelas State dan RemainInState
Kami sekarang menurunkan dua kelas dari BaseState . Pertama, kita memiliki kelas State , yang menyimpan referensi ke tindakan dan transisi, menyertakan dua daftar (satu untuk tindakan, yang lain untuk transisi), dan menimpa dan memanggil basis Execute pada tindakan dan transisi:
using System.Collections.Generic; using UnityEngine; namespace Demo.FSM { [CreateAssetMenu(menuName = "FSM/State")] public sealed class State : BaseState { public List<FSMAction> Action = new List<FSMAction>(); public List<Transition> Transitions = new List<Transition>(); public override void Execute(BaseStateMachine machine) { foreach (var action in Action) action.Execute(machine); foreach(var transition in Transitions) transition.Execute(machine); } } } Kedua, kami memiliki kelas RemainInState , yang memberi tahu FSM kapan tidak melakukan transisi:
using UnityEngine; namespace Demo.FSM { [CreateAssetMenu(menuName = "FSM/Remain In State", fileName = "RemainInState")] public sealed class RemainInState : BaseState { } } Perhatikan bahwa kelas-kelas ini tidak akan dikompilasi sampai kita menambahkan kelas FSMAction , Decision , dan Transition .
Menerapkan Kelas FSMAction
Dalam diagram Arsitektur FSM yang Diusulkan, kelas FSMAction dasar diberi label "Aksi." Namun, kita akan membuat kelas FSMAction dasar dan menggunakan nama FSMAction (karena Action sudah digunakan oleh namespace System .NET).
FSMAction , sebuah ScriptableObject , tidak dapat memproses fungsi secara independen, jadi kami akan mendefinisikannya sebagai kelas abstrak. Seiring perkembangan kami, kami mungkin memerlukan satu tindakan untuk melayani lebih dari satu negara bagian. Untungnya, kita dapat mengaitkan FSMAction dengan sebanyak mungkin status dari FSM sebanyak yang kita inginkan.
Kelas abstrak FSMAction terlihat seperti ini:
using UnityEngine; namespace Demo.FSM { public abstract class FSMAction : ScriptableObject { public abstract void Execute(BaseStateMachine stateMachine); } } Menerapkan Kelas Decision dan Transition
Untuk menyelesaikan FSM kami, kami akan mendefinisikan dua kelas lagi. Pertama, kami memiliki Decision , kelas abstrak dari mana semua keputusan lain akan menentukan perilaku kustom mereka:

using UnityEngine; namespace Demo.FSM { public abstract class Decision : ScriptableObject { public abstract bool Decide(BaseStateMachine state); } } Kelas kedua, Transition , berisi objek Decision dan dua status:
- Sebuah negara untuk transisi ke jika
Decisionmenghasilkan benar. - Status lain untuk transisi jika
Decisionmenghasilkan false.
Ini terlihat seperti ini:
using UnityEngine; namespace Demo.FSM { [CreateAssetMenu(menuName = "FSM/Transition")] public sealed class Transition : ScriptableObject { public Decision Decision; public BaseState TrueState; public BaseState FalseState; public void Execute(BaseStateMachine stateMachine) { if(Decision.Decide(stateMachine) && !(TrueState is RemainInState)) stateMachine.CurrentState = TrueState; else if(!(FalseState is RemainInState)) stateMachine.CurrentState = FalseState; } } }Semua yang telah kami bangun hingga saat ini harus dikompilasi tanpa kesalahan. Jika Anda mengalami masalah, periksa versi Editor Unity Anda, yang dapat menyebabkan kesalahan jika kedaluwarsa. Pastikan bahwa semua file telah dikloning dengan benar dari folder proyek asli dan semua variabel yang diakses publik tidak dinyatakan pribadi.
Membuat Tindakan dan Keputusan Kustom
Sekarang, setelah pekerjaan berat selesai, kami siap menerapkan tindakan dan keputusan khusus di folder scripts baru.
Menerapkan Kelas Patrol dan Chase
Ketika kami menganalisis Komponen Inti dari Diagram FSM Game Stealth Sampel kami, kami melihat bahwa NPC kami dapat berada di salah satu dari dua status:
- Patroli negara — Terkait dengan negara adalah:
- Satu tindakan: NPC mengunjungi titik patroli acak di seluruh dunia.
- Satu transisi: NPC memeriksa apakah pemain terlihat dan, jika demikian, transisi ke status pengejaran.
- Satu keputusan: NPC memeriksa apakah pemain sudah terlihat.
- Status pengejaran — Terkait dengan status adalah:
- Satu tindakan: NPC mengejar pemain.
Kita dapat menggunakan kembali implementasi transisi yang ada melalui GUI Unity, seperti yang akan kita bahas nanti. Ini menyisakan dua tindakan ( PatrolAction dan ChaseAction ) dan satu keputusan untuk kita kode.
Tindakan status patroli (yang berasal dari dasar FSMAction ) menimpa metode Execute untuk mendapatkan dua komponen:
-
PatrolPoints, yang melacak titik patroli. -
NavMeshAgent, implementasi Unity untuk navigasi dalam ruang 3D.
Override kemudian memeriksa apakah agen AI telah mencapai tujuannya dan, jika demikian, pindah ke tujuan berikutnya. Ini terlihat seperti ini:
using Demo.Enemy; using Demo.FSM; using UnityEngine; using UnityEngine.AI; namespace Demo.MyFSM { [CreateAssetMenu(menuName = "FSM/Actions/Patrol")] public class PatrolAction : FSMAction { public override void Execute(BaseStateMachine stateMachine) { var navMeshAgent = stateMachine.GetComponent<NavMeshAgent>(); var patrolPoints = stateMachine.GetComponent<PatrolPoints>(); if (patrolPoints.HasReached(navMeshAgent)) navMeshAgent.SetDestination(patrolPoints.GetNext().position); } } } Kami mungkin ingin mempertimbangkan untuk men-cache komponen PatrolPoints dan NavMeshAgent . Caching akan memungkinkan kami untuk berbagi ScriptableObject s untuk tindakan di antara agen tanpa dampak kinerja menjalankan GetComponent pada setiap kueri mesin keadaan terbatas.
Agar jelas, kami tidak dapat men-cache instance komponen dalam metode Execute . Jadi sebagai gantinya, kami akan menambahkan metode GetComponent kustom ke BaseStateMachine . GetComponent kustom kami akan meng-cache instance saat pertama kali dipanggil, mengembalikan instance yang di-cache pada panggilan berurutan. Untuk referensi, ini adalah implementasi BaseStateMachine dengan caching:
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() { CurrentState = _initialState; _cachedComponents = new Dictionary<Type, Component>(); } public BaseState CurrentState { get; set; } private void Update() { CurrentState.Execute(this); } 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; } } } Seperti mitranya PatrolAction , kelas ChaseAction menimpa metode Execute untuk mendapatkan komponen PatrolPoints dan NavMeshAgent . Namun sebaliknya, setelah memeriksa apakah agen AI telah mencapai tujuannya, class action ChaseAction menetapkan tujuan ke Player.position :
using Demo.Enemy; using Demo.FSM; using UnityEngine; using UnityEngine.AI; namespace Demo.MyFSM { [CreateAssetMenu(menuName = "FSM/Actions/Chase")] public class ChaseAction : FSMAction { public override void Execute(BaseStateMachine stateMachine) { var navMeshAgent = stateMachine.GetComponent<NavMeshAgent>(); var enemySightSensor = stateMachine.GetComponent<EnemySightSensor>(); navMeshAgent.SetDestination(enemySightSensor.Player.position); } } } Menerapkan Kelas InLineOfSightDecision
Bagian terakhir adalah kelas InLineOfSightDecision , yang mewarisi Decision dasar dan mendapatkan komponen EnemySightSensor untuk memeriksa apakah pemain berada dalam garis pandang NPC:
using Demo.Enemy; using Demo.FSM; using UnityEngine; namespace Demo.MyFSM { [CreateAssetMenu(menuName = "FSM/Decisions/In Line Of Sight")] public class InLineOfSightDecision : Decision { public override bool Decide(BaseStateMachine stateMachine) { var enemyInLineOfSight = stateMachine.GetComponent<EnemySightSensor>(); return enemyInLineOfSight.Ping(); } } }Melampirkan Perilaku ke Negara
Kami akhirnya siap untuk melampirkan perilaku ke agen Enemy . Ini dibuat di jendela Proyek Editor Unity.
Menambahkan Negara Patrol dan Chase
Mari kita buat dua negara bagian dan beri nama "Patroli" dan "Kejar":
- Klik Kanan > Buat > FSM > Status
Sementara di sini, mari kita juga membuat objek RemainInState :
- Klik Kanan > Buat > FSM > Tetap Dalam Keadaan
Sekarang, saatnya untuk membuat tindakan yang baru saja kita kodekan:
- Klik Kanan > Buat > FSM > Tindakan > Patroli
- Klik Kanan > Buat > FSM > Tindakan > Mengejar
Untuk mengkodekan Decision :
- Klik Kanan > Buat > FSM > Keputusan > Sejajar
Untuk mengaktifkan transisi dari PatrolState ke ChaseState , pertama-tama mari kita buat objek skrip transisi:
- Klik Kanan > Buat > FSM > Transisi
- Pilih nama yang Anda suka. Saya memanggil saya Spotted Enemy.
Kami akan mengisi jendela inspektur yang dihasilkan sebagai berikut:
Kemudian kita akan menyelesaikan dialog inspektur Chase State sebagai berikut:
Selanjutnya, kita akan menyelesaikan dialog Negara Patroli:
Terakhir, kita akan menambahkan komponen BaseStateMachine ke objek musuh: Di jendela Project Editor Unity, buka aset SampleScene, pilih objek Enemy dari panel Hierarchy, dan, di jendela Inspector, pilih Add Component > Base State Machine :
Untuk masalah apa pun, periksa kembali apakah objek game Anda dikonfigurasi dengan benar. Misalnya, konfirmasikan bahwa objek Musuh menyertakan komponen skrip PatrolPoints dan objek Point1 , Point2 , dll. Informasi ini dapat hilang dengan versi editor yang salah.
Sekarang Anda siap memainkan permainan contoh dan mengamati bahwa musuh akan mengikuti pemain ketika pemain melangkah ke garis pandang musuh.
Menggunakan FSM untuk Menciptakan Pengalaman Pengguna yang Menyenangkan dan Interaktif
Dalam tutorial mesin kondisi terbatas ini, kami membuat AI berbasis FSM yang sangat modular (dan repo GitHub yang sesuai) yang dapat kami gunakan kembali di proyek mendatang. Berkat modularitas ini, kami selalu dapat menambahkan daya ke AI kami dengan memperkenalkan komponen baru.
Tetapi arsitektur kami juga membuka jalan bagi desain FSM yang mengutamakan grafis, yang akan meningkatkan pengalaman pengembang kami ke tingkat profesionalisme yang baru. Kami kemudian dapat membuat FSM untuk game kami dengan lebih cepat—dan dengan akurasi kreatif yang lebih baik.
