Unity AI Geliştirme: Sonlu Durumlu Makine Eğitimi

Yayınlanan: 2022-03-11

Rekabetç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:

Diyagram: Soldaki takip durumudur. Bir ok (oyuncunun güç topunu yediğini gösterir) sağdaki kaçış durumuna götürür. İkinci bir ok (güç peletinin zaman aşımına uğradığını gösterir) soldaki takip durumuna geri döner.
Pac-Man Ghost Durumları Arasındaki Geçişler

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.

Bir döngüyü temsil eden elmas şeklindeki diyagram: Soldan başlayarak, karşılık gelen bir eylemi ima eden bir takip durumu vardır. Takip durumu daha sonra bir kararın olduğu tepeyi gösterir: Oyuncu bir güç peleti yerse, kaçınma durumuna devam ederiz ve sağdaki eylemden kaçınırız. Kaçınma durumu altta bir karara işaret eder: Güç peleti zaman aşımına uğradıysa, başlangıç ​​noktamıza geri döneriz.
Pac-Man Ghost FSM'nin Bileşenleri

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

Diyagram: Soldan/üstten görünüm sırasına göre açıklanan, birbirine bağlanan yedi kutu: BaseStateMachine etiketli kutu + CurrentState: BaseState'i içerir. BaseStateMachine, çift yönlü bir okla BaseState'e bağlanır. BaseState etiketli kutu şunları içerir + Execute(BaseStateMachine): void. BaseState, çift yönlü bir okla BaseStateMachine'e bağlanır. State ve RemainInState'den gelen tek yönlü oklar BaseState'e bağlanır. State etiketli kutuda + Execute(BaseStateMachine): void, + Actions: List<Action> ve + Transition: List<Transition> bulunur. State, tek yönlü bir okla BaseState'e, "1" etiketli tek yönlü bir okla Action'a ve "1" etiketli tek yönlü bir okla Geçiş'e bağlanır. RemainInState etiketli kutuda + Execute(BaseStateMachine): void bulunur. RemainInState, tek yönlü bir okla BaseState'e bağlanır. Eylem etiketli kutu şunları içerir: + Execute(BaseStateMachine): void. Durumdan "1" etiketli tek yönlü bir ok Eylem'e bağlanır. Geçiş etiketli kutuda + Decide(BaseStateMachine): void, + TransitionDecision: Decision, + TrueState: BaseState ve + FalseState: BaseState bulunur. Geçiş, Karar'a tek yönlü bir okla bağlanır. Durumdan "1" etiketli tek yönlü bir ok Geçiş'e bağlanır. Karar etiketli kutuda + Karar (BaseStateMachine): bool bulunur.
Önerilen FSM Mimarisi

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

Diyagram: Soldan/üstten görünüm sırasına göre açıklanan, birbirine bağlanan beş kutu: Devriye etiketli kutu, EĞER oynatıcı etiketli kutuya tek yönlü bir okla ve Devriye Eylem etiketli kutuya bağlanır. "durum" etiketli tek yönlü bir ok. EĞER oyuncu etiketli kutu, kutunun hemen altında ek bir "karar" etiketiyle görüş hattındadır. IF player is in line yazan kutu, Chase yazan kutuya tek yönlü bir ok ile bağlanır. Patrol etiketli kutudan tek yönlü bir ok, IF player görüş hattında etiketli kutuya bağlanır. Chase etiketli kutu, Chase Action etiketli kutuya "durum" etiketli tek yönlü bir okla bağlanır. EĞER oyuncu görüş hattında etiketli kutudan tek yönlü bir ok, Chase etiketli kutuya bağlanır. Patrol etiketli kutudan tek yönlü bir ok oku Patrol Action etiketli kutuya bağlanır. Chase etiketli kutudan tek yönlü bir ok oku, Chase Action etiketli kutuya bağlanır.
Örnek Gizli Oyun FSM'mizin Temel Bileşenleri

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:

  • Decision doğru çıkması halinde geçilecek bir durum.
  • Decision yanlış çı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:

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

  1. Devriye noktalarını izleyen PatrolPoints .
  2. 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:

Spotted Enemy (Geçiş) ekranı dört satır içerir: Script'in değeri "Geçiş" olarak ayarlanmıştır ve gri renktedir. Kararın değeri "LineOfSightDecision (Sight Line)" olarak ayarlanır. True State'in değeri "ChaseState (State)" olarak ayarlanmıştır. False State'in değeri "RemainInState (Remain In State)" olarak ayarlanmıştır.
Benekli Düşman (Geçiş) Müfettiş Penceresini Doldurma

Ardından Chase State denetçisi iletişim kutusunu aşağıdaki gibi tamamlayacağız:

Chase State (State) ekranı "Açık" etiketiyle başlar. Etiketin yanında "Script" "Durum" seçilir. "Action" etiketinin yanında "1" seçilir. "Action" açılır menüsünden "Element 0 Chase Action (Chase Action)" seçilir. Ardından bir artı işareti ve eksi işareti var. "Geçişler" etiketinin yanında "0" seçilir. "Geçişler" açılır menüsünden "Liste Boş" görüntülenir. Ardından bir artı işareti ve eksi işareti var.
Chase State Inspector Penceresini Doldurma

Ardından, Devriye Durumu iletişim kutusunu tamamlayacağız:

Devriye Durumu (Eyalet) ekranı "Açık" etiketiyle başlar. Etiketin yanında "Script" "Durum" seçilir. "Action" etiketinin yanında "1" seçilir. "Eylem" açılır menüsünden "Element 0 Devriye Eylemi (Devriye Eylemi)" seçilir. Ardından bir artı ve eksi işareti var. "Geçişler" etiketinin yanında "1" seçilir. "Geçişler" açılır menüsünden "Element 0 SpottedEnemy (Geçiş)" görüntülenir. Ardından bir artı işareti ve eksi işareti var.
Devriye Durumu Denetçisi Penceresini Doldurma

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:

Temel Durum Makinesi (Komut Dosyası) ekranı: Gri renkli "Script" etiketinin yanında "BaseStateMachine" seçili ve gri renklidir. "İlk Durum" etiketinin yanında "PatrolState (State)" seçilir.
Temel Durum Makinesi (Komut Dosyası) Bileşenini Ekleme

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.