Làm cho kỹ năng và khả năng của nhân vật như mệnh lệnh, thực hành tốt?


11

Tôi đang thiết kế một trò chơi bao gồm các nhân vật có kỹ năng tấn công độc đáo và các khả năng khác như xây dựng, sửa chữa, v.v. Người chơi có thể điều khiển nhiều nhân vật như vậy.

Tôi đang nghĩ đến việc đưa tất cả các kỹ năng và khả năng đó vào các lệnh riêng lẻ. Một bộ điều khiển tĩnh sẽ đăng ký tất cả các lệnh này vào một danh sách lệnh tĩnh. Danh sách tĩnh sẽ bao gồm tất cả các kỹ năng và khả năng có sẵn của tất cả các nhân vật trong trò chơi. Vì vậy, khi người chơi chọn một trong các ký tự và nhấp vào nút trên giao diện người dùng để sử dụng phép hoặc thực hiện một khả năng, Chế độ xem sẽ gọi bộ điều khiển tĩnh để lấy lệnh mong muốn từ danh sách và thực hiện lệnh đó.

Tuy nhiên, điều tôi không chắc chắn nếu đây là một thiết kế tốt cho rằng tôi đang xây dựng trò chơi của mình trong Unity. Tôi nghĩ rằng tôi có thể tạo ra tất cả các kỹ năng và khả năng như các thành phần riêng lẻ, sau đó sẽ được gắn vào GameObjects đại diện cho các nhân vật trong trò chơi. Sau đó, UI sẽ cần giữ GameObject của nhân vật và sau đó thực hiện lệnh.

Điều gì sẽ là một thiết kế tốt hơn và thực hành cho một trò chơi mà tôi đang thiết kế?


Trông có vẻ tốt! Chỉ cần ném sự thật liên quan này ra khỏi đó: Trong một số ngôn ngữ, bạn thậm chí có thể đi xa đến mức làm cho mỗi lệnh trở thành một chức năng cho chính nó. Điều này có một số lợi thế tuyệt vời để thử nghiệm, vì bạn có thể dễ dàng tự động hóa đầu vào. Ngoài ra, điều khiển đảo ngược có thể được thực hiện dễ dàng bằng cách gán lại một biến chức năng gọi lại cho một chức năng lệnh khác.
Anko

@Anko, còn phần tôi có tất cả các lệnh được đưa vào danh sách tĩnh thì sao? Tôi lo lắng rằng danh sách có thể trở nên rất lớn và mỗi khi cần một lệnh, nó phải truy vấn danh sách các lệnh lớn.
xenon

1
@xenon Bạn rất khó thấy các vấn đề về hiệu năng trong phần này của mã. Theo như một cái gì đó chỉ có thể xảy ra một lần trên mỗi tương tác của người dùng, thì nó sẽ phải được tính toán rất kỹ lưỡng để tạo ra một vết lõm đáng chú ý trong hiệu suất.
aaaaaaaaaaaa

Câu trả lời:


17

TL; DR

Câu trả lời này hơi điên rồ. Nhưng đó là vì tôi thấy bạn đang nói về việc thực hiện các khả năng của mình dưới dạng "Lệnh", ngụ ý các mẫu thiết kế C ++ / Java / .NET, ngụ ý một cách tiếp cận nặng về mã. Sự chấp thuận đó là hợp lệ, nhưng có một cách tốt hơn. Có lẽ bạn đã làm theo cách khác. Nếu vậy, oh tốt. Hy vọng những người khác thấy nó hữu ích nếu đó là trường hợp.

Nhìn vào Cách tiếp cận dựa trên dữ liệu dưới đây để cắt theo đuổi. Nhận CustomAssetUility của Jacob Pennock tại đây và đọc bài đăng của anh ấy về nó .

Làm việc với Unity

Giống như những người khác đã đề cập, đi qua danh sách 100-300 mặt hàng không phải là vấn đề lớn như bạn nghĩ. Vì vậy, nếu đó là một cách tiếp cận trực quan cho bạn, thì hãy làm điều đó. Tối ưu hóa cho hiệu quả não. Nhưng từ điển, như @Norguard đã thể hiện trong câu trả lời của mình , là cách dễ dàng không cần đến trí tuệ để loại bỏ vấn đề đó kể từ khi bạn nhận được chèn và truy xuất liên tục. Bạn có lẽ nên sử dụng nó.

Về mặt làm cho nó hoạt động tốt trong Unity, ruột của tôi nói với tôi rằng một MonoBehaviour cho mỗi khả năng là một con đường nguy hiểm để đi xuống. Nếu bất kỳ khả năng nào của bạn duy trì trạng thái theo thời gian chúng thực thi, bạn sẽ cần quản lý rằng cung cấp một cách để đặt lại trạng thái đó. Coroutines làm giảm bớt vấn đề này, nhưng bạn vẫn đang quản lý một tài liệu tham khảo IEnumerator trên mọi khung cập nhật của tập lệnh đó và phải đảm bảo chắc chắn rằng bạn có một cách chắc chắn để thiết lập lại các khả năng trong trường hợp không hoàn thành và bị mắc kẹt các khả năng lặng lẽ bắt đầu làm hỏng sự ổn định của trò chơi của bạn khi chúng không được chú ý. "Tất nhiên tôi sẽ làm điều đó!" bạn nói, "Tôi là một 'Lập trình viên giỏi'!". Nhưng thực sự, bạn biết đấy, tất cả chúng ta đều là những lập trình viên khách quan khủng khiếp và thậm chí cả những nhà nghiên cứu và nhà biên dịch AI vĩ đại nhất luôn vặn vẹo mọi lúc.

Trong tất cả các cách bạn có thể thực hiện khởi tạo và truy xuất lệnh trong Unity, tôi có thể nghĩ đến hai cách: một là ổn và sẽ không cung cấp cho bạn chứng phình động mạch, và cách khác cho phép TẠO ĐỘNG LỰC TUYỆT VỜI . Sắp xếp

Phương pháp tiếp cận trung tâm mã

Đầu tiên là một cách tiếp cận chủ yếu trong mã. Điều tôi khuyên là bạn nên tạo cho mỗi lệnh một lớp đơn giản, kế thừa từ lớp rút gọn BaseCommand hoặc thực hiện giao diện ICommand (Tôi giả sử vì sự ngắn gọn mà các Lệnh này sẽ chỉ là khả năng của nhân vật, không khó để kết hợp sử dụng khác). Hệ thống này giả định rằng mỗi lệnh là một ICommand, có một hàm tạo công khai không có tham số và yêu cầu cập nhật từng khung trong khi nó đang hoạt động.

Mọi thứ sẽ đơn giản hơn nếu bạn sử dụng một lớp cơ sở trừu tượng, nhưng phiên bản của tôi sử dụng các giao diện.

Điều quan trọng là MonoBehaviours của bạn gói gọn một hành vi cụ thể hoặc một hệ thống các hành vi liên quan chặt chẽ. Có rất nhiều MonoBehaviours chỉ cần ủy quyền cho các lớp C # đơn giản, nhưng nếu bạn thấy mình làm quá có thể cập nhật các cuộc gọi đến tất cả các loại đối tượng khác nhau đến mức nó bắt đầu giống như một trò chơi XNA, thì bạn ' lại gặp rắc rối nghiêm trọng và cần thay đổi kiến ​​trúc của bạn.

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

Điều này hoạt động hoàn toàn tốt, nhưng bạn có thể làm tốt hơn (cũng List<T>không phải là cấu trúc dữ liệu tối ưu để lưu trữ các khả năng theo thời gian, bạn có thể muốn một LinkedList<T>hoặc mộtSortedDictionary<float, T> ).

Phương pháp tiếp cận dựa trên dữ liệu

Có lẽ bạn có thể giảm các hiệu ứng khả năng của mình xuống thành các hành vi logic có thể được tham số hóa. Đây là những gì Unity đã thực sự được xây dựng cho. Bạn, với tư cách là một lập trình viên, thiết kế một hệ thống mà sau đó bạn hoặc nhà thiết kế có thể đi và thao tác trong trình chỉnh sửa để tạo ra nhiều hiệu ứng khác nhau. Điều này sẽ đơn giản hóa rất nhiều việc "gian lận" mã và tập trung hoàn toàn vào việc thực thi một khả năng. Không cần phải sắp xếp các lớp cơ sở hoặc giao diện và khái quát ở đây. Tất cả sẽ hoàn toàn là dữ liệu điều khiển (cũng đơn giản hóa việc khởi tạo các thể hiện lệnh).

Điều đầu tiên bạn cần là một ScriptableObject có thể mô tả khả năng của bạn. ScriptableObjects là tuyệt vời. Chúng được thiết kế để hoạt động như MonoBehaviours ở chỗ bạn có thể đặt các trường công khai của chúng trong trình kiểm tra của Unity và những thay đổi đó sẽ được tuần tự hóa vào đĩa. Tuy nhiên, chúng không được gắn vào bất kỳ đối tượng nào và không phải gắn vào đối tượng trò chơi trong một cảnh hoặc được khởi tạo. Chúng là các thùng dữ liệu bắt tất cả của Unity. Họ có thể tuần tự hóa các loại cơ bản, enum và các lớp đơn giản (không kế thừa) được đánh dấu[Serializable] . Các cấu trúc không thể được tuần tự hóa trong Unity và tuần tự hóa là thứ cho phép bạn chỉnh sửa các trường đối tượng trong thanh tra, vì vậy hãy nhớ điều đó.

Đây là một ScriptableObject cố gắng làm rất nhiều. Bạn có thể chia nó thành các lớp và ScriptableObject được tuần tự hóa hơn, nhưng điều này được cho là chỉ cho bạn ý tưởng về cách thực hiện nó. Thông thường, điều này có vẻ xấu trong một ngôn ngữ hướng đối tượng hiện đại tốt đẹp như C #, vì nó thực sự giống như một số C89 với tất cả các enum đó, nhưng sức mạnh thực sự ở đây là bây giờ bạn có thể tạo ra tất cả các khả năng khác nhau mà không cần phải viết mã mới để hỗ trợ họ Và nếu định dạng đầu tiên của bạn không làm những gì bạn cần, hãy tiếp tục thêm vào cho đến khi nó được thực hiện. Miễn là bạn không thay đổi tên trường, tất cả các tệp tài sản tuần tự cũ của bạn sẽ vẫn hoạt động.

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

Bạn có thể trừu tượng thêm phần Thiệt hại thành một lớp Nối tiếp để bạn có thể xác định các khả năng gây sát thương hoặc hồi máu và có nhiều loại sát thương trong một khả năng. Quy tắc duy nhất là không kế thừa trừ khi bạn sử dụng nhiều đối tượng tập lệnh và tham chiếu các tệp cấu hình thiệt hại phức tạp khác nhau trên đĩa.

Bạn vẫn cần MonoBehaviour của abilityActivator, nhưng bây giờ anh ấy làm việc nhiều hơn một chút.

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

Phần COOLEST

Vì vậy, giao diện và thủ thuật chung trong cách tiếp cận đầu tiên sẽ hoạt động tốt. Nhưng để thực sự tận dụng tối đa Unity, ScriptableObjects sẽ đưa bạn đến nơi bạn muốn. Unity tuyệt vời ở chỗ nó cung cấp một môi trường rất phù hợp và hợp lý cho các lập trình viên, nhưng cũng có tất cả các đặc tính nhập dữ liệu cho các nhà thiết kế và nghệ sĩ bạn nhận được từ GameMaker, UDK, et. al.

Tháng trước, nghệ sĩ của chúng tôi đã sử dụng loại ScriptableObject mạnh mẽ được cho là để xác định hành vi cho các loại tên lửa khác nhau, kết hợp nó với AnimationCurve và một hành vi khiến tên lửa bay lơ lửng trên mặt đất và tạo ra cú xoáy-khúc côn cầu mới điên rồ này vũ khí tử thần.

Tôi vẫn cần quay lại và thêm hỗ trợ cụ thể cho hành vi này để đảm bảo nó hoạt động hiệu quả. Nhưng bởi vì chúng tôi đã tạo ra giao diện mô tả dữ liệu chung chung này, anh ấy đã có thể rút ý tưởng này ra khỏi không khí mỏng và đưa nó vào trò chơi mà không cần lập trình viên biết rằng anh ấy đang cố gắng thực hiện cho đến khi anh ấy đến và nói: "Này các bạn, nhìn này tại điều tuyệt vời này! " Và bởi vì nó rõ ràng tuyệt vời, tôi rất vui khi được hỗ trợ nhiều hơn cho nó.


3

TL: DR - nếu bạn đang suy nghĩ về việc nhồi hàng trăm hoặc hàng nghìn khả năng vào một danh sách / mảng mà sau đó bạn sẽ lặp lại, mỗi khi có một hành động được gọi, để xem liệu hành động đó có tồn tại không và liệu có một nhân vật nào có thể thực hiện nó, sau đó đọc dưới đây.

Nếu không, thì đừng lo lắng về nó.
Nếu bạn đang nói về 6 ký tự / loại ký tự và có thể là 30 khả năng, thì thực sự sẽ không có vấn đề gì với bạn, bởi vì chi phí quản lý phức tạp thực sự có thể yêu cầu nhiều mã hơn và xử lý nhiều hơn là chỉ bỏ mọi thứ vào một đống và sắp xếp ...

Đó chính xác là lý do tại sao @eBusiness gợi ý rằng bạn khó có thể thấy các vấn đề về hiệu suất trong quá trình gửi sự kiện, bởi vì trừ khi bạn rất cố gắng để làm điều đó, không có nhiều công việc áp đảo ở đây, so với việc chuyển đổi vị trí của 3- triệu đỉnh trên màn hình, v.v ...

Ngoài ra, đây không phải là giải pháp , mà là giải pháp để quản lý các vấn đề tương tự lớn hơn ...

Nhưng...

Tất cả đều phụ thuộc vào việc bạn tạo ra trò chơi lớn như thế nào, có bao nhiêu nhân vật chia sẻ cùng một kỹ năng, có bao nhiêu nhân vật / kỹ năng khác nhau, phải không?

Có các kỹ năng là thành phần của nhân vật, nhưng việc họ đăng ký / hủy đăng ký khỏi giao diện lệnh khi các nhân vật tham gia hoặc rời khỏi sự kiểm soát của bạn (hoặc bị loại / v.v.) vẫn có ý nghĩa, theo cách rất StarCraft, với các phím nóng và thẻ lệnh.

Tôi đã có rất, rất ít kinh nghiệm với kịch bản của Unity, nhưng tôi rất thoải mái với JavaScript như một ngôn ngữ.
Nếu họ cho phép, tại sao danh sách đó không phải là một đối tượng đơn giản:

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

Và nó có thể được sử dụng như:

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

Trong đó hàm Dave (). Init có thể trông như sau:

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

Nếu nhiều người hơn Dave chỉ có .Repair(), nhưng bạn có thể đảm bảo rằng sẽ chỉ có một Dave, sau đó thay đổi nó thànhsystem.notify("register-ability", "dave:repair", this.Repair);

Và gọi kỹ năng bằng cách sử dụng system.notify("use-action", "dave:repair");

Tôi không chắc danh sách bạn đang sử dụng là như thế nào. (Xét về hệ thống loại UnityScript, VÀ xét về những gì đang diễn ra sau quá trình biên dịch).

Tôi có thể nói rằng nếu bạn có hàng trăm kỹ năng mà bạn dự định chỉ nhồi vào danh sách (thay vì đăng ký và không đăng ký, dựa trên những ký tự mà bạn hiện có sẵn), thì lặp đi lặp lại qua toàn bộ mảng JS (một lần nữa, nếu đó là những gì họ đang làm) để kiểm tra thuộc tính của một lớp / đối tượng, khớp với tên của hành động bạn muốn thực hiện, sẽ ít hiệu quả hơn so với điều này.

Nếu có các cấu trúc được tối ưu hóa nhiều hơn, thì chúng sẽ có hiệu suất cao hơn thế này.

Nhưng trong cả hai trường hợp, bây giờ bạn có Nhân vật điều khiển hành động của chính họ (tiến thêm một bước và biến chúng thành thành phần / thực thể, nếu bạn muốn), VÀ bạn có một hệ thống điều khiển yêu cầu tối thiểu lặp lại (như bạn chỉ là làm bảng tra cứu theo tên).

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.