I work on my first Java project, which is a basic roleplaying game. Now I work on spells, and I need some OOD guidance.
I have Character
, which is an abstract class
. Character
has some subclasses
(like mage
, fighter
, rogue
, cleric
).
Mage
and cleric
(as for now
, cleric doesn't have mana, but it might change) are both spell-casters.
I also have a Spell
class, with some info (like spell name
, mana cost
etc). MageSpellsList
and ClericSpellsList
are another classes and both have lists of class Spell. and I also have Effects
class(casting a spell should use it).
What would be a good object oriented design for dealing with spells (the solution shouldn't include Effects
class, I can deal with that later) ?
Maybe using a "SpellCaster" interface with some methods like castSpell and showSpellbook, so Mage and Cleric will implement the interface? .
Maybe MageSpellsList and ClericSpellsList should be a subclass of Spell ? My goal is to use castSpell("spell name here") and let castSpell do the job, by using a good OOD, rather than writing a specific method for each spell (and without duplicate code between mage and Cleric)
Mage.java:
public class Mage extends Character {
private List<Spell> spellBook;
private int mana;
private int CurrentMana;
public Mage(String name) {
super(name);
setName(name);
setCharacterClass("Mage");
setLevel(1);
setHitDice(4);
setStrength(10);
setConstitution(10);
setDexterity(14);
setIntelligence(16);
setWisdom(14);
setCharisma(10);
setHp((int) (4 + getModifier(getConstitution())));
setCurrentHp(getHp());
setArmorClass(10 + getModifier(getDexterity()));
setBaseAttackBonus(0);
setMana(20 + 2 * getModifier(getIntelligence()));
setCurrentMana(getMana());
spellBook = new ArrayList<Spell>();
}
public int getMana() {
return mana;
}
public int getCurrentMana() {
return CurrentMana;
}
protected void setMana(int mna) {
mana = mna;
}
protected void setCurrentMana(int CurrMana) {
CurrentMana = CurrMana;
}
public void showSpellBook() {
for (Iterator<Spell> iter = spellBook.iterator(); iter.hasNext(); ) {
Spell spell = iter.next();
System.out.println("Spell name: " + spell.getSpellName());
System.out.println("Spell effect: " + spell.getEffect());
}
}
public void addToSpellBook(String spellName) {
Spell newSpell;
newSpell = MageSpellsList.getSpell(spellName);
spellBook.add(newSpell);
System.out.println(newSpell.getSpellName() + " has been added to the spellbook");
}
public void chooseSpells() {
System.out.println();
}
void castSpell(String spellName, Character hero, Character target) {
try {
Spell spell = MageSpellsList.getSpell(spellName);
System.out.println("You casted: " + spellName);
System.out.println("Spell effect: " + spell.getEffect());
} catch (Exception e) {
System.out.println("No such spell");
}
}
}
Spell.java:
public class Spell {
private String name;
private int spellLevel;
private String effect;
private int manaCost;
private int duration;
Spell(String name, int spellLevel, String effect, int manaCost, int duration) {
this.name = name;
this.spellLevel = spellLevel;
this.effect = effect;
this.manaCost = manaCost;
this.duration= duration;
}
String getSpellName() { return name; }
int getSpellLevel() { return spellLevel; }
String getEffect() { return effect; }
int getManaCost() {
return manaCost;
}
int getDuration() { return duration; }
}
MageSpellsList.java:
public class MageSpellsList {
static List<Spell> MageSpellsList = new ArrayList<Spell>();
static {
MageSpellsList.add(new Spell("Magic Missiles", 1, "damage", 2, 0));
MageSpellsList.add(new Spell("Magic Armor", 1, "changeStat", 2, 0));
MageSpellsList.add(new Spell("Scorching Ray ", 2, "damage", 4, 0));
MageSpellsList.add(new Spell("Fireball", 3, "damage", 5,0 ));
MageSpellsList.add(new Spell("Ice Storm", 4, "damage", 8, 0));
}
static void showSpellsOfLevel(int spellLevel) {
try {
for (Iterator<Spell> iter = MageSpellsList.iterator(); iter.hasNext(); ) {
Spell spell = iter.next();
if (spellLevel == spell.getSpellLevel()) {
System.out.println("Spell name: " + spell.getSpellName());
System.out.println("Spell effect: " + spell.getEffect());
}
}
} catch (Exception e){
System.out.println("Epells of level " + spellLevel + " haven't been found in spells-list");
}
}
static Spell getSpell(String spellName) {
try {
for (Iterator<Spell> iter = MageSpellsList.iterator(); iter.hasNext(); ) {
Spell spell = iter.next();
if (spellName.equals(spell.getSpellName())) {
return spell;
}
}
} catch (Exception e){
System.out.println(spellName + " haven't been found in spells-list");
return null;
}
return null;
}
}
Effects.java:
public class Effects {
public void damage(int dice, Character attacker, Character target){
int damage = DiceRoller.roll(dice);
System.out.println(attacker.getName() + " dealt " + damage + " damage to " + target.getName());
target.setCurrentHp(target.getCurrentHp() - damage);
}
public static void damage(int n, int dice, int bonus, Character target) {
int damage = DiceRoller.roll(n,dice,bonus);
System.out.println("You dealt" + damage + "damage to " + target.getName());
target.setCurrentHp(target.getCurrentHp() - damage);
}
public static void heal(int n, int dice, int bonus, Character target) {
int heal = DiceRoller.roll(n,dice,bonus);
if (heal + target.getCurrentHp() >= target.getHp()) {
target.setCurrentHp(target.getHp());
} else {
target.setCurrentHp(target.getCurrentHp() + heal);
}
System.out.println("You healed" + heal + " hit points!");
}
public static void changeStat(String stat, int mod, Character target){
System.out.println(stat + " + " + mod);
switch (stat) {
case "strength":
target.setStrength(target.getStrength() + mod);
break;
case "constitution":
target.setConstitution(target.getConstitution() + mod);
break;
case "dexterity":
target.setDexterity(target.getDexterity() + mod);
break;
case "intelligence":
target.setIntelligence(target.getIntelligence() + mod);
break;
case "wisdom":
target.setWisdom(target.getWisdom() + mod);
break;
case "charisma":
target.setCharisma(target.getCharisma() + mod);
break;
case "armorClass":
target.setArmorClass(target.getArmorClass() + mod);
break;
}
}
}
Preamble
I try to generalise the classes as much as possible, so I do not end up with lots of specific classes that just represent different data, instead of a different structure. Also, I try to separate data structures from game mechanics. In particular, I try to keep the combat mechanics all in one place, instead of splitting them across different classes, and I try not to hard-code any data. In this answer, we will cover the characters, their abilities/spells, the effects of the abilities, and the combat mechanics.
Characters
Consider, for instance, a
PlayableCharacter
, that represents your characters. This is a standard data class. It provides methods for increasing or decreasing health and mana, and a collection of available abilities.Abilities
Abilities are equally data classes. They represent mana costs, triggered effects, and so on. I often represent this as a normal class, and then read the individual abilities from external data files. Here we can skip that and declare them with enumerations.
Effects
Finally the effects tell what an ability does. How much damage, how long it lasts, how it affects a character. Again, this is all data, no game logic.
The mechanics are just an enumeration.
Mechanics
Now it is time to make things work properly. This is the class that your game loop will be interacting with, and you must feed it the game state (which characters are battling, for instance).
How you implement each mechanic is up to you. It can range from an infernal switch or if/else for each
Mechanic
, or you can move the code to theMechanic
enum, or to private nested classes and use anEnumMap
to retrieve each handler.Example Mechanic
Why treat spells different than abilities? A fighter class might not have spells as magic spells, but it should be able to perform class specific moves like a whirlwind.
Class PlayableCharacter: abstract class, defines the abstract methods for handling resources(regen rate, max, effects on character), abilities, gear. And implements all the basics.
Class ManaCharacter: extends PlayableCharacter handles it resource as mana.
Class Mage extends ManaCharacter: Will just implement the methods to define what kind of gear it can use, the special abilities it can perform, etc.
I think your idea of having a
SpellCaster
interface (which includes thecastSpell()
) is a good one. This defines the behavior or ability of the character.I would include the list of available spells as an instance field in the Mage or Cleric classes. Come to think of it, maybe it would be a good idea to create an abstract class called
SpellCaster
which extendsCharacter
. TheSpellCaster
class can declare the list of spells and subclasses (Mage
andCleric
) can add specific spells to it.I'm going to discard the
Effects
class for now. Each spell can take care of its own behavior. So for example, when callingcastSpell("spellName", hero, target)
you can pass the required parameters to the spell object and it can take care of dealing the damage or changing stats.In addition, there could be multiple
Spell
subclasses. For example,DamageSpell
,Buff
,Debuff
. The superclassSpell
has a methodapply()
and each subclass can implement it with it's own behavior. When callingcastSpell()
then you delegate the control to a specific subclass of aSpell
which has encapsulated the behavior and knows exactly if it should deal damage or change stats. That's essentially the Strategy Pattern.The
SpellCaster
spellbook should be aMap<String, Spell>
so you can look it up by name when it is cast. TheSpell
class should define an abstract method for applying the effects to aCharacter
. I don't see the point of a "SpellCaster" interface because the implementation of the castSpell() method is always the same (the behavior is delegated to the Spell itself).Here is a sample scenario:
Attribute.java
Spell.java
SpellCaster.java (snippet)
Fireball.java
Icestorm.java
Heal.java
Here is an example of how you can use
enum
instead of strings in yourEffects
class. I took the liberty of renaming yourCharacter
class toPlayerCharacter
to avoid collision withjava.lang.Character
.Effects.java:
A little bit cleaner, isn't it? How it works? The magic is all in the
enum
:Stat.java:
The two mysterious types
ToIntFunction
andObjIntConsumer
are functional interfaces:ToIntFunction
takes some kind of object as input (here: aPlayerCharacter
) and returns anint
.ObjIntConsumer
takes some kind of object (here: aPlayerCharacter
) and anint
as input, and returns nothing.You could also create your own functional interface if you like, like so:
Effect.java:
Stat.java:
Then you can do this in
changeStat
:This way you can decide in the
Effects
class what will happen. Well, I don't imagine the character stats to change much from spells, but a similar mechanic can be used for HP and such :)The
x -> x + mod
bit could come from the spell itself too. It's a function that takes anint
and returns anint
, which is called anIntUnaryOperator
in Java:Effects.java:
Here the spell (boost in this case, which I just invented!) will increase the player's strength (the
STRENGTH
constant) by a dice roll. It accomplishes this by calling thechangeStat
with three parameters:STRENGTH
→ tells the method what status to change.As you can see, there is no need here to know how to find the strength value, or how to set it to something else. That is all handled by the
enum
, so you can keep your spell code clean.You could even inline the
changeStat
method directly in the spell method this way, since there isn't really any "real" code in it anymore – that logic is hidden in theenum
.Clean and neat :)