Alternativas à herança múltipla para minha arquitetura (NPCs em um jogo de estratégia em tempo real)?

11

Codificação não é tão difícil, na verdade . A parte difícil é escrever um código que faça sentido, seja legível e compreensível. Então, eu quero ter um desenvolvedor melhor e criar uma arquitetura sólida.

Então, eu quero criar uma arquitetura para NPCs em um videogame. É um jogo de estratégia em tempo real como Starcraft, Age of Empires, Command & Conquers, etc. etc. Portanto, terei diferentes tipos de NPCs. Um NPC pode ter um para muitas habilidades (métodos) destes: Build(), Farm()e Attack().

Exemplos:
Trabalhador pode Build()e Farm()
Guerreiro pode Attack()
Cidadão podem Build(), Farm()e Attack()
Fisherman pode Farm()eAttack()

Espero que tudo esteja claro até agora.

Então agora eu tenho meus tipos de NPCs e suas habilidades. Mas vamos ao aspecto técnico / programático.
Qual seria uma boa arquitetura programática para meus diferentes tipos de NPCs?

Ok, eu poderia ter uma classe base. Na verdade, acho que essa é uma boa maneira de manter o princípio DRY . Para que eu possa ter métodos como WalkTo(x,y)na minha classe base, já que todo NPC poderá se mover. Mas agora vamos ao verdadeiro problema. Onde eu implemento minhas habilidades? (lembre-se: Build(), Farm()e Attack())

Como as habilidades consistirão na mesma lógica, seria irritante / violar o princípio DRY implementá-las para cada NPC (Worker, Warrior, ..).

Ok, eu poderia implementar as habilidades dentro da classe base. Isso exigiria algum tipo de lógica que verifica se um NPC pode usar a habilidade X. IsBuilder, CanBuild, .. Eu acho que é claro que eu quero expressar.
Mas não me sinto muito bem com essa ideia. Isso soa como uma classe base inchada com muita funcionalidade.

Eu uso c # como linguagem de programação. Portanto, herança múltipla não é uma opção aqui. Meios: Ter classes base extras como Fisherman : Farmer, Attackernão vai funcionar.

Brettetete
fonte
3
Eu estava olhando para este exemplo que parece ser uma solução para isso: en.wikipedia.org/wiki/Composition_over_inheritance
Shawn Mclean
4
Esta questão se encaixaria muito melhor em gamedev.stackexchange.com
Philipp

Respostas:

14

A Herança de Composição e Interface são as alternativas usuais à herança múltipla clássica.

Tudo o que você descreveu acima que começa com a palavra "pode" é um recurso que pode ser representado com uma interface, como em ICanBuild, ou ICanFarm. Você pode herdar tantas dessas interfaces quanto achar necessário.

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

    #region ICanFarm Implementation
    // Farm
    #endregion
}

Você pode passar objetos de construção e agricultura "genéricos" através do construtor de sua classe. É aí que entra a composição.

Robert Harvey
fonte
Só de pensar alto: O 'relacionamento' seria (internamente) ser tem em vez de é (trabalhador tem Builder vez Trabalhador é Builder). O exemplo / padrão que você explicou faz sentido e soa como uma boa maneira dissociada de resolver meu problema. Mas estou tendo dificuldades para conseguir esse relacionamento.
Brettetete
3
Isso é ortogonal à solução de Robert, mas você deve favorecer a imutabilidade (ainda mais que o normal) ao fazer uso pesado da composição para evitar a criação de redes complicadas de efeitos colaterais. Idealmente, suas implementações de build / farm / ataque capturam e expelem dados sem tocar em mais nada.
Doval
1
Worker ainda é um construtor, porque você herdou de ICanBuild. O fato de você também ter ferramentas para construção é uma preocupação secundária na hierarquia de objetos.
Robert Harvey
Faz sentido. Vou tentar este princípio. Obrigado pela sua resposta amigável e descritiva. Eu realmente aprecio isso. Vou deixar esta aberta questão por algum tempo embora :)
Brettetete
4
@Brettetete Talvez ajude pensar nas implementações Build, Farm, Attack, Hunt, etc. como habilidades , que seus personagens possuem. BuildSkill, FarmSkill, AttackSkill, etc. Então a composição faz muito mais sentido.
Eric King
4

Como outros pôsteres mencionaram, uma arquitetura de componentes pode ser a solução para esse problema.

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

Nesse caso, estenda a função de atualização para implementar qualquer funcionalidade que o NPC deva ter.

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

Iterar um contêiner de componente e atualizar cada componente permite que a funcionalidade específica de cada componente seja aplicada.

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

  list<component> components;
}

Como cada tipo de objeto é desejado, você pode usar alguma forma do padrão de fábrica para construir os tipos individuais desejados.

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

Eu sempre encontro problemas à medida que os componentes ficam cada vez mais complexos. Manter a complexidade do componente baixa (uma responsabilidade) pode ajudar a aliviar esse problema.

Connor Hollis
fonte
3

Se você definir interfaces como essa ICanBuild, seu sistema de jogo deve inspecionar todos os NPCs por tipo, o que normalmente é desaprovado no OOP. Por exemplo: if (npc is ICanBuild).

Você está melhor com:

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

Então:

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, é claro, você pode usar várias arquiteturas diferentes, como algum tipo de linguagem específica de domínio na forma de uma interface fluente para definir diferentes tipos de NPCs, etc., onde você cria tipos de NPC combinando comportamentos diferentes:

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

var workerFactory = new NPCFactory(workerType);

var worker = workerFactory.Build();

O construtor NPC pode implementar um DefaultWalkBehaviorque possa ser substituído no caso de um NPC que voe, mas não possa andar, etc.

Scott Whitlock
fonte
Bem, pensando alto novamente: na verdade, não há muita diferença entre if(npc is ICanBuild)e if(npc.CanBuild). Mesmo quando eu preferiria o npc.CanBuildcaminho da minha atitude atual.
Brettetete
3
@Brettetete - Você pode ver nesta pergunta por que alguns programadores realmente não gostam de inspecionar o tipo.
22414 Scott Whitlock
0

Eu colocaria todas as habilidades em classes separadas que herdam do Ability. Em seguida, na classe de tipo de unidade, tenha uma lista de habilidades e outras coisas, como ataque, defesa, vida máxima, etc.

Defina todos os tipos de unidades em um arquivo de configuração ou em um banco de dados, em vez de codificá-lo.

Então o designer de níveis pode ajustar facilmente as habilidades e estatísticas dos tipos de unidades sem recompilar o jogo, e também as pessoas podem escrever mods para o jogo.

user3970295
fonte