Alternatywy dla wielokrotnego dziedziczenia mojej architektury (NPC w grze strategicznej w czasie rzeczywistym)?

11

Kodowanie nie jest trudne w rzeczywistości . Trudność polega na pisaniu kodu, który ma sens, jest czytelny i zrozumiały. Chcę więc uzyskać lepszego programistę i stworzyć solidną architekturę.

Więc chcę zrobić stworzyć architekturę NPC w grze wideo. Jest to gra strategiczna w czasie rzeczywistym, taka jak Starcraft, Age of Empires, Command & Conquers, itp. Itd. Więc będę mieć różnego rodzaju NPC. NPC może mieć jeden do wielu możliwości (metody) tych: Build(), Farm()i Attack().

Przykłady:
Pracownik może Build()i Farm()
Wojownik może Attack()
Obywatel może Build(), Farm()a Attack()
rybak może Farm()iAttack()

Mam nadzieję, że do tej pory wszystko jest jasne.

Teraz mam swoje typy NPC i ich umiejętności. Ale przejdźmy do aspektu technicznego / programowego.
Jaka byłaby dobra architektura programowa dla moich różnych postaci niezależnych?

Okej, mógłbym mieć klasę podstawową. Właściwie uważam, że jest to dobry sposób na przestrzeganie zasady OSUSZANIA . Mogę mieć metody jak WalkTo(x,y)w mojej klasie podstawowej, ponieważ każdy NPC będzie mógł się poruszać. Ale teraz przejdźmy do prawdziwego problemu. Gdzie wdrażam swoje umiejętności? (pamiętaj: Build(), Farm()i Attack())

Ponieważ umiejętności będą się składały z tej samej logiki, irytująca / łamająca zasadę SUCHEGO zastosowania będzie wdrażana dla każdego NPC (Robotnika, Wojownika, ...).

Okej, mógłbym wdrożyć umiejętności w klasie podstawowej. Wymagałoby to pewnego rodzaju logiką, który sprawdza, czy NPC może używać zdolności X. IsBuilder, CanBuild.. Myślę, że to jest jasne, co chcę wyrazić.
Ale nie czuję się dobrze z tym pomysłem. To brzmi jak rozdęta klasa podstawowa o zbyt dużej funkcjonalności.

Używam C # jako języka programowania. Zatem wielokrotne dziedziczenie nie jest tutaj opcją. Oznacza: Posiadanie dodatkowych klas podstawowych takich jak Fisherman : Farmer, Attackernie będzie działać.

Brettetete
źródło
3
Patrzyłem na ten przykład, który wydaje się rozwiązaniem tego problemu: en.wikipedia.org/wiki/Composition_over_inheritance
Shawn Mclean
4
To pytanie pasowałoby znacznie lepiej na gamedev.stackexchange.com
Philipp

Odpowiedzi:

14

Skład i dziedziczenie interfejsu są zwykłymi alternatywami dla klasycznego dziedziczenia wielokrotnego.

Wszystko, co opisano powyżej, zaczynające się od słowa „może”, jest funkcją, którą można przedstawić za pomocą interfejsu, jak w ICanBuildlub ICanFarm. Możesz odziedziczyć tyle interfejsów, ile potrzebujesz.

public class Worker: ICanBuild, ICanFarm
{
    #region ICanBuild Implementation
    // Build
    #endregion

    #region ICanFarm Implementation
    // Farm
    #endregion
}

Możesz przekazywać „ogólne” obiekty budowlane i rolnicze przez konstruktora swojej klasy. Tam właśnie wchodzi kompozycja.

Robert Harvey
źródło
Wystarczy myśleć głośno: The „związek” to (wewnętrznie) będzie ma zamiast Is (pracownik ma Builder zamiast Pracownik jest Builder). Przykład / wzór, który wyjaśniłeś, ma sens i brzmi jak niezłą metodę oddzielenia od siebie, aby rozwiązać mój problem. Ale trudno mi zdobyć ten związek.
Brettetete,
3
Jest to ortogonalne w stosunku do rozwiązania Roberta, ale powinieneś faworyzować niezmienność (nawet bardziej niż zwykle), kiedy intensywnie używasz kompozycji, aby uniknąć tworzenia skomplikowanych sieci efektów ubocznych. Idealnie byłoby, gdyby Twoje implementacje kompilacji / farmy / ataku pobierały dane i wypluwały je bez dotykania czegokolwiek innego.
Doval
1
Pracownik nadal jest konstruktorem, ponieważ odziedziczyłeś po nim ICanBuild. To, że masz również narzędzia do budowania, ma drugorzędne znaczenie w hierarchii obiektów.
Robert Harvey
Ma sens. Spróbuję tego księcia. Dziękujemy za przyjazną i opisową odpowiedź. Naprawdę to doceniam. Pozwolę jednak otworzyć to pytanie na jakiś czas :)
Brettetete
4
@Brettetete Może pomóc myśleć o implementacjach Build, Farm, Attack, Hunt itp. Jako umiejętnościach , które posiadają twoje postacie. BuildSkill, FarmSkill, AttackSkill itp. Wtedy skład ma znacznie większy sens.
Eric King,
4

Jak wspomniano w innych plakatach, rozwiązaniem problemu może być architektura komponentów.

class component // Some generic base component structure
{
  void update() {} // With an empty update function
} 

W takim przypadku rozszerz funkcję aktualizacji, aby zaimplementować dowolną funkcjonalność NPC.

class build : component
{
  override void update()
  { 
    // Check if the right object is selected, resources, etc
  }
}

Iteracja kontenera komponentu i aktualizacja każdego komponentu pozwala na zastosowanie określonej funkcjonalności każdego komponentu.

class npc
{
  void update()
  {
    foreach(component c in components)
      c.update();
  }

  list<component> components;
}

Ponieważ każdy typ obiektu jest pożądany, możesz użyć jakiejś formy wzorca fabrycznego, aby skonstruować poszczególne typy, które chcesz.

npc worker;
worker.add_component<build>();
worker.add_component<walk>();
worker.add_component<attack>();

Zawsze miałem problemy, ponieważ komponenty stawały się coraz bardziej złożone. Utrzymanie niskiej złożoności komponentów (jedna odpowiedzialność) może pomóc w rozwiązaniu tego problemu.

Connor Hollis
źródło
3

Jeśli zdefiniujesz interfejsy w taki sposób, ICanBuildto twój system gry musi sprawdzać każdego NPC według typu, co zwykle jest niesmaczne w OOP. Na przykład: if (npc is ICanBuild).

Lepiej jest z:

interface INPC
{
    bool CanBuild { get; }
    void Build(...);
    bool CanFarm { get; }
    void Farm(...);
    ... etc.
}

Następnie:

class Worker : INPC
{
    private readonly IBuildStrategy buildStrategy;
    public Worker(IBuildStrategy buildStrategy)
    {
        this.buildStrategy = buildStrategy;
    }

    public bool CanBuild { get { return true; } }
    public void Build(...)
    {
        this.buildStrategy.Build(...);
    }
}

... lub oczywiście możesz użyć wielu różnych architektur, takich jak język specyficzny dla domeny w postaci płynnego interfejsu do definiowania różnych typów NPC itp., w których budujesz typy NPC poprzez łączenie różnych zachowań:

var workerType = NPC.Builder
    .Builds(new WorkerBuildBehavior())
    .Farms(new WorkerFarmBehavior())
    .Build();

var workerFactory = new NPCFactory(workerType);

var worker = workerFactory.Build();

Konstruktor NPC może zaimplementować coś, DefaultWalkBehaviorco może zostać zastąpione w przypadku NPC, który lata, ale nie może chodzić itp.

Scott Whitlock
źródło
Cóż, po prostu znów myślę głośno: tak naprawdę nie ma dużej różnicy między if(npc is ICanBuild)i if(npc.CanBuild). Nawet gdybym wolał npc.CanBuilddrogę od mojego obecnego nastawienia.
Brettetete
3
@Brettetete - W tym pytaniu możesz zobaczyć, dlaczego niektórzy programiści naprawdę nie lubią sprawdzać typu.
Scott Whitlock,
0

Umieściłbym wszystkie umiejętności w osobnych klasach, które dziedziczą po Umiejętności. Następnie w klasie typu jednostki masz listę umiejętności i innych rzeczy, takich jak atak, obrona, maksymalne zdrowie itp. Posiadasz również klasę jednostek, która ma bieżące zdrowie, lokalizację itp. I ma typ jednostki jako zmienną członka.

Zdefiniuj wszystkie typy jednostek w pliku konfiguracyjnym lub bazie danych, zamiast kodować na stałe.

Następnie projektant poziomów może łatwo dostosować umiejętności i statystyki typów jednostek bez ponownej kompilacji gry, a także ludzie mogą pisać mody do gry.

użytkownik3970295
źródło