Architecture OOP pour Hero avec de nombreux attributs

14

Je suis sur le point de commencer un RPG de texte de navigateur simple, avec des personnages qui peuvent (passivement) combattre d'autres personnes. Cela implique une liste d'environ 10 compétences telles que la force, la dextérité, etc., avec des compétences supplémentaires pour différentes armes.

Y a-t-il une meilleure façon de concevoir cette classe de personnage que d'avoir simplement ces compétences comme attribut de classe? Cela semble facile, mais je suis réticent parce que c'est de la maladresse.

class Char(self):
    int strength
    int dexterity
    int agility
    ...
    int weaponless
    int dagger
    ...
Sven
la source
1
Vous devriez vérifier ce guide alltogether pour les jeux d' écriture et de la façon dont certaines des classes communes pourrait ressembler lien
dragons
@dragons Merci pour le lien intéressant, mais je ne vois pas d'explication plus approfondie pour la conception de la Charactorclasse?
Sven
1
Que trouvez-vous exactement "maladroit" dans cette conception?
Thomas

Réponses:

17

Tant que vous gardez votre système relativement simple, cela devrait fonctionner. Mais lorsque vous ajoutez des éléments comme des modificateurs de compétences temporaires, vous verrez bientôt beaucoup de code en double. Vous rencontrerez également des problèmes avec différentes armes en utilisant différentes compétences. Parce que chaque compétence est une variable différente, vous devrez écrire un code différent pour chaque type de compétence qui fait essentiellement la même chose (ou utiliser des hacks de réflexion laids - à condition que votre langage de programmation les supporte).

Pour cette raison, je vous recommande de stocker les compétences et les compétences dans une structure de données associative qui mappe les constantes de compétences aux valeurs. Comment faire cela diffère élégamment d'un langage de programmation à un langage de programmation. Lorsque votre langue le prend en charge, les constantes doivent être dans un enum.

Pour vous donner un exemple de la façon dont cela fonctionnerait dans la pratique, votre code de calcul des dégâts d'attaque ressemblerait alors à ceci:

int damage = attacker.getSkill(STRENGTH) + 
             attacker.getProficiency(weapon.getProficiencyRequired()) -
             defender.getSkill(TOUGHNESS);
Philipp
la source
Créer une méthode comme cela getSkill()va à l'encontre du principe fondamental de la programmation orientée objet .
Jephir
4

Pourquoi ne pas utiliser des tableaux associés ?, cela donne l'avantage d'être facilement étendu (en utilisant PHP par exemple)

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

pour des choses comme les armes, vous voudrez probablement créer des classes de base

Arme -> Arme de corps à corps, Arme à distance

puis créez vos armes à partir de là.

Le résultat final que je viserais est une classe ressemblant à ceci

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;

    }
}

Vous pouvez tout stocker dans un tableau si vous le souhaitez vraiment.

Grimston
la source
C'est à peu près ce que @Philipp a dit?
AturSams
1
@Philipp a suggéré l'utilisation d'énumérations, les tableaux sont une autre option.
Grimston
Il dit en fait: "... structure de données associative qui mappe les constantes de compétences aux valeurs", les constantes dans le dictionnaire pourraient être des chaînes.
AturSams
3

Je vais essayer de répondre à cette question de la manière la plus POO (ou du moins ce que je pense que ce serait). Cela peut être complètement exagéré, selon les évolutions que vous voyez sur les statistiques.

Vous pourriez imaginer une classe SkillSet (ou Stats ) (j'utilise une syntaxe de type C pour cette réponse):

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

    }

}

Le héros aurait alors un champ intrinsicStats du type SkillSet. Une arme peut également avoir un jeu de compétences de modificateur.

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

}

Ceci est bien sûr un exemple pour vous donner une idée. Vous pouvez également envisager d'utiliser le modèle de conception Decorator, de sorte que les modificateurs des statistiques fonctionnent comme des "filtres" appliqués l'un après l'autre…

Pierre Arlaud
la source
Comment est ce C-like?
bogglez
@bogglez: J'ai également tendance à utiliser "C'ish" pour faire référence à un pseudocode qui ressemble vaguement à un langage à accolades, même si des classes sont impliquées. Cependant, vous avez un point: cela ressemble à une syntaxe beaucoup plus spécifique - c'est un changement mineur ou deux de Java compilable.
cHao
Je ne pense pas que je sois trop strict ici. Ce n'est pas seulement une différence de syntaxe, mais une différence de sémantique. En C, il n'y a pas de classe, de modèles, de qualificatifs protégés, de méthodes, de fonctions virtuelles, etc.
bogglez
1
Comme les autres l'ont dit, la syntaxe des accolades vient de C. La syntaxe de Java (ou C # d'ailleurs) est fortement inspirée de ce style.
Pierre Arlaud
3

La façon la plus OOP de faire les choses serait probablement de faire quelque chose avec héritage. Votre classe de base (ou super classe selon la langue) serait une personne, alors peut-être que les méchants et les héros hériteraient de la classe de base. Ensuite, vos héros basés sur la force et vos héros basés sur le vol se ramifieraient, car leur mode de transport est différent, par exemple. Cela a l'avantage supplémentaire que vos joueurs sur ordinateur peuvent avoir la même classe de base que les joueurs humains et cela vous simplifiera, espérons-le, votre vie.

L'autre chose concernant les attributs, et cela est moins spécifique à la POO, serait de représenter vos attributs de caractère sous forme de liste afin que vous n'ayez pas à les avoir tous définis explicitement dans votre code. Alors peut-être auriez-vous une liste d'armes et une liste d'attributs physiques. Créez une sorte de classe de base pour ces attributs afin qu'ils puissent interagir, de sorte que chacun soit défini en termes de dégâts, de coût énergétique, etc. Vous parcourrez la liste des attributs de chaque personnage et calculez les dommages que l'un fait à l'autre avec un degré de probabilité dans chaque interaction.

L'utilisation d'une liste vous aidera à éviter de réécrire beaucoup de code car pour ajouter un caractère avec un attribut auquel vous n'aviez pas encore pensé, vous devez simplement vous assurer qu'il a une interaction qui fonctionne avec votre système existant.

GenericJam
la source
4
Je pense que le point est de dissocier les statistiques de la classe du héros. L'hérédité n'est pas nécessairement la meilleure solution de POO (dans ma réponse, j'ai plutôt utilisé la composition).
Pierre Arlaud
1
J'appuie le commentaire précédent. Que se passe-t-il si un caractère C possède des propriétés des caractères A et B (qui ont tous deux la même classe de base)? Vous dupliquez du code ou rencontrez des problèmes concernant l' héritage multiple . Je préférerais la composition à l'héritage dans ce cas.
ComFreek
2
C'est suffisant. Je donnais juste au PO quelques options. Il ne semble pas qu'il y ait beaucoup de pierre gravée à ce stade et je ne connais pas leur niveau d'expérience. Il serait probablement un peu difficile de s'attendre à ce qu'un débutant réussisse un polymorphisme complet, mais l'héritage est assez simple pour qu'un débutant puisse le comprendre. J'essayais d'aider à résoudre le problème du sentiment du code «maladroit» qui, je ne peux que supposer, fait référence à des champs codés en dur qui peuvent ou non être utilisés. L'utilisation d'une liste pour ces types de valeurs non définies à mon humble avis est une bonne option.
GenericJam
1
-1: "La façon la plus OOP de faire les choses serait probablement de faire quelque chose avec héritage." L'héritage n'est pas particulièrement orienté objet.
Sean Middleditch
3

Je recommanderais un gestionnaire de types de statistiques, rempli à partir d'un fichier de données (par exemple, j'utilise XML) et d'objets Stat, avec un type et une valeur stockés dans l'insatnce de caractère sous forme de table de hachage, avec l'ID unique du type de statistique comme clé.

Edit: Code 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;
    }
}
DFreeman
la source
Je pense que la StatTypeclasse n'est pas nécessaire, utilisez simplement le namedu StatTypecomme clé. Comme l'a fait #Grimston. La même chose avec Stats.
giannis christofakis
Je préfère utiliser une classe dans des cas comme celui-ci afin que vous puissiez modifier librement le nom (tandis que l'id reste constant). Trop dans ce cas? Peut-être, mais puisque cette technique peut être utilisée pour quelque chose de similaire ailleurs dans un programme, je le ferais comme ça pour la cohérence.
DFreeman
0

Je vais essayer de vous donner un exemple sur la façon dont vous pouvez concevoir votre arsenal et votre arsenal.

Notre objectif est de découpler les entités, donc l'arme devrait être une interface.

interface Weapon {
    public int getDamage();
}

Supposons que chaque joueur ne puisse posséder qu'une seule arme, nous pouvons utiliser le Strategy pattern pour changer facilement d'armes.

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 autre modèle utile serait le modèle d'objet nul au cas où le joueur n'est pas armé.

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

Quant à l'armurerie, nous pouvons porter plusieurs équipements de défense.

// 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;
    }    
}

Pour le découplage, j'ai créé une interface pour le défenseur.

interface Defender {
    int getDefended();
}

Et 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;
    }

}

Ajoutons un peu de 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);      
    }
}

Voila!

giannis christofakis
la source
2
Cette réponse serait plus utile lorsque vous expliqueriez votre raisonnement derrière vos choix de conception.
Philipp