Pengembangan Unity AI: Tutorial Mesin kondisi-terbatas

Diterbitkan: 2022-03-11

Dalam 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:

Diagram: Di sebelah kiri adalah status pengejaran. Sebuah panah (menunjukkan bahwa pemain memakan pelet kekuatan) mengarah ke keadaan menghindar di sebelah kanan. Panah kedua (menunjukkan bahwa waktu pelet daya habis) mengarah kembali ke status pengejaran di sebelah kiri.
Transisi Antara Keadaan Hantu Pac-Man

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.

Diagram berbentuk berlian yang mewakili sebuah siklus: Dimulai di sebelah kiri, ada status pengejaran yang menyiratkan tindakan yang sesuai. Status pengejaran kemudian menunjuk ke atas, di mana ada keputusan: Jika pemain memakan pelet kekuatan, kami melanjutkan ke status menghindar dan menghindari tindakan di sebelah kanan. Status menghindar menunjukkan keputusan di bagian bawah: Jika pelet daya habis, kami melanjutkan kembali ke titik awal kami.
Komponen dari Pac-Man Ghost FSM

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:

Diagram: Tujuh kotak yang terhubung satu sama lain, dijelaskan dalam urutan tampilan, dari kiri/atas: Kotak berlabel BaseStateMachine termasuk + CurrentState: BaseState. BaseStateMachine terhubung ke BaseState dengan panah dua arah. Kotak berlabel BaseState termasuk + Execute(BaseStateMachine): void. BaseState terhubung ke BaseStateMachine dengan panah dua arah. Panah satu arah dari State dan RemainInState terhubung ke BaseState. Kotak berlabel Status mencakup + Jalankan(BaseStateMachine): batal, + Tindakan: Daftar<Tindakan>, dan + Transisi: Daftar<Transisi>. Status terhubung ke BaseState dengan panah satu arah, ke Action dengan panah satu arah berlabel "1," dan ke Transisi dengan panah satu arah berlabel "1." Kotak berlabel RemainInState termasuk + Execute(BaseStateMachine): void. RemainInState terhubung ke BaseState dengan panah satu arah. Kotak berlabel Action termasuk + Execute(BaseStateMachine): void. Panah satu arah berlabel "1" dari Status terhubung ke Tindakan. Kotak berlabel Transition meliputi + Putuskan(BaseStateMachine): void, + TransitionDecision: Decision, + TrueState: BaseState, dan + FalseState: BaseState. Transisi terhubung ke Keputusan dengan panah satu arah. Panah satu arah berlabel "1" dari Status terhubung ke Transisi. Kotak berlabel Keputusan termasuk + Putuskan (BaseStateMachine): bool.
Arsitektur FSM 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:

Diagram: Lima kotak yang terhubung satu sama lain, dijelaskan dalam urutan penampilan, dari kiri/atas: Kotak berlabel Patroli terhubung ke kotak berlabel JIKA pemain berhadapan dengan panah satu arah, dan ke kotak berlabel Aksi Patroli dengan panah satu arah yang diberi label "negara". Kotak berlabel IF player berada di depan mata, dengan tambahan elabel "decision", tepat di bawah kotak. Kotak berlabel IF player is in line of sight terhubung ke kotak berlabel Chase dengan panah satu arah. Panah satu arah dari kotak berlabel Patroli terhubung ke kotak berlabel JIKA pemain berada di garis pandang. Kotak berlabel Chase terhubung ke kotak berlabel Chase Action dengan panah satu arah yang diberi label "state". Panah satu arah dari kotak berlabel JIKA pemain berada dalam garis pandang terhubung ke kotak berlabel Chase. Panah panah satu arah dari kotak berlabel Patroli terhubung ke kotak berlabel Aksi Patroli. Panah panah satu arah dari kotak berlabel Chase terhubung ke kotak berlabel Chase Action.
Komponen Inti dari Contoh Game Stealth FSM Kami

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 Decision menghasilkan benar.
  • Status lain untuk transisi jika Decision menghasilkan 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:

  1. 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.
  2. 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:

  1. PatrolPoints , yang melacak titik patroli.
  2. 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:

Layar Spotted Enemy (Transisi) mencakup empat baris: Nilai skrip diatur ke "Transisi" dan berwarna abu-abu. Nilai Keputusan disetel ke "LineOfSightDecision (Dalam Garis Penglihatan)". Nilai True State diatur ke "ChaseState (State)." Nilai False State diatur ke "RemainInState (Remain In State)."
Mengisi Jendela Inspektur Musuh yang Terlihat (Transisi)

Kemudian kita akan menyelesaikan dialog inspektur Chase State sebagai berikut:

Layar Chase State (Negara Bagian) dimulai dengan label "Buka". Di samping label "Script" "State" dipilih. Di samping label "Tindakan", "1" dipilih. Dari tarik-turun "Tindakan", "Elemen 0 Tindakan Mengejar (Aksi Mengejar)" dipilih. Ada tanda plus dan tanda minus yang mengikuti. Di samping label "Transisi", "0" dipilih. Dari tarik-turun "Transisi", "Daftar Kosong" ditampilkan. Ada tanda plus dan tanda minus yang mengikuti.
Mengisi Jendela Inspektur Chase State

Selanjutnya, kita akan menyelesaikan dialog Negara Patroli:

Layar Patrol State (Negara Bagian) dimulai dengan label "Buka." Di samping label "Script" "State" dipilih. Di samping label "Tindakan", "1" dipilih. Dari tarik-turun "Aksi", "Elemen 0 Tindakan Patroli (Aksi Patroli)" dipilih. Ada tanda plus dan minus yang mengikuti. Di samping label "Transisi", "1" dipilih. Dari tarik-turun "Transisi", "Elemen 0 SpottedEnemy (Transisi)" ditampilkan. Ada tanda plus dan tanda minus yang mengikuti.
Mengisi Jendela Inspektur 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 :

Layar Base State Machine (Script): Di samping label "Script" berwarna abu-abu, "BaseStateMachine" dipilih dan berwarna abu-abu. Di samping label "Initial State", "PatrolState (State)" dipilih.
Menambahkan Komponen Base State Machine (Script)

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.