Unity AI Geliştirme: Sonlu Durumlu Makine Eğitimi
Yayınlanan: 2022-03-11Rekabetçi oyun dünyasında geliştiriciler, yarattığımız oyuncu olmayan karakterlerle (NPC'ler) etkileşim kuranlar için eğlenceli bir kullanıcı deneyimi sunmaya çalışır. Geliştiriciler, NPC'lerimizde zekayı simüle eden AI çözümleri oluşturmak için sonlu durum makineleri (FSM'ler) kullanarak bu etkileşimi sağlayabilir.
AI eğilimleri davranışsal ağaçlara kaydı, ancak FSM'ler alakalı olmaya devam ediyor. Neredeyse her elektronik oyuna - şu veya bu kapasitede - dahil edilirler.
Bir FSM'nin Anatomisi
Bir FSM, bir seferde sınırlı sayıda varsayımsal durumdan yalnızca birinin aktif olabileceği bir hesaplama modelidir. Bir FSM, koşullara veya girdilere yanıt vererek bir durumdan diğerine geçiş yapar. Temel bileşenleri şunları içerir:
| Bileşen | Tanım |
|---|---|
| Belirtmek, bildirmek | Bir FSM'nin mevcut genel durumunu gösteren sonlu seçeneklerden biri; herhangi bir durum, ilişkili bir dizi eylemi içerir |
| Aksiyon | FSM onu sorguladığında bir durum ne yapar? |
| Karar | Bir geçişin ne zaman gerçekleştiğini belirleyen mantık |
| Geçiş | Devlet değiştirme süreci |
Yapay zeka uygulaması perspektifinden FSM'lere odaklanacak olsak da, animasyon durum makineleri ve genel oyun durumları gibi kavramlar da FSM şemsiyesi altına giriyor.
Bir FSM'yi Görselleştirme
Klasik atari oyunu Pac-Man örneğini ele alalım. Oyunun ilk durumunda ("kovalama" durumu), NPC'ler, oyuncuyu takip eden ve sonunda onu geride bırakan renkli hayaletlerdir. Oyuncu bir güç peleti yediğinde ve bir güçlenme yaşadığında hayaletler kaçış durumuna geçer ve hayaletleri yeme yeteneği kazanır. Artık mavi renkli olan hayaletler, güç verme süresi dolana ve hayaletler, orijinal davranışlarının ve renklerinin geri yüklendiği kovalama durumuna geri dönene kadar oyuncudan kaçar.
Bir Pac-Man hayaleti her zaman iki durumdan birindedir: kovalamak ya da kaçmak. Doğal olarak, biri kovalamadan kaçışa, diğeri kaçıştan kovalamaya iki geçiş sağlamalıyız:
Sonlu durum makinesi tasarımı gereği mevcut durumu sorgular ve bu durumun karar(lar)ını ve eylem(ler)ini sorgular. Aşağıdaki şema, Pac-Man örneğimizi temsil etmekte ve oyuncunun açılış durumunu kontrol eden bir kararı göstermektedir. Bir güçlendirme başladıysa, NPC'ler kovalamacadan kaçınmaya geçer. Bir güçlendirme sona ermişse, NPC'ler kaçıştan kovalamaya geçer. Son olarak, herhangi bir açılış değişikliği olmazsa geçiş olmaz.
ölçeklenebilirlik
FSM'ler modüler yapay zeka oluşturmamız için bizi özgürleştiriyor. Örneğin, tek bir yeni eylemle, yeni bir davranışa sahip bir NPC oluşturabiliriz. Böylece, Pac-Man hayaletlerimizden birine, oyuncudan kaçarken güç topaklarını yeme yeteneği kazandıran yeni bir eylem - bir güç peletini yemek - atfedebiliriz. Bu davranışı desteklemek için mevcut eylemleri, kararları ve geçişleri yeniden kullanabiliriz.
Benzersiz bir NPC geliştirmek için gereken kaynaklar minimum olduğundan, birden fazla benzersiz NPC'nin gelişen proje gereksinimlerini karşılamak için iyi bir konumdayız. Öte yandan, aşırı sayıda durum ve geçiş, bizi bir spagetti durumu makinesinde karıştırabilir - aşırı sayıda bağlantı, hata ayıklamayı ve bakımını zorlaştıran bir FSM.
Unity'de FSM Uygulaması
Unity'de bir sonlu durum makinesinin nasıl uygulanacağını göstermek için basit bir gizli oyun oluşturalım. Mimarimiz, uygulama boyunca bilgi depolayabilen ve paylaşabilen veri kapsayıcıları olan ScriptableObject s'yi içerecek, böylece onu yeniden üretmemize gerek kalmayacak. ScriptableObject s, eylemleri çağırma ve kararları sorgulama gibi sınırlı işleme yeteneğine sahiptir. Unity'nin resmi belgelerine ek olarak, daha derine inmek istiyorsanız , Scriptable Objects ile eski Oyun Mimarisi konuşması mükemmel bir kaynak olmaya devam ediyor.
Bu derlemeye hazır projeye yapay zekayı eklemeden önce önerilen mimariyi göz önünde bulundurun:
Örnek oyunumuzda düşman (mavi bir kapsülle temsil edilen bir NPC) devriye geziyor. Düşman oyuncuyu gördüğünde (gri bir kapsülle temsil edilir), düşman oyuncuyu takip etmeye başlar:
Pac-Man'in aksine, oyunumuzdaki düşman, oyuncuyu takip ettikten sonra varsayılan durumuna ("devriye") geri dönmeyecek.
Sınıflar Oluşturma
Sınıflarımızı oluşturarak başlayalım. Yeni bir scripts klasöründe, önerilen tüm mimari yapı taşlarını C# komut dosyaları olarak ekleyeceğiz.
BaseStateMachine Sınıfını Uygulama
BaseStateMachine sınıfı, AI özellikli NPC'lerimize erişmek için ekleyeceğimiz tek MonoBehavior . Basitlik adına, BaseStateMachine çıplak kemik olacak. Ancak istersek, ek parametreler ve ek bileşenlere referanslar depolayan devralınan özel bir FSM ekleyebiliriz. Daha sonra öğreticimizde yapacağımız BaseState sınıfımızı ekleyene kadar kodun düzgün bir şekilde derlenmeyeceğini unutmayın.
BaseStateMachine kodu, eylemleri gerçekleştirmek ve bir geçişin garanti edilip edilmediğini görmek için mevcut durumu ifade eder ve yürütür:
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); } } } BaseState Sınıfını Uygulamak
Durumumuz, bir ScriptableObject öğesinden türetilen BaseState . BaseState , bağımsız değişken olarak BaseStateMachine alarak ve ona eylemler ve geçişler ileten Execute adında tek bir yöntem içerir. BaseState şöyle görünür:
using UnityEngine; namespace Demo.FSM { public class BaseState : ScriptableObject { public virtual void Execute(BaseStateMachine machine) { } } } State ve RemainInState Sınıflarını Uygulama
Şimdi BaseState'den iki sınıf BaseState . İlk olarak, eylemlere ve geçişlere referansları depolayan, iki liste içeren (biri eylemler için, diğeri geçişler için) State sınıfına sahibiz ve bunları geçersiz kılar ve eylemler ve geçişlerde Execute tabanını çağırır:
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); } } } İkinci olarak, FSM'ye ne zaman geçiş yapmayacağını söyleyen RemainInState sınıfına sahibiz:
using UnityEngine; namespace Demo.FSM { [CreateAssetMenu(menuName = "FSM/Remain In State", fileName = "RemainInState")] public sealed class RemainInState : BaseState { } } FSMAction , Decision ve Transition sınıflarını ekleyene kadar bu sınıfların derlenmeyeceğini unutmayın.
FSMAction Sınıfını Uygulamak
Önerilen FSM Mimarisi diyagramında, temel FSMAction sınıfı "Eylem" olarak etiketlenmiştir. Ancak, temel FSMAction sınıfını oluşturacağız ve FSMAction adını kullanacağız (çünkü Action zaten .NET System ad alanı tarafından kullanılıyor).
Bir ScriptableObject olan FSMAction , işlevleri bağımsız olarak işleyemez, bu nedenle onu soyut bir sınıf olarak tanımlayacağız. Gelişimimiz ilerledikçe, birden fazla devlete hizmet etmek için tek bir eyleme ihtiyaç duyabiliriz. Neyse ki, FSMAction'ı istediğimiz kadar FSMAction çok sayıda durumla ilişkilendirebiliriz.
FSMAction soyut sınıfı şöyle görünür:
using UnityEngine; namespace Demo.FSM { public abstract class FSMAction : ScriptableObject { public abstract void Execute(BaseStateMachine stateMachine); } } Decision ve Transition Sınıflarının Uygulanması
FSM'mizi bitirmek için iki sınıf daha tanımlayacağız. İlk olarak, diğer tüm kararların özel davranışlarını tanımlayacağı soyut bir sınıf olan Decision var:

using UnityEngine; namespace Demo.FSM { public abstract class Decision : ScriptableObject { public abstract bool Decide(BaseStateMachine state); } } İkinci sınıf olan Transition , Decision nesnesini ve iki durumu içerir:
-
Decisiondoğru çıkması halinde geçilecek bir durum. -
Decisionyanlış çıkması durumunda geçiş yapılacak başka bir durum.
Şuna benziyor:
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; } } }Bu noktaya kadar inşa ettiğimiz her şey hatasız bir şekilde derlenmelidir. Sorun yaşıyorsanız, güncel olmadığında hatalara neden olabilecek Unity Editor sürümünüzü kontrol edin. Tüm dosyaların orijinal proje klasöründen düzgün bir şekilde kopyalandığından ve genel olarak erişilen tüm değişkenlerin özel olarak bildirilmediğinden emin olun.
Özel Eylemler ve Kararlar Oluşturma
Şimdi, yapılan ağır kaldırma ile yeni bir scripts klasöründe özel eylemleri ve kararları uygulamaya hazırız.
Patrol ve Chase Sınıflarının Uygulanması
Örnek Gizli Oyun FSM diyagramımızın Temel Bileşenlerini analiz ettiğimizde, NPC'mizin iki durumdan birinde olabileceğini görüyoruz:
- Devriye durumu - Devletle ilişkili olanlar:
- Bir eylem: NPC, dünya çapında rastgele devriye noktalarını ziyaret eder.
- Bir geçiş: NPC, oyuncunun görüş alanında olup olmadığını kontrol eder ve eğer öyleyse, kovalama durumuna geçer.
- Bir karar: NPC, oyuncunun görünürde olup olmadığını kontrol eder.
- Chase durumu - Durumla ilişkili:
- Bir eylem: NPC, oyuncuyu kovalar.
Daha sonra tartışacağımız gibi, mevcut geçiş uygulamamızı Unity'nin GUI'si aracılığıyla yeniden kullanabiliriz. Bu, iki eylem ( PatrolAction ve ChaseAction ) ve kodlamamız için bir karar bırakır.
Devriye durumu eylemi ( FSMAction tabanından türetilen), iki bileşen elde etmek için Execute yöntemini geçersiz kılar:
- Devriye noktalarını izleyen
PatrolPoints. -
NavMeshAgent, Unity'nin 3B uzayda gezinme uygulaması.
Geçersiz kılma daha sonra AI aracısının hedefine ulaşıp ulaşmadığını kontrol eder ve eğer öyleyse bir sonraki hedefe hareket eder. Şuna benziyor:
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); } } } PatrolPoints ve NavMeshAgent bileşenlerini önbelleğe almayı düşünmek isteyebiliriz. Önbelleğe alma, sonlu durum makinesinin her sorgusunda GetComponent çalıştırmanın performans etkisi olmadan aracılar arasında eylemler için ScriptableObject 'leri paylaşmamıza olanak tanır.
Açık olmak gerekirse, Execute yönteminde bileşen örneklerini önbelleğe alamayız. Bunun yerine, BaseStateMachine özel bir GetComponent yöntemi ekleyeceğiz. Özel GetComponent , örneği ilk çağrıldığında önbelleğe alır ve ardışık çağrılarda önbelleğe alınan örneği döndürür. Başvuru için, bu, önbelleğe alma ile BaseStateMachine uygulamasıdır:
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; } } } PatrolAction gibi, ChaseAction sınıfı da PatrolPoints ve NavMeshAgent bileşenlerini almak için Execute yöntemini geçersiz kılar. Buna karşın, AI aracısının hedefine ulaşıp ulaşmadığını kontrol ettikten sonra, ChaseAction sınıf eylemi hedefi Player.position olarak ayarlar:
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); } } } InLineOfSightDecision Sınıfını Uygulama
Son parça, temel Decision devralan ve oyuncunun NPC'nin görüş alanında olup olmadığını kontrol etmek için EnemySightSensor bileşenini alan InLineOfSightDecision sınıfıdır:
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(); } } }Davranışları Devletlere Bağlamak
Sonunda Enemy ajanına davranışlar eklemeye hazırız. Bunlar, Unity Editörünün Proje penceresinde oluşturulur.
Patrol ve Chase Durumlarını Ekleme
İki durum oluşturalım ve bunlara “Patrol” ve “Chase” adını verelim:
- Sağ Tık > Oluştur > FSM > Durum
Buradayken ayrıca bir RemainInState nesnesi oluşturalım:
- Sağ Tık > Oluştur > FSM > Durumda Kal
Şimdi, az önce kodladığımız eylemleri oluşturma zamanı:
- Sağ Tık > Oluştur > FSM > Eylem > Devriye
- Sağ Tık > Oluştur > FSM > Eylem > Takip
Decision kodlamak için:
- Sağ Tık > Oluştur > FSM > Kararlar > Görüş Alanında
PatrolState ChaseState geçişi etkinleştirmek için, önce geçiş kodlanabilir nesnesini oluşturalım:
- Sağ Tık > Oluştur > FSM > Geçiş
- Beğendiğiniz bir isim seçin. Ben benimki Benekli Düşmanı aradım.
Ortaya çıkan denetçi penceresini aşağıdaki gibi dolduracağız:
Ardından Chase State denetçisi iletişim kutusunu aşağıdaki gibi tamamlayacağız:
Ardından, Devriye Durumu iletişim kutusunu tamamlayacağız:
Son olarak, BaseStateMachine bileşenini düşman nesnesine ekleyeceğiz: Unity Editor's Project penceresinde SampleScene varlığını açın, Hiyerarşi panelinden Enemy nesnesini seçin ve Inspector penceresinde Add Component > Base State Machine öğesini seçin:
Herhangi bir sorun için oyun nesnelerinizin doğru yapılandırıldığını iki kez kontrol edin. Örneğin, Enemy nesnesinin PatrolPoints komut dosyası bileşenini ve Point1 , Point2 , vb. nesneleri içerdiğini doğrulayın. Bu bilgiler yanlış düzenleyici sürümü oluşturma ile kaybolabilir.
Artık örnek oyunu oynamaya ve oyuncu düşmanın görüş alanına girdiğinde düşmanın oyuncuyu takip edeceğini gözlemlemeye hazırsınız.
Eğlenceli, Etkileşimli Bir Kullanıcı Deneyimi Oluşturmak için FSM'leri Kullanma
Bu sonlu durumlu makine eğitiminde, gelecekteki projelerde yeniden kullanabileceğimiz oldukça modüler bir FSM tabanlı yapay zeka (ve buna karşılık gelen GitHub deposu) oluşturduk. Bu modülerlik sayesinde, yeni bileşenler sunarak yapay zekamıza her zaman güç katabiliriz.
Ancak mimarimiz, geliştirici deneyimimizi yeni bir profesyonellik düzeyine çıkaracak, grafik öncelikli FSM tasarımının da yolunu açıyor. Daha sonra oyunlarımız için FSM'leri daha hızlı ve daha iyi yaratıcı doğrulukla oluşturabiliriz.
