¿Alternativas a la herencia múltiple para mi arquitectura (NPC en un juego de estrategia en tiempo real)?

11

La codificación no es tan difícil en realidad . La parte difícil es escribir código que tenga sentido, que sea legible y comprensible. Así que quiero conseguir un mejor desarrollador y crear una arquitectura sólida.

Así que quiero crear una arquitectura para NPC en un videojuego. Es un juego de estrategia en tiempo real como Starcraft, Age of Empires, Command & Conquers, etc. Así que tendré diferentes tipos de NPC. Un NPC puede tener de uno a muchas habilidades (métodos) de estos: Build(), Farm()y Attack().

Ejemplos:
trabajador puede Build()y Farm()
Guerrero puede Attack()
ciudadano puede Build(), Farm()y Attack()
Fisherman puede Farm()yAttack()

Espero que todo esté claro hasta ahora.

Así que ahora tengo mis tipos de NPC y sus habilidades. Pero pasemos al aspecto técnico / programático.
¿Cuál sería una buena arquitectura programática para mis diferentes tipos de NPC?

Bien, podría tener una clase base. En realidad, creo que esta es una buena manera de seguir con el principio DRY . Así que puedo tener métodos como WalkTo(x,y)en mi clase base ya que cada NPC podrá moverse. Pero ahora vamos al verdadero problema. ¿Dónde implemento mis habilidades? (recuerde: Build(), Farm()y Attack())

Dado que las habilidades consistirán en la misma lógica, sería molesto / romper el principio DRY implementarlas para cada NPC (Trabajador, Guerrero, ...).

Bien, podría implementar las habilidades dentro de la clase base. Para ello sería necesario algún tipo de lógica que verifica si un NPC puede usar la habilidad X. IsBuilder, CanBuild.. creo que es claro lo que quiero expresar.
Pero no me siento muy bien con esta idea. Esto suena como una clase base hinchada con demasiada funcionalidad.

Yo uso C # como lenguaje de programación. Entonces, la herencia múltiple no es una opción aquí. Medios: Tener clases base adicionales como Fisherman : Farmer, Attackerno funcionará.

Brettetete
fuente
3
Estaba mirando este ejemplo que parece una solución para esto: en.wikipedia.org/wiki/Composition_over_inheritance
Shawn Mclean
44
Esta pregunta encajaría mucho mejor en gamedev.stackexchange.com
Philipp

Respuestas:

14

La composición y la herencia de interfaz son las alternativas habituales a la herencia múltiple clásica.

Todo lo que describió anteriormente que comienza con la palabra "can" es una capacidad que se puede representar con una interfaz, como en ICanBuild, o ICanFarm. Puede heredar tantas de esas interfaces como crea que necesita.

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

    #region ICanFarm Implementation
    // Farm
    #endregion
}

Puede pasar objetos de construcción y agricultura "genéricos" a través del constructor de su clase. Ahí es donde entra la composición.

Robert Harvey
fuente
Sólo de pensar en voz alta: La 'relación' sería (internamente) que tiene lugar de decir (Trabajador Constructor tiene lugar Trabajador es constructor). El ejemplo / patrón que ha explicado tiene sentido y suena como una buena forma desacoplada para resolver mi problema. Pero estoy teniendo dificultades para conseguir esta relación.
Brettetete
3
Esto es ortogonal a la solución de Robert, pero debe favorecer la inmutabilidad (incluso más de lo habitual) cuando hace un uso intensivo de la composición para evitar crear redes complicadas de efectos secundarios. Idealmente, sus implementaciones de construcción / granja / ataque toman datos y los escupen sin tocar nada más.
Doval
1
El trabajador sigue siendo un constructor, porque heredaste de ICanBuild. Que también tenga herramientas para construir es una preocupación secundaria en la jerarquía de objetos.
Robert Harvey
Tiene sentido. Voy a probar este princpile. Gracias por su respuesta amigable y descriptiva. Realmente aprecio eso. Sin embargo, dejaré que esta pregunta se abra por un tiempo :)
Brettetete
44
@Brettetete Puede ayudar pensar en las implementaciones de Build, Farm, Attack, Hunt, etc. como habilidades que tienen tus personajes. BuildSkill, FarmSkill, AttackSkill, etc. Entonces la composición tiene mucho más sentido.
Eric King
4

Como otros carteles han mencionado, una arquitectura de componentes podría ser la solución a este problema.

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

En este caso, extienda la función de actualización para implementar cualquier funcionalidad que deba tener el NPC.

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

Iterar un contenedor de componentes y actualizar cada componente permite aplicar la funcionalidad específica de cada componente.

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

  list<component> components;
}

Como se desea cada tipo de objeto, puede usar alguna forma del patrón de fábrica para construir los tipos individuales que desea.

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

Siempre me he encontrado con problemas a medida que los componentes se vuelven cada vez más complejos. Mantener baja la complejidad de los componentes (una responsabilidad) puede ayudar a aliviar ese problema.

Connor Hollis
fuente
3

Si define interfaces como ICanBuildentonces, su sistema de juego tiene que inspeccionar cada NPC por tipo, lo que generalmente está mal visto en OOP. Por ejemplo: if (npc is ICanBuild).

Estás mejor con:

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

Entonces:

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

... o, por supuesto, podría usar una serie de arquitecturas diferentes, como algún tipo de lenguaje específico de dominio en forma de una interfaz fluida para definir diferentes tipos de NPC, etc., donde construye tipos de NPC combinando diferentes comportamientos:

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

var workerFactory = new NPCFactory(workerType);

var worker = workerFactory.Build();

El constructor de NPC podría implementar un DefaultWalkBehaviorque podría ser anulado en el caso de un NPC que vuela pero no puede caminar, etc.

Scott Whitlock
fuente
Bueno, sólo pensando en voz alta otra vez: En realidad no hay mucha diferencia entre if(npc is ICanBuild)y if(npc.CanBuild). Incluso cuando preferiría el npc.CanBuildcamino de mi actitud actual.
Brettetete
3
@Brettetete: puede ver en esta pregunta por qué a algunos programadores realmente no les gusta inspeccionar el tipo.
Scott Whitlock
0

Pondría todas las habilidades en clases separadas que heredan de Habilidad. Luego, en la clase de tipo de unidad, tenga una lista de habilidades y otras cosas como ataque, defensa, salud máxima, etc. También tenga una clase de unidad que tenga salud actual, ubicación, etc. y que tenga el tipo de unidad como variable miembro.

Defina todos los tipos de unidades en un archivo de configuración o una base de datos, en lugar de codificarlo.

Luego, el diseñador de niveles puede ajustar fácilmente las habilidades y estadísticas de los tipos de unidades sin volver a compilar el juego, y también las personas pueden escribir modificaciones para el juego.

usuario3970295
fuente