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:
- Unity'de, Paket Yöneticisi penceresini başlatmak için Pencere > Paket Yöneticisi'ne tıklayın.
- 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.
- Etiketlenmemiş metin kutusuna
https://github.com/siccity/xNode.gityazı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.
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.
ChasevePatroldurumlarını oluşturmak için:Yeni bir düğüm oluşturmak için sağ tıklayın ve Durum'u seçin.
Düğümü
Chaseolarak adlandırın.Açılır menüye dönün, ikinci bir düğüm oluşturmak için tekrar Durum'u seçin.
Düğüme
Patroladını verin.Mevcut
ChasevePatroleylemlerini yeni oluşturulan ilgili durumlarına sürükleyip bırakın.
Geçişi oluşturmak için:
Yeni bir düğüm oluşturmak için sağ tıklayın ve Geçiş'i seçin.
LineOfSightDecisionnesnesini geçişinDecisionalanına atayın.
RemainInStatedüğümünü oluşturmak için:- Yeni bir düğüm oluşturmak için sağ tıklayın ve RemainInState'i seçin.
Grafiği bağlamak için:
PatroldüğümününTransitionsçıkışınıTransitiondüğümününEntrygirişine bağlayın.TransitiondüğümününTrue Stateçıkışını,ChasedüğümününEntrygirişine bağlayın.TransitiondüğümününFalse Stateçıkışını,Remain In StatedüğümününEntrygirişine bağlayın.
Grafik şöyle görünmelidir:
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:
- Yeni bir düğüm oluşturmak için sağ tıklayın ve İlk Düğüm'ü seçin.
- FSM Düğümü çıkışını
PatroldüğümününEntrygirişine ekleyin.
Grafik şimdi şöyle görünmelidir:
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:
SampleScene varlığını açın.
Unity hiyerarşi penceresinde
Enemyoyun nesnemizi bulun.BaseStateMachinebileşeniniBaseStateMachineGraphbileşeniyle değiştirin:Bileşen Ekle'ye tıklayın ve doğru
BaseStateMachineGraphkomut dosyasını seçin.FSM grafiğimiz
EnemyGraphBaseStateMachineGraphbileşenininGraphalanına atayın.BaseStateMachinebileşenini (artık gerekmediğinden) sağ tıklayıp Remove Component öğesini seçerek silin.
Enemy oyun nesnesi şöyle görünmelidir:
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.
