Alternativen zur Mehrfachvererbung für meine Architektur (NPCs in einem Echtzeit-Strategiespiel)?

11

Codierung ist eigentlich nicht so schwer . Der schwierige Teil besteht darin, Code zu schreiben, der Sinn macht, lesbar und verständlich ist. Ich möchte also einen besseren Entwickler finden und eine solide Architektur erstellen.

Also möchte ich eine Architektur für NPCs in einem Videospiel erstellen . Es ist ein Echtzeit-Strategiespiel wie Starcraft, Age of Empires, Command & Conquers usw. usw. Also werde ich verschiedene Arten von NPCs haben. Ein NPC kann viele Fähigkeiten (Methoden) von ihnen haben ein zu: Build(), Farm()und Attack().

Beispiele:
Arbeiter können Build()und Farm()
Krieger können Attack()
Citizen kann Build(), Farm()und Attack()
Fischer können Farm()undAttack()

Ich hoffe alles ist soweit klar.

Jetzt habe ich meine NPC-Typen und ihre Fähigkeiten. Kommen wir aber zum technischen / programmatischen Aspekt.
Was wäre eine gute programmatische Architektur für meine verschiedenen Arten von NPCs?

Okay, ich könnte eine Basisklasse haben. Eigentlich denke ich, dass dies ein guter Weg ist, um am DRY- Prinzip festzuhalten . So kann ich Methoden wie WalkTo(x,y)in meiner Basisklasse haben, da jeder NPC sich bewegen kann. Aber jetzt kommen wir zum eigentlichen Problem. Wo setze ich meine Fähigkeiten um? (Denken Sie daran : Build(), Farm()und Attack())

Da die Fähigkeiten aus der gleichen Logik bestehen, wäre es ärgerlich / DRY-Prinzip zu brechen, sie für jeden NPC (Arbeiter, Krieger, ..) zu implementieren.

Okay, ich könnte die Fähigkeiten innerhalb der Basisklasse implementieren. Dies würde eine Art von Logik erfordern , dass überprüft , wenn ein NPC Fähigkeit X verwenden kann IsBuilder, CanBuild.. Ich denke , es ist klar , was ich ausdrücken will.
Aber ich fühle mich mit dieser Idee nicht sehr wohl. Das klingt nach einer aufgeblähten Basisklasse mit zu viel Funktionalität.

Ich benutze C # als Programmiersprache. Mehrfachvererbung ist hier also keine Option. Mittel: Zusätzliche Basisklassen wie Fisherman : Farmer, Attackerfunktionieren nicht.

Brettetete
quelle
3
Ich habe mir dieses Beispiel angesehen, das wie eine Lösung für dieses Problem erscheint
Shawn Mclean
4
Diese Frage würde viel besser auf gamedev.stackexchange.com
Philipp

Antworten:

14

Zusammensetzung und Schnittstellenvererbung sind die üblichen Alternativen zur klassischen Mehrfachvererbung.

Alles, was Sie oben beschrieben haben und mit dem Wort "can" beginnen, kann mit einer Schnittstelle wie in ICanBuildoder dargestellt werden ICanFarm. Sie können so viele dieser Schnittstellen erben, wie Sie für erforderlich halten.

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

    #region ICanFarm Implementation
    // Farm
    #endregion
}

Sie können "generische" Gebäude- und Landwirtschaftsobjekte über den Konstruktor Ihrer Klasse übergeben. Hier kommt die Komposition ins Spiel.

Robert Harvey
quelle
Nur laut denken: Die 'Beziehung' wäre (intern) hat statt ist (Arbeiter hat Builder statt Arbeiter ist Builder). Das Beispiel / Muster, das Sie erklärt haben, macht Sinn und klingt nach einer schönen entkoppelten Möglichkeit, mein Problem zu lösen. Aber es fällt mir schwer, diese Beziehung zu bekommen.
Brettetete
3
Dies ist orthogonal zu Roberts Lösung, aber Sie sollten die Unveränderlichkeit (sogar mehr als gewöhnlich) bevorzugen, wenn Sie die Komposition stark nutzen, um zu vermeiden, dass komplizierte Netzwerke von Nebenwirkungen entstehen. Im Idealfall nehmen Ihre Build- / Farm- / Angriffsimplementierungen Daten auf und spucken sie aus, ohne etwas anderes zu berühren.
Doval
1
Worker ist immer noch ein Baumeister, weil Sie von geerbt haben ICanBuild. Dass Sie auch Tools zum Erstellen haben, ist in der Objekthierarchie von untergeordneter Bedeutung.
Robert Harvey
Macht Sinn. Ich werde dieses Prinzip versuchen. Vielen Dank für Ihre freundliche und beschreibende Antwort. Ich weiß das wirklich sehr zu schätzen. Ich werde diese Frage für einige Zeit offen lassen :)
Brettetete
4
@Brettetete Es könnte hilfreich sein, sich die Implementierungen Build, Farm, Attack, Hunt usw. als Fähigkeiten vorzustellen , über die Ihre Charaktere verfügen. BuildSkill, FarmSkill, AttackSkill usw. Dann macht Komposition viel mehr Sinn.
Eric King
4

Wie bereits auf anderen Postern erwähnt, könnte eine Komponentenarchitektur die Lösung für dieses Problem sein.

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

In diesem Fall erweitern Sie die Aktualisierungsfunktion, um alle Funktionen zu implementieren, die der NPC haben sollte.

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

Durch Iterieren eines Komponentencontainers und Aktualisieren jeder Komponente kann die spezifische Funktionalität jeder Komponente angewendet werden.

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

  list<component> components;
}

Da jeder Objekttyp gewünscht wird, können Sie eine Form des Factory-Musters verwenden, um die einzelnen gewünschten Typen zu erstellen.

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

Ich bin immer auf Probleme gestoßen, da die Komponenten immer komplexer werden. Wenn Sie die Komplexität der Komponenten niedrig halten (eine Verantwortung), können Sie dieses Problem beheben.

Connor Hollis
quelle
3

Wenn Sie Schnittstellen wie diese definieren, muss ICanBuildIhr Spielsystem jeden NPC nach Typ untersuchen, was in OOP normalerweise verpönt ist. Zum Beispiel : if (npc is ICanBuild).

Du bist besser dran mit:

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

Dann:

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

... oder Sie können natürlich eine Reihe verschiedener Architekturen verwenden, z. B. eine domänenspezifische Sprache in Form einer fließenden Schnittstelle zum Definieren verschiedener Arten von NPCs usw., in der Sie NPC-Typen erstellen, indem Sie verschiedene Verhaltensweisen kombinieren:

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

var workerFactory = new NPCFactory(workerType);

var worker = workerFactory.Build();

Der NPC-Builder könnte einen implementieren DefaultWalkBehavior, der im Fall eines fliegenden NPC, der aber nicht laufen kann, überschrieben werden kann.

Scott Whitlock
quelle
Nun, ich denke nur noch einmal laut: Es gibt eigentlich keinen großen Unterschied zwischen if(npc is ICanBuild)und if(npc.CanBuild). Auch wenn ich den npc.CanBuildWeg von meiner jetzigen Einstellung vorziehen würde .
Brettetete
3
@Brettetete - Sie können in dieser Frage sehen, warum einige Programmierer es wirklich nicht mögen, den Typ zu überprüfen .
Scott Whitlock
0

Ich würde alle Fähigkeiten in separate Klassen einteilen, die von Fähigkeit erben. Dann haben Sie in der Einheitentypklasse eine Liste von Fähigkeiten und anderen Dingen wie Angriff, Verteidigung, maximale Gesundheit usw. Haben Sie auch eine Einheitenklasse, die die aktuelle Gesundheit, den aktuellen Standort usw. hat und den Einheitentyp als Mitgliedsvariable hat.

Definieren Sie alle Einheitentypen in einer Konfigurationsdatei oder einer Datenbank, anstatt sie fest zu codieren.

Dann kann der Level-Designer die Fähigkeiten und Statistiken der Einheitentypen leicht anpassen, ohne das Spiel neu zu kompilieren, und die Leute können Mods für das Spiel schreiben.

user3970295
quelle