6
\$\begingroup\$

Question description

I am currently developing an RPG to gain programming experience. An RPG consists of many individual components such as the combat system, quest system and the item system. I decided to develop an item system first, before I develop the other parts.

I have made some philosophical thoughts on the question of how items can be mapped in object oriented code so that the code is easily understandable and suitable for further development. I would like to present my current status here and ask you whether my item concept is suitable for further development, or whether I have made design mistakes that lead to poor results in further development.

First of all, I took care of consumable items like food and potions.

Short description of my Item concept and the code

All items, whether they are items of equipment or can be consumed, have general values such as a name, a price, and can be assigned to a category. Therefore there is a corresponding base class.

Item.java

public class Item {
    protected String name;
    protected String category;
    protected int price;
    
    public Item(String name, String category, int price) {
        this.name = name;
        this.category = category;
        this.price = price;
    }
    
    public Item(String name, String category) {
        this(name, category, 0);
    }
    
    public String getName() {
        return name;
    }
    
    public String getCategory() {
        return category;
    }
    
    public int getPrice() {
        return price;
    }
}

Consumable Items are special: They can be consumed und then unfold an effect on the being that consumes an Item. I decided to create an interface that says that.

Consumable.java

public interface Consumable {
    public void unfoldEffect(Character character);
}

There are two ressources a player has: He has health and energy. The amount of health points decides how much blows he can take before he dies. The amount of energy decides how strong his attacks will be. And so there are items that fill up health and energy. Both are represented in different classes that are based on the Item-class and the Consumable-Interface.

HealthItem.java

// can be food (bread, water) or healing potions or stuff like that
public class HealthItem extends Item implements Consumable {
    private int healthPoints;
    
    public HealthItem(String name, String category, int price, int healthPoints) {
        super(name, category, price);
        this.healthPoints = healthPoints;
    }
    
    public int getHealthPoints() {
        return healthPoints;
    }
    
    public void unfoldEffect(Character character) {
        character.regenerateHealthPoints(healthPoints);
    }
}

And the enery-item-class:

EnergyItem.java

public class EnergyItem extends Item implements Consumable {
    private int energyPoints;
    
    public EnergyItem(String name, String category, int price, int energyPoints) {
        super(name, category, price);
        this.energyPoints = energyPoints;
    }
    
    public int getEnergyPoints() {
        return energyPoints;
    }
    
    public void unfoldEffect(Character character) {
        character.regenerateEnergyPoints(energyPoints);
    }   
}

And of course there has to be a class that represents characters that can act in the game. These class is the base class for human players and NPCs.

Character.java

public class Character {
    // could be an orc, animal, human and what the imagination has to offer
    private String species;
    // not every char must have a name. E. g. pigs usually dont have names
    private String name;
    private int healthPoints;
    private int healthPointsMax;
    private int energyPoints;
    private int energyPointsMax;
    
    public Character(String species, String name, int healthPoints, int healthPointsMax ,int energyPoints, int energyPointsMax) {
        this.species = species;
        this.name = name;
        this.healthPoints = healthPoints;
        this.healthPointsMax = healthPointsMax;
        this.energyPoints = energyPoints;
        this.energyPointsMax = energyPointsMax;
    }
    
    public Character(String species, String name, int healthPoints, int energyPoints) {
        this(species, name, healthPoints, healthPoints, energyPoints, energyPoints);
    }
    
    public Character(String species, int healthPoints, int energyPoints) {
        this(species, null, healthPoints, energyPoints);
    }
    
    public String getName() {
        return name;
    }
    
    public int getHealthPoints() {
        return healthPoints;
    }
    
    public int getHealthPointsMax() {
        return healthPointsMax;
    }
    
    public int getEnergyPoints() {
        return energyPoints;
    }
    
    public int getEnergyPointsMax() {
        return energyPointsMax;
    }
    
    public void regenerateHealthPoints(int healthPoints) {
        if (this.healthPoints + healthPoints > this.healthPointsMax) {
            this.healthPoints = healthPointsMax;
        } else {
            this.healthPoints += healthPoints;
        }
    }
    
    public void regenerateEnergyPoints(int energyPoints) {
        if (this.energyPoints + energyPoints > energyPointsMax) {
            this.energyPoints = energyPointsMax;
        } else {
            this.energyPoints += energyPoints;
        }
    }
}

The game output will be printed to the console. So there also should be a class that handles some recurring output tasks.

Display.java

public class Display {
    public static void health(Character character) {
        System.out.println("health points: " + character.getHealthPoints() + " / " + character.getHealthPointsMax());
    }
    
    public static void energy(Character character) {
        System.out.println("energy points: " + character.getEnergyPoints() + " / " + character.getEnergyPointsMax());
    }
    
    public static void healthAndEnergy(Character character) {
        health(character);
        energy(character);
    }
    
    public static void basicDetails(Item item) {
        System.out.println("name: " + item.getName());
        System.out.println("category: " + item.getCategory());
        System.out.println("price: " + item.getPrice());
    }
    
    public static void details(HealthItem item) {
        basicDetails(item);
        System.out.println("health bonus: " + item.getHealthPoints());
    }
    
    public static void details(EnergyItem item) {
        basicDetails(item);
        System.out.println("energy bonus: " + item.getEnergyPoints());
    }
}

And here is a test class that tests my program.

ItemTestMain.java

public class ItemTestMain {
    public static void main(String[] args) {
        // Create Items
        HealthItem bred = new HealthItem("bred", "food", 0, 20);
        EnergyItem manaPotion =  new EnergyItem("mana potion", "potion", 0, 50);
        
        // create character
        Character klaus = new Character("Man", "Klaus Dieter", 5, 100, 20, 50);
        Display.healthAndEnergy(klaus);
        
        // Let the character consume items
        System.out.println("The Character consumes: ");
        Display.details(bred);
        bred.unfoldEffect(klaus);
        Display.healthAndEnergy(klaus);
        
        System.out.println("The Character consumes: ");
        Display.details(manaPotion);
        manaPotion.unfoldEffect(klaus);
        Display.healthAndEnergy(klaus);
    }
}
\$\endgroup\$
1
  • \$\begingroup\$ Without doing a full review, what you're trying to build here with classes and inheritance, would be better modeled using an Entity-Component-System. As that would easily allow for a single item to have multiple effects. Your code also looks quite clean and good to me. \$\endgroup\$ – Bobby Nov 11 '20 at 20:22
4
\$\begingroup\$

Avoid needless Inheritance

Actually never use Inheritance when you just want to share some code. Purpose of Inheritance is not a code reuse. Inheritance is meant to model Is-A relationship. And you don't need to model Is-A relationship very often. Code sharing is a side effect of this. Composition is a way better technique of code reuse. Take a look at this quick draft of what Composition based Consumable might look like.

public class Consumable {

    private final List<Effect> effects;

    public Consumable(final List<Effect> effects) {
        this.effects = new ArrayList<>(effects);
    }

    public void useOn(Character character) {
        for (Effect effect : this.effects) {
            effect.affect(character);
        }
    }

}

public interface Effect {
    void affect(Character character);
}

public class HealthGain implements Effect {
    private final int amount;

    public HealthGain(final int amount) {
        this.amount = amount;
    }

    public void affect(final Character character) {
        character.regenerateHealthPoints(this.amount);
    }

}

public class EnergyGain implements Effect {
    private final int amount;

    public EnergyGain(final int amount) {
        this.amount = amount;
    }

    public void affect(final Character character) {
        character.regenerateEnergyPoints(this.amount);
    }

}

public static void main(String[] args) {
    final Consumable lesserHealingPotion = new Consumable(List.of(
       new HealthGain(10) 
    ));
    final Consumable uberRestorationPotion = new Consumable(List.of(
        new HealthGain(1000),
        new EnergyGain(1000)
    ));
}

Keep your features separate

The thing that both buying and using an item works with an item does not mean that both of these features have to be implemented by a single Item class. Each and every such feature will only bloat your Item and you will end up with gigantic class where each feature uses only part of it.. Instead of having a single Item consider having a representation for each domain in which an item figures. Such as Merchandise for shopping, Consumable for use or Equipment for use in combat. By keeping them separate you can easily add related behavior such as limited use, cooldowns or durability without cluttering the class too much.

Avoid static functions

Static functions cause a very strong coupling and as a result make testing needlessly difficult. Especially in the context of display you wouldn't be able to substitute the Display for any test-friendly implementation. Take a look at this example.

public class Display {
    void show(Displayable displayable) {
        // ...
    }
}

interface Displayable {
    String asText();
}

class Merchandise implements Displayable {
    
    // ...
    
    @Override
    public String asText() {
        final StringBuilder builder = new StringBuilder();
        builder.append("name: " + this.name);
        builder.append("category: " + this.category);
        builder.append("price: " + this.price);
        return builder.toString();
    }

}

Test your code

Take a look at some testing libraries such as JUnit so that you can properly test your code.

PS: Take those examples with a grain of salt. They are mean to show that there are many ways to build a program and what actually works for any use case can be discovered only by digging in.

\$\endgroup\$
0
\$\begingroup\$

A rpg is not an easy project to start but good luck and it's probably a good training for OO.

When your item system will be more elaborated you'll probably be interested in using a factory pattern as it can help a lot while using loot table (and random generation?).

Will you allow inventory (limited in space, with item of different size, weight), character inventory relative to body part (weapons, armors, jewelry....) do they need specific requirements? (level, statistics...). All those things you need to think carefully before coding.

\$\endgroup\$
2
  • \$\begingroup\$ This...really is more of a comment, isn't it? \$\endgroup\$ – Bobby Nov 11 '20 at 20:19
  • \$\begingroup\$ probably, i just did'nt have right to comment when i did post this answer \$\endgroup\$ – jolrael Nov 13 '20 at 19:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.