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.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.
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.
Chase
vePatrol
durumlarını oluşturmak için:Yeni bir düğüm oluşturmak için sağ tıklayın ve Durum'u seçin.
Düğümü
Chase
olarak 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
Patrol
adını verin.Mevcut
Chase
vePatrol
eylemlerini 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.
LineOfSightDecision
nesnesini geçişinDecision
alanına atayın.
RemainInState
düğü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:
Patrol
düğümününTransitions
çıkışınıTransition
düğümününEntry
girişine bağlayın.Transition
düğümününTrue State
çıkışını,Chase
düğümününEntry
girişine bağlayın.Transition
düğümününFalse State
çıkışını,Remain In State
düğümününEntry
giriş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ı
Patrol
düğümününEntry
giriş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
Enemy
oyun nesnemizi bulun.BaseStateMachine
bileşeniniBaseStateMachineGraph
bileşeniyle değiştirin:Bileşen Ekle'ye tıklayın ve doğru
BaseStateMachineGraph
komut dosyasını seçin.FSM grafiğimiz
EnemyGraph
BaseStateMachineGraph
bileşenininGraph
alanına atayın.BaseStateMachine
bileşenini (artık gerekmediğinden) sağ tıklayıp Remove Component öğesini seçerek silin.
Enemy
oyun nesnesi şöyle görünmelidir:
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.