Tworzenie solidnego systemu przedmiotów

12

Moim celem jest stworzenie modułowego / jak najbardziej ogólnego systemu przedmiotów, który mógłby obsługiwać takie rzeczy jak:

  • Przedmioty do ulepszenia (+6 Katana)
  • Modyfikatory statystyk (+15 do zręczności)
  • Modyfikatory przedmiotów (% X szansy na obrażenia Y, szansa na zamrożenie)
  • Akumulatory (Magiczny personel z 30 użyciem)
  • Zestaw przedmiotów (wyposaż 4 elementy zestawu X, aby aktywować funkcję Y)
  • Rzadkość (powszechna, niepowtarzalna, legendarna)
  • Disenchantable (włamuje się do niektórych materiałów rzemieślniczych)
  • Do wytworzenia (może być wykonany z określonych materiałów)
  • Materiał eksploatacyjny (5 min% siły ataku X, leczenie +15 KM)

* Byłem w stanie rozwiązać funkcje, które są pogrubione w następującej konfiguracji.

Teraz próbowałem dodać wiele opcji, aby odzwierciedlić to, co mam na myśli. Nie planuję dodawać wszystkich tych niezbędnych funkcji, ale chciałbym móc je wdrażać według własnego uznania. Są one również kompatybilne z systemem ewidencji i serializacji danych.

Mam zamiar nie używać dziedziczenia w ogóle, ale raczej jednostka jednoskładnikowy dane / napędzany podejście. Początkowo myślałem o systemie, który ma:

  • BaseStat: ogólna klasa, która przechowuje statystyki na bieżąco (może być również używana do statystyk przedmiotów i postaci)
  • Element: klasa przechowująca dane, takie jak lista, nazwa, typ elementu i rzeczy związane z interfejsem użytkownika, nazwa akcji, opis itp.
  • IWeapon: interfejs do broni. Każda broń będzie miała swoją klasę z zaimplementowanym IWeapon. Będzie to miało Atak i odniesienie do statystyk postaci. Gdy broń jest wyposażona, jej dane (statystyka klasy przedmiotu) zostaną wstrzyknięte do statystyki postaci (bez względu na to, co posiada BaseStat, zostanie dodana do klasy postaci jako bonus Stat). Na przykład, chcemy wyprodukować miecz (myśląc produkuj klasy przedmiotów za pomocą jsona), więc miecz doda 5 statystyk do statystyk postaci. Mamy więc BaseStat jako („Attack”, 5) (możemy również użyć enum). Ta statystyka zostanie dodana do statystyki „Ataku” postaci jako BonusStat (która byłaby inną klasą) po jej wyposażeniu. Tak więc klasa o nazwie Sword implementująca IWeapon zostanie utworzona, gdy „ klasy przedmiotów . Statystyki postaci do tego miecza, a podczas ataku może odzyskać całkowitą statystykę Ataku ze statystyk postaci i zadawać obrażenia metodą Ataku.
  • BonusStat: sposób dodawania statystyk jako bonusów bez dotykania BaseStat.
  • IConsumable: Taka sama logika jak w przypadku IWeapon. Dodawanie statystyk bezpośrednich jest dość łatwe (+15 KM), ale nie jestem pewien, czy przy tej konfiguracji dodam tymczasowe bronie (% x do ataku przez 5 minut).
  • IUpgradeable: Można to zaimplementować za pomocą tej konfiguracji. Myślę, że UpgradeLevel to podstawowa statystyka, która zwiększa się po ulepszeniu broni. Po ulepszeniu możemy ponownie obliczyć wartość BaseStat broni, aby dopasować ją do poziomu ulepszenia.

Do tego momentu widzę, że system jest dość dobry. Ale w przypadku innych funkcji, myślę, że potrzebujemy czegoś innego, ponieważ na przykład nie mogę zaimplementować w tym funkcji Craftable, ponieważ mój BaseStat nie byłby w stanie obsłużyć tej funkcji i tutaj utknąłem. Mogę dodać wszystkie składniki jako statystyki, ale to nie miałoby sensu.

Aby ułatwić Ci udział w tym, oto kilka pytań, w których możesz pomóc:

  • Czy powinienem kontynuować tę konfigurację, aby wdrożyć inne funkcje? Czy byłoby to możliwe bez dziedziczenia?
  • Czy istnieje jakikolwiek sposób na wdrożenie wszystkich tych funkcji bez dziedziczenia?
  • O modyfikatorach przedmiotów, jak można to osiągnąć? Ponieważ ma bardzo ogólny charakter.
  • Co można zrobić, aby ułatwić proces budowania tego rodzaju architektury, jakieś zalecenia?
  • Czy są jakieś źródła, które mogę wykopać, związane z tym problemem?
  • Naprawdę staram się unikać dziedziczenia, ale czy sądzisz, że można je z łatwością rozwiązać / osiągnąć dzięki dziedziczeniu, zachowując przy tym możliwość jego utrzymania?

Odpowiedz na tylko jedno pytanie, ponieważ pytania były bardzo szerokie, aby uzyskać wiedzę z różnych aspektów / osób.


EDYTOWAĆ


Po odpowiedzi @ jjimenezg93, stworzyłem bardzo prosty system w języku C # do testowania, działa! Sprawdź, czy możesz coś do niego dodać:

public interface IItem
{
    List<IAttribute> Components { get; set; }

    void ReceiveMessage<T>(T message);
}

public interface IAttribute
{
    IItem source { get; set; }
    void ReceiveMessage<T>(T message);
}

Do tej pory IItem i IAttribute są interfejsami podstawowymi. Nie było potrzeby (o której myślę) posiadania podstawowego interfejsu / atrybutu komunikatu, więc utworzymy bezpośrednio testową klasę komunikatu. Teraz dla klas testowych:


public class TestItem : IItem
{
    private List<IAttribute> _components = new List<IAttribute>();
    public List<IAttribute> Components
    {
        get
        {
            return _components;
        }

        set
        {
            _components = value;
        }
    }

    public void ReceiveMessage<T>(T message)
    {
        foreach (IAttribute attribute in Components)
        {
            attribute.ReceiveMessage(message);
        }
    }
}

public class TestAttribute : IAttribute
{
    string _infoRequiredFromMessage;

    public TestAttribute(IItem source)
    {
        _source = source;
    }

    private IItem _source;
    public IItem source
    {
        get
        {
            return _source;
        }

        set
        {
            _source = value;
        }
    }

    public void ReceiveMessage<T>(T message)
    {
        TestMessage convertedMessage = message as TestMessage;
        if (convertedMessage != null)
        {
            convertedMessage.Execute();
            _infoRequiredFromMessage = convertedMessage._particularInformationThatNeedsToBePassed;
            Debug.Log("Message passed : " + _infoRequiredFromMessage);

        }
    }
} 

public class TestMessage
{
    private string _messageString;
    private int _messageInt;
    public string _particularInformationThatNeedsToBePassed;
    public TestMessage(string messageString, int messageInt, string particularInformationThatNeedsToBePassed)
    {
        _messageString = messageString;
        _messageInt = messageInt;
        _particularInformationThatNeedsToBePassed = particularInformationThatNeedsToBePassed;
    }
    //messages should not have methods, so this is here for fun and testing.
    public void Execute()
    {
        Debug.Log("Desired Execution Method: \nThis is test message : " + _messageString + "\nThis is test int : " + _messageInt);
    }
} 

Są to potrzebne ustawienia. Teraz możemy korzystać z systemu (następuje dla Unity).

public class TestManager : MonoBehaviour
{

    // Use this for initialization
    void Start()
    {
        TestItem testItem = new TestItem();
        TestAttribute testAttribute = new TestAttribute(testItem);
        testItem.Components.Add(testAttribute);
        TestMessage testMessage = new TestMessage("my test message", 1, "VERYIMPORTANTINFO");
        testItem.ReceiveMessage(testMessage);
    }

}

Dołącz ten skrypt TestManager do komponentu w scenie, a podczas debugowania zobaczysz, że komunikat został pomyślnie przekazany.


W celu wyjaśnienia rzeczy: Każdy przedmiot w grze będzie implementował interfejs IItem, a każdy Atrybut (nazwa nie powinna cię mylić, oznacza to cechę / system przedmiotów. Podobnie jak Upgrade lub disnchantable) zaimplementuje IAttribute. Następnie mamy metodę przetwarzania wiadomości (dlaczego potrzebujemy wiadomości zostanie wyjaśniona w kolejnym przykładzie). Tak więc w kontekście możesz dołączyć atrybuty do elementu, a reszta zrobi za Ciebie. Co jest bardzo elastyczne, ponieważ możesz łatwo dodawać / usuwać atrybuty. Tak więc pseudo-przykład byłby Odczuwalny. Będziemy mieć klasę o nazwie Disenchantable (IAttribute), która w metodzie Disenchant poprosi o:

  • Lista składników (gdy przedmiot jest odczarowany, jaki przedmiot powinien zostać przekazany graczowi) uwaga: IItem należy rozszerzyć, aby zawierał ItemType, ItemID itp.
  • int resultModifier (jeśli wprowadzisz rodzaj wzmocnienia funkcji odczarowywania, możesz przekazać int, aby zwiększyć ilość składników otrzymywanych po odczarowaniu)
  • int failureChance (jeśli proces odczarowania ma szansę niepowodzenia)

itp.

Informacje te zostaną dostarczone przez klasę o nazwie DisenchantManager, otrzyma przedmiot i utworzy tę wiadomość zgodnie z przedmiotem (składniki przedmiotu po odczarowaniu) i postępem gracza (resultModifier i failureChance). Aby przekazać tę wiadomość, utworzymy klasę DisenchantMessage, która będzie działać jako ciało dla tej wiadomości. Tak więc DisenchantManager zapełni DisenchantMessage i wyśle ​​go do elementu. Produkt otrzyma wiadomość i przekaże ją do wszystkich dołączonych atrybutów. Ponieważ metoda ReceiveMessage klasy Disenchantable będzie szukać DisenchantMessage, tylko atrybut Disenchantable otrzyma ten komunikat i będzie na nim działał. Mam nadzieję, że to wszystko wyczyściło tak samo jak dla mnie :).

Vandarthul
źródło
1
Przydatne inspiracje możesz znaleźć w systemach modyfikatorów używanych w For Honor
DMGregory
@DMGregory Hej! Dzięki za link. Chociaż wydaje się to bardzo pomysłowe, niestety potrzebuję prawdziwej rozmowy, aby zrozumieć tę koncepcję. I staram się znaleźć prawdziwą rozmowę, która jest niestety tylko dla członków GDCVault (495 $ za rok jest szalone!). (Dyskusję można znaleźć tutaj, jeśli masz członkostwo w GDCVault -, -
Vandarthul
Jak dokładnie twoja koncepcja „BaseStat” wyklucza wytwarzanie broni?
Attackfarm
To tak naprawdę nie wyklucza, ale nie pasuje do kontekstu w moim umyśle. Do receptury rzemieślniczej można dodać „Drewno”, 2 i „Żelazo”, 5 jako BaseStat, co dałoby miecz. Myślę, że zmiana nazwy BaseStat na BaseAttribute byłaby lepsza w tym kontekście. Ale system nie będzie w stanie spełnić swojego celu. Pomyśl o materiale eksploatacyjnym o sile ataku 5min -% 50. Jak mógłbym przekazać go jako BaseStat? „Perc_AttackPower”, 50 należy to rozwiązać jako „jeśli jest to Perc, należy traktować liczbę całkowitą jako procent” i pominąć informację o minutach. Mam nadzieję, że rozumiesz.
Vandarthul
@Attackfarm na drugie przemyślenie, tę koncepcję „BaseStat” można rozszerzyć o listę ints zamiast jednej int. więc dla wzmocnienia eksploatacyjnego mogę podać „Atak”, 50, 5, 1, a IConsumable szukałby 3 liczb całkowitych, 1. - wartości, 2. - minut, 3. - czy to procent, czy nie. Ale czuje się niesforny, gdy inne systemy wchodzą i zmuszone są tłumaczyć się tylko w int.
Vandarthul,

Odpowiedzi:

6

Myślę, że możesz osiągnąć to, co chcesz, jeśli chodzi o skalowalność i łatwość konserwacji, używając Systemu Elementów z podstawowym dziedzictwem i systemem przesyłania wiadomości. Oczywiście należy pamiętać, że ten system jest najbardziej modułowy / konfigurowalny / skalowalny, o jakim mogę myśleć, ale prawdopodobnie będzie działał gorzej niż obecne rozwiązanie.

Wyjaśnię dalej:

Przede wszystkim tworzysz interfejs IItemi interfejs IComponent. Każdy element, który chcesz przechowywać, musi dziedziczyć IItem, a każdy element, na który chcesz wpływać, musi dziedziczyć IComponent.

IItembędzie miał szereg komponentów i metodę obsługi IMessage. Ta metoda obsługi po prostu wysyła każdą odebraną wiadomość do wszystkich przechowywanych komponentów. Następnie komponenty, które są zainteresowane danym komunikatem, będą działać odpowiednio, a inne go zignorują.

Jedna wiadomość, na przykład, ma obrażenia typu i informuje zarówno atakującego, jak i atakowanego, więc wiesz, ile trafiłeś, i być może ładujesz pasek furii na podstawie tych obrażeń. Albo AI przeciwnika może zdecydować się na bieg, jeśli trafi cię i zadaje obrażenia mniejsze niż 2 KM. Są to głupie przykłady, ale używając systemu podobnego do tego, o którym wspominam, nie trzeba nic więcej robić niż tworzenie wiadomości i odpowiednich operacji, aby dodać większość tego rodzaju mechaniki.

Mam implementację dla ECS z messaging tutaj , ale ten jest stosowany dla jednostek zamiast przedmioty i używa C ++. W każdym razie, myślę, że to może pomóc, jeśli przyjrzeć się component.h, entity.hi messages.h. Jest wiele rzeczy do poprawienia, ale zadziałało to w tej prostej pracy na uniwersytecie.

Mam nadzieję, że to pomoże.

jjimenezg93
źródło
Hej @ jjimenezg93, dziękuję za odpowiedź. Aby więc rozwinąć prosty przykład tego, co wyjaśniłeś: Chcemy miecza, który: - Disenchantable [Component] - Stat Modifier [Component] - Upgradeable [Component] Mam listę działań, które zawierają wszystkie rzeczy takie jak - DISENCHANT - MODIFY_STAT - AKTUALIZACJA Ilekroć element odbiera tę wiadomość, przechodzi przez wszystkie jej składniki i wysyła tę wiadomość, każdy element będzie wiedział, co zrobić z daną wiadomością. Teoretycznie wydaje się to niesamowite! Nie sprawdziłem twojego przykładu, ale zrobię to, wielkie dzięki!
Vandarthul
@ Vandarthul Tak, to w zasadzie pomysł. W ten sposób przedmiot nie będzie wiedział nic o swoich komponentach, więc nie będzie żadnego sprzężenia, a jednocześnie będzie posiadał wszystkie pożądane funkcje, które mogą być również dzielone między różnymi rodzajami przedmiotów. Mam nadzieję, że pasuje do twoich potrzeb!
jjimenezg93