Создание навыков и способностей персонажа в качестве команд, хорошая практика?

11

Я разрабатываю игру, состоящую из персонажей, обладающих уникальными наступательными навыками и другими способностями, такими как строительство, ремонт и т. Д. Игроки могут управлять несколькими такими персонажами.

Я думаю о том, чтобы объединить все эти навыки и способности в отдельные команды. Статический контроллер регистрирует все эти команды в списке статических команд. Статический список будет состоять из всех доступных навыков и способностей всех персонажей в игре. Поэтому, когда игрок выбирает одного из персонажей и нажимает кнопку в пользовательском интерфейсе, чтобы наложить заклинание или выполнить способность, представление вызывает статический контроллер, чтобы выбрать нужную команду из списка и выполнить ее.

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

Что будет лучшим дизайном и практикой для игры, которую я разрабатываю?

ксенон
источник
Выглядит неплохо! Просто расскажу об этом связанном факте: в некоторых языках вы можете даже пойти, сделав каждую команду функцией для себя. Это имеет некоторые удивительные преимущества для тестирования, поскольку вы можете легко автоматизировать ввод. Кроме того, привязка управления может быть легко осуществлена ​​путем переназначения переменной функции обратного вызова на другую командную функцию.
Анко
@ Анко, а как насчет части, в которой все команды помещены в статический список? Я беспокоюсь, что список может стать огромным, и каждый раз, когда требуется команда, он должен запросить огромный список команд.
ксенон
1
@xenon Вы вряд ли увидите проблемы с производительностью в этой части кода. Поскольку что-то может произойти только один раз за взаимодействие с пользователем, это должно быть очень требовательным к вычислениям, чтобы заметно повлиять на производительность.
аааааааааааа

Ответы:

17

TL; DR

Этот ответ немного сходит с ума. Но это потому, что я вижу, что вы говорите о реализации своих способностей как «Команд», что подразумевает шаблоны проектирования C ++ / Java / .NET, что подразумевает подход с большим количеством кода. Этот подход действителен, но есть лучший способ. Может быть, вы уже делаете по-другому. Если так, хорошо. Надеюсь, другие найдут это полезным, если это так.

Посмотрите на управляемый данными подход ниже, чтобы перейти к погоне. Получите CustomAssetUility Джейкоба Пеннока здесь и прочитайте его пост об этом .

Работа с Unity

Как уже упоминали другие, обход списка из 100-300 предметов - не такая большая проблема, как вы думаете. Так что, если это интуитивный подход для вас, просто сделайте это. Оптимизировать для эффективности мозга. Но Словарь, как продемонстрировал @Norguard в своем ответе , - это простой, не требующий усилий мозг, способ устранить эту проблему, поскольку вы получаете постоянные вставки и извлечения. Вы должны вероятно использовать это.

Что касается правильной работы в Unity, моя интуиция говорит мне, что одно MonoBehaviour на каждую способность - это опасный путь для снижения. Если какая-либо из ваших способностей поддерживает состояние с течением времени, когда они выполняются, вам нужно управлять этим, чтобы обеспечить способ сбросить это состояние. Сопрограммы снимают эту проблему, но вы по-прежнему управляете ссылкой IEnumerator на каждый кадр обновления этого сценария и должны быть абсолютно уверены, что у вас есть надежный способ сброса способностей, чтобы не завершиться и не застрять в цикле состояния Способности незаметно начинают портить стабильность вашей игры, когда они остаются незамеченными. "Конечно, я сделаю это!" Вы говорите: «Я хороший программист!». Но на самом деле, вы знаете, мы все объективно ужасные программисты и даже величайшие исследователи ИИ и авторы компиляторов все время напортачили.

Из всех способов, которыми вы могли бы реализовать создание и извлечение команд в 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>не оптимальная структура данных для хранения временных возможностей, вы можете захотеть LinkedList<T>илиSortedDictionary<float, T> ).

Управляемый данными подход

Возможно, вы сможете уменьшить эффекты своей способности до логического поведения, которое можно параметризировать. Для этого и был построен Unity. Вы, как программист, проектируете систему, которую затем вы или дизайнер можете использовать и редактировать в редакторе для создания широкого спектра эффектов. Это значительно упростит «подтасовку» кода и сосредоточится исключительно на выполнении способности. Нет необходимости манипулировать базовыми классами или интерфейсами и дженериками здесь. Все это будет зависеть только от данных (что также упрощает инициализацию экземпляров команд).

Первое, что вам нужно, это 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 и других. и др.

В прошлом месяце наш художник взял powerup-тип ScriptableObject, который должен был определять поведение для различных типов самонаводящихся ракет, сочетал его с AnimationCurve и поведением, которое заставляло ракеты парить над землей, и делал эту новую безумную вращающуюся хоккейную шайбу. орудия смерти.

Мне все еще нужно вернуться и добавить определенную поддержку для этого поведения, чтобы убедиться, что оно работает эффективно. Но поскольку мы создали этот общий интерфейс описания данных, он смог вытащить эту идею из воздуха и воплотить ее в игру без нас, программистов, даже не подозревая, что он пытается это сделать, пока он не подошел и не сказал: «Эй, ребята, посмотрите на эту классную вещь! И так как это было просто потрясающе, я рад добавить более надежную поддержку.

michael.bartnett
источник
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();

Где функция 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 (опять же, если это то, что они делают), чтобы проверить свойство класса / объекта, которое соответствует имени действия, которое вы хотите выполнить, будет менее производительным, чем это.

Если есть более оптимизированные структуры, то они будут более производительными, чем эта.

Но в любом случае, теперь у вас есть Персонажи, которые контролируют свои собственные действия (сделайте шаг вперед и сделайте их компонентами / сущностями, если хотите), И у вас есть система управления, которая требует минимума итерации (поскольку вы просто делать поиск таблиц по имени).

Norguard
источник