Alternative all'ereditarietà multipla per la mia architettura (NPC in un gioco di strategia in tempo reale)?


11

Coding non è così difficile in realtà . La parte difficile è scrivere codice che abbia un senso, sia leggibile e comprensibile. Quindi voglio ottenere uno sviluppatore migliore e creare una solida architettura.

Quindi voglio creare un'architettura per NPC in un videogioco. È un gioco di strategia in tempo reale come Starcraft, Age of Empires, Command & Conquers, ecc. Ecc. Quindi avrò diversi tipi di NPC. Un NPC può avere uno a molte abilità (metodi) di questi: Build(), Farm()e Attack().

Esempi:
operaio può Build()e Farm()
Warrior può Attack()
cittadino può Build(), Farm()e Attack()
Fisherman può Farm()eAttack()

Spero che sia tutto chiaro finora.

Quindi ora ho i miei tipi di NPC e le loro abilità. Ma veniamo all'aspetto tecnico / programmatico.
Quale sarebbe una buona architettura programmatica per i miei diversi tipi di NPC?

Ok, potrei avere una classe base. In realtà penso che questo sia un buon modo per attenersi al principio DRY . Quindi posso avere metodi come WalkTo(x,y)nella mia classe di base poiché ogni NPC sarà in grado di muoversi. Ma ora veniamo al vero problema. Dove posso implementare le mie capacità? (ricordate: Build(), Farm()e Attack())

Poiché le abilità saranno costituite dalla stessa logica, sarebbe fastidioso / infrangere il principio DRY per implementarle per ogni NPC (lavoratore, guerriero, ...).

Ok, potrei implementare le abilità all'interno della classe base. Ciò richiederebbe un qualche tipo di logica che verifica se un NPC può usare l'abilità X IsBuilder, CanBuild, .. penso che sia chiaro quello che voglio esprimere.
Ma non mi sento molto bene con questa idea. Sembra una classe base gonfia con troppe funzionalità.

Uso C # come linguaggio di programmazione. Quindi l'ereditarietà multipla non è un'opzione qui. Mezzi: avere classi base extra come Fisherman : Farmer, Attackernon funzionerà.


3
Stavo guardando questo esempio che sembra una soluzione a questo: en.wikipedia.org/wiki/Composition_over_inheritance
Shawn Mclean

4
Questa domanda si adatterebbe molto meglio su gamedev.stackexchange.com
Philipp,

Risposte:


14

L'ereditarietà della composizione e dell'interfaccia sono le solite alternative all'eredità multipla classica.

Tutto ciò che hai descritto sopra che inizia con la parola "can" è una capacità che può essere rappresentata con un'interfaccia, come in ICanBuildo ICanFarm. Puoi ereditare tante interfacce quante ne pensi di aver bisogno.

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

    #region ICanFarm Implementation
    // Farm
    #endregion
}

Puoi passare oggetti di costruzione e agricoltura "generici" attraverso il costruttore della tua classe. Ecco dove entra in gioco la composizione.


Solo pensando ad alta voce: la 'relazione' sarebbe (internamente) ha invece di è (Worker ha Builder invece Worker è Builder). L'esempio / modello che hai spiegato ha senso e suona come un bel modo disaccoppiato per risolvere il mio problema. Ma ho difficoltà a ottenere questa relazione.
Brettetete,

3
Questo è ortogonale alla soluzione di Robert ma dovresti favorire l'immutabilità (anche più del solito) quando fai un uso intenso della composizione per evitare di creare reti complicate di effetti collaterali. Idealmente, le implementazioni di build / farm / attack catturano i dati e li sputano senza toccare nient'altro.
Doval,

1
Il lavoratore è ancora un costruttore, perché hai ereditato ICanBuild. Il fatto che tu abbia anche strumenti per la costruzione è di secondaria importanza nella gerarchia degli oggetti.
Robert Harvey,

Ha senso. Proverò questo principio. Grazie per la tua risposta amichevole e descrittiva. Lo apprezzo davvero. Lascerò questa domanda aperta per qualche tempo :)
Brettetete,

4
@Brettetete Potrebbe essere utile pensare alle implementazioni di Build, Farm, Attack, Hunt, ecc. Come abilità che hanno i tuoi personaggi. BuildSkill, FarmSkill, AttackSkill, ecc. Quindi la composizione ha molto più senso.
Eric King,

4

Come altri poster hanno menzionato, un'architettura componente potrebbe essere la soluzione a questo problema.

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

In questo caso, estendi la funzione di aggiornamento per implementare qualsiasi funzionalità dovrebbe avere l'NPC.

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

L'iterazione di un contenitore di componenti e l'aggiornamento di ciascun componente consente di applicare la funzionalità specifica di ciascun componente.

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

  list<component> components;
}

Poiché ogni tipo di oggetto è desiderato, è possibile utilizzare una qualche forma del modello di fabbrica per costruire i singoli tipi desiderati.

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

Ho sempre riscontrato problemi man mano che i componenti diventano sempre più complessi. Mantenere bassa la complessità dei componenti (una responsabilità) può aiutare ad alleviare quel problema.


3

Se definisci interfacce come ICanBuildallora, il tuo sistema di gioco deve ispezionare ogni NPC per tipo, che è generalmente disapprovato in OOP. Ad esempio: if (npc is ICanBuild).

Stai meglio con:

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

Poi:

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 ovviamente potresti usare diverse architetture diverse, come una specie di linguaggio specifico di dominio sotto forma di un'interfaccia fluida per la definizione di diversi tipi di NPC, ecc., in cui costruisci tipi di NPC combinando comportamenti diversi:

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

var workerFactory = new NPCFactory(workerType);

var worker = workerFactory.Build();

Il costruttore NPC potrebbe implementare un oggetto DefaultWalkBehaviorche potrebbe essere ignorato nel caso di un NPC che vola ma non può camminare, ecc.


Bene, ripensandoci a voce alta: in realtà non c'è molta differenza tra if(npc is ICanBuild)e if(npc.CanBuild). Anche quando preferirei la npc.CanBuildstrada dal mio atteggiamento attuale.
Brettetete,

3
@Brettetete - In questa domanda puoi vedere perché ad alcuni programmatori non piace davvero ispezionare il tipo.
Scott Whitlock,

0

Metterei tutte le abilità in classi separate che ereditano da Abilità. Quindi nella classe del tipo di unità ha un elenco di abilità e altre cose come attacco, difesa, salute massima ecc. Inoltre ha una classe di unità che ha salute, posizione, ecc. Attuali e ha il tipo di unità come variabile membro.

Definire tutti i tipi di unità in un file di configurazione o in un database, piuttosto che codificarlo.

Quindi il progettista di livelli può facilmente regolare le abilità e le statistiche dei tipi di unità senza ricompilare il gioco, e anche le persone possono scrivere mod per il gioco.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.