Umiejętności i umiejętności postaci jako polecenia, dobra praktyka?


11

Projektuję dla gry, która składa się z postaci, które mają unikalne umiejętności ofensywne i inne zdolności, takie jak budowanie, naprawa itp. Gracze mogą kontrolować wiele takich postaci.

Zastanawiam się nad umieszczeniem wszystkich takich umiejętności i zdolności w poszczególnych poleceniach. Kontroler statyczny zarejestruje wszystkie te polecenia na statycznej liście poleceń. Statyczna lista składałaby się ze wszystkich dostępnych umiejętności i zdolności wszystkich postaci w grze. Kiedy więc gracz wybierze jedną z postaci i kliknie przycisk w interfejsie użytkownika, aby rzucić zaklęcie lub wykonać zdolność, Widok wywoła kontroler statyczny, aby pobrać żądane polecenie z listy i wykonać je.

Nie jestem jednak pewien, czy jest to dobry projekt, biorąc pod uwagę, że buduję swoją grę w Unity. Myślę, że mógłbym wykorzystać wszystkie umiejętności i zdolności jako pojedyncze elementy, które następnie zostałyby dołączone do GameObjects reprezentujących postacie w grze. Następnie interfejs użytkownika musiałby zatrzymać obiekt GameObject postaci, a następnie wykonać polecenie.

Jaki byłby lepszy projekt i ćwiczenie dla gry, którą projektuję?


Brzmi dobrze! Po prostu ujawniam ten pokrewny fakt: w niektórych językach możesz nawet posunąć się do uczynienia każdego polecenia funkcją dla siebie. Ma to niesamowite zalety w testowaniu, ponieważ można łatwo zautomatyzować wprowadzanie danych. Ponowne przypisanie sterowania można również łatwo wykonać, przypisując zmienną funkcji zwrotnej do innej funkcji polecenia.
Anko,

@ Anko, a co z częścią, w której mam wszystkie polecenia umieszczone na liście statycznej? Martwię się, że lista może stać się ogromna i za każdym razem, gdy potrzebne jest polecenie, musi wykonać zapytanie do ogromnej listy poleceń.
ksenon

1
@xenon W tej części kodu bardzo mało prawdopodobne jest wystąpienie problemów z wydajnością. O ile coś może się zdarzyć tylko raz na interakcję użytkownika, musiałoby to być bardzo intensywne obliczeniowo, aby zauważalnie obniżyć wydajność.
aaaaaaaaaaaa

Odpowiedzi:


17

TL; DR

Ta odpowiedź jest trochę szalona. Ale to dlatego, że widzę, że mówisz o implementowaniu swoich umiejętności jako „Poleceń”, co implikuje wzorce projektowe C ++ / Java / .NET, co implikuje duże obciążenie kodu. To podejście jest poprawne, ale istnieje lepszy sposób. Może już robisz w drugą stronę. Jeśli tak, to dobrze. Mam nadzieję, że inni uznają to za przydatne, jeśli tak jest.

Spójrz na podejście oparte na danych poniżej, aby przejść do sedna. Uzyskaj CustomAssetUility Jacoba Pennock jest tutaj i przeczytać jego wpis o niej .

Praca z jednością

Jak inni wspominali, przeglądanie listy 100-300 przedmiotów nie jest tak wielką sprawą, jak mogłoby się wydawać. Więc jeśli jest to intuicyjne podejście, po prostu zrób to. Zoptymalizuj wydajność mózgu. Ale Słownik, jak pokazał @Norguard w swojej odpowiedzi , jest łatwym , niewymagającym siły mózgowej sposobem na wyeliminowanie tego problemu, ponieważ dostajesz ciągłe wstawianie i pobieranie. Prawdopodobnie powinieneś go użyć.

Jeśli chodzi o sprawienie, by działało dobrze w Unity, moje wnętrzności mówią mi, że jeden MonoBehaviour na umiejętność jest niebezpieczną ścieżką zejścia. Jeśli którakolwiek z twoich umiejętności utrzymuje stan w czasie, w którym się wykonuje, musisz zarządzać, aby zapewnić sposób na zresetowanie tego stanu. Korytarze łagodzą ten problem, ale nadal zarządzasz referencją IEnumerator w każdej ramce aktualizacji tego skryptu i musisz absolutnie upewnić się, że masz pewny sposób na zresetowanie umiejętności, aby nie była niekompletna i utknęła w pętli stanu umiejętności cicho zaczynają popsuć stabilność gry, gdy pozostają niezauważone. „Oczywiście, że to zrobię!” mówicie: „Jestem„ dobrym programistą ”!”. Ale tak naprawdę, wszyscy jesteśmy obiektywnie okropnymi programistami, a nawet najwięksi badacze sztucznej inteligencji i autorzy kompilatorów cały czas coś psują.

Spośród wszystkich sposobów, w jakie możesz wdrożyć tworzenie instancji i wyszukiwanie poleceń w Unity, mogę myśleć o dwóch: jednym jest w porządku i nie da ci tętniaka, a drugim pozwala na NIEOGRANICZONĄ MAGICZNĄ TWÓRCZOŚĆ . Raczej.

Podejście zorientowane na kod

Pierwszy to podejście oparte głównie na kodzie. Zalecam, aby uczynić każdą komendę prostą klasą, która albo dziedziczy z absencji klasy BaseCommand, albo implementuje interfejs ICommand (dla zwięzłości zakładam, że te komendy będą tylko zdolnościami postaci, nie jest trudno włączyć Inne zastosowania). Ten system zakłada, że ​​każde polecenie jest komendą IC, ma konstruktor publiczny, który nie przyjmuje parametrów i wymaga aktualizacji każdej ramki, gdy jest ona aktywna.

Sprawa jest prostsza, jeśli używasz abstrakcyjnej klasy bazowej, ale moja wersja używa interfejsów.

Ważne jest, aby Twoje MonoBehaviours obejmowały jedno konkretne zachowanie lub system zachowań ściśle powiązanych. W porządku jest mieć wiele MonoBehaviours, które skutecznie po prostu pośredniczą w zwykłych klasach C #, ale jeśli sam też to zrobisz, może aktualizować wywołania do różnego rodzaju obiektów do tego stopnia, że ​​zaczyna wyglądać jak gra XNA, wtedy „ masz poważne kłopoty i musisz zmienić architekturę.

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

Działa to całkowicie dobrze, ale możesz to zrobić lepiej (również List<T>nie jest to optymalna struktura danych do przechowywania zdolności czasowych, możesz chcieć a LinkedList<T>lub a SortedDictionary<float, T>).

Podejście oparte na danych

Prawdopodobnie możliwe jest zredukowanie efektów umiejętności do logicznych zachowań, które można sparametryzować. Po to naprawdę zbudowano Unity. Jako programista projektujesz system, w którym Ty lub projektant możecie manipulować w edytorze, aby uzyskać różnorodne efekty. To znacznie uprości „fałszowanie” kodu i skupi się wyłącznie na wykonaniu umiejętności. Nie ma potrzeby żonglowania klasami podstawowymi ani interfejsami i rodzajami tutaj. Wszystko będzie oparte wyłącznie na danych (co również uprości inicjowanie instancji poleceń).

Pierwszą rzeczą, której potrzebujesz, jest obiekt skryptowy, który może opisywać twoje umiejętności. Obiekty ScriptableObjects są niesamowite. Są zaprojektowane tak, aby działały jak MonoBehaviours, ponieważ możesz ustawić ich publiczne pola w inspektorze Unity, a zmiany te zostaną serializowane na dysk. Nie są one jednak przyłączone do żadnego obiektu i nie muszą być dołączone do obiektu gry w scenie ani utworzone w instancji. Są to zbiorcze pakiety danych Unity. Mogą serializować podstawowe typy, wyliczenia i proste klasy (bez dziedziczenia) oznaczone [Serializable]. Struktury nie mogą być serializowane w Unity, a serializacja umożliwia edycję pól obiektów w inspektorze, więc pamiętaj o tym.

Oto ScriptableObject, który próbuje wiele zrobić. Możesz podzielić to na bardziej szeregowe klasy i obiekty ScriptableObjects, ale ma to po prostu dać ci wyobrażenie, jak to zrobić. Zwykle wygląda to brzydko w ładnym nowoczesnym języku obiektowym, takim jak C #, ponieważ naprawdę wydaje się, że to trochę gówna C89 z tymi wszystkimi wyliczeniami, ale prawdziwa moc polega na tym, że teraz możesz tworzyć różnego rodzaju umiejętności bez pisania nowego kodu do obsługi im. A jeśli twój pierwszy format nie robi tego, czego potrzebujesz, po prostu dodawaj go, aż to zrobi. Dopóki nie zmienisz nazw pól, wszystkie stare serializowane pliki zasobów będą nadal działać.

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

Możesz dodatkowo wyodrębnić sekcję Obrażenia do klasy możliwej do serializacji, abyś mógł zdefiniować umiejętności zadające obrażenia lub leczące się i posiadające wiele rodzajów obrażeń w jednej umiejętności. Jedyną regułą jest brak dziedziczenia, chyba że użyjesz wielu obiektów skryptowalnych i odniesiesz się do różnych plików konfiguracji szkód złożonych na dysku.

Nadal potrzebujesz MonoBehaviour Aktywatora Umiejętności, ale teraz on wykonuje trochę więcej pracy.

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

NAJBARDZIEJ CZĘŚĆ

Interfejs i ogólne oszustwo w pierwszym podejściu będą działać dobrze. Ale aby naprawdę wyciągnąć jak najwięcej z Unity, ScriptableObjects zabierze Cię tam, gdzie chcesz być. Jedność jest świetna pod tym względem, że zapewnia bardzo spójne i logiczne środowisko dla programistów, ale ma także wszystkie funkcje wprowadzania danych dla projektantów i artystów, które otrzymujesz od GameMaker, UDK, i in. glin.

W zeszłym miesiącu nasz artysta wziął ulepszony typ ScriptableObject, który miał określać zachowanie dla różnych rodzajów pocisków naprowadzających, połączył go z AnimationCurve i zachowaniem, które spowodowało, że pociski zawisły nad ziemią i sprawiły, że ten nowy szalony krążek hokejowy broń śmierci.

Nadal muszę wrócić i dodać konkretne wsparcie dla tego zachowania, aby upewnić się, że działa ono skutecznie. Ale ponieważ stworzyliśmy ten ogólny interfejs opisu danych, był on w stanie wyciągnąć ten pomysł z powietrza i wprowadzić go do gry bez nas, programistów, nawet wiedzących, że próbował to zrobić, dopóki nie przyszedł i powiedział: „Cześć, patrzcie na tej fajnej rzeczy! ” A ponieważ było wyraźnie niesamowite, cieszę się, że mogę dodać bardziej solidne wsparcie dla tego.


3

TL: DR - jeśli zastanawiasz się nad wpakowaniem setek lub tysięcy umiejętności do listy / tablicy, którą następnie iterowałbyś, za każdym razem, gdy wywoływana jest akcja, aby sprawdzić, czy akcja istnieje i czy istnieje postać, która może wykonaj to, a następnie przeczytaj poniżej.

Jeśli nie, nie przejmuj się tym.
Jeśli mówisz o 6 znakach / typach znaków i może 30 zdolnościach, to tak naprawdę nie będzie to miało znaczenia, co robisz, ponieważ narzut związany z zarządzaniem złożoności może wymagać więcej kodu i więcej przetwarzania niż tylko zrzucenie wszystkiego na stos i sortowanie...

Właśnie dlatego @eBusiness sugeruje, że mało prawdopodobne jest wystąpienie problemów z wydajnością podczas wysyłania zdarzeń, ponieważ chyba że naprawdę się starasz, nie ma tu zbyt wiele przytłaczającej pracy, w porównaniu do przekształcania pozycji 3- milion wierzchołków na ekranie itp.

Nie jest to również rozwiązanie , ale rozwiązanie do zarządzania większymi zestawami podobnych problemów ...

Ale...

Wszystko sprowadza się do tego, jak duża grasz, ile postaci ma te same umiejętności, ile jest różnych postaci / różnych umiejętności, prawda?

Umiejętności są elementami postaci, ale rejestracja / wyrejestrowanie się z interfejsu poleceń, gdy postacie dołączają lub pozostawiają kontrolę (lub zostają znokautowane / itp.), Nadal ma sens, w bardzo podobny sposób do StarCrafta, za pomocą skrótów klawiszowych i karta dowodzenia.

Mam bardzo, bardzo małe doświadczenie ze skryptami Unity, ale bardzo dobrze czuję się w JavaScript jako języku.
Jeśli na to pozwalają, to niech lista będzie prostym obiektem:

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

I może być używany jako:

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

Gdzie może wyglądać funkcja init Dave ().:

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

Jeśli ma więcej osób niż tylko Dave .Repair(), ale możesz zagwarantować, że będzie tylko jeden Dave, zmień go nasystem.notify("register-ability", "dave:repair", this.Repair);

I sprawdź umiejętność za pomocą system.notify("use-action", "dave:repair");

Nie jestem pewien, jak wyglądają używane listy. (Pod względem systemu typów UnityScript ORAZ pod względem tego, co dzieje się po kompilacji).

Prawdopodobnie mogę powiedzieć, że jeśli masz setki umiejętności, które planujesz po prostu wpisywać na listę (zamiast rejestrować i wyrejestrowywać, na podstawie dostępnych postaci), to iterowanie po całej tablicy JS (ponownie, jeśli to właśnie robią), aby sprawdzić właściwość klasy / obiektu, która odpowiada nazwie akcji, którą chcesz wykonać, będzie mniej wydajna niż ta.

Jeśli istnieją bardziej zoptymalizowane struktury, będą one bardziej wydajne niż to.

Ale w obu przypadkach masz teraz Postacie, które kontrolują własne działania (przejdź o krok dalej i uczyń je komponentami / bytami, jeśli chcesz) ORAZ masz system kontroli, który wymaga minimum iteracji (ponieważ jesteś po prostu wyszukiwanie tabel według nazwy).

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.