Architektura OOP dla Hero z wieloma atrybutami

14

Mam zamiar rozpocząć prostą grę RPG z tekstem przeglądarki, w której postacie mogą (pasywnie) walczyć z innymi ludźmi. Obejmuje to listę około 10 umiejętności, takich jak siła, zręczność itd., Z dodatkowymi umiejętnościami dla różnych rodzajów broni.

Czy istnieje lepszy sposób zaprojektowania tej klasy postaci niż posiadanie tych umiejętności jako atrybutu klasy? Wydaje się to łatwe, ale niechętnie, bo to niezręczność.

class Char(self):
    int strength
    int dexterity
    int agility
    ...
    int weaponless
    int dagger
    ...
Sven
źródło
1
Powinieneś sprawdzić cały ten przewodnik do pisania gier i jak niektóre z typowych klas mogą wyglądać jak link
smoki
@dragons Dzięki za interesujący link, ale nie widzę głębszych wyjaśnień dotyczących projektowania Charactorklasy?
Sven
1
Co dokładnie uważasz za „niezgrabne” w tym projekcie?
Thomas

Odpowiedzi:

17

Tak długo, jak utrzymujesz swój system względnie prosty, powinno to działać. Ale kiedy dodasz takie tymczasowe modyfikatory umiejętności, wkrótce zobaczysz dużo zduplikowanego kodu. Będziesz także mieć problemy z różnymi rodzajami broni używającymi różnych umiejętności. Ponieważ każda umiejętność jest inną zmienną, będziesz musiał napisać inny kod dla każdego rodzaju umiejętności, który zasadniczo robi to samo (lub użyj brzydkich refleksów - pod warunkiem, że obsługuje je Twój język programowania).

Z tego powodu zaleciłbym przechowywanie zarówno umiejętności, jak i biegłości w asocjacyjnej strukturze danych, która odwzorowuje stałe umiejętności na wartości. Jak to zrobić elegancko różni się od języka programowania do języka programowania. Kiedy twój język to obsługuje, stałe powinny być w enum.

Aby dać przykład, jak to działałoby w praktyce, kod do obliczania obrażeń od ataku wyglądałby następująco:

int damage = attacker.getSkill(STRENGTH) + 
             attacker.getProficiency(weapon.getProficiencyRequired()) -
             defender.getSkill(TOUGHNESS);
Philipp
źródło
Stworzenie takiej metody getSkill()jest sprzeczne z podstawową zasadą programowania obiektowego .
Jefir
4

Dlaczego nie skorzystać z powiązanych tablic ?, daje to zaletę łatwego rozszerzenia (na przykład za pomocą PHP)

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

dla rzeczy takich jak broń, prawdopodobnie chciałbyś stworzyć pewne klasy podstawowe

Broń -> Broń biała, Broń dystansowa

i stamtąd stwórz swoją broń.

Ostatecznym rezultatem, do którego dążyłbym, jest klasa wyglądająca tak

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;

    }
}

Możesz również przechowywać wszystko w tablicy, jeśli naprawdę tego chcesz.

Grimston
źródło
To właściwie to, co powiedział @Philipp?
AturSams,
1
@Filipp zasugerował użycie wyliczeń, tablice to kolejna opcja.
Grimston,
W rzeczywistości mówi: „... asocjacyjna struktura danych, która odwzorowuje stałe umiejętności na wartości”, stałe w słowniku mogą być łańcuchami.
AturSams,
3

Postaram się odpowiedzieć na to pytanie w jak najbardziej OOP (a przynajmniej tak, jak sądzę, będzie). Może to być całkowicie przesada, w zależności od ewolucji, które widzisz na temat statystyk.

Możesz sobie wyobrazić klasę SkillSet (lub Stats ) (do tej odpowiedzi używam składni podobnej do C):

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

    }

}

Wtedy bohater miałby wewnętrzne pole typu SkillSet. Broń może mieć także umiejętność modyfikatora.

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

}

Jest to oczywiście przykład, który da ci pomysł. Możesz również rozważyć użycie wzorca projektowego Decorator, aby modyfikatory statystyk działały jako „filtry”, które są stosowane jeden po drugim…

Pierre Arlaud
źródło
Jak to jest w kształcie litery C?
bogglez
@bogglez: Ja również używam „C'ish” w odniesieniu do pseudokodu, który luźno przypomina język nawiasów klamrowych, nawet jeśli zaangażowane są klasy. Masz jednak rację: wygląda to na bardziej konkretną składnię - to niewielka zmiana lub dwie od bycia kompilowanym Javą.
cHao
Nie sądzę, żebym była tu zbyt surowa. To nie jest tylko różnica w składni, ale różnica w semantyce. W C nie ma czegoś takiego jak klasa, szablony, chronione kwalifikatory, metody, funkcje wirtualne itp. Po prostu nie lubię tego przypadkowego nadużywania terminów.
bogglez
1
Jak powiedzieli inni, składnia nawiasów klamrowych pochodzi z C. Składnia Java (lub C # w tym przypadku) jest bardzo zainspirowana tym stylem.
Pierre Arlaud
3

Najbardziej otwartym sposobem na zrobienie rzeczy prawdopodobnie byłoby zrobienie czegoś z dziedziczeniem. Twoją klasą podstawową (lub superklasą w zależności od języka) byłaby osoba, a może czarne charaktery i bohaterowie odziedziczą po klasie podstawowej. Wtedy twoi bohaterowie oparti na sile i bohaterowie lotni rozgałęziają się, ponieważ ich sposób transportu jest inny, na przykład. Ma to dodatkową zaletę, że gracze komputerowi mogą mieć tę samą klasę podstawową, co gracze będący ludźmi, i mam nadzieję, że uprości to twoje życie.

Inną kwestią dotyczącą atrybutów, a jest to mniej specyficzne dla OOP, byłoby reprezentowanie atrybutów postaci jako listy, więc nie musisz mieć ich wszystkich zdefiniowanych w kodzie. Więc może masz listę broni i listę atrybutów fizycznych. Stwórz jakąś klasę bazową dla tych atrybutów, aby mogły ze sobą współdziałać, aby każda z nich była zdefiniowana w kategoriach obrażeń, kosztów energii itp., Więc kiedy dwie osoby spotkają się razem, jest stosunkowo jasne, w jaki sposób nastąpi interakcja. Powtarzalibyśmy listę atrybutów każdej postaci i obliczali obrażenia zadawane drugiemu z pewnym prawdopodobieństwem w każdej interakcji.

Korzystanie z listy pomoże ci uniknąć przepisywania dużej ilości kodu, ponieważ aby dodać znak z atrybutem, o którym jeszcze nie pomyślałeś, musisz tylko upewnić się, że ma interakcję, która działa z istniejącym systemem.

GenericJam
źródło
4
Myślę, że chodzi o to, aby oddzielić statystyki od klasy bohatera. Dziedziczenie niekoniecznie jest najlepszym rozwiązaniem OOP (w mojej odpowiedzi użyłem zamiast tego kompozycji).
Pierre Arlaud,
1
Popieram poprzedni komentarz. Co się stanie, jeśli znak C ma właściwości od znaku A i B (oba mają tę samą klasę podstawową)? Duplikujesz kod lub napotykasz problemy związane z wielokrotnym dziedziczeniem . W tej sprawie wolałbym kompozycję niż dziedziczenie.
ComFreek,
2
Słusznie. Dałem OP tylko kilka opcji. Wygląda na to, że na tym etapie nie ma zbyt wiele kamienia i nie znam ich doświadczenia. Prawdopodobnie byłoby trochę dużo oczekiwać od początkującego od rozwinięcia w pełni rozwiniętego polimorfizmu, ale dziedziczenie jest wystarczająco proste, aby początkujący mógł go zrozumieć. Próbowałem pomóc w rozwiązaniu problemu, że kod wydaje się „niezdarny”, co, jak sądzę, odnosi się do posiadania na stałe zakodowanych pól, które mogą, ale nie muszą, zostać użyte. Korzystanie z listy dla tego typu niezdefiniowanych wartości jest dobrym rozwiązaniem.
GenericJam
1
-1: „Najbardziej zaawansowanym sposobem wykonywania rzeczy prawdopodobnie byłoby zrobienie czegoś z dziedziczeniem.” Dziedziczenie nie jest szczególnie zorientowane obiektowo.
Sean Middleditch
3

Poleciłbym menedżera typów statystyk, wypełnionego z pliku danych (np. Używam XML) i obiektów Stat, z typem i wartością przechowywaną w insatnce znaku jako hashtable, z unikalnym identyfikatorem typu stat jako kluczem.

Edycja: Kod 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
źródło
Myślę, że StatTypeklasa w niepotrzebny, wystarczy użyć namez StatTypejako klucz. Tak jak #Grimston. To samo z Stats.
giannis christofakis
Wolę używać klasy w takich przypadkach, abyś mógł dowolnie modyfikować nazwę (podczas gdy id pozostaje stały). Przesadzenie w tym przypadku? Być może, ale ponieważ tę technikę można zastosować do czegoś podobnego gdzie indziej w programie, zrobiłbym to w ten sposób dla zachowania spójności.
DFreeman
0

Ι postaram się dać ci przykład, jak zaprojektować swój arsenał i zbrojownię.

Naszym celem jest oddzielenie bytów, dlatego broń powinna być interfejsem.

interface Weapon {
    public int getDamage();
}

Załóżmy, że każdy gracz może posiadać tylko jedną broń, możemy jej użyć Strategy patternw celu łatwej zmiany broni.

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

Innym przydatnym wzorem byłby wzorzec zerowy na wypadek, gdyby gracz nie był uzbrojony.

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

Jeśli chodzi o zbrojownię, możemy nosić wiele ekwipunku obronnego.

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

Dla oddzielenia stworzyłem interfejs dla obrońcy.

interface Defender {
    int getDefended();
}

I Playerklasa.

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

}

Dodajmy trochę rozgrywki.

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
źródło
2
Ta odpowiedź byłaby bardziej pomocna, gdy wyjaśniłbyś swoje uzasadnienie przy wyborze projektu.
Philipp