Alternatives à l'héritage multiple pour mon architecture (PNJ dans un jeu de stratégie en temps réel)?

11

Le codage n'est pas si difficile en fait . La partie difficile est d'écrire du code qui a du sens, qui est lisible et compréhensible. Je veux donc obtenir un meilleur développeur et créer une architecture solide.

Je veux donc créer une architecture pour les PNJ dans un jeu vidéo. Il s'agit d'un jeu de stratégie en temps réel comme Starcraft, Age of Empires, Command & Conquers, etc. etc. J'aurai donc différents types de PNJ. Un PNJ peut avoir un à plusieurs capacités (méthodes) de celles - ci: Build(), Farm()etAttack() .

Exemples:
travailleur peut Build()et Farm()
guerrier peut Attack()
citoyen peut Build(), Farm()et Attack()
pêcheur peut Farm()etAttack()

J'espère que tout est clair jusqu'à présent.

Alors maintenant, j'ai mes types de PNJ et leurs capacités. Mais revenons à l'aspect technique / programmatique.
Quelle serait une bonne architecture programmatique pour mes différents types de PNJ?

D'accord, je pourrais avoir une classe de base. En fait, je pense que c'est une bonne façon de s'en tenir au principe DRY . Je peux donc avoir des méthodes comme WalkTo(x,y)dans ma classe de base car chaque PNJ pourra se déplacer. Mais maintenant, venons-en au vrai problème. Où dois-je mettre en œuvre mes capacités? (rappelez - vous: Build(), Farm()et Attack())

Puisque les capacités seront constituées de la même logique, il serait ennuyeux / de casser le principe SEC pour les implémenter pour chaque PNJ (Ouvrier, Guerrier, ..).

D'accord, je pourrais implémenter les capacités au sein de la classe de base. Cela nécessiterait une sorte de logique qui vérifie si un PNJ peut utiliser X. capacité IsBuilder, CanBuild.. Je pense qu'il est clair ce que je veux exprimer.
Mais je ne me sens pas très bien avec cette idée. Cela ressemble à une classe de base gonflée avec trop de fonctionnalités.

J'utilise C # comme langage de programmation. L'héritage multiple n'est donc pas une option ici. Moyens: Avoir des classes de base supplémentaires comme Fisherman : Farmer, Attackerne fonctionnera pas.

Brettetete
la source
3
Je regardais cet exemple qui semble être une solution à cela: en.wikipedia.org/wiki/Composition_over_inheritance
Shawn Mclean
4
Cette question conviendrait beaucoup mieux sur gamedev.stackexchange.com
Philipp

Réponses:

14

La composition et l'héritage d'interface sont les alternatives habituelles à l'héritage multiple classique.

Tout ce que vous avez décrit ci-dessus et commençant par le mot «peut» est une capacité qui peut être représentée par une interface, comme dans ICanBuildou ICanFarm. Vous pouvez hériter d'autant de ces interfaces que vous pensez en avoir besoin.

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

    #region ICanFarm Implementation
    // Farm
    #endregion
}

Vous pouvez passer des objets de construction et d'agriculture "génériques" via le constructeur de votre classe. C'est là que la composition entre en jeu.

Robert Harvey
la source
Penser simplement fort: la «relation» serait (en interne) être a au lieu de est (Worker a Builder au lieu de Worker is Builder). L'exemple / modèle que vous avez expliqué est logique et sonne comme une belle façon découplée de résoudre mon problème. Mais j'ai du mal à obtenir cette relation.
Brettetete
3
C'est orthogonal à la solution de Robert, mais vous devriez privilégier l'immuabilité (encore plus que d'habitude) lorsque vous faites un usage intensif de la composition pour éviter de créer des réseaux complexes d'effets secondaires. Idéalement, vos implémentations de build / ferme / attaque prennent des données et crachent des données sans rien toucher d'autre.
Doval
1
Worker est toujours un constructeur, car vous avez hérité de ICanBuild. Le fait que vous disposiez également d'outils pour la construction est une préoccupation secondaire dans la hiérarchie des objets.
Robert Harvey
Logique. Je vais essayer ce principe. Merci pour votre réponse amicale et descriptive. J'apprécie vraiment ça. Je vais cependant laisser cette question ouverte pendant un certain temps :)
Brettetete
4
@Brettetete Il pourrait être utile de considérer les implémentations Build, Farm, Attack, Hunt, etc. comme des compétences que possèdent vos personnages. BuildSkill, FarmSkill, AttackSkill, etc. La composition prend alors beaucoup plus de sens.
Eric King du
4

Comme d'autres affiches l'ont mentionné, une architecture de composants pourrait être la solution à ce problème.

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

Dans ce cas, étendez la fonction de mise à jour pour implémenter toutes les fonctionnalités que le NPC devrait avoir.

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

L'itération d'un conteneur de composants et la mise à jour de chaque composant permettent d'appliquer la fonctionnalité spécifique de chaque composant.

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

  list<component> components;
}

Comme chaque type d'objet est souhaité, vous pouvez utiliser une forme de modèle d'usine pour construire les types individuels que vous souhaitez.

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

J'ai toujours rencontré des problèmes car les composants deviennent de plus en plus complexes. Garder la complexité des composants faible (une responsabilité) peut aider à atténuer ce problème.

Connor Hollis
la source
3

Si vous définissez des interfaces comme celles-ci, ICanBuildvotre système de jeu doit inspecter chaque PNJ par type, ce qui est généralement mal vu dans la POO. Par exemple: if (npc is ICanBuild).

Vous êtes mieux avec:

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

Ensuite:

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

... ou bien sûr, vous pouvez utiliser un certain nombre d'architectures différentes, comme une sorte de langage spécifique au domaine sous la forme d'une interface fluide pour définir différents types de NPC, etc., où vous construisez des types de NPC en combinant différents comportements:

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

var workerFactory = new NPCFactory(workerType);

var worker = workerFactory.Build();

Le constructeur de PNJ pourrait implémenter un DefaultWalkBehaviorqui pourrait être annulé dans le cas d'un PNJ qui vole mais ne peut pas marcher, etc.

Scott Whitlock
la source
Eh bien, je pense à nouveau fort: il n'y a en fait pas beaucoup de différence entre if(npc is ICanBuild)et if(npc.CanBuild). Même quand je préférerais la npc.CanBuildvoie à mon attitude actuelle.
Brettetete
3
@Brettetete - Vous pouvez voir dans cette question pourquoi certains programmeurs n'aiment pas vraiment inspecter le type.
Scott Whitlock
0

Je mettrais toutes les capacités dans des classes distinctes qui héritent de la capacité. Ensuite, dans la classe de type d'unité, ayez une liste de capacités et d'autres choses comme l'attaque, la défense, la santé maximale, etc.

Définissez tous les types d'unités dans un fichier de configuration ou une base de données, plutôt que de le coder en dur.

Ensuite, le concepteur de niveaux peut facilement ajuster les capacités et les statistiques des types d'unités sans recompiler le jeu, et les gens peuvent également écrire des mods pour le jeu.

user3970295
la source