Unity AI Geliştirme: xNode tabanlı Grafik FSM Eğitimi

Yayınlanan: 2022-08-12

"Unity AI Geliştirme: Sonlu Durumlu Bir Makine Eğitimi"nde, basit bir gizli oyun yarattık - modüler bir FSM tabanlı AI. Oyunda, bir düşman ajanı oyun alanında devriye geziyor. Oyuncuyu tespit ettiğinde, düşman durumunu değiştirir ve devriye gezmek yerine oyuncuyu takip eder.

Unity yolculuğumuzun bu ikinci ayağında, sonlu durum makinemizin (FSM) temel bileşenlerini daha hızlı ve geliştirilmiş Unity geliştirici deneyimiyle oluşturmak için bir grafik kullanıcı arabirimi (GUI) oluşturacağız.

Hızlı Tazeleme

Önceki öğreticide ayrıntıları verilen FSM, C# komut dosyaları olarak mimari bloklardan oluşturulmuştur. Özel ScriptableObject eylemlerini ve kararlarını sınıflar olarak ekledik. ScriptableObject yaklaşımı, bakımı kolay ve özelleştirilebilir bir FSM'ye izin verdi. Bu öğreticide, FSM'nin sürükle ve bırak ScriptableObject 'lerini bir grafik seçeneğiyle değiştiriyoruz.

Ayrıca oyunu kazanmayı kolaylaştırmak isteyenler için güncellenmiş bir senaryo yazdım. Uygulamak için, oyuncu tespit komut dosyasını düşmanın görüş alanını daraltan bununla değiştirin.

xNode'a Başlarken

Grafik düzenleyicimizi, FSM'mizin akışını görsel olarak gösterecek düğüm tabanlı davranış ağaçları için bir çerçeve olan xNode'u kullanarak oluşturacağız. Unity'nin GraphView'ı işi başarabilse de, API'si hem deneysel hem de yetersiz belgelenmiştir. xNode'un kullanıcı arayüzü, FSM'nin prototiplenmesini ve hızlı bir şekilde genişletilmesini kolaylaştıran üstün bir geliştirici deneyimi sunar.

Unity Paket Yöneticisini kullanarak projemize Git bağımlılığı olarak xNode ekleyelim:

  1. Unity'de, Paket Yöneticisi penceresini başlatmak için Pencere > Paket Yöneticisi'ne tıklayın.
  2. Pencerenin sol üst köşesindeki + (artı işaretini) tıklayın ve bir metin alanı görüntülemek için git URL'den paket ekle'yi seçin.
  3. Etiketlenmemiş metin kutusuna https://github.com/siccity/xNode.git yazın veya yapıştırın ve Ekle düğmesini tıklayın.

Artık derinlere inmeye ve xNode'un temel bileşenlerini anlamaya hazırız:

Node sınıfı Grafiğin en temel birimi olan bir düğümü temsil eder. Bu xNode eğitiminde, özel işlevler ve rollerle donatılmış düğümleri bildiren Node sınıfından yeni sınıflar türetiyoruz.
NodeGraph sınıfı Bir düğüm koleksiyonunu ( Node sınıfı örnekleri) ve bunları birbirine bağlayan kenarları temsil eder. Bu xNode eğitiminde, NodeGraph düğümleri yöneten ve değerlendiren yeni bir sınıf türetiyoruz.
NodePort sınıfı Bir NodeGraph 'daki Node örnekleri arasında bulunan bir iletişim geçidini, giriş veya çıkış türünde bir bağlantı noktasını temsil eder. NodePort sınıfı, xNode'a özgüdür.
[Input] özelliği Bir bağlantı noktasına [Input] özniteliğinin eklenmesi, onu bir giriş olarak belirler ve bağlantı noktasının, parçası olduğu düğüme değerleri iletmesini sağlar. [Input] niteliğini bir fonksiyon parametresi olarak düşünün.
[Output] özelliği [Output] özniteliğinin bir bağlantı noktasına eklenmesi, bağlantı noktasının parçası olduğu düğümden değerleri iletmesine olanak tanıyan bir çıktı olarak belirler. [Output] niteliğini bir işlevin dönüş değeri olarak düşünün.

xNode Yapı Ortamını Görselleştirme

xNode'da, her State ve Transition bir düğüm şeklini aldığı grafiklerle çalışıyoruz. Giriş ve/veya çıkış bağlantıları, düğümün grafiğimizdeki diğer tüm düğümlerle veya herhangi biriyle ilişki kurmasını sağlar.

Üç girdi değerine sahip bir düğüm düşünelim: iki keyfi ve bir boole. Düğüm, boole girişinin doğru veya yanlış olmasına bağlı olarak, rastgele tipteki iki giriş değerinden birinin çıktısını verir.

Merkezde büyük bir dikdörtgenle temsil edilen Dal düğümü, "If C == True A Else B" sözde kodunu içerir. Solda, her birinde Dal düğümünü gösteren bir ok bulunan üç dikdörtgen bulunur: "A (keyfi)," "B (keyfi)" ve "C (boolean)." Şube düğümü, son olarak, bir "Çıktı" dikdörtgenine işaret eden bir oka sahiptir.
Örnek bir Branch Düğümü

Mevcut FSM'mizi bir grafiğe dönüştürmek için State ve Transition sınıflarını ScriptableObject sınıfı yerine Node sınıfını devralacak şekilde değiştiririz. Tüm State ve Transition nesnelerimizi içermesi için NodeGraph türünde bir grafik nesnesi oluşturuyoruz.

BaseStateMachine Temel Tür Olarak Kullanacak Şekilde Değiştirme

Mevcut BaseStateMachine sınıfımıza iki yeni sanal yöntem ekleyerek grafik arayüzü oluşturmaya başlayın:

Init CurrentState özelliğine ilk durumu atar
Execute Mevcut durumu yürütür

Bu yöntemleri sanal olarak bildirmek, onları geçersiz kılmamıza izin verir, böylece başlatma ve yürütme için BaseStateMachine sınıfını miras alan sınıfların özel davranışlarını tanımlayabiliriz:

 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; } } }

Ardından, FSM klasörümüzün altında şunu oluşturalım:

FSMGraph Bir klasör
BaseStateMachineGraph FSMGraph içindeki AC# sınıfı

Şu an için BaseStateMachineGraph yalnızca BaseStateMachine sınıfını devralır:

 using UnityEngine; namespace Demo.FSM.Graph { public class BaseStateMachineGraph : BaseStateMachine { } }

Temel düğüm türümüzü oluşturana kadar BaseStateMachineGraph işlevsellik ekleyemiyoruz; sonra yapalım.

NodeGraph Uygulama ve Temel Düğüm Türü Oluşturma

Yeni oluşturduğumuz FSMGraph klasörümüzün altında şunları oluşturacağız:

FSMGraph Bir sınıf

Şimdilik, FSMGraph yalnızca NodeGraph sınıfını devralacak (ek işlevsellik olmadan):

 using UnityEngine; using XNode; namespace Demo.FSM.Graph { [CreateAssetMenu(menuName = "FSM/FSM Graph")] public class FSMGraph : NodeGraph { } }

Düğümlerimiz için sınıflar oluşturmadan önce şunu ekleyelim:

FSMNodeBase Tüm düğümlerimiz tarafından temel sınıf olarak kullanılacak bir sınıf

FSMNodeBase sınıfı, düğümleri birbirine bağlamamızı sağlamak için FSMNodeBase türünde Entry adlı bir girdi içerecektir.

Ayrıca iki yardımcı fonksiyon ekleyeceğiz:

GetFirst İstenen çıkışa bağlı ilk düğümü alır
GetAllOnPort İstenen çıktıya bağlanan tüm kalan düğümleri alır
 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; } } }

Sonuç olarak, iki tür durum düğümümüz olacak; bunları desteklemek için bir sınıf ekleyelim:

BaseStateNode Hem StateNode hem de RemainInStateNode destekleyen bir temel sınıf
 namespace Demo.FSM.Graph { public abstract class BaseStateNode : FSMNodeBase { } }

Ardından, BaseStateMachineGraph sınıfını değiştirin:

 using UnityEngine; namespace Demo.FSM.Graph { public class BaseStateMachineGraph : BaseStateMachine { public new BaseStateNode CurrentState { get; set; } } }

Burada, temel sınıftan devralınan CurrentState özelliğini gizledik ve türünü BaseState BaseStateNode .

FSM Grafiğimiz için Yapı Taşları Oluşturma

Ardından, FSM'nin ana yapı taşlarını oluşturmak için FSMGraph klasörümüze üç yeni sınıf ekleyelim:

StateNode Bir aracının durumunu temsil eder. Yürütme sırasında StateNode , StateNode çıkış bağlantı noktasına bağlı TransitionNode s üzerinde yinelenir (bir yardımcı yöntemle alınır). StateNode , her birini düğümün farklı bir duruma mı geçirileceğini yoksa düğümün durumunu olduğu gibi mi bırakacağını sorgular.
RemainInStateNode Bir düğümün geçerli durumda kalması gerektiğini belirtir.
TransitionNode Farklı bir duruma geçme veya aynı durumda kalma kararını verir.

Önceki Unity FSM öğreticisinde, State sınıfı geçişler listesini yineler. Burada xNode'da StateNode, StateNode yardımcı GetAllOnPort aracılığıyla alınan düğümler üzerinde yineleme yapmak için State 'in eşdeğeri olarak hizmet eder.

Şimdi, GUI'nin bir parçası olmaları gerektiğini belirtmek için giden bağlantılara (geçiş düğümleri) bir [Output] özniteliği ekleyin. xNode'un tasarımına göre, özniteliğin değeri kaynak düğümden kaynaklanır: [Output] özniteliği ile işaretlenmiş alanı içeren düğüm. xNode GUI tarafından ayarlanacak ilişkileri ve bağlantıları tanımlamak için [Output] ve [Input] özniteliklerini kullandığımızdan, bu değerlere normalde yaptığımız gibi davranamayız. Actions ve Transitions arasında nasıl yineleme yaptığımızı düşünün:

 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); } } }

Bu durumda, Transitions çıktısının kendisine bağlı birden çok düğümü olabilir; [Output] bağlantılarının bir listesini elde etmek için GetAllOnPort yardımcı yöntemini çağırmalıyız.

RemainInStateNode , açık ara en basit sınıfımızdır. Hiçbir mantık RemainInStateNode , yalnızca aracımıza - oyunumuzun durumunda, düşmana - mevcut durumunda kalmasını bildirir:

 namespace Demo.FSM.Graph { [CreateNodeMenu("Remain In State")] public sealed class RemainInStateNode : BaseStateNode { } }

Bu noktada, TransitionNode sınıfı hala eksiktir ve derlenmeyecektir. Sınıfı güncellediğimizde ilgili hatalar temizlenecektir.

TransitionNode oluşturmak için, StateNode'u oluşturduğumuzda yaptığımız gibi, StateNode çıktı değerinin kaynak düğümden kaynaklandığı gereksinimini aşmamız gerekir. StateNode ve TransitionNode arasındaki önemli bir fark, TransitionNode çıktısının yalnızca bir düğüme bağlanabilmesidir. Bizim durumumuzda GetFirst , bağlantı noktalarımızın her birine bağlı bir düğümü getirecektir (doğru durumda geçiş yapılacak bir durum düğümü ve yanlış durumda geçiş yapılacak bir durum düğümü):

 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; } } }

Kodumuzun grafiksel sonuçlarına bir göz atalım.

Görsel Grafiğin Oluşturulması

Tüm FSM sınıfları sıralandığında, oyunun düşman ajanı için FSM Grafiğimizi oluşturmaya devam edebiliriz. Unity proje penceresinde, EnemyAI klasörüne sağ tıklayın ve şunu seçin: Create > FSM > FSM Graph . Grafiğimizin tanımlanmasını kolaylaştırmak için, onu EnemyGraph olarak yeniden adlandıralım.

xNode Graph düzenleyici penceresinde, State , Transition ve RemainInState listeleyen bir açılır menüyü görüntülemek için sağ tıklayın. Pencere görünmüyorsa, xNode Graph düzenleyici penceresini başlatmak için EnemyGraph dosyasına çift tıklayın.

  1. Chase ve Patrol durumlarını oluşturmak için:

    1. Yeni bir düğüm oluşturmak için sağ tıklayın ve Durum'u seçin.

    2. Düğümü Chase olarak adlandırın.

    3. Açılır menüye dönün, ikinci bir düğüm oluşturmak için tekrar Durum'u seçin.

    4. Düğüme Patrol adını verin.

    5. Mevcut Chase ve Patrol eylemlerini yeni oluşturulan ilgili durumlarına sürükleyip bırakın.

  2. Geçişi oluşturmak için:

    1. Yeni bir düğüm oluşturmak için sağ tıklayın ve Geçiş'i seçin.

    2. LineOfSightDecision nesnesini geçişin Decision alanına atayın.

  3. RemainInState düğümünü oluşturmak için:

    1. Yeni bir düğüm oluşturmak için sağ tıklayın ve RemainInState'i seçin.
  4. Grafiği bağlamak için:

    1. Patrol düğümünün Transitions çıkışını Transition düğümünün Entry girişine bağlayın.

    2. Transition düğümünün True State çıkışını, Chase düğümünün Entry girişine bağlayın.

    3. Transition düğümünün False State çıkışını, Remain In State düğümünün Entry girişine bağlayın.

Grafik şöyle görünmelidir:

Dört düğüm, her biri sol üst taraflarında Giriş giriş daireleri bulunan dört dikdörtgen olarak temsil edilir. Devriye durumu düğümü soldan sağa bir eylem görüntüler: Devriye Eylemi. Devriye durumu düğümü ayrıca, sağ alt tarafında Geçiş düğümünün Giriş çemberine bağlanan bir Geçişler çıkış çemberi içerir. Geçiş düğümü bir karar görüntüler: LineOfSight. Sağ alt tarafında True State ve False State olmak üzere iki çıkış dairesi vardır. True State, üçüncü yapımız olan Chase durum düğümünün Giriş çemberine bağlanır. İzleme durumu düğümü bir eylemi görüntüler: İzleme Eylemi. Chase durum düğümünün bir Geçişler çıkış dairesi vardır. Transition'ın iki çıkış çemberinden ikincisi olan False State, dördüncü ve son yapımız olan RemainInState düğümünün (Chase durum düğümünün altında görünen) Giriş çemberine bağlanır.
FSM Grafiğimize İlk Bakış

Grafikteki hiçbir şey, hangi düğümün ( Patrol veya Chase durumu) ilk düğümümüz olduğunu göstermez. BaseStateMachineGraph sınıfı dört düğüm algılar, ancak hiçbir gösterge bulunmadığından ilk durumu seçemez.

Bu sorunu çözmek için şunu oluşturalım:

FSMInitialNode StateNode türündeki tek çıkışı StateNode olarak adlandırılan bir InitialNode

InitialNode başlangıç ​​durumunu belirtir. Ardından, FSMInitialNode içinde şunu oluşturun:

NextNode InitialNode çıkışına bağlı düğümü getirmemizi sağlayan bir özellik
 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; } } } }

Artık FSMInitialNode sınıfını oluşturduğumuza göre, onu başlangıç ​​durumunun Entry girişine bağlayabilir ve NextNode özelliği aracılığıyla başlangıç ​​durumuna geri dönebiliriz.

Grafiğimize geri dönelim ve ilk düğümü ekleyelim. xNode düzenleyici penceresinde:

  1. Yeni bir düğüm oluşturmak için sağ tıklayın ve İlk Düğüm'ü seçin.
  2. FSM Düğümü çıkışını Patrol düğümünün Entry girişine ekleyin.

Grafik şimdi şöyle görünmelidir:

Diğer dört dikdörtgenin soluna bir eklenmiş FSM Düğümü yeşil dikdörtgeni ile önceki resmimizdekiyle aynı grafik. Devriye düğümünün "Giriş" girişine (koyu kırmızı bir daire ile temsil edilir) bağlanan bir İlk Düğüm çıkışına (mavi bir daire ile temsil edilir) sahiptir.
Devriye Durumuna Bağlı İlk Düğümlü FSM Grafiğimiz

Hayatlarımızı kolaylaştırmak için FSMGraph şunları ekleyeceğiz:

InitialState bir mülk

InitialState özelliğinin değerini ilk kez almaya çalıştığımızda, özelliğin alıcısı FSMInitialNode bulmaya çalışırken grafiğimizdeki tüm düğümleri geçecektir. FSMInitialNode bulunduğunda, ilk durum düğümümüzü bulmak için NextNode özelliğini kullanırız:

 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; } } }

Ardından, BaseStateMachineGraph , FSMGraph ve BaseStateMachine Init ve Execute yöntemlerini geçersiz kılalım. Init geçersiz kılmak, CurrentState grafiğin ilk durumu olarak ayarlar ve Execute çağrılarını geçersiz kılmak Execute on 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); } } }

Şimdi grafiği Enemy nesnesine uygulayalım ve onu çalışırken görelim.

FSM Grafiğinin Test Edilmesi

Teste hazırlanırken, Unity Editörünün Proje penceresinde:

  1. SampleScene varlığını açın.

  2. Unity hiyerarşi penceresinde Enemy oyun nesnemizi bulun.

  3. BaseStateMachine bileşenini BaseStateMachineGraph bileşeniyle değiştirin:

    1. Bileşen Ekle'ye tıklayın ve doğru BaseStateMachineGraph komut dosyasını seçin.

    2. FSM grafiğimiz EnemyGraph BaseStateMachineGraph bileşeninin Graph alanına atayın.

    3. BaseStateMachine bileşenini (artık gerekmediğinden) sağ tıklayıp Remove Component öğesini seçerek silin.

Enemy oyun nesnesi şöyle görünmelidir:

Inspector ekranında Enemy'nin yanında yukarıdan aşağıya bir onay işareti var. Etiket açılır menüsünde "Oyuncu" seçilir, Katman açılır menüsünde "Düşman" seçilir. Dönüştür açılır menüsü konumu, dönüşü ve ölçeği gösterir. Kapsül açılır menüsü sıkıştırılır ve Mesh Oluşturucu, Kapsül Çarpıştırıcısı ve Nav Mesh Aracısı açılır menüleri, sollarında bir onay işaretiyle sıkıştırılmış olarak görünür. Düşman Görüş Sensörü açılır menüsü, Komut Dosyasını ve Yoksayma Maskesini gösterir. PatrolPoints açılır menüsü, Komut Dosyasını ve dört PatrolPoint'i gösterir. Temel Durum Makine Grafiği (Komut Dosyası) açılır listesinin yanında bir onay işareti vardır. Komut dosyası "BaseStateMachineGraph"ı gösterir, İlk Durum "Yok (Temel Durum)" gösterir ve Grafik "Düşman Grafiği (FSM Grafiği)" gösterir. BT.
Enemy Oyun Nesnemiz

Bu kadar! Artık grafik düzenleyicili modüler bir FSM'miz var. Oynat düğmesine tıklamak, grafik olarak oluşturulan düşman AI'nın tam olarak daha önce oluşturulan ScriptableObject düşmanımız gibi çalıştığını gösterir.

İlerlemek: FSM'mizi Optimize Etmek

Bir uyarı: Oyununuz için daha karmaşık yapay zeka geliştirdikçe, durumların ve geçişlerin sayısı artar ve FSM kafa karıştırıcı ve okunması zor hale gelir. Grafik düzenleyici, birden çok durumda başlayan ve birden çok geçişte sona eren bir çizgiler ağına benzeyecek şekilde büyür ve bunun tersi de FSM'nin hata ayıklamasını zorlaştırır.

Önceki eğitimde olduğu gibi, sizi kodu kendiniz yapmaya, gizli oyununuzu optimize etmeye ve bu endişeleri gidermeye davet ediyoruz. Bir düğümün etkin olup olmadığını belirtmek için durum düğümlerinizi renkle kodlamanın veya ekran gayrimenkullerini sınırlamak için RemainInState ve Initial düğümleri yeniden boyutlandırmanın ne kadar yararlı olacağını hayal edin.

Bu tür geliştirmeler sadece kozmetik değildir. Renk ve boyut referansları, nerede ve ne zaman hata ayıklanacağını belirlemeye yardımcı olur. Gözü yormayan bir grafiğin değerlendirilmesi, analiz edilmesi ve anlaşılması da daha kolaydır. Sonraki tüm adımlar size kalmış; grafik düzenleyicimizin temeli yerine oturduğunda, yapabileceğiniz geliştirici deneyimi iyileştirmelerinin sınırı yoktur.

Toptal Mühendislik Blogunda Daha Fazla Okuma:

  • Unity Geliştiricilerinin Yaptığı En Yaygın 10 Hata
  • MVC ile Unity: Oyun Geliştirmenizin Seviyesini Nasıl Yükseltebilirsiniz?
  • Unity'de 2D Kameralarda Uzmanlaşma: Oyun Geliştiricileri İçin Bir Eğitim
  • Toptal Geliştiricilerinden Unity En İyi Uygulamaları ve İpuçları

Toptal Engineering Blog, bu makalenin uzmanlığı ve teknik incelemesi için Goran Lalic'e şükranlarını sunar.