OOP-Architektur für Hero mit vielen Attributen

14

Ich bin dabei, ein einfaches Browser-Text-RPG zu starten, mit Charakteren, die (passiv) gegen andere Leute kämpfen können. Dies beinhaltet eine Liste von ungefähr 10 Fertigkeiten wie Stärke, Geschicklichkeit usw. mit zusätzlichen Fähigkeiten für verschiedene Waffen.

Gibt es eine bessere Möglichkeit, diese Charakterklasse zu entwerfen, als nur diese Fähigkeiten als Klassenattribut zu haben? Es scheint einfach, aber ich zögere, weil es Ungeschicklichkeit ist.

class Char(self):
    int strength
    int dexterity
    int agility
    ...
    int weaponless
    int dagger
    ...
Sven
quelle
1
Sie sollten diese alltogether Anleitung zum Schreiben Spiele überprüfen und , wie einige der gemeinsamen Klassen aussehen könnte Link
Drachen
@dragons Danke für den interessanten Link, aber ich sehe keine tiefere Erklärung für das Entwerfen der CharactorKlasse?
Sven
1
Was genau finden Sie "ungeschickt" an diesem Design?
Thomas

Antworten:

17

Solange Sie Ihr System relativ einfach halten, sollte dies funktionieren. Wenn Sie jedoch Dinge wie temporäre Fähigkeitsmodifikatoren hinzufügen, werden Sie bald viel doppelten Code sehen. Sie werden auch auf Probleme mit verschiedenen Waffen mit unterschiedlichen Fähigkeiten stoßen. Da jeder Skill eine andere Variable ist, müssen Sie für jeden Skill-Typ einen anderen Code schreiben, der im Grunde dasselbe tut (oder einige hässliche Reflection-Hacks verwenden - unter der Voraussetzung, dass Ihre Programmiersprache sie unterstützt).

Aus diesem Grund würde ich Ihnen empfehlen, sowohl Fähigkeiten als auch Fertigkeiten in einer assoziativen Datenstruktur zu speichern, die Fähigkeitskonstanten auf Werte abbildet. Wie das elegant gemacht wird, ist von Programmiersprache zu Programmiersprache unterschiedlich. Wenn Ihre Sprache dies unterstützt, sollten sich die Konstanten in einem befinden enum.

Um Ihnen ein Beispiel zu geben, wie dies in der Praxis funktionieren würde, würde Ihr Code zur Berechnung des Angriffsschadens ungefähr so ​​aussehen:

int damage = attacker.getSkill(STRENGTH) + 
             attacker.getProficiency(weapon.getProficiencyRequired()) -
             defender.getSkill(TOUGHNESS);
Philipp
quelle
Das Erstellen einer Methode widersprichtgetSkill() dem Grundprinzip der objektorientierten Programmierung .
Jephir
4

Warum nicht verknüpfte Arrays verwenden? Dies bietet den Vorteil einer einfachen Erweiterung (beispielsweise mit PHP).

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

Für Dinge wie Waffen möchten Sie wahrscheinlich einige Basisklassen erstellen

Waffe -> Nahkampfwaffe, Fernkampfwaffe

und dann erstellen Sie Ihre Waffen von dort.

Das Endergebnis, das ich anstreben würde, ist eine Klasse, die so aussieht

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;

    }
}

Sie könnten alles in einem Array speichern, wenn Sie es auch wirklich wollten.

Grimston
quelle
Das ist so ziemlich das, was @Philipp gesagt hat?
Wolfdawn
1
@Philipp schlug die Verwendung von Enums vor, Arrays sind eine weitere Option.
Grimston
Tatsächlich heißt es: "... assoziative Datenstruktur, die Fertigkeitskonstanten auf Werte abbildet", die Konstanten im Wörterbuch könnten Zeichenfolgen sein.
Wolfdawn
3

Ich werde versuchen, diese Frage so schnell wie möglich zu beantworten (oder zumindest so, wie ich es mir vorstelle). Es kann völlig übertrieben sein, abhängig von den Entwicklungen, die Sie über die Statistiken sehen.

Sie können sich eine SkillSet (oder Stats ) -Klasse vorstellen ( für diese Antwort verwende ich eine C-ähnliche Syntax):

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

    }

}

Dann hätte der Held ein intrinsicStats-Feld vom Typ SkillSet. Eine Waffe könnte auch einen Modifikator skillSet haben.

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

}

Dies ist natürlich ein Beispiel, um Ihnen eine Idee zu geben. Sie können auch das Decorator-Entwurfsmuster verwenden, damit die Modifikatoren in den Statistiken als "Filter" fungieren, die nacheinander angewendet werden.

Pierre Arlaud
quelle
Wie ist das C-like?
Bogglez
@bogglez: Ich neige ebenfalls dazu, "C'ish" zu verwenden, um auf einen Pseudocode zu verweisen, der einer geschweiften Klammer ähnelt, auch wenn Klassen beteiligt sind. Sie haben jedoch einen Punkt: Dies sieht nach einer spezifischeren Syntax aus - es ist eine kleine Änderung oder zwei davon entfernt, kompilierbares Java zu sein.
CHAO
Ich glaube nicht, dass ich hier zu streng bin. Dies ist nicht nur ein Unterschied in der Syntax, sondern auch in der Semantik. In C gibt es keine Klasse, keine Vorlagen, keine geschützten Qualifikationsmerkmale, keine Methoden, keine virtuellen Funktionen usw. Ich mag diesen gelegentlichen Missbrauch von Begriffen einfach nicht.
Bogglez
1
Wie die anderen sagten, stammt die Syntax für geschweifte Klammern von C. Die Syntax von Java (oder auch C #) ist stark von diesem Stil inspiriert.
Pierre Arlaud
3

Die beste Art, Dinge zu tun, wäre wahrscheinlich, etwas mit Vererbung zu tun. Ihre Basisklasse (oder Superklasse, abhängig von der Sprache) wäre eine Person, dann erben vielleicht Schurken und Helden von der Basisklasse. Dann würden sich deine kraftbasierten Helden und flugbasierten Helden verzweigen, da sich beispielsweise ihr Transportmittel unterscheidet. Dies hat den zusätzlichen Bonus, dass Ihre Computerspieler die gleiche Basisklasse wie die menschlichen Spieler haben können und dies wird hoffentlich Ihr Leben vereinfachen.

Die andere Sache in Bezug auf Attribute, und dies ist weniger OOP-spezifisch, besteht darin, Ihre Zeichenattribute als Liste darzustellen, damit Sie sie nicht alle explizit in Ihrem Code definieren müssen. Vielleicht hätten Sie eine Liste für Waffen und eine Liste für physische Attribute. Erstellen Sie eine Art Basisklasse für diese Attribute, damit sie interagieren können. Jedes Attribut wird in Bezug auf Schaden, Energiekosten usw. definiert. Wenn also zwei Personen zusammenkommen, ist relativ klar, wie die Interaktion stattfinden wird. Sie würden die Liste der Attribute jedes Charakters durchlaufen und den Schaden, den ein Charakter dem anderen zufügt, mit einer gewissen Wahrscheinlichkeit in jeder Interaktion berechnen.

Mithilfe einer Liste können Sie vermeiden, dass viel Code neu geschrieben wird, da Sie zum Hinzufügen eines Zeichens mit einem Attribut, an das Sie noch nicht gedacht haben, nur sicherstellen müssen, dass es eine Interaktion mit Ihrem vorhandenen System aufweist.

GenericJam
quelle
4
Ich denke, es geht darum, die Statistiken von der Klasse des Helden abzukoppeln. Vererbung ist nicht unbedingt die beste OOP-Lösung (in meiner Antwort habe ich stattdessen Komposition verwendet).
Pierre Arlaud
1
Ich stimme dem vorherigen Kommentar zu. Was passiert, wenn ein Zeichen C Eigenschaften von Zeichen A und B hat (die beide dieselbe Basisklasse haben)? Sie duplizieren entweder Code oder haben Probleme mit der Mehrfachvererbung . In diesem Fall würde ich die Komposition der Vererbung vorziehen.
ComFreek
2
Meinetwegen. Ich habe dem OP nur ein paar Optionen gegeben. Es sieht nicht so aus, als wäre zu diesem Zeitpunkt viel in Stein gemeißelt, und ich kenne ihr Erfahrungsniveau nicht. Es wäre wahrscheinlich ein bisschen viel zu erwarten, dass ein Anfänger einen ausgewachsenen Polymorphismus vollbringt, aber die Vererbung ist einfach genug, damit ein Anfänger sie begreifen kann. Ich habe versucht, das Problem zu lösen, dass sich der Code "ungeschickt" anfühlt, von dem ich nur annehmen kann, dass er Felder enthält, die hart codiert sind und möglicherweise nicht verwendet werden. Die Verwendung einer Liste für diese Arten von undefinierten Werten ist eine gute Option.
GenericJam
1
-1: "Die meiste OOP-Methode wäre wahrscheinlich, etwas mit Vererbung zu tun." Vererbung ist nicht besonders objektorientiert.
Sean Middleditch
3

Ich würde einen Stat-Typ-Manager empfehlen, der aus einer Datendatei (z. B. ich verwende XML) und Stat-Objekten mit einem Typ und einem Wert, die in der Character Insatnce als Hash-Tabelle gespeichert sind, und der eindeutigen ID des Stat-Typs als Schlüssel besteht.

Bearbeiten: 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
quelle
Ich halte den StatTypeUnterricht für unnötig, benutze einfach das namevon StatTypeals Schlüssel. Wie #Grimston. Das selbe mit Stats.
Giannis Christofakis
Ich bevorzuge die Verwendung einer Klasse in solchen Fällen, damit Sie den Namen frei ändern können (während die ID konstant bleibt). Übermaß in diesem Fall? Vielleicht, aber da diese Technik für etwas Ähnliches an anderer Stelle in einem Programm verwendet werden kann, würde ich es aus Gründen der Beständigkeit so machen.
DFreeman
0

Ich werde versuchen, Ihnen ein Beispiel zu geben, wie Sie Ihr Arsenal und Ihre Waffenkammer entwerfen können.

Unser Ziel ist es, Entitäten zu entkoppeln, daher sollte die Waffe eine Schnittstelle sein.

interface Weapon {
    public int getDamage();
}

Angenommen, jeder Spieler kann nur eine Waffe besitzen, dann können wir die verwenden Strategy pattern um Waffen einfach zu wechseln.

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

Ein weiteres nützliches Muster wäre das Null-Objektmuster falls der Spieler ist.

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

Was die Waffenkammer betrifft, können wir mehrere Verteidigungsausrüstungen tragen.

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

Zum Entkoppeln habe ich ein Interface für den Verteidiger erstellt.

interface Defender {
    int getDefended();
}

Und die Player Klasse.

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

}

Fügen wir etwas Gameplay hinzu.

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
quelle
2
Diese Antwort wäre hilfreicher, wenn Sie Ihre Überlegungen zu Ihren Entwurfsentscheidungen erläutern würden.
Philipp