Architettura OOP per Hero con molti attributi


14

Sto per iniziare un semplice gioco di ruolo di testo con browser, con personaggi che possono (passivamente) combattere altre persone. Ciò comporta un elenco di circa 10 abilità come forza, destrezza e così via, con competenze aggiuntive per diverse armi.

Esiste un modo migliore per progettare questa classe di personaggi che avere solo quelle abilità come attributo di classe? Sembra facile, ma sono riluttante perché è goffa.

class Char(self):
    int strength
    int dexterity
    int agility
    ...
    int weaponless
    int dagger
    ...

1
Dovresti controllare questa guida completa per la scrittura di giochi e come alcune delle classi comuni potrebbero apparire come link
draghi

@dragons Grazie per l'interessante link, ma non vedo spiegazioni più profonde per la progettazione della Charactorclasse?
Sven

1
Cosa trovi esattamente "goffo" in questo design?
Thomas

Risposte:


17

Fintanto che mantieni il tuo sistema relativamente semplice, questo dovrebbe funzionare. Ma quando aggiungi elementi come modificatori di abilità temporanei, vedrai presto un sacco di codice duplicato. Incontrerai anche problemi con armi diverse usando competenze diverse. Poiché ogni abilità è una variabile diversa, dovrai scrivere un codice diverso per ogni tipo di abilità che sostanzialmente fa lo stesso (o usare alcuni brutti hack di riflessione - a condizione che il tuo linguaggio di programmazione li supporti).

Per questo motivo, ti consiglio di archiviare sia le competenze che le competenze in una struttura di dati associativa che associ costanti di abilità ai valori. Come farlo differisce elegantemente dal linguaggio di programmazione al linguaggio di programmazione. Quando la tua lingua lo supporta, le costanti dovrebbero essere in un enum.

Per darti un esempio di come funzionerebbe in pratica, il tuo codice per il calcolo del danno da attacco sarebbe simile a questo:

int damage = attacker.getSkill(STRENGTH) + 
             attacker.getProficiency(weapon.getProficiencyRequired()) -
             defender.getSkill(TOUGHNESS);

La creazione di un metodo come getSkill()va contro il principio fondamentale della programmazione orientata agli oggetti .
Jephir il

4

Perché non usare array associati ?, questo offre il vantaggio di essere facilmente esteso (usando PHP per esempio)

$Stats["Strength"] = "8";
$Stats["Dexterity"] = "8";

per cose come le armi, probabilmente vorrai creare alcune classi di base

Arma -> Arma da mischia, Arma a distanza

e quindi crea le tue armi da lì.

Il risultato finale a cui vorrei mirare è una classe che assomigli a questa

class Character
{
    public $Stats;
    public $RightHand;
    public $LeftHand;
    public $Armor;
    public $Name;
    public $MaxHealth;
    public $CurrentHealth;

    public function __construct()
    {
        //Basic
        $this->Name = "Fred";
        $this->MaxHealth = "10";
        $this->CurrentHealth = "10";

        //Stats
        $this->Stats["Strength"] = 8;
        $this->Stats["Dexterity"] = 8;
        $this->Stats["Intellect"] = 8;
        $this->Stats["Constitution"] = 8;

        //Items
        $this->RightHand = NULL;
        $this->LeftHand  = NULL;
        $this->Armor = NULL;

    }
}

Potresti archiviare tutto in un array se lo desideri davvero.


È praticamente quello che ha detto @Philipp?
AturSams

1
@Philipp ha suggerito l'uso di enum, gli array sono un'altra opzione.
Grimston,

In realtà dice: "... struttura associativa dei dati che associa le costanti di abilità ai valori", le costanti nel dizionario potrebbero essere stringhe.
AturSams

3

Proverò a rispondere a questa domanda nel modo più OOP (o almeno quello che penso sarebbe). Potrebbe essere completamente eccessivo, a seconda delle evoluzioni che vedi sulle statistiche.

Potresti immaginare una classe SkillSet (o Stats ) (sto usando una sintassi simile a C per questa risposta):

class SkillSet {

    // Consider better data encapsulation
    int strength;
    int dexterity;
    int agility;

    public static SkillSet add(SkillSet stats) {
        strength += stats.strength;
        dexterity += stats.dexterity;
        agility += stats.agility;
    }

    public static SkillSet apply(SkillModifier modifier) {
        strength *= modifier.getStrengthModifier();
        dexterity *= modifier.getDexterityModifier();
        agility *= modifier.getAgilityModifier();

    }

}

Quindi l'eroe avrebbe un campo intrinseco di tipo SkillSet. Un'arma potrebbe avere anche un'abilità modificatore.

public abstract class Hero implements SkillSet {

    SkillSet intrinsicStats;
    Weapon weapon;

    public SkillSet getFinalStats() {
        SkillSet finalStats;
        finalStats = intrinsicStats;
        finalStats.add(weapon.getStats());
        foreach(SkillModifier modifier : getEquipmentModifiers()) {
            finalStats.apply(modifier);
        }
        return finalStats;
    }

    protected abstract List<SkillModifier> getEquipmentModifiers();

}

Questo è ovviamente un esempio per darti un'idea. Puoi anche prendere in considerazione l'uso del modello di progettazione di Decorator, in modo che i modificatori delle statistiche funzionino come "filtri" che vengono applicati uno dopo l'altro ...


Com'è questa C-like?
Bogglez,

@bogglez: Anch'io tendo ad usare "C'ish" per riferirmi allo pseudocodice che assomiglia vagamente ad un linguaggio a parentesi graffa, anche se sono coinvolte le classi. Hai un punto, però: sembra una sintassi molto più specifica: è una modifica minore o due rispetto all'essere Java compilabile.
cHao,

Non penso di essere troppo severo qui. Questa non è solo una differenza nella sintassi, ma una differenza nella semantica. In C non esistono cose come una classe, modelli, qualificazioni protette, metodi, funzioni virtuali, ecc. Non mi piace questo abuso casuale di termini.
Bogglez,

1
Come hanno detto gli altri, la sintassi dei parentesi graffe viene da C. La sintassi di Java (o C # per quella materia) è fortemente ispirata da questo stile.
Pierre Arlaud,

3

Il modo più OOP di fare le cose sarebbe probabilmente fare qualcosa con l'eredità. La tua classe base (o super classe a seconda della lingua) sarebbe persona, quindi forse i cattivi e gli eroi ereditano dalla classe base. Quindi i tuoi eroi basati sulla forza e quelli basati sul volo si diramerebbero poiché il loro modo di trasporto è diverso, per esempio. Questo ha l'ulteriore vantaggio che i tuoi giocatori di computer possono avere la stessa classe di base dei giocatori umani e si spera che ti semplifichi la vita.

L'altra cosa riguardante gli attributi, e questo è meno specifico di OOP, sarebbe quella di rappresentare gli attributi dei tuoi personaggi come un elenco, quindi non devi averli definiti in modo esplicito nel tuo codice. Quindi forse avresti un elenco di armi e un elenco di attributi fisici. Crea una sorta di classe base per questi attributi in modo che possano interagire, quindi ognuno è definito in termini di danno, costo dell'energia, ecc. Quindi quando due individui si incontrano è relativamente chiaro come avverrà l'interazione. Dovresti scorrere l'elenco degli attributi di ciascun personaggio e calcolare il danno che uno fa all'altro con un grado di probabilità in ogni interazione.

L'uso di un elenco ti aiuterà a evitare di riscrivere molto codice poiché per aggiungere un carattere con un attributo a cui non avevi ancora pensato, devi solo assicurarti che abbia un'interazione che funzioni con il tuo sistema esistente.


4
Penso che il punto sia di separare le statistiche dalla classe dell'eroe. L'ereditarietà non è necessariamente la migliore soluzione OOP (nella mia risposta ho usato invece la composizione).
Pierre Arlaud,

1
Secondo il commento precedente. Cosa succede se un personaggio C ha proprietà dei caratteri A e B (che hanno entrambi la stessa classe base)? Si duplica il codice o si incontrano alcuni problemi relativi all'ereditarietà multipla . In questo caso preferirei la composizione rispetto all'eredità.
ComFreek,

2
Giusto. Stavo solo dando all'OP alcune opzioni. Non sembra che ci sia molto incastonato in questa fase e non conosco il loro livello di esperienza. Probabilmente sarebbe un po 'troppo aspettarsi che un principiante tiri fuori il polimorfismo completo, ma l'eredità è abbastanza semplice da comprendere per un principiante. Stavo cercando di aiutare a risolvere il problema del sentimento del codice "maldestro" che posso solo supporre si riferisca al fatto che i campi sono codificati in modo tale da poter essere o meno utilizzati. L'uso di un elenco per questi tipi di valori indefiniti imho è una buona opzione.
GenericJam

1
-1: "Il modo più OOP di fare le cose sarebbe probabilmente fare qualcosa con l'eredità." L'ereditarietà non è particolarmente orientata agli oggetti.
Sean Middleditch,

3

Consiglierei un gestore di tipi di statistiche, popolato da un file di dati (ad esempio io uso XML) e oggetti Stat, con un tipo e un valore memorizzati nell'inserimento di caratteri come hashtable, con l'ID univoco del tipo di statistica come chiave.

Modifica: codice psudo

Class StatType
{
    int ID;
    string Name;

    public StatType(int _id, string _name)
    {
        ID = _id;
        Name = _name;
    }
}


Class StatTypeManager
{
    private static Hashtable statTypes;

    public static void Init()
    {
        statTypes = new Hashtable();

        StatType type;

        type = new StatType(0, "Strength");
        statTypes.add(type.ID, type);

        type = new StatType(1, "Dexterity");
        statTypes.add(type.ID, type);

        //etc

        //Recommended: Load your stat types from an external resource file, e.g. xml
    }

    public static StatType getType(int _id)
    {
        return (StatType)statTypes[_id];
    }
}

class Stat
{
    StatType Type;
    int Value;

    public Stat(StatType _type, int _value)
    {
        Type = _type;
        Value = _value;
    }
}

Class Char
{
    Hashtable Stats;

    public Char(Stats _stats)
    {
        Stats = _stats;
    }

    public int GetStatValue(int _id)
    {
        return ((Stat)Stats[_id]).Value;
    }
}

Penso StatTypeclasse inutili, basta usare la namedel StatTypecome chiave. Come ha fatto #Grimston. Lo stesso con Stats.
giannis christofakis,

Preferisco usare una classe in casi come questo in modo da poter modificare liberamente il nome (mentre l'id rimane costante). Overkill in questo caso? Forse, ma poiché questa tecnica può essere usata per qualcosa di simile altrove in un programma, lo farei così per coerenza.
DFreeman,

0

Ι proverò a darti un esempio di come puoi progettare il tuo arsenale e il tuo arsenale.

Il nostro obiettivo è quello di disaccoppiare le entità, quindi l'arma dovrebbe essere un'interfaccia.

interface Weapon {
    public int getDamage();
}

Supponiamo che ogni giocatore possa possedere solo un'arma, possiamo usarla Strategy patternper cambiare facilmente le armi.

class Knife implements Weapon {
    private int damage = 10;
    @Override
    public int getDamage() {
        return this.damage;
    }
}

class Sword implements Weapon {
    private int damage = 40;
    @Override
    public int getDamage() {
        return this.damage;
    }
}

Un altro modello utile sarebbe il modello a oggetti nulli nel caso in cui il giocatore sia disarmato.

class Weaponless implements Weapon {
    private int damage = 0;
    @Override
    public int getDamage() {
        return this.damage;
    }
}

Per quanto riguarda l'armeria, possiamo indossare più attrezzature di difesa.

// Defence classes,interfaces

interface Armor {
    public int defend();
}

class Defenseless implements Armor {

    @Override
    public int defend() {
        return 0;
    }
}

abstract class Armory implements Armor {

    private Armor armory;
    protected int defence;

    public Armory() {
        this(new Defenseless());
    }

    public Armory(Armor force) {
        this.armory = force;
    }

    @Override
    public int defend() {
        return this.armory.defend() + this.defence;
    }

}

// Defence implementations

class Helmet extends Armory {
    {
        this.defence = 30;
    }    
}

class Gloves extends Armory {
    {
        this.defence = 10;
    }    
}

class Boots extends Armory {
    {
        this.defence = 10;
    }    
}

Per il disaccoppiamento, ho creato un'interfaccia per il difensore.

interface Defender {
    int getDefended();
}

E la Playerclasse.

class Player implements Defender {

    private String title;

    private int health = 100;
    private Weapon weapon = new Weaponless();
    private List<Armor> armory = new ArrayList<Armor>(){{ new Defenseless(); }};


    public Player(String name) {
        this.title = name;
    }

    public Player() {
        this("John Doe");
    }

    public String getName() {
        return this.title;
    }


    public void setWeapon(Weapon weapon) {
        this.weapon = weapon;
    }

    public void attack(Player enemy) {

        System.out.println(this.getName() + " attacked " + enemy.getName());

        int attack = enemy.getDefended() + enemy.getHealth()- this.weapon.getDamage();

        int health = Math.min(enemy.getHealth(),attack);

        System.out.println("After attack " + enemy.getName() + " health is " + health);

        enemy.setHealth(health);
    }

    public int getHealth() {
        return health;
    }

    private void setHealth(int health) {
        /* Check for die */
        this.health = health;
    }

    public void addArmory(Armor armor) {
        this.armory.add(armor);
    }


    @Override
    public int getDefended() {
        int defence = this.armory.stream().mapToInt(armor -> armor.defend()).sum();
        System.out.println(this.getName() + " defended , armory points are " + defence);
        return defence;
    }

}

Aggiungiamo un po 'di gameplay.

public class Game {
    public static void main(String[] args) {
        Player yannis = new Player("yannis");
        Player sven = new Player("sven");


        yannis.setWeapon(new Knife());
        sven.setWeapon(new Sword());


        sven.addArmory(new Helmet());
        sven.addArmory(new Boots());

        yannis.attack(sven);      
        sven.attack(yannis);      
    }
}

Ecco!


2
Questa risposta sarebbe più utile quando spiegheresti il ​​tuo ragionamento alla base delle tue scelte progettuali.
Philipp
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.