Формування навичок та вмінь персонажа як команди, належної практики?


11

Я розробляю гру, яка складається з персонажів, які володіють унікальними навичками наступальних дій та іншими здібностями, такими як будівництво, ремонт тощо. Гравці можуть контролювати кілька таких персонажів.

Я думаю розмістити всі такі вміння та навички в окремих командах. Статичний контролер реєструє всі ці команди в статичний список команд. Статичний список складатиметься з усіх наявних умінь та навичок усіх персонажів гри. Отже, коли гравець вибирає одного з символів і натискає кнопку в інтерфейсі, щоб подати заклинання або виконати вміння, View закликає статичний контролер, щоб отримати потрібну команду зі списку і виконати її.

У чому я не впевнений, якщо це хороший дизайн, враховуючи, що я будую свою гру в Unity. Я думаю, що я міг би зробити всі вміння та навички як окремі компоненти, які потім були б прикріплені до GameObjects, що представляють персонажів у грі. Тоді інтерфейс повинен утримувати GameObject символу та виконувати команду.

Що було б кращим дизайном та практикою для гри, яку я розробляю?


Здається, добре! Просто викидаючи цей пов'язаний факт там: У деяких мовах ви можете навіть піти так далеко, щоб зробити кожну команду функцією для себе. Це має певні переваги для тестування, оскільки ви можете легко автоматизувати введення даних. Крім того, керування повторним зв'язком можна легко здійснити шляхом переназначення змінної функції зворотного виклику на іншу командну функцію.
Анко

@Anko, а як з тією частиною, де я маю всі команди, поміщені в статичний список? Я переживаю, що список може бути величезним, і кожен раз, коли потрібна команда, він повинен запитувати величезний список команд.
ксенон

1
@xenon У цій частині коду навряд чи ви побачите проблеми з продуктивністю. Наскільки щось може статися лише один раз за кожну взаємодію з користувачем, це повинно бути дуже інтенсивним для обчислень, щоб зробити помітну вм'ятину у виконанні.
aaaaaaaaaaaa

Відповіді:


17

TL; DR

Ця відповідь трохи божевільна. Але це тому, що я бачу, що ви говорите про реалізацію своїх здібностей як "Команди", що передбачає шаблони дизайну C ++ / Java / .NET, що передбачає важкий підхід. Цей підхід справедливий, але є кращий спосіб. Можливо, ви вже робите інший шлях. Якщо так, то добре. Сподіваємось, інші вважають його корисним, якщо це так.

Подивіться на підхід, керований даними, нижче, щоб скоротити погоню. Отримайте користувальницьку програму CustomAssetUtility Jacob Pennock тут і прочитайте його публікацію про це .

Робота з єдністю

Як і інші згадували, проїзд списку 100-300 предметів - не така вже й велика справа, як ви могли подумати. Тож якщо це інтуїтивний підхід для вас, тоді просто зробіть це. Оптимізуйте ефективність роботи мозку. Але Словник, як @Norguard продемонстрував у своїй відповіді , є простим способом усунення цієї проблеми , не потребуючим мозкових сил, оскільки ви отримуєте вставлення та пошук постійного часу. Вам, мабуть, варто скористатися.

Що стосується того, щоб ця робота була добре в Unity, моя кишка говорить мені, що один MonoBehaviour на здатність - це небезпечний шлях, який слід знижувати. Якщо хтось із ваших здібностей підтримує стан протягом часу, коли вони виконуються, вам потрібно буде керувати цим способом, щоб забезпечити спосіб скидання цього стану. Розслідування усувають цю проблему, але ви все ще керуєте посиланням на IEnumerator у кожному оновлювальному кадрі цього сценарію, і ви повинні абсолютно впевнитись, що у вас є безпечний спосіб скидання здібностей, щоб уникнути неповного та зациклічного стану контуру здібності тихо починають викручувати стабільність вашої гри, коли вони проходять непомітно. "Звичайно, я це зроблю!" ви кажете: "Я" хороший програміст "!". Але насправді, ви знаєте, всі ми об’єктивно жахливі програмісти і навіть найвидатніші дослідники AI та автори-компілятори весь час накручують справи.

З усіх способів, яким ви могли б реалізувати інстанціювання та пошук команд в Unity, я можу подумати про два: один прекрасний і не дасть вам аневризми, а другий дозволяє отримати необмежену магічну творчість . Типу.

Кодоцентричний підхід

По-перше, це підхід в основному в коді. Що я рекомендую, щоб ви зробили кожну команду простим класом, який або успадковує клас абстрагування BaseCommand, або реалізує інтерфейс 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>це не оптимальна структура даних для зберігання приурочених здібностей, можливо, ви хочете отримати a LinkedList<T>або a SortedDictionary<float, T>).

Підхід, керований даними

Можливо, ви можете зменшити ефекти своєї здатності до логічної поведінки, яку можна параметризувати. Саме для цього було створено Єдність. Ви, як програміст, проектуєте систему, яка потім або ви, або дизайнер зможете запускати та маніпулювати в редакторі для отримання найрізноманітніших ефектів. Це значно спростить "фальсифікацію" коду і зосередиться виключно на виконанні вміння. Тут не потрібно жонглювати базовими класами чи інтерфейсами та загальними характеристиками. Все це буде виключно керованим даними (що також спрощує ініціалізацію екземплярів команди).

Перше, що вам потрібно - це ScriptableObject, який може описати ваші здібності. ScriptableObjects є приголомшливими. Вони розроблені так, як MonoBehaviours, оскільки ви можете встановити їхні публічні поля в інспектора Unity, і ці зміни будуть серіалізовані на диск. Однак вони не прикріплені до будь-якого об’єкта і не повинні бути приєднані до ігрового об’єкта в сцені або інстанційним. Вони є сукупністю даних Unity. Вони можуть серіалізувати основні типи, перерахунки та прості класи (без успадкування) [Serializable]. Структури не можна серіалізувати в Unity, а серіалізація - це те, що дозволяє редагувати об’єкти поля в інспекторі, тому пам’ятайте про це.

Ось ScriptableObject, який намагається зробити багато. Ви можете розділити це на більш серіалізовані класи та ScriptableObjects, але це, як передбачається, просто дасть вам уявлення про те, як це робити. Зазвичай це виглядає некрасиво в чудовій сучасній об'єктно-орієнтованій мові на зразок C #, оскільки це справді відчуває, що якесь лайно C89 з усіма цими перерахунками, але справжня сила тут полягає в тому, що тепер ви можете створювати всілякі різні здібності, не писаючи ніколи нового коду для підтримки їх. І якщо ваш перший формат не робить те, що вам потрібно для цього, просто продовжуйте додавати його до тих пір, поки він не стане. Поки ви не зміните назви полів, всі ваші старі серіалізовані файли активів все ще працюватимуть.

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

Ви можете додатково абстрагувати розділ «Пошкодження» класом «Серіалізація», щоб ви могли визначити здібності, які завдають шкоди, або заживають, і мають декілька типів пошкоджень в одній здатності. Єдине правило - це не успадкування, якщо ви не використовуєте декілька об'єктів, що проглядаються, та не посилаєтесь на різні складні файли конфігурації пошкоджень на диску.

Вам все ще потрібен 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 отримає вас там, де ви хочете бути. Unity чудовий тим, що він забезпечує дуже послідовне та логічне середовище для програмістів, але також має всі приємності для введення даних для дизайнерів та художників, які ви отримуєте від GameMaker, UDK та ін. ін.

Минулого місяця наш художник взяв потужний тип ScriptableObject, який повинен був визначати поведінку для різних видів навісних ракет, поєднав його з AnimationCurve та поведінкою, яка змусила ракети ширяти по землі, і зробила цей божевільний новий спінінг-хокей-шайбу- зброї смерті.

Мені все одно потрібно повернутися назад і додати конкретну підтримку такої поведінки, щоб переконатися, що вона працює ефективно. Але оскільки ми створили цей загальний інтерфейс опису даних, він зміг витягнути цю ідею з повітря і втілити її в гру без нас, програмістів, навіть не знаючи, що намагається це зробити, поки він не підійшов і сказав: "Ей, хлопці, подивіться на цій крутій річ! " І тому, що це було явно приголомшливо, я дуже радий досягти більш надійної підтримки для цього.


3

TL: DR - якщо ви замислюєтесь про те, щоб вписати сотні чи тисячі здібностей у список / масив, який ви потім повторюватимете, кожен раз, коли буде викликана дія, щоб побачити, чи існує дія, і чи є персонаж, який може виконайте його, а потім прочитайте нижче.

Якщо ні, то не турбуйтеся про це.
Якщо ви говорите про 6 символів / типів персонажів і, можливо, 30 здібностей, то насправді не буде важливо, чим ви займаєтесь, тому що накладні витрати на управління складностями можуть насправді вимагати більше коду та більше обробки, ніж просто скидання всього в купу і сортування ...

Саме тому @eBusiness говорить про те, що ви навряд чи зможете побачити проблеми ефективності під час відправки подій, тому що якщо ви не намагаєтеся зробити це, тут не так вже й великої роботи, порівняно з перетворенням позиції 3- мільйон вершин на екрані тощо ...

Крім того, це не рішення , а швидше рішення для управління більшими наборами подібних питань ...

Але ...

Все зводиться до того, наскільки великою ви створюєте гру, скільки персонажів ділиться однаковими навичками, скільки різних персонажів / різних навичок є, правда?

Наявність навичок бути компонентами символу, але їх реєстрація / скасування реєстрації з командного інтерфейсу, коли персонажі приєднуються або залишають свій контроль (або вибивають / тощо), все ще має сенс, в дуже старий спосіб StarCraft, за допомогою гарячих клавіш і командна картка.

Я мав дуже-дуже мало досвіду роботи з сценаріями 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();

Де функція 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 (знову ж, якщо це те, що вони роблять) для перевірки властивості класу / об'єкта, що відповідає імені дії, яке ви хочете виконати, буде менш ефективним, ніж це.

Якщо існують оптимізованіші структури, то вони будуть більш ефективними, ніж це.

Але в будь-якому випадку тепер у вас є персонажі, які контролюють свої власні дії (зробіть це на крок далі та зробіть їх компонентами / сутностями, якщо хочете), і у вас є система управління, яка вимагає мінімум ітерації (як ви просто робити табличні пошуки за назвою).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.