การพัฒนา Unity AI: บทช่วยสอนเกี่ยวกับเครื่องจักรที่มีสถานะจำกัด

เผยแพร่แล้ว: 2022-03-11

ในโลกแห่งการแข่งขันของเกม นักพัฒนามุ่งมั่นที่จะมอบประสบการณ์ผู้ใช้ที่สนุกสนานสำหรับผู้ที่โต้ตอบกับตัวละครที่ไม่ใช่ผู้เล่น (NPC) ที่เราสร้างขึ้น นักพัฒนาสามารถส่งมอบการโต้ตอบนี้ได้โดยใช้เครื่อง finite-state (FSM) เพื่อสร้างโซลูชัน AI ที่จำลองข้อมูลอัจฉริยะใน NPC ของเรา

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

กายวิภาคของ FSM

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

ส่วนประกอบ คำอธิบาย
สถานะ หนึ่งในชุดตัวเลือกที่มีขอบเขตจำกัดซึ่งระบุถึงสภาพโดยรวมในปัจจุบันของ FSM สถานะใด ๆ รวมถึงชุดของการกระทำที่เกี่ยวข้อง
หนังบู๊ รัฐทำอะไรเมื่อ FSM ทำการสอบถาม
การตัดสินใจ ตรรกะที่เกิดขึ้นเมื่อการเปลี่ยนแปลงเกิดขึ้น
การเปลี่ยนแปลง กระบวนการเปลี่ยนสถานะ

ในขณะที่เราจะมุ่งเน้นไปที่ FSM จากมุมมองของการนำ AI ไปใช้ แนวคิดเช่นเครื่องแสดงสถานะแอนิเมชั่นและสถานะเกมทั่วไปก็ตกอยู่ภายใต้ร่ม FSM

การแสดงภาพ FSM

ลองมาดูตัวอย่างของเกมอาร์เคดสุดคลาสสิค Pac-Man ในสถานะเริ่มต้นของเกม (สถานะ "ไล่ล่า") NPC เป็นผีหลากสีที่ไล่ตามและแซงหน้าผู้เล่นในที่สุด ผีจะเข้าสู่สถานะหลบเลี่ยงเมื่อใดก็ตามที่ผู้เล่นกินยาเม็ดพลังและสัมผัสกับการเพิ่มพลัง ทำให้ได้รับความสามารถในการกินผี ผีซึ่งตอนนี้เป็นสีน้ำเงิน จะหลบเลี่ยงผู้เล่นจนกว่าจะหมดเวลาเพิ่มพลัง และผีจะเปลี่ยนกลับไปเป็นสถานะการไล่ล่า ซึ่งพฤติกรรมและสีดั้งเดิมของพวกมันจะกลับคืนมา

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

แผนภาพ: ทางด้านซ้ายคือสถานะการไล่ล่า ลูกศร (แสดงว่าผู้เล่นกินเม็ดพลัง) นำไปสู่สถานะการหลบเลี่ยงทางด้านขวา ลูกศรที่สอง (ซึ่งแสดงว่าเม็ดพลังงานหมดเวลา) จะนำกลับไปที่สถานะไล่ล่าทางด้านซ้าย
การเปลี่ยนผ่านระหว่างรัฐ Pac-Man Ghost

โดยการออกแบบเครื่องจำกัดสถานะจะสอบถามสถานะปัจจุบัน ซึ่งจะสอบถามการตัดสินใจและการดำเนินการของสถานะนั้น ไดอะแกรมต่อไปนี้เป็นตัวอย่าง Pac-Man ของเราและแสดงการตัดสินใจที่ตรวจสอบสถานะของการเพิ่มพลังของผู้เล่น หากการเพิ่มพลังได้เริ่มขึ้น NPC จะเปลี่ยนไปจากการไล่ล่าเป็นการหลบเลี่ยง หากการเพิ่มพลังสิ้นสุดลง NPC จะเปลี่ยนจากการหลบเลี่ยงเป็นการไล่ล่า สุดท้าย หากไม่มีการเปลี่ยนแปลงการเพิ่มพลัง การเปลี่ยนแปลงจะไม่เกิดขึ้น

ไดอะแกรมรูปเพชรแสดงถึงวัฏจักร: เริ่มต้นที่ด้านซ้าย มีสถานะการไล่ล่าซึ่งแสดงถึงการกระทำที่สอดคล้องกัน จากนั้นสถานะการไล่ล่าจะชี้ไปที่ด้านบน ซึ่งจะมีการตัดสินว่า: หากผู้เล่นกินยาเม็ดพลังงาน เราจะไปยังสถานะหลบเลี่ยงและหลบเลี่ยงการกระทำทางด้านขวา สถานะการหลบเลี่ยงชี้ไปที่การตัดสินใจที่ด้านล่าง: หากเม็ดพลังงานหมดเวลา เราจะกลับไปที่จุดเริ่มต้นของเราต่อไป
ส่วนประกอบของ Pac-Man Ghost FSM

ความสามารถในการปรับขนาด

FSM ปล่อยให้เราสร้าง AI แบบแยกส่วนได้ ตัวอย่างเช่น ด้วยการกระทำใหม่เพียงครั้งเดียว เราสามารถสร้าง NPC ที่มีพฤติกรรมใหม่ได้ ดังนั้น เราสามารถกำหนดการกระทำใหม่—การกินเม็ดพลัง—ให้กับผี Pac-Man ตัวหนึ่งของเรา ทำให้มันสามารถกินเม็ดพลังในขณะที่หลบเลี่ยงผู้เล่น เราสามารถนำการกระทำ การตัดสินใจ และการเปลี่ยนแปลงที่มีอยู่มาใช้ซ้ำเพื่อสนับสนุนพฤติกรรมนี้ได้

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

การนำ FSM ไปใช้ใน Unity

เพื่อสาธิตวิธีการใช้เครื่องจำกัดสถานะใน Unity ให้สร้างเกมการลักลอบอย่างง่าย สถาปัตยกรรมของเราจะรวม ScriptableObject s ซึ่งเป็นที่เก็บข้อมูลที่สามารถจัดเก็บและแบ่งปันข้อมูลทั่วทั้งแอปพลิเคชัน เพื่อที่เราจะไม่ต้องทำซ้ำ ScriptableObject มีความสามารถในการประมวลผลที่จำกัด เช่น การเรียกใช้การดำเนินการและการตัดสินใจสอบถาม นอกเหนือจากเอกสารอย่างเป็นทางการของ Unity แล้ว Game Architecture รุ่นเก่าที่มี Scriptable Objects talk ยังคงเป็นแหล่งข้อมูลที่ยอดเยี่ยมหากคุณต้องการเจาะลึกลงไปอีก

ก่อนที่เราจะเพิ่ม AI ในโครงการพร้อมคอมไพล์เริ่มต้นนี้ ให้พิจารณาสถาปัตยกรรมที่เสนอ:

แผนภาพ: กล่องเจ็ดกล่องที่เชื่อมต่อซึ่งกันและกัน โดยอธิบายตามลำดับลักษณะที่ปรากฏ จากซ้าย/บน: กล่องที่มีป้ายกำกับ BaseStateMachine ประกอบด้วย + CurrentState: BaseState BaseStateMachine เชื่อมต่อกับ BaseState ด้วยลูกศรสองทิศทาง กล่องที่มีป้ายกำกับ BaseState ประกอบด้วย + Execute(BaseStateMachine): ถือเป็นโมฆะ BaseState เชื่อมต่อกับ BaseStateMachine ด้วยลูกศรสองทิศทาง ลูกศรทิศทางเดียวจาก State และ RemainInState เชื่อมต่อกับ BaseState กล่องที่มีป้ายกำกับ State ประกอบด้วย + Execute(BaseStateMachine): void, + Actions: List<Action> และ + Transition: List<Transition> State เชื่อมต่อกับ BaseState ด้วยลูกศรทิศทางเดียว ไปยัง Action ด้วยลูกศรทิศทางเดียวที่มีป้ายกำกับ "1" และไปยัง Transition ด้วยลูกศรทิศทางเดียวที่มีป้ายกำกับ "1" กล่องที่ระบุว่า RemainInState ประกอบด้วย + Execute(BaseStateMachine): ถือเป็นโมฆะ RemainInState เชื่อมต่อกับ BaseState ด้วยลูกศรทิศทางเดียว กล่องที่มีข้อความว่า Action ประกอบด้วย + Execute(BaseStateMachine): ถือเป็นโมฆะ ลูกศรทิศทางเดียวที่มีป้ายกำกับ "1" จากสถานะเชื่อมต่อกับการดำเนินการ กล่องที่ชื่อ Transition ประกอบด้วย + Decide(BaseStateMachine): void, + TransitionDecision: Decision, + TrueState: BaseState และ + FalseState: BaseState การเปลี่ยนผ่านเชื่อมต่อกับการตัดสินใจด้วยลูกศรทิศทางเดียว ลูกศรทิศทางเดียวที่มีป้ายกำกับ "1" จากสถานะเชื่อมต่อกับการเปลี่ยน กล่องที่ระบุว่า Decision ประกอบด้วย + Decide(BaseStateMachine): bool
สถาปัตยกรรม FSM ที่เสนอ

ในเกมตัวอย่างของเรา ศัตรู (NPC ที่แทนด้วยแคปซูลสีน้ำเงิน) ลาดตระเวน เมื่อศัตรูเห็นผู้เล่น (แสดงด้วยแคปซูลสีเทา) ศัตรูจะเริ่มติดตามผู้เล่น:

แผนภาพ: กล่องห้ากล่องที่เชื่อมต่อซึ่งกันและกัน โดยอธิบายตามลำดับลักษณะที่ปรากฏ จากซ้าย/บน: กล่องที่ระบุว่า Patrol เชื่อมต่อกับกล่องที่ระบุว่า IF ผู้เล่นอยู่ในแนวสายตาด้วยลูกศรทิศทางเดียว และไปยังกล่องที่มีป้ายกำกับ Patrol Action ด้วย ลูกศรทิศทางเดียวที่มีป้ายกำกับว่า "สถานะ" กล่องที่ระบุว่าผู้เล่น IF อยู่ในแนวสายตา โดยมี "การตัดสินใจ" เพิ่มเติมใต้กล่อง กล่องที่ระบุว่าผู้เล่น IF อยู่ในแนวสายตาเชื่อมต่อกับกล่องที่ระบุว่า Chase พร้อมลูกศรทิศทางเดียว ลูกศรทิศทางเดียวจากกล่องที่มีป้าย Patrol เชื่อมต่อกับกล่องที่ระบุว่า IF ผู้เล่นอยู่ในสายตา กล่องที่ชื่อ Chase เชื่อมต่อกับกล่องที่ชื่อ Chase Action ด้วยลูกศรทิศทางเดียวที่เขียนว่า "state" ลูกศรทิศทางเดียวจากกล่องที่ระบุว่าผู้เล่น IF อยู่ในแนวสายตาเชื่อมต่อกับกล่องที่มีป้ายกำกับว่า Chase ลูกศรลูกศรทิศทางเดียวจากกล่องที่มีป้ายกำกับว่า Patrol เชื่อมต่อกับกล่องที่มีป้ายกำกับว่า Patrol Action ลูกศรลูกศรทิศทางเดียวจากกล่องที่ชื่อว่า Chase เชื่อมต่อกับกล่องที่มีข้อความว่า Chase Action
ส่วนประกอบหลักของเกม Stealth ตัวอย่างของเรา FSM

ตรงกันข้ามกับ Pac-Man ศัตรูในเกมของเราจะไม่กลับสู่สถานะเริ่มต้น ("การลาดตระเวน") เมื่อมันติดตามผู้เล่น

การสร้างชั้นเรียน

เริ่มต้นด้วยการสร้างชั้นเรียนของเรา ในโฟลเดอร์ scripts ใหม่ เราจะเพิ่มบล็อคการสร้างสถาปัตยกรรมที่เสนอทั้งหมดเป็นสคริปต์ C#

การใช้งาน BaseStateMachine Class

คลาส BaseStateMachine เป็น MonoBehavior เดียวที่เราจะเพิ่มเพื่อเข้าถึง NPC ที่เปิดใช้งาน AI ของเรา เพื่อความเรียบง่าย BaseStateMachine ของเราจะเป็นแบบเปล่าๆ อย่างไรก็ตาม หากเราต้องการ เราสามารถเพิ่ม FSM แบบกำหนดเองที่สืบทอดมาซึ่งเก็บพารามิเตอร์เพิ่มเติมและการอ้างอิงไปยังส่วนประกอบเพิ่มเติม โปรดทราบว่าโค้ดจะไม่คอมไพล์อย่างถูกต้องจนกว่าเราจะเพิ่มคลาส BaseState ซึ่งเราจะทำในภายหลังในบทช่วยสอน

รหัสสำหรับ BaseStateMachine อ้างถึงและดำเนินการสถานะปัจจุบันเพื่อดำเนินการและดูว่ารับประกันการเปลี่ยนแปลงหรือไม่:

 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 Class

สถานะของเราอยู่ในประเภท BaseState ซึ่งเราได้มาจาก ScriptableObject BaseState มีเมธอดเดียว Execute รับ BaseStateMachine เป็นอาร์กิวเมนต์และส่งผ่านไปยังแอคชันและทรานซิชัน นี่คือลักษณะ BaseState มีลักษณะ:

 using UnityEngine; namespace Demo.FSM { public class BaseState : ScriptableObject { public virtual void Execute(BaseStateMachine machine) { } } }

การดำเนินการของ State และ RemainInState ชั้นเรียนของรัฐ

ตอนนี้เราได้รับสองคลาสจาก BaseState อันดับแรก เรามี State class ซึ่งจัดเก็บการอ้างอิงถึงการดำเนินการและการเปลี่ยน รวมถึงสองรายการ (รายการหนึ่งสำหรับการดำเนินการ อีกรายการสำหรับการเปลี่ยน) และการแทนที่และเรียกฐาน Execute เกี่ยวกับการดำเนินการและการเปลี่ยน:

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

ประการที่สอง เรามีคลาส RemainInState ซึ่งบอก FSM เมื่อไม่ทำการเปลี่ยนแปลง:

 using UnityEngine; namespace Demo.FSM { [CreateAssetMenu(menuName = "FSM/Remain In State", fileName = "RemainInState")] public sealed class RemainInState : BaseState { } }

โปรดทราบว่าคลาสเหล่านี้จะไม่คอมไพล์จนกว่าเราจะเพิ่ม FSMAction , Decision และ Transition

การใช้งาน FSMAction Class

ในไดอะแกรมสถาปัตยกรรม FSM ที่เสนอ คลาส FSMAction พื้นฐานจะมีป้ายกำกับว่า "การดำเนินการ" อย่างไรก็ตาม เราจะสร้างคลาส FSMAction พื้นฐาน และใช้ชื่อ FSMAction (เนื่องจากมี Action ใช้งานเนมสเปซ .NET System แล้ว)

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

คลาสนามธรรม FSMAction มีลักษณะดังนี้:

 using UnityEngine; namespace Demo.FSM { public abstract class FSMAction : ScriptableObject { public abstract void Execute(BaseStateMachine stateMachine); } }

การดำเนินการตามชั้นเรียนการ Decision และการ Transition

เพื่อเสร็จสิ้น FSM ของเรา เราจะกำหนดอีกสองคลาส อันดับแรก เรามี Decision ซึ่งเป็นคลาสนามธรรมซึ่งการตัดสินใจอื่น ๆ ทั้งหมดจะกำหนดพฤติกรรมที่กำหนดเอง:

 using UnityEngine; namespace Demo.FSM { public abstract class Decision : ScriptableObject { public abstract bool Decide(BaseStateMachine state); } }

ชั้นที่สอง Transition ประกอบด้วย Decision object และสองสถานะ:

  • สถานะที่จะเปลี่ยนไปหากการ Decision เป็นจริง
  • สถานะอื่นที่จะเปลี่ยนไปหากการ Decision ให้ผลเท็จ

ดูเหมือนว่านี้:

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

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

การสร้างการดำเนินการและการตัดสินใจที่กำหนดเอง

ตอนนี้ เมื่อเสร็จสิ้นภาระกิจ เราพร้อมที่จะใช้การดำเนินการและการตัดสินใจแบบกำหนดเองในโฟลเดอร์ scripts ใหม่

การดำเนินการตามชั้นเรียน Patrol และ Chase

เมื่อเราวิเคราะห์องค์ประกอบหลักของแผนภาพ FSM ตัวอย่างเกม Stealth ของเรา เราจะเห็นว่า NPC ของเราสามารถอยู่ในสถานะใดสถานะหนึ่งจากสองสถานะ:

  1. รัฐตระเวน — ที่เกี่ยวข้องกับรัฐคือ:
    • หนึ่งการกระทำ: NPC เยี่ยมชมจุดลาดตระเวนแบบสุ่มทั่วโลก
    • หนึ่งช่วงการเปลี่ยนภาพ: NPC จะตรวจสอบว่าผู้เล่นอยู่ในสายตาหรือไม่ และหากใช่ จะเปลี่ยนเป็นสถานะการไล่ล่า
    • หนึ่งการตัดสินใจ: NPC ตรวจสอบว่าผู้เล่นอยู่ในสายตาหรือไม่
  2. Chase state — ที่เกี่ยวข้องกับรัฐคือ:
    • หนึ่งการกระทำ: NPC ไล่ล่าผู้เล่น

เราสามารถนำการนำการเปลี่ยนแปลงที่มีอยู่ไปใช้ซ้ำได้ผ่าน GUI ของ Unity ตามที่เราจะพูดถึงในภายหลัง การดำเนินการนี้เหลือสองการดำเนินการ ( PatrolAction และ ChaseAction ) และการตัดสินใจให้เราเขียนโค้ด

การดำเนินการสถานะตระเวน (ซึ่งมาจาก FSMAction ฐาน) จะแทนที่เมธอด Execute เพื่อรับสององค์ประกอบ:

  1. PatrolPoints ซึ่งติดตามจุดลาดตระเวน
  2. NavMeshAgent การใช้งานของ Unity สำหรับการนำทางในพื้นที่ 3 มิติ

การแทนที่จะตรวจสอบว่าเอเจนต์ AI ไปถึงปลายทางหรือไม่ และหากเป็นเช่นนั้น ให้ย้ายไปยังปลายทางถัดไป ดูเหมือนว่านี้:

 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 และ NavMeshAgent การแคชจะทำให้เราสามารถแบ่งปัน ScriptableObject สำหรับการดำเนินการระหว่างเอเจนต์โดยไม่กระทบต่อประสิทธิภาพการทำงานของการรัน GetComponent ในแต่ละเคียวรีของเครื่อง finite-state

เพื่อความชัดเจน เราไม่สามารถแคชอินสแตนซ์ของส่วนประกอบในวิธี Execute การได้ ดังนั้น เราจะเพิ่มเมธอด GetComponent แบบกำหนดเองให้กับ BaseStateMachine แทน GetComponent แบบกำหนดเองของเราจะแคชอินสแตนซ์ในครั้งแรกที่มีการเรียก โดยคืนค่าอินสแตนซ์ที่แคชไว้ในการเรียกต่อเนื่องกัน สำหรับการอ้างอิง นี่คือการใช้งาน 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() { 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 คลาส ChaseAction จะแทนที่เมธอด Execute เพื่อรับส่วนประกอบ PatrolPoints และ NavMeshAgent ในทางตรงกันข้าม หลังจากตรวจสอบว่าเอเจนต์ AI ไปถึงที่หมายแล้วหรือไม่ การดำเนินการคลาส ChaseAction จะตั้งค่าปลายทางเป็น Player.position :

 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 Class ไปใช้

ชิ้นสุดท้ายคือคลาส InLineOfSightDecision ซึ่งสืบทอดการ Decision พื้นฐานและรับส่วนประกอบ EnemySightSensor เพื่อตรวจสอบว่าผู้เล่นอยู่ในสายตาของ NPC หรือไม่:

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

การแนบพฤติกรรมกับรัฐ

ในที่สุดเราก็พร้อมที่จะแนบพฤติกรรมกับตัวแทน Enemy สิ่งเหล่านี้ถูกสร้างขึ้นในหน้าต่างโครงการของ Unity Editor

เพิ่ม Patrol และ Chase รัฐ

มาสร้างสองสถานะและตั้งชื่อพวกเขาว่า "Patrol" และ "Chase":

  • คลิกขวา > สร้าง > FSM > สถานะ

ขณะอยู่ที่นี่ เรามาสร้างวัตถุ RemainInState กัน:

  • คลิกขวา > สร้าง > FSM > อยู่ในสถานะ

ถึงเวลาสร้างการกระทำที่เราเพิ่งเขียนโค้ด:

  • คลิกขวา > สร้าง > FSM > การดำเนินการ > Patrol
  • คลิกขวา > สร้าง > FSM > การกระทำ > Chase

เพื่อเข้ารหัสการ Decision :

  • คลิกขวา > สร้าง > FSM > การตัดสินใจ > อยู่ในแนวสายตา

ในการเปิดใช้งานการเปลี่ยนจาก PatrolState เป็น ChaseState อื่นให้สร้างวัตถุทรานซิชันสคริปต์ได้ก่อน:

  • คลิกขวา > สร้าง > FSM > การเปลี่ยนผ่าน
  • เลือกชื่อที่คุณชอบ ฉันเรียกฉันว่า Spotted Enemy

เราจะเติมหน้าต่างตัวตรวจสอบที่เป็นผลลัพธ์ดังนี้:

หน้าจอ Spotted Enemy (การเปลี่ยนผ่าน) ประกอบด้วยสี่บรรทัด: ค่าของสคริปต์ถูกตั้งค่าเป็น "การเปลี่ยนแปลง" และเป็นสีเทา ค่าของการตัดสินใจถูกตั้งค่าเป็น "LineOfSightDecision (In Line Of Sight)" ค่า True State ถูกตั้งค่าเป็น "ChaseState (State)" ค่าของ False State ถูกตั้งค่าเป็น "RemainInState (อยู่ในสถานะ)"
การกรอกหน้าต่างสารวัตรศัตรูที่เห็น (ช่วงเปลี่ยนผ่าน)

จากนั้นเราจะดำเนินการกล่องโต้ตอบตัวตรวจสอบ Chase State ให้สมบูรณ์ดังนี้:

หน้าจอ Chase State (สถานะ) เริ่มต้นด้วยป้ายกำกับ "เปิด" ข้างป้ายกำกับ "สคริปต์" "สถานะ" ถูกเลือก ข้างป้ายกำกับ "Action" เลือก "1" จากดรอปดาวน์ "Action" เลือก "Element 0 Chase Action (Chase Action)" มีเครื่องหมายบวกและเครื่องหมายลบที่ตามมา ข้างป้ายกำกับ "การเปลี่ยน" มีการเลือก "0" จากดรอปดาวน์ "การเปลี่ยนแปลง" "รายการว่างเปล่า" จะปรากฏขึ้น มีเครื่องหมายบวกและเครื่องหมายลบที่ตามมา
กรอกหน้าต่างสารวัตรไล่ล่า

ต่อไป เราจะทำกล่องโต้ตอบ Patrol State ให้เสร็จ:

หน้าจอ Patrol State (State) เริ่มต้นด้วยป้ายกำกับ "เปิด" ข้างป้ายกำกับ "สคริปต์" "สถานะ" ถูกเลือก ข้างป้ายกำกับ "Action" เลือก "1" จากดรอปดาวน์ "Action" เลือก "Element 0 Patrol Action (Patrol Action)" มีเครื่องหมายบวกและลบที่ตามมา ข้างป้ายกำกับ "การเปลี่ยนผ่าน" มีการเลือก "1" จากเมนูดร็อปดาวน์ "การเปลี่ยนผ่าน" "องค์ประกอบ 0 SpottedEnemy (การเปลี่ยนผ่าน)" จะปรากฏขึ้น มีเครื่องหมายบวกและเครื่องหมายลบที่ตามมา
กรอกหน้าต่างสารวัตรรัฐตระเวน

สุดท้าย เราจะเพิ่มองค์ประกอบ BaseStateMachine ให้กับวัตถุศัตรู: ในหน้าต่างโครงการของ Unity Editor เปิดเนื้อหา SampleScene เลือกวัตถุศัตรูจากแผงลำดับชั้น และในหน้าต่างตัวตรวจสอบ ให้เลือก เพิ่มส่วนประกอบ > เครื่องสถานะฐาน :

หน้าจอ Base State Machine (สคริปต์): ข้างป้ายกำกับ "สคริปต์" ที่เป็นสีเทา "BaseStateMachine" จะถูกเลือกและเป็นสีเทา ข้างป้ายกำกับ "สถานะเริ่มต้น" เลือก "PatrolState (State)"
การเพิ่มส่วนประกอบ Base State Machine (สคริปต์)

สำหรับปัญหาใดๆ ให้ตรวจสอบอีกครั้งว่าวัตถุเกมของคุณได้รับการกำหนดค่าอย่างถูกต้อง ตัวอย่างเช่น ยืนยันว่าอ็อบเจ็กต์ Enemy มีส่วนประกอบสคริปต์ PatrolPoints และอ็อบเจ็กต์ Point1 , Point2 เป็นต้น ข้อมูลนี้อาจสูญหายได้ด้วยการกำหนดเวอร์ชันของตัวแก้ไขที่ไม่ถูกต้อง

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

การใช้ FSM เพื่อสร้างประสบการณ์ผู้ใช้แบบโต้ตอบที่สนุกสนาน

ในบทช่วยสอนเกี่ยวกับเครื่องที่มีสถานะจำกัดนี้ เราได้สร้าง AI ที่ใช้ FSM แบบโมดูลาร์สูง (และ GitHub repo ที่สอดคล้องกัน) ที่เราสามารถนำมาใช้ซ้ำได้ในโครงการในอนาคต ต้องขอบคุณโมดูลนี้ เราสามารถเพิ่มพลังให้กับ AI ของเราได้เสมอโดยการแนะนำส่วนประกอบใหม่

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