ทำให้ทักษะและความสามารถของตัวละครเป็นคำสั่งฝึกดีไหม?


11

ฉันกำลังออกแบบเกมที่ประกอบด้วยตัวละครที่มีทักษะการโจมตีที่ไม่เหมือนใครและความสามารถอื่น ๆ เช่นการสร้างการซ่อมแซมเป็นต้นผู้เล่นสามารถควบคุมตัวละครหลายตัวได้

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

สิ่งที่ฉันไม่แน่ใจ แต่ถ้าเป็นการออกแบบที่ดีเพราะฉันกำลังสร้างเกมของฉันใน Unity ฉันคิดว่าฉันสามารถสร้างทักษะและความสามารถทั้งหมดเป็นส่วนประกอบแต่ละอย่างซึ่งจะติดกับ GameObjects ซึ่งเป็นตัวแทนของตัวละครในเกม จากนั้น UI จะต้องเก็บ GameObject ของตัวละครไว้แล้วจึงดำเนินการคำสั่ง

อะไรจะเป็นการออกแบบและการฝึกฝนที่ดีกว่าสำหรับเกมที่ฉันกำลังออกแบบ?


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

@Anko แล้วส่วนไหนที่ฉันมีคำสั่งทั้งหมดที่ใส่ไว้ในรายการคงที่? ฉันกังวลว่ารายการอาจมีขนาดใหญ่มากและทุกครั้งที่ต้องการคำสั่งต้องค้นหารายการคำสั่งขนาดใหญ่ทุกครั้ง
ซีนอน

1
@xenon คุณไม่น่าจะเห็นปัญหาประสิทธิภาพการทำงานในส่วนนี้ของรหัส เท่าที่บางสิ่งบางอย่างสามารถเกิดขึ้นได้เพียงครั้งเดียวต่อการโต้ตอบของผู้ใช้มันจะต้องมีการคำนวณที่เข้มข้นมาก
aaaaaaaaaaaa

คำตอบ:


17

TL; DR

คำตอบนี้บ้าไปหน่อย แต่เป็นเพราะฉันเห็นว่าคุณกำลังพูดถึงการใช้ความสามารถของคุณในฐานะ "คำสั่ง" ซึ่งหมายถึงรูปแบบการออกแบบ C ++ / Java / .NET ซึ่งแสดงถึงวิธีการที่ใช้รหัสหนัก Approah นั้นถูกต้อง แต่ก็มีวิธีที่ดีกว่า บางทีคุณอาจกำลังทำอย่างอื่นอยู่แล้ว ถ้าเป็นเช่นนั้น หวังว่าคนอื่นจะพบว่ามีประโยชน์ถ้าเป็นเช่นนั้น

ดูที่ Data-Driven Approach ด้านล่างเพื่อตัดการไล่ล่า รับ CustomAssetUility ที่กำหนดเองของ Jacob Pennock ที่นี่และอ่านโพสต์ของเขาเกี่ยวกับเรื่องนี้

การทำงานกับ Unity

เช่นเดียวกับคนอื่น ๆ ที่กล่าวถึงการสำรวจรายการ 100-300 รายการนั้นไม่ใช่เรื่องใหญ่อย่างที่คุณคิด ดังนั้นถ้านั่นเป็นวิธีการที่ใช้งานง่ายสำหรับคุณก็แค่ทำเช่นนั้น ปรับให้เหมาะสมเพื่อประสิทธิภาพของสมอง แต่พจนานุกรมดังที่ @Norguard แสดงให้เห็นในคำตอบของเขาเป็นวิธีที่ไม่ต้องใช้สมองในการกำจัดปัญหานั้นได้ง่ายเนื่องจากคุณได้รับการแทรกและเรียกค้นเวลาอย่างต่อเนื่อง คุณควรใช้มัน

ในแง่ของการทำให้งานนี้เป็นไปได้ดีใน Unity ไส้ในของฉันบอกฉันว่า MonoBehaviour ต่อความสามารถเป็นเส้นทางที่อันตรายที่จะลงไป หากความสามารถใด ๆ ของคุณรักษาสถานะเมื่อเวลาผ่านไปที่พวกเขาดำเนินการคุณจะต้องจัดการที่ให้วิธีการตั้งค่าสถานะนั้น Coroutines บรรเทาปัญหานี้ แต่คุณยังคงจัดการการอ้างอิง IEnumerator ในทุกเฟรมการอัพเดทของสคริปต์นั้นและต้องทำให้แน่ใจว่าคุณมีวิธีที่แน่นอนในการรีเซ็ตความสามารถเพื่อมิให้เกิดความไม่สมบูรณ์ ความสามารถในการเริ่มต้นอย่างเงียบ ๆ ทำให้เสียความมั่นคงของเกมของคุณเมื่อพวกเขาไปไม่มีใครสังเกตเห็น "แน่นอนฉันจะทำอย่างนั้น!" คุณพูดว่า "ฉันเป็น 'โปรแกรมเมอร์ที่ดี'!" แต่คุณรู้ไหมว่าพวกเราทุกคนเป็นนักเขียนโปรแกรมที่แย่มากและแม้แต่นักวิจัย AI และนักเขียนคอมไพเลอร์ที่ยิ่งใหญ่ที่สุดก็ทำอะไรผิดพลาดอยู่ตลอดเวลา

ทุกวิธีที่คุณสามารถใช้ instantiation คำสั่งและการดึงในความสามัคคีผมจะคิดว่าสองหนึ่งคือดีและจะไม่ให้หลอดเลือดโป่งพองและอื่น ๆ ที่จะช่วยให้การมากมาย MAGICAL ความคิดสร้างสรรค์ เรียงจาก

แนวทางการใช้รหัสเป็นศูนย์กลาง

ครั้งแรกเป็นวิธีการส่วนใหญ่ในรหัส สิ่งที่ฉันแนะนำคือคุณทำให้แต่ละคำสั่งเป็นคลาสแบบง่าย ๆ ซึ่งสืบทอดมาจาก BaseCommand abtract class หรือใช้ ICommand อินเตอร์เฟส (ฉันสมมติว่าเพื่อความกะทัดรัดที่คำสั่งเหล่านี้จะเป็นความสามารถของตัวละครเท่านั้นไม่ยากที่จะรวม การใช้งานอื่น ๆ ) ระบบนี้อนุมานว่าคำสั่งแต่ละคำสั่งเป็น ICommand มีตัวสร้างสาธารณะที่ไม่มีพารามิเตอร์และต้องอัปเดตแต่ละเฟรมในขณะที่ใช้งานอยู่

สิ่งต่าง ๆ นั้นง่ายกว่าถ้าคุณใช้คลาสพื้นฐานที่เป็นนามธรรม แต่เวอร์ชันของฉันใช้อินเทอร์เฟซ

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

// ICommand.cs
public interface ICommand
{
    public void Execute(AbilityActivator originator, TargetingInfo targets);
    public void Update();
    public bool IsActive { get; }
}


// CommandList.cs
// Attach this to a game object in your loading screen
public static class CommandList
{
    public static ICommand GetInstance(string key)
    {
        return commandDict[key].GetRef();
    }


    static CommandListInitializerScript()
    {
        commandDict = new Dictionary<string, ICommand>() {

            { "SwordSpin", new CommandRef<SwordSpin>() },

            { "BellyRub", new CommandRef<BellyRub>() },

            { "StickyShield", new CommandRef<StickyShield>() },

            // Add more commands here
        };
    }


    private class CommandRef<T> where T : ICommand, new()
    {
        public ICommand GetNew()
        {
            return new T();
        }
    }

    private static Dictionary<string, ICommand> commandDict;
}


// AbilityActivator.cs
// Attach this to your character objects
public class AbilityActivator : MonoBehaviour
{
    List<ICommand> activeAbilities = new List<ICommand>();

    void Update()
    {
        string activatedAbility = GetActivatedAbilityThisFrame();
        if (!string.IsNullOrEmpty(acitvatedAbility))
            ICommand command = CommandList.Get(activatedAbility).GetRef();
            command.Execute(this, this.GetTargets());
            activeAbilities.Add(command);
        }

        foreach (var ability in activeAbilities) {
            ability.Update();
        }

        activeAbilities.RemoveAll(a => !a.IsActive);
    }
}

สิ่งนี้ใช้ได้ดีทั้งหมด แต่คุณสามารถทำได้ดีกว่า (เช่นกันList<T>ไม่ใช่โครงสร้างข้อมูลที่ดีที่สุดสำหรับการจัดเก็บความสามารถตามเวลาที่กำหนดคุณอาจต้องการLinkedList<T>หรือ a SortedDictionary<float, T>)

วิธีการที่ขับเคลื่อนด้วยข้อมูล

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

สิ่งแรกที่คุณต้องการคือ ScriptableObject ที่สามารถอธิบายความสามารถของคุณ ScriptableObjects ยอดเยี่ยม พวกมันถูกออกแบบมาให้ทำงานเหมือนกับ MonoBehaviours โดยที่คุณสามารถตั้งค่าเขตข้อมูลสาธารณะของพวกเขาในผู้ตรวจสอบของ Unity และการเปลี่ยนแปลงเหล่านั้นจะได้รับการเรียงลำดับเป็นดิสก์ อย่างไรก็ตามพวกเขาไม่ได้ยึดติดกับวัตถุใด ๆ และไม่จำเป็นต้องแนบกับวัตถุเกมในฉากหรืออินสแตนซ์ พวกมันคือที่เก็บข้อมูลทั้งหมดของ Unity พวกเขาสามารถเป็นอันดับประเภทพื้นฐาน, enums และชั้นเรียนง่าย (มรดก) [Serializable]ทำเครื่องหมาย โครงสร้างไม่สามารถต่อเนื่องใน Unity และการทำให้เป็นอันดับเป็นสิ่งที่ช่วยให้คุณแก้ไขฟิลด์วัตถุในตัวตรวจสอบดังนั้นโปรดจำไว้ว่า

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

// CommandAbilityDescription.cs
public class CommandAbilityDecription : ScriptableObject
{

    // Identification and information
    public string displayName; // Name used for display purposes for the GUI
    // We don't need an identifier field, because this will actually be stored
    // as a file on disk and thus implicitly have its own identifier string.

    // Description of damage to targets

    // I put this enum inside the class for answer readability, but it really belongs outside, inside a namespace rather than nested inside a class
    public enum DamageType
    {
        None,
        SingleTarget,
        SingleTargetOverTime,
        Area,
        AreaOverTime,
    }

    public DamageType damageType;
    public float damage; // Can represent either insta-hit damage, or damage rate over time (depend)
    public float duration; // Used for over-time type damages, or as a delay for insta-hit damage

    // Visual FX
    public enum EffectPlacement
    {
        CenteredOnTargets,
        CenteredOnFirstTarget,
        CenteredOnCharacter,
    }

    [Serializable]
    public class AbilityVisualEffect
    {
        public EffectPlacement placement;
        public VisualEffectBehavior visualEffect;
    }

    public AbilityVisualEffect[] visualEffects;
}

// VisualEffectBehavior.cs
public abtract class VisualEffectBehavior : MonoBehaviour
{
    // When an artist makes a visual effect, they generally make a GameObject Prefab.
    // You can extend this base class to support different kinds of visual effects
    // such as particle systems, post-processing screen effects, etc.
    public virtual void PlayEffect(); 
}

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

คุณยังคงต้องการ AbilityActivator MonoBehaviour แต่ตอนนี้เขาทำงานได้อีกเล็กน้อย

// AbilityActivator.cs
public class AbilityActivator : MonoBehaviour
{
    public void ActivateAbility(string abilityName)
    {
        var command = (CommandAbilityDescription) Resources.Load(string.Format("Abilities/{0}", abilityName));
        ProcessCommand(command);
    }

    private void ProcessCommand(CommandAbilityDescription command)
    {

        foreach (var fx in command.visualEffects) {
            fx.PlayEffect();
        }

        switch(command.damageType) {
            // yatta yatta yatta
        }

        // and so forth, whatever your needs require

        // You could even make a copy of the CommandAbilityDescription
        var myCopy = Object.Instantiate(command);

        // So you can keep track of state changes (ie: damage duration)
    }
}

ส่วนสุดยอด

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

เมื่อเดือนที่แล้วศิลปินของเราใช้ powerup ScriptableObject ซึ่งควรจะกำหนดพฤติกรรมสำหรับขีปนาวุธประเภทต่าง ๆ รวมกับ AnimationCurve และพฤติกรรมที่ทำให้ขีปนาวุธลอยไปตามพื้น อาวุธแห่งความตาย

ฉันยังต้องย้อนกลับไปและเพิ่มการสนับสนุนเฉพาะสำหรับพฤติกรรมนี้เพื่อให้แน่ใจว่าทำงานได้อย่างมีประสิทธิภาพ แต่เนื่องจากเราได้สร้างอินเทอร์เฟซคำอธิบายข้อมูลทั่วไปเขาจึงสามารถดึงความคิดนี้ออกมาจากอากาศและวางลงในเกมโดยที่เราไม่ได้เขียนโปรแกรมแม้กระทั่งรู้ว่าเขาพยายามทำมันจนกว่าเขาจะมาและพูดว่า "เฮ้พวก ในสิ่งที่ยอดเยี่ยมนี้! " และเพราะมันยอดเยี่ยมมากฉันจึงตื่นเต้นที่จะได้รับการสนับสนุนที่แข็งแกร่งยิ่งขึ้น


3

TL: DR - ถ้าคุณกำลังคิดที่จะบรรจุความสามารถหลายร้อยหรือหลายพันรายการลงในรายการ / อาร์เรย์ที่คุณทำซ้ำแล้วซ้ำอีกทุกครั้งที่มีการกระทำที่เรียกว่าเพื่อดูว่ามีการกระทำอยู่หรือไม่และมีตัวละครที่สามารถ ดำเนินการแล้วอ่านด้านล่าง

ถ้าไม่เช่นนั้นไม่ต้องกังวล
หากคุณกำลังพูดถึง 6 ตัวอักษร / ประเภทตัวละครและความสามารถ 30 อาจจะไม่สำคัญว่าคุณจะทำอะไรเพราะค่าใช้จ่ายในการจัดการความซับซ้อนอาจต้องใช้รหัสมากขึ้นและการประมวลผลมากกว่าการทิ้งทุกอย่างในกองและ การเรียงลำดับ ...

ซึ่งเป็นเหตุผลที่ @eBusiness ชี้ให้เห็นว่าคุณไม่น่าจะเห็นปัญหาด้านประสิทธิภาพในระหว่างการส่งเหตุการณ์เพราะถ้าคุณพยายามอย่างหนักที่จะทำเช่นนั้นไม่มีงานที่หนักหน่วงมากมายเมื่อเทียบกับการเปลี่ยนตำแหน่งที่ 3 ล้านจุดบนหน้าจอ ฯลฯ ...

นอกจากนี้นี่ไม่ใช่วิธีแก้ปัญหาแต่เป็นวิธีแก้ปัญหาสำหรับการจัดการปัญหาที่คล้ายกันขนาดใหญ่กว่า ...

แต่...

ทุกอย่างลงมาว่าคุณสร้างเกมมากแค่ไหนมีตัวละครกี่ตัวที่ใช้ทักษะเดียวกันมีตัวละคร / ทักษะที่แตกต่างกันกี่ตัวใช่ไหม?

การมีทักษะเป็นองค์ประกอบของตัวละคร แต่ให้พวกเขาลงทะเบียน / ถอนการลงทะเบียนจากส่วนต่อประสานคำสั่งเมื่อตัวละครเข้าร่วมหรือออกจากการควบคุมของคุณ (หรือทำให้ล้มลง / ฯลฯ ) การ์ดคำสั่ง

ฉันมีประสบการณ์น้อยมากในการเขียนสคริปต์ของ Unity แต่ฉันรู้สึกสบายใจกับ JavaScript เป็นภาษา
หากพวกเขาอนุญาตทำไมไม่มีรายการนั้นเป็นวัตถุง่าย ๆ :

// Command interface wraps this
var registered_abilities = {},

    register = function (name, callback) {
        registered_abilities[name] = callback;
    },
    unregister = function (name) {
        registered_abilities[name] = null;
    },

    call = function (name,/*arr/undef*/params) {
        var callback = registered_abilities[name];
        if (callback) { callback(params); }
    },

    public_interface = {
        register : register,
        unregister : unregister,
        call : call
    };

return public_interface;

และมันอาจจะถูกใช้เช่น:

var command_card = new CommandInterface();

// one-time setup
system.listen("register-ability",   command_card.register  );
system.listen("unregister-ability", command_card.unregister);
system.listen("use-action",         command_card.call      );

// init characters
var dave = new PlayerCharacter("Dave"); // Character Factory pulls out Dave + dependencies
dave.init();

ฟังก์ชัน Dave (). init อาจมีลักษณะที่:

// Inside of Dave class
init = function () {
    // other instance-level stuff ...

    system.notify("register-ability", "repair",  this.Repair );
    system.notify("register-ability", "science", this.Science);
},

die = function () {
    // other clean-up stuff ...

    system.notify("unregister-ability", "repair" );
    system.notify("unregister-ability", "science");
},

resurrect = function () { /* same idea as init */ };

หากผู้คนมากกว่าเดฟมี.Repair()แต่คุณสามารถรับประกันได้ว่าจะมีเดฟเพียงคนเดียวจากนั้นเปลี่ยนเป็นsystem.notify("register-ability", "dave:repair", this.Repair);

และเรียกทักษะโดยใช้ system.notify("use-action", "dave:repair");

ฉันไม่แน่ใจว่ารายการที่คุณใช้นั้นเป็นอย่างไร (ในแง่ของระบบ UnityScript และในแง่ของสิ่งที่เกิดขึ้นหลังการคอมไพล์)

ฉันอาจพูดได้ว่าถ้าคุณมีทักษะหลายร้อยรายการที่คุณวางแผนที่จะบรรจุลงในรายการ (แทนที่จะลงทะเบียนและไม่ลงทะเบียนโดยขึ้นอยู่กับตัวละครที่คุณมีอยู่ในขณะนี้) ที่วนซ้ำแถว JS ทั้งหมด (อีกครั้ง ถ้านั่นคือสิ่งที่พวกเขากำลังทำ) เพื่อตรวจสอบคุณสมบัติของคลาส / วัตถุซึ่งตรงกับชื่อของการกระทำที่คุณต้องการที่จะดำเนินการจะเป็นนักแสดงน้อยกว่านี้

หากมีโครงสร้างที่ปรับให้เหมาะสมมากขึ้นก็จะมีประสิทธิภาพมากกว่านี้

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

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.