Архітектура OOP для героя з багатьма атрибутами


14

Я збираюся розпочати простий текстовий RPG браузера з символами, які можуть (пасивно) боротися з іншими людьми. Сюди входить список близько 10 навичок, таких як сила, спритність тощо, з додатковими знаннями для різних видів зброї.

Чи є кращий спосіб спроектувати цей клас символів, ніж просто мати такі навички як атрибут класу? Здається, легко, але я неохоче, тому що це незграбність.

class Char(self):
    int strength
    int dexterity
    int agility
    ...
    int weaponless
    int dagger
    ...

1
Ви повинні ознайомитись із цим посібником із написання ігор, і як деякі з загальних занять можуть виглядати як посилання
дракони

@dragons Дякую за цікаве посилання, але я не бачу більш глибокого пояснення для проектування Charactorкласу?
Свен

1
Що саме ви вважаєте «незграбним» у цьому дизайні?
Томас

Відповіді:


17

Поки ви зберігаєте свою систему відносно просто, це має працювати. Але додавши такі речі, як тимчасові модифікатори навичок, незабаром ви побачите безліч дублікатів коду. Ви також матимете проблеми з різною зброєю, використовуючи різні знання. Оскільки кожен вміння є різною змінною, вам доведеться писати різний код для кожного типу вміння, який в основному робить те саме (або використовувати деякі некрасиві хакерські відбиття - за умови, що їх підтримує їх мова програмування).

З цієї причини я рекомендую вам зберігати як вміння, так і знання в асоціативній структурі даних, яка відображає константи навичок до значень. Як це елегантно відрізняється від мови програмування до мови програмування. Коли ваша мова підтримує це, константи повинні бути в enum.

Щоб навести приклад того, як це буде працювати на практиці, ваш код для обчислення шкоди від атаки виглядатиме приблизно так:

int damage = attacker.getSkill(STRENGTH) + 
             attacker.getProficiency(weapon.getProficiencyRequired()) -
             defender.getSkill(TOUGHNESS);

Створення такого способу getSkill()суперечить основному принципу об'єктно-орієнтованого програмування .
Джефір

4

Чому б не використовувати пов’язані масиви? Це дає перевагу легкого розширення (наприклад, використання PHP)

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

для таких речей, як зброя, ви, ймовірно, хочете створити деякі базові класи

Зброя -> MeleeWeapon, RangedWeapon

а потім створіть свою зброю звідти.

Кінцевим результатом, до якого я б прагнув, є такий клас

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;

    }
}

Ви можете зберігати все в масиві, якщо теж дуже хочете.


Це майже все, що сказав @Philipp?
AturSams

1
@Philipp запропонував використовувати перерахунки, масиви - це ще один варіант.
Грімстон

Насправді сказано: "... асоціативна структура даних, яка відображає константи навичок до значень", константи в словнику можуть бути рядками.
AturSams

3

Я спробую відповісти на це питання самим способом OOP (або принаймні тим, що я думаю, що це було б). Це може бути повністю надмірним, залежно від еволюції, яку ви бачите щодо статистики.

Ви можете собі уявити клас SkillSet (або Stats ) (для цієї відповіді я використовую 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();

    }

}

Тоді герой мав би поле внутрішніх статистичних даних типу SkillSet. Зброя також може мати модифікатор skillSet.

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

}

Це, звичайно, приклад, щоб дати вам уявлення. Ви також можете скористатися схемою дизайну Decorator, щоб модифікатори на статистиці працювали як "фільтри", що застосовуються один за одним ...


Як це С-подібне?
bogglez

@bogglez: Я також схильний використовувати "C'ish" для позначення псевдокоду, який дуже нагадує мову фігурної дужки, навіть якщо в них беруть участь заняття. Однак у вас є сенс: це виглядає як спосіб більш конкретного синтаксису - це незначна зміна або дві від того, що не можна компілювати Java.
cHao

Я не думаю, що я тут надто суворий. Це не просто різниця в синтаксисі, а різниця в семантиці. У C немає такого поняття, як клас, шаблони, захищені класифікатори, методи, віртуальні функції тощо. Мені просто не подобається це випадкове зловживання умовами.
bogglez

1
Як говорили інші, фігури-фігурні синтаксиси походять від C. Синтаксис Java (або C # для цього питання) сильно натхненний цим стилем.
П'єр Арло

3

Напевно, найочікуванішим способом ведення речей, певно, було б робити щось із спадщиною. Ваш базовий клас (або суперклас, залежно від мови) був би людиною, то, можливо, лиходії та герої успадковують від базового класу. Тоді ваші герої, що базуються на силі, і герої польотів відводяться, оскільки, наприклад, їхній вид транспорту відрізняється. Це має додатковий бонус за те, що ваші комп'ютерні гравці можуть мати той самий базовий клас, що і гравці людини, і, сподіваємось, це спростить ваше життя.

Інша річ, що стосується атрибутів, і це менш специфічно для OOP було б представляти ваші символьні атрибути як список, так що не потрібно, щоб вони були чітко визначені у вашому коді. Тож, можливо, у вас був би список для зброї та список фізичних ознак. Створіть якийсь базовий клас для цих атрибутів, щоб вони могли взаємодіяти, тому кожен визначається з точки зору пошкодження, енерговитрат тощо, тож коли двоє людей збираються разом, відносно зрозуміло, як відбуватиметься взаємодія. Ви б переглянули список атрибутів кожного символу та обчислили збитки, які один завдає іншому, зі ступенем вірогідності в кожній взаємодії.

Використання списку допоможе вам не переписати багато коду, оскільки для того, щоб додати символ з атрибутом, про який ви ще не думали, вам просто потрібно переконатися, що він взаємодіє з вашою існуючою системою.


4
Я думаю, що справа в тому, щоб відірвати статистику від класу героя. Спадщина - це не обов'язково найкраще рішення OOP (у своїй відповіді я замість цього використовував склад).
П’єр Арло

1
Я другий попередній коментар. Що станеться, якщо символ C має властивості символів A і B (які обоє мають однаковий базовий клас)? Ви або дублюєте код або стикаєтеся з деякими проблемами, що стосуються багатократного успадкування . Я б віддав перевагу складу над спадщиною в цьому випадку.
ComFreek

2
Справедливо. Я просто давав ОП деякі варіанти. Не схоже, що на цьому етапі багато каменю, і я не знаю рівня їх досвіду. Напевно, було б трохи очікувати, що новачок може зняти повноцінний поліморфізм, але успадкування досить просте, щоб початківець зрозумів. Я намагався допомогти вирішити проблему коду, відчуваючи себе «незграбним», і я можу лише припустити, що це стосується того, що поля мають жорсткий код, які можуть або не можуть бути використані. Використання списку для цих типів невизначених значень imho - хороший варіант.
GenericJam

1
-1: "Найпопулярнішим способом ведення справ, напевно, було б робити щось із спадщиною". Спадкування дуже не особливо об'єктно-орієнтоване.
Шон Міддлічч

3

Я рекомендую менеджер типу stat, заповнений з файлу даних (наприклад, я використовую XML) та об'єктів Stat, з типом і значенням, що зберігаються в монтажі символів як хештебль, з унікальним ідентифікатором типу stat як ключовим.

Редагувати: Псудо-код

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

Я думаю, що StatTypeзаняття непотрібне, просто використовуйте nameклавішу StatTypeяк ключ. Як це зробив #Grimston Те саме з Stats.
giannis christofakis

Я вважаю за краще використовувати клас у таких випадках, щоб ви могли вільно змінювати ім’я (поки ідентифікатор залишається постійним). Перевищення в цьому випадку? Можливо, але так як ця методика може використовуватися для чогось подібного в іншому місці програми, я б зробив це так, як послідовність.
DFreeman

0

Я спробую навести вам приклад того, як ви можете спроектувати свій арсенал та зброю.

Наша мета - роз'єднати сутності, тому зброя повинна бути інтерфейсом.

interface Weapon {
    public int getDamage();
}

Припустимо, що кожен гравець може володіти лише однією зброєю, ми можемо використовувати його Strategy patternдля того, щоб легко змінити зброю.

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

Ще однією корисною схемою буде Null Object Pattern у випадку, якщо гравець беззбройний.

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

Що стосується озброєння, ми можемо носити декілька оборонних засобів.

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

Для роз’єднання я створив інтерфейс для захисника.

interface Defender {
    int getDefended();
}

І Playerклас.

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

}

Додамо трохи геймплея.

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

Вуаля!


2
Ця відповідь була б кориснішою, коли ви поясните свої міркування, що стоять за вашим вибором дизайну.
Філіп
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.