การพัฒนา Unity AI: การสอน FSM แบบกราฟิกบน xNode

เผยแพร่แล้ว: 2022-08-12

ใน “การพัฒนา Unity AI: A Finite-state Machine Tutorial” เราได้สร้างเกมการลอบเร้นที่เรียบง่าย ซึ่งเป็น AI แบบโมดูลาร์ที่ใช้ FSM ในเกม เจ้าหน้าที่ศัตรูลาดตระเวนพื้นที่เกม เมื่อพบผู้เล่น ศัตรูจะเปลี่ยนสถานะและติดตามผู้เล่นแทนการลาดตระเวน

ในเลกที่สองของการเดินทาง Unity ของเรา เราจะสร้างส่วนต่อประสานกราฟิกกับผู้ใช้ (GUI) เพื่อสร้างส่วนประกอบหลักของเครื่อง finite-state (FSM) ของเราอย่างรวดเร็วยิ่งขึ้น และด้วยประสบการณ์นักพัฒนา Unity ที่ได้รับการปรับปรุง

ทบทวนอย่างรวดเร็ว

รายละเอียด FSM ในบทช่วยสอนก่อนหน้านี้สร้างขึ้นจากบล็อกสถาปัตยกรรมเป็นสคริปต์ C# เราได้เพิ่มการกระทำและการตัดสินใจ ScriptableObject แบบกำหนดเองเป็นคลาส วิธีการ ScriptableObject ช่วยให้ FSM บำรุงรักษาและปรับแต่งได้ง่าย ในบทช่วยสอนนี้ เราจะแทนที่ ScriptableObject แบบลากและวางของ FSM ด้วยตัวเลือกแบบกราฟิก

ฉันยังเขียนสคริปต์ที่อัปเดตสำหรับผู้ที่ต้องการทำให้เกมชนะได้ง่ายขึ้น ในการใช้งาน เพียงแค่แทนที่สคริปต์การตรวจหาผู้เล่นด้วยสคริปต์นี้ที่จำกัดขอบเขตการมองเห็นของศัตรูให้แคบลง

เริ่มต้นใช้งาน xNode

เราจะสร้างตัวแก้ไขกราฟิกของเราโดยใช้ xNode ซึ่งเป็นเฟรมเวิร์กสำหรับแผนผังพฤติกรรมแบบโหนดที่จะแสดงโฟลว์ของ FSM ของเราเป็นภาพ แม้ว่า GraphView ของ Unity จะสามารถทำงานได้สำเร็จ แต่ API ของมันก็มีทั้งแบบทดลองและเอกสารเพียงเล็กน้อย อินเทอร์เฟซผู้ใช้ของ xNode มอบประสบการณ์นักพัฒนาที่เหนือกว่า อำนวยความสะดวกในการสร้างต้นแบบและขยาย FSM ของเราอย่างรวดเร็ว

มาเพิ่ม xNode ในโครงการของเราเป็นการพึ่งพา Git โดยใช้ Unity Package Manager:

  1. ใน Unity คลิก Window > Package Manager เพื่อเปิดหน้าต่าง Package Manager
  2. คลิก + (เครื่องหมายบวก) ที่มุมซ้ายบนของหน้าต่าง และเลือก Add package from git URL เพื่อแสดงช่องข้อความ
  3. พิมพ์หรือวาง https://github.com/siccity/xNode.git ในกล่องข้อความที่ไม่มีป้ายกำกับ แล้วคลิกปุ่ม เพิ่ม

ตอนนี้เราพร้อมที่จะเจาะลึกและทำความเข้าใจองค์ประกอบหลักของ xNode แล้ว:

คลาส Node แสดงถึงโหนด ซึ่งเป็นหน่วยพื้นฐานที่สุดของกราฟ ในบทช่วยสอน xNode นี้ เราได้มาจากคลาส Node คลาสใหม่ที่ประกาศโหนดพร้อมกับฟังก์ชันและบทบาทที่กำหนดเอง
คลาส NodeGraph แสดงถึงชุดของโหนด (อินสแตนซ์คลาส Node ) และขอบที่เชื่อมต่อ ในบทช่วยสอน xNode นี้ เราได้มาจาก NodeGraph ซึ่งเป็นคลาสใหม่ที่จัดการและประเมินโหนด
คลาส NodePort หมายถึงเกตการสื่อสาร พอร์ตของประเภทอินพุตหรือเอาต์พุตประเภท ซึ่งอยู่ระหว่างอินสแตนซ์ของ Node ใน NodeGraph คลาส NodePort มีลักษณะเฉพาะสำหรับ xNode
[Input] แอตทริบิวต์ การเพิ่มแอตทริบิวต์ [Input] ให้กับพอร์ตที่กำหนดให้เป็นอินพุต ทำให้พอร์ตสามารถส่งผ่านค่าไปยังโหนดที่เป็นส่วนหนึ่งของ คิดว่าแอตทริบิวต์ [Input] เป็นพารามิเตอร์ของฟังก์ชัน
[Output] แอตทริบิวต์ การเพิ่มแอตทริบิวต์ [Output] ให้กับพอร์ตที่กำหนดให้เป็นเอาต์พุต ทำให้พอร์ตสามารถส่งผ่านค่าจากโหนดที่เป็นส่วนหนึ่งของ คิดว่าแอตทริบิวต์ [Output] เป็นค่าส่งคืนของฟังก์ชัน

การแสดงภาพสภาพแวดล้อมการสร้าง xNode

ใน xNode เราทำงานกับกราฟโดยที่แต่ละ State และช่วงการ Transition จะอยู่ในรูปแบบของโหนด การเชื่อมต่ออินพุตและ/หรือเอาต์พุตทำให้โหนดสัมพันธ์กับโหนดใดๆ หรือทั้งหมดในกราฟของเรา

ลองนึกภาพโหนดที่มีค่าอินพุตสามค่า: สองค่าโดยพลการและหนึ่งค่าบูลีน โหนดจะส่งออกค่าอินพุตประเภทใดก็ได้หนึ่งในสองค่า ขึ้นอยู่กับว่าอินพุตบูลีนเป็นจริงหรือเท็จ

โหนดสาขาซึ่งแสดงด้วยสี่เหลี่ยมขนาดใหญ่ตรงกลาง รวมถึงรหัสเทียม "ถ้า C == จริง A อื่น B" ทางด้านซ้ายเป็นรูปสี่เหลี่ยมผืนผ้าสามรูป ซึ่งแต่ละอันมีลูกศรที่ชี้ไปที่โหนดสาขา: "A (ตามอำเภอใจ)," "B (พล)" และ "C (บูลีน)" ในที่สุดโหนดสาขามีลูกศรที่ชี้ไปที่สี่เหลี่ยม "เอาท์พุท"
ตัวอย่างโหนด Branch

ในการแปลง FSM ที่มีอยู่ให้เป็นกราฟ เราแก้ไขคลาส State และ Transition เพื่อสืบทอดคลาส Node แทนคลาส ScriptableObject เราสร้างวัตถุกราฟประเภท NodeGraph เพื่อให้มีวัตถุ State และการ Transition ทั้งหมดของเรา

การปรับเปลี่ยน BaseStateMachine เพื่อใช้เป็น Base Type

เริ่มต้นสร้างส่วนต่อประสานกราฟิกโดยเพิ่มวิธีการเสมือนใหม่สองวิธีในคลาส BaseStateMachine ที่มีอยู่ของเรา:

Init กำหนดสถานะเริ่มต้นให้กับคุณสมบัติ CurrentState
Execute ดำเนินการสถานะปัจจุบัน

การประกาศเมธอดเหล่านี้เป็นเสมือนช่วยให้เราสามารถแทนที่ได้ ดังนั้นเราสามารถกำหนดพฤติกรรมที่กำหนดเองของคลาสที่สืบทอดคลาส BaseStateMachine สำหรับการเริ่มต้นและการดำเนินการ:

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

ต่อไป ภายใต้โฟลเดอร์ FSM ของเรา มาสร้าง:

FSMGraph โฟลเดอร์
BaseStateMachineGraph คลาส AC# ภายใน FSMGraph

ในขณะนี้ BaseStateMachineGraph จะสืบทอดเฉพาะคลาส BaseStateMachine :

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

เราไม่สามารถเพิ่มฟังก์ชันการทำงานให้กับ BaseStateMachineGraph จนกว่าเราจะสร้างประเภทโหนดฐานของเรา มาทำกันต่อไป

การใช้ NodeGraph และการสร้าง Base Node Type

ภายใต้โฟลเดอร์ FSMGraph ที่สร้างขึ้นใหม่ เราจะสร้าง:

FSMGraph ห้องเรียน

สำหรับตอนนี้ FSMGraph จะสืบทอดเฉพาะคลาส NodeGraph (โดยไม่มีฟังก์ชันเพิ่มเติม):

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

ก่อนที่เราจะสร้างคลาสสำหรับโหนดของเรา ให้เพิ่ม:

FSMNodeBase คลาสที่จะใช้เป็นคลาสพื้นฐานโดยโหนดทั้งหมดของเรา

คลาส FSMNodeBase จะมีอินพุตชื่อ Entry ของประเภท FSMNodeBase เพื่อให้เราสามารถเชื่อมต่อโหนดเข้าด้วยกันได้

เราจะเพิ่มฟังก์ชันตัวช่วยสองอย่าง:

GetFirst ดึงโหนดแรกที่เชื่อมต่อกับเอาต์พุตที่ร้องขอ
GetAllOnPort ดึงโหนดที่เหลือทั้งหมดที่เชื่อมต่อกับเอาต์พุตที่ร้องขอ
 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; } } }

ในที่สุด เราจะมีโหนดสถานะสองประเภท มาเพิ่มคลาสเพื่อรองรับสิ่งเหล่านี้:

BaseStateNode คลาสพื้นฐานเพื่อรองรับทั้ง StateNode และ RemainInStateNode
 namespace Demo.FSM.Graph { public abstract class BaseStateNode : FSMNodeBase { } }

ถัดไป แก้ไขคลาส BaseStateMachineGraph :

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

ที่นี่ เราได้ซ่อนคุณสมบัติ CurrentState ที่สืบทอดมาจากคลาสพื้นฐาน และเปลี่ยนประเภทจาก BaseState เป็น BaseStateNode

การสร้างบล็อคส่วนประกอบสำหรับกราฟ FSM ของเรา

ต่อไป เพื่อสร้างหน่วยการสร้างหลักของ FSM ให้เพิ่มคลาสใหม่สามคลาสในโฟลเดอร์ FSMGraph ของเรา:

StateNode แสดงถึงสถานะของตัวแทน ในการดำเนินการ StateNode จะวนซ้ำบน TransitionNode ที่เชื่อมต่อกับพอร์ตเอาต์พุตของ StateNode (ดึงข้อมูลโดยวิธีตัวช่วย) StateNode สอบถามแต่ละรายการว่าจะเปลี่ยนโหนดเป็นสถานะอื่นหรือปล่อยให้สถานะของโหนดเป็นอยู่
RemainInStateNode บ่งชี้ว่าโหนดควรอยู่ในสถานะปัจจุบัน
TransitionNode ตัดสินใจเปลี่ยนไปเป็นสถานะอื่นหรืออยู่ในสถานะเดียวกัน

ในบทช่วยสอน Unity FSM ก่อนหน้านี้ คลาส State จะวนซ้ำในรายการการเปลี่ยน ที่นี่ใน xNode StateNode ทำหน้าที่เป็น State เทียบเท่ากับการวนซ้ำโหนดที่ดึงข้อมูลผ่านวิธีการช่วยเหลือ GetAllOnPort ของเรา

ตอนนี้เพิ่มแอตทริบิวต์ [Output] ให้กับการเชื่อมต่อขาออก (โหนดการเปลี่ยนแปลง) เพื่อระบุว่าควรเป็นส่วนหนึ่งของ GUI โดยการออกแบบของ xNode ค่าของแอตทริบิวต์เริ่มต้นในโหนดต้นทาง: โหนดที่มีฟิลด์ที่มีเครื่องหมายแอตทริบิวต์ [Output] เนื่องจากเราใช้แอตทริบิวต์ [Output] และ [Input] เพื่ออธิบายความสัมพันธ์และการเชื่อมต่อที่จะกำหนดโดย xNode GUI เราจึงไม่สามารถปฏิบัติกับค่าเหล่านี้ได้ตามปกติ พิจารณาว่าเราทำซ้ำผ่าน Actions กับ Transitions อย่างไร :

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

ในกรณีนี้ เอาต์พุตของ Transitions สามารถมีโหนดหลายโหนดติดอยู่ เราต้องเรียกใช้เมธอดตัวช่วย GetAllOnPort เพื่อรับรายการการเชื่อมต่อ [Output]

RemainInStateNode เป็นคลาสที่ง่ายที่สุดของเรา ไม่ใช้ตรรกะ RemainInStateNode เพียงบ่งชี้ตัวแทนของเรา—ในกรณีของเกมของเราคือศัตรู—ให้อยู่ในสถานะปัจจุบัน:

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

ณ จุดนี้ คลาส TransitionNode ยังไม่สมบูรณ์และจะไม่คอมไพล์ ข้อผิดพลาดที่เกี่ยวข้องจะล้างเมื่อเราอัปเดตชั้นเรียน

ในการสร้าง TransitionNode เราจำเป็นต้องหลีกเลี่ยงข้อกำหนดของ xNode ที่ว่าค่าของเอาต์พุตมีต้นกำเนิดมาจากโหนดต้นทาง เช่นเดียวกับที่เราทำเมื่อเราสร้าง StateNode ข้อแตกต่างที่สำคัญระหว่าง StateNode และ TransitionNode คือเอาต์พุตของ TransitionNode อาจแนบกับโหนดเดียวเท่านั้น ในกรณีของเรา GetFirst จะดึงโหนดหนึ่งโหนดที่เชื่อมต่อกับแต่ละพอร์ตของเรา (โหนดสถานะหนึ่งโหนดจะเปลี่ยนเป็นกรณีจริงและอีกโหนดหนึ่งเพื่อเปลี่ยนเป็นกรณีเท็จ):

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

มาดูผลลัพธ์กราฟิกจากโค้ดของเรากัน

การสร้าง Visual Graph

เมื่อแยกประเภทคลาส FSM ทั้งหมดแล้ว เราสามารถดำเนินการสร้างกราฟ FSM สำหรับตัวแทนศัตรูของเกมได้ ในหน้าต่างโปรเจ็กต์ Unity ให้คลิกขวาที่โฟลเดอร์ EnemyAI แล้วเลือก: Create > FSM > FSM Graph เพื่อให้ระบุกราฟของเราได้ง่ายขึ้น ให้เปลี่ยนชื่อเป็น EnemyGraph

ในหน้าต่างตัวแก้ไขกราฟ xNode ให้คลิกขวาเพื่อแสดงเมนูแบบเลื่อนลงที่แสดงรายการ State , Transition และ RemainInState หากมองไม่เห็นหน้าต่าง ให้ดับเบิลคลิกที่ไฟล์ EnemyGraph เพื่อเปิดหน้าต่างตัวแก้ไข xNode Graph

  1. ในการสร้างรัฐ Chase and Patrol :

    1. คลิกขวาและเลือก สถานะ เพื่อสร้างโหนดใหม่

    2. ตั้งชื่อโหนด Chase

    3. กลับไปที่เมนูแบบเลื่อนลง เลือก สถานะ อีกครั้งเพื่อสร้างโหนดที่สอง

    4. ตั้งชื่อโหนด Patrol

    5. ลากและวางการดำเนินการ Chase และ Patrol ที่มีอยู่ไปยังสถานะที่เกี่ยวข้องที่สร้างขึ้นใหม่

  2. ในการสร้างการเปลี่ยนแปลง:

    1. คลิกขวาและเลือก การ เปลี่ยน เพื่อสร้างโหนดใหม่

    2. กำหนดออบเจ็กต์ LineOfSightDecision ให้กับฟิลด์ Decision ของการเปลี่ยนแปลง

  3. ในการสร้างโหนด RemainInState :

    1. คลิกขวาและเลือก RemainInState เพื่อสร้างโหนดใหม่
  4. ในการเชื่อมต่อกราฟ:

    1. เชื่อมต่อเอาต์พุต Transitions ของโหนด Patrol กับอินพุต Entry ของ Transition node

    2. เชื่อมต่อเอาต์พุต True State ของโหนดท Transition สิชันกับอินพุต Entry ของโหนด Chase

    3. เชื่อมต่อเอาต์พุต False State ของโหนดท Transition ชันกับอินพุต Entry ของโหนด Remain In State

กราฟควรมีลักษณะดังนี้:

สี่โหนดแสดงเป็นรูปสี่เหลี่ยมผืนผ้าสี่อัน โดยแต่ละอันมีวงกลมอินพุตรายการที่ด้านซ้ายบน จากซ้ายไปขวา โหนดสถานะตระเวนแสดงหนึ่งการดำเนินการ: การดำเนินการตระเวน โหนดสถานะตระเวนยังมีวงกลมเอาต์พุตการเปลี่ยนที่ด้านล่างขวาที่เชื่อมต่อกับวงกลมรายการของโหนดการเปลี่ยน โหนดการเปลี่ยนแสดงหนึ่งการตัดสินใจ: LineOfSight มีวงกลมแสดงผลสองวงที่ด้านล่างขวา สถานะจริง และสถานะเท็จ สถานะทรูเชื่อมต่อกับวงกลมรายการของโครงสร้างที่สามของเรา โหนดสถานะการไล่ล่า โหนดสถานะ Chase แสดงหนึ่งการกระทำ: Chase Action โหนดสถานะ Chase มีวงจรเอาต์พุตการเปลี่ยน วงกลมเอาต์พุตที่สองของ Transition คือ False State เชื่อมต่อกับ Entry Circle ของโครงสร้างที่สี่และสุดท้ายของเรา นั่นคือโหนด RemainInState (ซึ่งปรากฏอยู่ใต้โหนด Chase state)
รูปลักษณ์เบื้องต้นที่กราฟ FSM ของเรา

ไม่มีสิ่งใดในกราฟที่บ่งชี้ว่าโหนดใด - รัฐ Patrol หรือ Chase - เป็นโหนดเริ่มต้นของเรา คลาส BaseStateMachineGraph ตรวจพบสี่โหนด แต่ไม่มีตัวบ่งชี้ ไม่สามารถเลือกสถานะเริ่มต้นได้

เพื่อแก้ไขปัญหานี้ มาสร้าง:

FSMInitialNode คลาสที่มีเอาต์พุตเดี่ยวของประเภท StateNode ชื่อ InitialNode

เอาต์พุต InitialNode ของเราแสดงถึงสถานะเริ่มต้น ถัดไป ใน FSMInitialNode ให้สร้าง:

NextNode คุณสมบัติที่ช่วยให้เราสามารถดึงโหนดที่เชื่อมต่อกับ InitialNode output
 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; } } } }

ตอนนี้เราสร้างคลาส FSMInitialNode แล้ว เราสามารถเชื่อมต่อกับอินพุต Entry ของสถานะเริ่มต้นและส่งคืนสถานะเริ่มต้นผ่านคุณสมบัติ NextNode

กลับไปที่กราฟของเราและเพิ่มโหนดเริ่มต้น ในหน้าต่างตัวแก้ไข xNode:

  1. คลิกขวาและเลือก โหนดเริ่มต้น เพื่อสร้างโหนดใหม่
  2. แนบเอาต์พุตของ โหนด FSM เข้ากับอินพุต Entry ของโหนด Patrol

กราฟควรมีลักษณะดังนี้:

กราฟเดียวกันกับในภาพก่อนหน้าของเรา โดยเพิ่มสี่เหลี่ยมสีเขียว FSM Node หนึ่งรายการทางด้านซ้ายของอีกสี่สี่เหลี่ยมที่เหลือ มีเอาต์พุต Initial Node (แสดงด้วยวงกลมสีน้ำเงิน) ที่เชื่อมต่อกับอินพุต "Entry" ของ Patrol node (แสดงด้วยวงกลมสีแดงเข้ม)
กราฟ FSM ของเราที่มีโหนดเริ่มต้นที่ติดอยู่กับสถานะการลาดตระเวน

เพื่อให้ชีวิตของเราง่ายขึ้น เราจะเพิ่ม FSMGraph :

InitialState ทรัพย์สิน

ครั้งแรกที่เราพยายามดึงค่าคุณสมบัติ InitialState ตัวรับคุณสมบัติจะข้ามโหนดทั้งหมดในกราฟของเราขณะที่พยายามค้นหา FSMInitialNode เมื่อ FSMInitialNode แล้ว เราจะใช้คุณสมบัติ NextNode เพื่อค้นหาโหนดสถานะเริ่มต้นของเรา:

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

ต่อไป ใน BaseStateMachineGraph เรามาอ้างอิง FSMGraph และแทนที่เมธอด Init และ Execute ของ BaseStateMachine การแทนที่ Init ตั้งค่า CurrentState เป็นสถานะเริ่มต้นของกราฟ และการแทนที่ Execute เรียก Execute บน 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); } } }

ตอนนี้ ลองใช้กราฟกับออบเจกต์ Enemy และดูการทำงานจริง

การทดสอบกราฟ FSM

ในการเตรียมตัวสำหรับการทดสอบ ในหน้าต่างโครงการของ Unity Editor:

  1. เปิดเนื้อหา SampleScene

  2. ค้นหาวัตถุเกม Enemy ของเราในหน้าต่างลำดับชั้นของความสามัคคี

  3. แทนที่ส่วนประกอบ BaseStateMachine ด้วยส่วนประกอบ BaseStateMachineGraph :

    1. คลิก เพิ่มส่วนประกอบ และเลือกสคริปต์ BaseStateMachineGraph ที่ถูกต้อง

    2. กำหนดกราฟ FSM ของเรา EnemyGraph ให้กับฟิลด์ Graph ขององค์ประกอบ BaseStateMachineGraph

    3. ลบองค์ประกอบ BaseStateMachine (เนื่องจากไม่จำเป็นอีกต่อไป) โดยคลิกขวาและเลือก Remove Component

วัตถุเกม Enemy ควรมีลักษณะดังนี้:

จากบนลงล่าง ในหน้าจอ Inspector จะมีเครื่องหมายถูกข้าง Enemy "ผู้เล่น" ถูกเลือกในรายการดรอปดาวน์แท็ก "ศัตรู" ถูกเลือกในเมนูดรอปดาวน์ของเลเยอร์ เมนูดรอปดาวน์ Transform จะแสดงตำแหน่ง การหมุน และมาตราส่วน เมนูดรอปดาวน์ Capsule ถูกบีบอัด และรายการแบบเลื่อนลง Mesh Renderer, Capsule Collider และ Nav Mesh Agent จะปรากฏขึ้นพร้อมเครื่องหมายถูกที่ด้านซ้าย ดรอปดาวน์ Enemy Sight Sensor จะแสดง Script และ Ignore Mask ดรอปดาวน์ PatrolPoints จะแสดงสคริปต์และ PatrolPoints สี่ตัว มีเครื่องหมายถูกข้างเมนูแบบเลื่อนลง Base State Machine Graph (Script) สคริปต์แสดง "BaseStateMachineGraph" สถานะเริ่มต้นแสดง "ไม่มี (สถานะฐาน) และกราฟแสดง "EnemyGraph (กราฟ FSM)" สุดท้าย ดรอปดาวน์ Blue Enemy (Material) ถูกบีบอัด และปุ่ม "เพิ่มส่วนประกอบ" จะปรากฏขึ้นด้านล่าง มัน.
วัตถุเกม Enemy ของเรา

แค่นั้นแหละ! ตอนนี้เรามี FSM แบบแยกส่วนพร้อมโปรแกรมแก้ไขกราฟิก การคลิกปุ่ม เล่น แสดงว่า AI ศัตรูที่สร้างแบบกราฟิกทำงานเหมือนกับศัตรู ScriptableObject ที่เราสร้างไว้ก่อนหน้านี้ทุกประการ

ก้าวไปข้างหน้า: เพิ่มประสิทธิภาพ FSM . ของเรา

คำเตือน: เมื่อคุณพัฒนา AI ที่ซับซ้อนมากขึ้นสำหรับเกมของคุณ จำนวนสถานะและการเปลี่ยนภาพจะเพิ่มขึ้น และ FSM จะสับสนและอ่านยาก ตัวแก้ไขกราฟิกมีลักษณะเป็นเว็บของบรรทัดที่มีต้นกำเนิดในหลายสถานะและสิ้นสุดที่การเปลี่ยนหลายครั้ง และในทางกลับกัน ทำให้ FSM ยากต่อการดีบัก

เช่นเดียวกับในบทช่วยสอนก่อนหน้านี้ เราขอเชิญคุณสร้างโค้ดของคุณเอง เพิ่มประสิทธิภาพเกมการพรางตัวของคุณ และจัดการกับข้อกังวลเหล่านี้ ลองนึกภาพว่าจะเป็นประโยชน์เพียงใดในการเขียนโค้ดสีให้กับโหนดสถานะของคุณเพื่อระบุว่าโหนดทำงานอยู่หรือไม่ใช้งาน หรือปรับขนาดโหนด RemainInState และ Initial เพื่อจำกัดพื้นที่หน้าจอของพวกเขา

การปรับปรุงดังกล่าวไม่ได้เป็นเพียงเครื่องสำอางเท่านั้น การอ้างอิงสีและขนาดจะช่วยระบุว่าจะดีบักที่ไหนและเมื่อใด กราฟที่มองเห็นได้ง่ายยังง่ายต่อการประเมิน วิเคราะห์ และทำความเข้าใจอีกด้วย ขั้นตอนถัดไปขึ้นอยู่กับคุณ ด้วยพื้นฐานของตัวแก้ไขกราฟิกของเรา การปรับปรุงประสบการณ์ของนักพัฒนาซอฟต์แวร์ที่คุณทำได้นั้นไม่มีขีดจำกัด

อ่านเพิ่มเติมในบล็อก Toptal Engineering:

  • 10 ข้อผิดพลาดที่พบบ่อยที่สุดที่นักพัฒนา Unity สร้างขึ้น
  • ความสามัคคีกับ MVC: วิธีเพิ่มระดับการพัฒนาเกมของคุณ
  • การเรียนรู้กล้อง 2D ใน Unity: บทช่วยสอนสำหรับนักพัฒนาเกม
  • แนวทางปฏิบัติที่ดีที่สุดและเคล็ดลับของ Unity โดย Toptal Developers

บล็อก Toptal Engineering ขอขอบคุณ Goran Lalic สำหรับความเชี่ยวชาญและการตรวจสอบทางเทคนิคของบทความนี้