Arquitectura OOP para Hero con muchos atributos

14

Estoy a punto de comenzar un simple juego de rol de texto del navegador, con personajes que pueden (pasivamente) luchar contra otras personas. Esto implica una lista de aproximadamente 10 habilidades como fuerza, destreza, etc., con habilidades adicionales para diferentes armas.

¿Hay una mejor manera de diseñar esta clase de personaje que simplemente tener esas habilidades como atributo de clase? Parece fácil, pero soy reacio porque es torpeza.

class Char(self):
    int strength
    int dexterity
    int agility
    ...
    int weaponless
    int dagger
    ...
Sven
fuente
1
Deberías consultar esta guía completa para escribir juegos y cómo algunas de las clases comunes podrían parecer enlaces
dragones
@ Dragones Gracias por el interesante enlace, pero no veo una explicación más profunda para diseñar la Charactorclase.
Sven
1
¿Qué es exactamente lo que le parece "torpe" de este diseño?
Thomas

Respuestas:

17

Mientras mantenga su sistema relativamente simple, esto debería funcionar. Pero cuando agrega cosas como modificadores de habilidades temporales, pronto verá una gran cantidad de código duplicado. También te encontrarás con problemas con diferentes armas usando diferentes habilidades. Debido a que cada habilidad es una variable diferente, tendrás que escribir un código diferente para cada tipo de habilidad que básicamente hace lo mismo (o usar algunos trucos de reflexión feos, bajo la condición de que tu lenguaje de programación los admita).

Por esa razón, le recomendaría que almacene tanto las habilidades como las competencias en una estructura de datos asociativa que asigne constantes de habilidades a valores. Cómo hacerlo difiere elegantemente del lenguaje de programación al lenguaje de programación. Cuando su idioma lo admite, las constantes deben estar en unenum .

Para darle un ejemplo de cómo funcionaría esto en la práctica, su código para calcular el daño de ataque se vería así:

int damage = attacker.getSkill(STRENGTH) + 
             attacker.getProficiency(weapon.getProficiencyRequired()) -
             defender.getSkill(TOUGHNESS);
Philipp
fuente
Crear un método como getSkill()va en contra del principio fundamental de la programación orientada a objetos .
Jephir
4

¿Por qué no usar matrices asociadas ?, esto brinda el beneficio de ser fácilmente extendido (usando PHP, por ejemplo)

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

para cosas como las armas, probablemente quieras crear algunas clases base

Arma -> MeleeWeapon, RangedWeapon

y luego crea tus armas desde allí.

El resultado final al que apuntaría es una clase como esta

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;

    }
}

Podrías almacenar todo en una matriz si realmente quisieras también.

Grimston
fuente
¿Eso es más o menos lo que dijo @Philipp?
AturSams
1
@Philipp sugirió el uso de enumeraciones, las matrices son otra opción.
Grimston
En realidad dice: "... estructura de datos asociativa que asigna constantes de habilidad a valores", las constantes en el diccionario podrían ser cadenas.
AturSams
3

Trataré de responder a esta pregunta de la manera más orientada a los usuarios (o al menos lo que creo que sería) Puede ser completamente exagerado, dependiendo de las evoluciones que veas sobre las estadísticas.

Podrías imaginar una clase SkillSet (o Stats ) (estoy usando una sintaxis tipo C para esta respuesta):

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

    }

}

Entonces el héroe tendría un campo intrínsecoStats del tipo SkillSet. Un arma también podría tener una habilidad modificadora.

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

}

Por supuesto, este es un ejemplo para darle una idea. También puede considerar usar el patrón de diseño Decorador, para que los modificadores en las estadísticas funcionen como "filtros" que se aplican uno tras otro ...

Pierre Arlaud
fuente
¿Cómo es esto como C?
bogglez
@bogglez: Del mismo modo, tiendo a usar "C'ish" para referirme al pseudocódigo que se asemeja a un lenguaje de llaves, incluso si hay clases involucradas. Sin embargo, tiene un punto: esto parece una sintaxis mucho más específica: es un cambio menor o dos lejos de ser Java compilable.
cHao
No creo que esté siendo demasiado estricto aquí. Esto no es solo una diferencia en la sintaxis, sino una diferencia en la semántica. En C no existe una clase, plantillas, calificadores protegidos, métodos, funciones virtuales, etc. Simplemente no me gusta este abuso casual de términos.
bogglez
1
Como dijeron los demás, la sintaxis de llaves se deriva de C. La sintaxis de Java (o C # para el caso) está muy inspirada en este estilo.
Pierre Arlaud
3

La forma más eficaz de hacer las cosas probablemente sería hacer algo con herencia. Tu clase base (o superclase dependiendo del idioma) sería una persona, entonces quizás los villanos y héroes hereden de la clase base. Entonces, tus héroes basados ​​en la fuerza y ​​los héroes basados ​​en el vuelo se ramificarían, ya que su modo de transporte es diferente, por ejemplo. Esto tiene la ventaja adicional de que los jugadores de tu computadora pueden tener la misma clase base que los jugadores humanos y, con suerte, simplificará tu vida.

La otra cosa con respecto a los atributos, y esto es menos específico de OOP, sería representar los atributos de tu personaje como una lista para que no tengas que tenerlos definidos explícitamente en tu código. Entonces, tal vez tendrías una lista de armas y una lista de atributos físicos. Haga algún tipo de clase base para estos atributos para que puedan interactuar, de modo que cada uno se defina en términos de daño, costo de energía, etc., de modo que cuando dos personas se unan, sea relativamente claro cómo se llevará a cabo la interacción. Recorrería la lista de atributos de cada personaje y calcularía el daño que uno hace al otro con un grado de probabilidad en cada interacción.

El uso de una lista lo ayudará a evitar reescribir una gran cantidad de código, ya que para agregar un carácter con un atributo que aún no había pensado, solo debe asegurarse de que tenga una interacción que funcione con su sistema existente.

GenericJam
fuente
44
Creo que el punto es desacoplar las estadísticas de la clase del héroe. La herencia no es necesariamente la mejor solución de OOP (en mi respuesta usé composición en su lugar).
Pierre Arlaud
1
Secundo el comentario anterior. ¿Qué sucede si un personaje C tiene propiedades de los caracteres A y B (que tienen la misma clase base)? O duplica el código o enfrenta algunos problemas relacionados con la herencia múltiple . Favorecería la composición sobre la herencia en este caso.
ComFreek
2
Lo suficientemente justo. Solo le estaba dando al OP algunas opciones. No parece que haya mucho en piedra en esta etapa y no sé su nivel de experiencia. Probablemente sería un poco demasiado esperar que un principiante logre un polimorfismo completo, pero la herencia es lo suficientemente simple como para que un principiante la comprenda. Intenté ayudar a abordar el problema del código que me parecía "torpe", lo que solo puedo suponer que se refiere a tener campos codificados que pueden o no usarse. Usar una lista para estos tipos de valores indefinidos es una buena opción.
GenericJam
1
-1: "La forma más obvia de hacer las cosas probablemente sería hacer algo con herencia". La herencia no está muy orientada a objetos.
Sean Middleditch
3

Recomiendo un administrador de tipos de estadísticas, poblado de un archivo de datos (por ejemplo, uso XML) y objetos de estadísticas, con un tipo y un valor almacenados en el carácter insatnce como una tabla hash, con la identificación única del tipo de estadísticas como clave.

Editar: Psudocódigo

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
fuente
Creo que la StatTypeclase es innecesaria, solo use la namede StatTypecomo clave. Como #Grimston hizo. Lo mismo con Stats.
giannis christofakis
Prefiero usar una clase en casos como este para que pueda modificar libremente el nombre (mientras que la identificación permanece constante). ¿Exceso en esta instancia? Quizás, pero dado que esta técnica puede usarse para algo similar en otro lugar de un programa, lo haría así por consistencia.
DFreeman
0

Trataré de darle un ejemplo de cómo puede diseñar su arsenal y su arsenal.

Nuestro objetivo es desacoplar entidades, por lo tanto, el arma debe ser una interfaz.

interface Weapon {
    public int getDamage();
}

Supongamos que cada jugador solo puede poseer un arma, podemos usarla Strategy patternpara cambiar las armas fácilmente.

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

Otro patrón útil sería el Patrón de objeto nulo en caso de que el jugador esté desarmado.

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

En cuanto a la armería, podemos usar múltiples equipos de defensa.

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

Para desacoplar, creé una interfaz para el defensor.

interface Defender {
    int getDefended();
}

Y la Playerclase

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

}

Agreguemos algo de juego.

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
fuente
2
Esta respuesta sería más útil cuando explicara su razonamiento detrás de sus elecciones de diseño.
Philipp