Arquitetura OOP para Hero com muitos atributos

14

Estou prestes a iniciar um simples RPG de texto no navegador, com personagens que podem (passivamente) combater outras pessoas. Isso envolve uma lista de cerca de 10 habilidades, como força, destreza e assim por diante, com proficiências adicionais para armas diferentes.

Existe uma maneira melhor de projetar essa classe de personagem do que ter essas habilidades como atributo de classe? Parece fácil, mas estou relutante porque é desajeitado.

class Char(self):
    int strength
    int dexterity
    int agility
    ...
    int weaponless
    int dagger
    ...
Sven
fonte
1
Você deve verificar essa guia alltogether para escrever jogos e como algumas das classes comuns pode parecer ligação
dragões
@dragons Obrigado pelo link interessante, mas não vejo uma explicação mais profunda para projetar a Charactorclasse?
Sven
1
O que exatamente você acha "desajeitado" nesse design?
Thomas

Respostas:

17

Contanto que você mantenha seu sistema relativamente simples, isso deve funcionar. Mas quando você adiciona itens como modificadores de habilidades temporários, em breve você verá muitos códigos duplicados. Você também terá problemas com armas diferentes, usando diferentes habilidades. Como cada habilidade é uma variável diferente, você terá que escrever um código diferente para cada tipo de habilidade, que basicamente faz o mesmo (ou usa alguns truques feios de reflexão - sob a condição de que sua linguagem de programação os suporta).

Por esse motivo, recomendo que você armazene habilidades e proficiências em uma estrutura de dados associativa que mapeie constantes de habilidades em valores. Como fazer isso difere elegantemente da linguagem de programação para a linguagem de programação. Quando seu idioma suportar, as constantes devem estar em um enum.

Para dar um exemplo de como isso funcionaria na prática, seu código para calcular o dano de ataque ficaria assim:

int damage = attacker.getSkill(STRENGTH) + 
             attacker.getProficiency(weapon.getProficiencyRequired()) -
             defender.getSkill(TOUGHNESS);
Philipp
fonte
Criar um método como getSkill()vai contra o princípio fundamental da programação orientada a objetos .
Jephir 27/03
4

Por que não usar matrizes associadas ?, isso oferece o benefício de ser facilmente estendido (usando PHP, por exemplo)

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

para coisas como armas, você provavelmente desejaria criar algumas classes base

Arma -> Arma corpo a corpo, Arma de longo alcance

e então crie suas armas a partir daí.

O resultado final que eu buscaria é uma classe parecida com 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;

    }
}

Você pode armazenar tudo em uma matriz se realmente quiser.

Grimston
fonte
Foi o que o @Philipp disse?
Lobo-da-
1
@ Philipp sugeriu o uso de enums, arrays são outra opção.
Grimston
Na verdade, ele diz: "... estrutura de dados associativa que mapeia constantes de habilidades em valores", as constantes no dicionário podem ser cadeias de caracteres.
wolfdawn
3

Vou tentar responder a essa pergunta da maneira mais OOP (ou pelo menos o que eu acho que seria). Pode ser um exagero, dependendo das evoluções que você vê sobre as estatísticas.

Você pode imaginar uma classe SkillSet (ou Stats ) (estou usando a sintaxe do tipo C para esta resposta):

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

    }

}

Então o herói teria um campo intrínsecoStats do tipo SkillSet. Uma arma também pode ter uma habilidade 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();

}

Obviamente, este é um exemplo para você ter uma ideia. Você também pode considerar o uso do padrão de design Decorator, para que os modificadores nas estatísticas funcionem como "filtros" aplicados um após o outro…

Pierre Arlaud
fonte
Como é isso C-like?
bogglez
@bogglez: Eu também costumo usar "C'ish" para me referir ao pseudocódigo que se parece muito com uma linguagem de chaves, mesmo se houver aulas envolvidas. Você tem um ponto: porém, isso parece uma sintaxe bem mais específica - é uma pequena alteração ou duas de distância de ser Java compilável.
procurando
Eu não acho que estou sendo muito rigoroso aqui. Esta não é apenas uma diferença na sintaxe, mas uma diferença na semântica. Em C, não existe classe, modelos, qualificadores protegidos, métodos, funções virtuais etc. Não gosto muito desse abuso casual de termos.
bogglez
1
Como os outros disseram, a sintaxe entre chaves vem do C. A sintaxe do Java (ou C #) é altamente inspirada nesse estilo.
Pierre Arlaud 20/03
3

A maneira mais OOP de fazer as coisas provavelmente seria fazer algo com herança. Sua classe base (ou super classe dependendo do idioma) seria pessoal, então talvez vilões e heróis herdem da classe base. Então seus heróis baseados em força e heróis baseados em vôo se ramificariam, pois seu modo de transporte é diferente, por exemplo. Isso tem o bônus adicional de que os jogadores de computador podem ter a mesma classe base que os jogadores humanos e, com sorte, isso simplificará sua vida.

A outra coisa sobre os atributos, e isso é menos específico do OOP, seria representar os atributos do seu personagem como uma lista, para que você não precise defini-los todos explicitamente no seu código. Então, talvez você tenha uma lista de armas e uma lista de atributos físicos. Faça algum tipo de classe base para esses atributos para que eles possam interagir, para que cada um seja definido em termos de dano, custo de energia etc. Para que, quando dois indivíduos se reúnam, fique relativamente claro como a interação ocorrerá. Você percorre a lista de atributos de cada personagem e calcula o dano que um causa ao outro com um grau de probabilidade em cada interação.

O uso de uma lista ajudará a evitar a reescrita de muito código, pois, para adicionar um caractere com um atributo que você ainda não tinha pensado, basta garantir que ele tenha uma interação que funcione com o sistema existente.

GenericJam
fonte
4
Eu acho que o ponto é separar as estatísticas da classe do herói. A herança não é necessariamente a melhor solução de POO (na minha resposta, usei composição).
22614 Pierre Arlaud
1
Eu segundo o comentário anterior. O que acontece se um caractere C tiver propriedades do caractere A e B (ambos com a mesma classe base)? Você duplica o código ou enfrenta alguns problemas relacionados à herança múltipla . Eu preferiria a composição à herança neste caso.
ComFreek
2
Justo. Eu estava apenas dando ao OP algumas opções. Não parece que haja muita coisa gravada nesse estágio e eu não conheço o nível de experiência deles. Provavelmente seria um pouco demais esperar que um iniciante desenvolvesse o polimorfismo completo, mas a herança é simples o suficiente para que um iniciante possa entender. Eu estava tentando ajudar a resolver o problema do código, sentindo-se 'desajeitado', que só posso assumir que se refere a campos codificados que podem ou não ser utilizados. Usar uma lista para esses tipos de valores indefinidos é uma boa opção.
GenericJam
1
-1: "A maneira mais OOP de fazer as coisas provavelmente seria fazer algo com herança." A herança não é muito particularmente orientada a objetos.
Sean Middleditch
3

Eu recomendaria um gerenciador de tipos de estatísticas, preenchido a partir de um arquivo de dados (por exemplo, uso XML) e objetos Stat, com um tipo e um valor armazenados no caractere inserido como uma hashtable, com o ID exclusivo do tipo de estatística como chave.

Edit: Psudo code

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
fonte
Eu acho que a StatTypeaula é desnecessária, basta usar o nameda StatTypecomo uma chave. Como #Grimston fez. O mesmo com Stats.
Giannis christofakis
Prefiro usar uma classe em instâncias como essa para que você possa modificar livremente o nome (enquanto o ID permanece constante). Excesso neste caso? Talvez, mas como essa técnica pode ser usada para algo semelhante em outro lugar de um programa, eu o faria assim para garantir a consistência.
DFreeman
0

Vou tentar dar um exemplo de como você pode projetar seu arsenal e seu arsenal.

Nosso objetivo é desacoplar entidades, portanto, a arma deve ser uma interface.

interface Weapon {
    public int getDamage();
}

Suponhamos que cada jogador possua apenas uma arma; podemos usá-la Strategy patternpara trocar de arma facilmente.

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

Outro padrão útil seria o Padrão de Objeto Nulo, caso o jogador esteja desarmado.

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

Quanto ao arsenal, podemos usar vários equipamentos de defesa.

// 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 dissociar, criei uma interface para o defensor.

interface Defender {
    int getDefended();
}

E a Playerturma.

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

}

Vamos adicionar um pouco de jogabilidade.

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
fonte
2
Essa resposta seria mais útil quando você explicaria seu raciocínio por trás de suas escolhas de design.
Philipp