面向对象设计 – 法术

我正在开发我的第一个Java项目,这是一个基本的角色扮演游戏。 现在我处理法术,我需要一些OOD指导。

我有Character ,这是一个abstract classCharacter有一些subclasses (如magefighterroguecleric )。

Magecleric (至于now ,牧师没有法术力 ,但可能会改变)都是施法者。

我还有一个Spell课,有一些信息(如spell namemana cost等)。 MageSpellsListClericSpellsList是另一个类,都有类Spell列表。 而且我也有Effects类(施放法术应该使用它)。

什么是一个很好的面向对象设计来处理法术(解决方案不应该包括Effects类,我可以稍后处理)?

也许使用“SpellCaster”界面和一些方法,比如castSpell和showSpellbook,那么Mage和Cleric会实现这个界面吗? 。

也许MageSpellsList和ClericSpellsList应该是Spell的子类? 我的目标是使用castSpell(“spell name here”)并让castSpell通过使用一个好的OOD来完成工作,而不是为每个法术编写一个特定的方法(并且在mage和Cleric之间没有重复的代码)

Mage.java:

 public class Mage extends Character { private List 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(); } 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 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 MageSpellsList = new ArrayList(); 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 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 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; } } } 

前言

我尝试尽可能地推广类,所以我最终没有很多特定的类来代表不同的数据,而不是不同的结构。 此外,我尝试将数据结构与游戏机制分开。 特别是,我尝试将战斗机制保存在一个地方,而不是将它们分成不同的类,我尽量不对任何数据进行硬编码。 在这个答案中,我们将涵盖角色 ,他们的能力/法术能力效果战斗机制

人物

例如,考虑一个代表你的角色的PlayableCharacter 。 这是一个标准数据类。 它提供增加或减少健康和法力的方法,以及可用能力的集合。

 class PlayableCharacter { private final int maxHealth; private int health; private final int maxResource; // mana, energy and so on private int resource; private final Collection abilities; // getters and setters } 

能力

能力同样是数据类。 它们代表法术力费用,触发效果等。 我经常将其表示为普通类,然后从外部数据文件中读取各个function。 在这里,我们可以跳过它并使用枚举声明它们。

 enum Ability { FIREBALL("Fireball", 3, 5, new Effect[] { new Effect(Mechanic.DAMAGE, 10, 0), new Effect(Mechanic.BURN, 2, 3) }); private final String name; private final int level; private final int cost; private final List effects; } 

效果

最后,效果告诉我们能力的作用。 多少伤害,持续多长时间,它如何影响角色。 同样,这是所有数据,没有游戏逻辑。

 class Effect { private final Mechanic effect; private final int value; private final int duration; } 

机制只是一个枚举。

 enum Mechanic { DAMAGE, BURN; } 

机械学

现在是时候让事情正常运转了。 这是您的游戏循环将与之交互的类,您必须将其提供给游戏状态(例如,角色正在与之作斗争)。

 class BattleEngine { void useAbility(PlayableCharacter source, PlayableCharacter target, Ability ability) { // ... } } 

您如何实施每种机制取决于您自己。 它可以是一个地狱开关,也可以是每个Mechanic if / else,或者你可以将代码移动到Mechanic枚举,或者移动到私有嵌套类,并使用EnumMap来检索每个处理程序。

示例技工

 interface MechanicHandler { void apply(PlayableCharacter source, PlayableCharacter target, Effect effect); } 
 class BattleEngine { private final Map mechanics; void useAbility(PlayableCharacter source, PlayableCharacter target, Ability ability) { source.decreaseResource(ability.getCost()); for (Effect effect: ability.getEffects()) { MechanicHandler mh = mechanics.get(e.getMechanic()); mh.apply(source, target, effect); } } private static final class DicePerLevel implements MechanicHandler { @Override public void apply(PlayableCharacter source, PlayableCharacter target, Effect effect) { int levels = Math.min(effect.getValue(), source.getLevel()); int damage = 0; for (int i = 0; i < levels; ++i) { int roll; // roll a d6 die damage += roll; } target.decreaseHealth(damage); } } } 

SpellCaster法术书应该是一个Map这样你就可以在施法时通过名字查找它。 Spell类应该定义一个将效果应用于Character的抽象方法。 我没有看到“SpellCaster”界面的意义,因为castSpell()方法的实现始终是相同的(行为被委托给Spell本身)。

以下是一个示例场景:

 Mage fireMage = new Mage("Red Niminim"); fireMage.addSpell(new Fireball()); fireMage.addAttribute(Attribute.RESIST_FIRE); fireMage.addAttribute(Attribute.WEAK_TO_COLD); Mage iceMage = new Mage("Blue Niminim"); fireMage.addSpell(new Icestorm()); fireMage.addAttribute(Attribute.RESIST_COLD); fireMage.addAttribute(Attribute.WEAK_TO_FIRE); Cleric cleric = new Cleric("Friar Joe"); cleric.addSpell(new Heal()); // battle! fireMage.castSpell("Fireball", cleric); // 15 damage fireMage.castSpell("Fireball", iceMage); // 30 damage fireMage.castSpell("Fireball", fireMage); // 0 damage iceMage.castSpell("Icestorm", cleric); // 15 damage iceMage.castSpell("Icestorm", fireMage); // 30 damage iceMage.castSpell("Icestorm", iceMage); // 0 damage cleric.castSpell("Heal", cleric); // 15 healed 

Attribute.java

 public enum Attribute { RESIST_FIRE, WEAK_TO_FIRE, RESIST_COLD, WEAK_TO_COLD; } 

Spell.java

 public abstract class Spell { private String name; private int manaCost; public Spell(String name, int manaCost) { this.name = name; this.manaCost = manaCost; } public abstract void apply(Character character); public String getName() { return name; } public int getManaCost() { return manaCost; } } 

SpellCaster.java(片段)

 public void castSpell(String name, Character character) { getSpellBook().get(name).apply(character); } public void addSpell(Spell spell) { getSpellBook().put(spell.getName(), spell); } 

Fireball.java

 public class Fireball extends Spell { private static final String NAME = "Fireball"; private static final int MANA_COST = 8; private static final int DAMAGE_AMOUNT = 15; public Fireball() { super(NAME, MANA_COST); } @Override public void apply(Character character) { int damage = DAMAGE_AMOUNT; if (character.getAttributes().contains(Attribute.RESIST_FIRE)) { damage = 0; } else if (character.getAttributes().contains(Attribute.WEAK_TO_FIRE)) { damage = damage * 2; } character.setCurrentHp(character.getCurrentHp() - damage); } } 

Icestorm.java

 public class Icestorm extends Spell { private static final String NAME = "Icestorm"; private static final int MANA_COST = 8; private static final int DAMAGE_AMOUNT = 15; public Icestorm() { super(NAME, MANA_COST); } @Override public void apply(Character character) { int damage = DAMAGE_AMOUNT; if (character.getAttributes().contains(Attribute.RESIST_COLD)) { damage = 0; } else if (character.getAttributes().contains(Attribute.WEAK_TO_COLD)) { damage = damage * 2; } character.setCurrentHp(character.getCurrentHp() - damage); } } 

Heal.java

 public class Heal extends Spell { private static final String NAME = "Heal"; private static final int MANA_COST = 10; private static final int HEAL_AMOUNT = 15; public Heal() { super(NAME, MANA_COST); } @Override public void apply(Character character) { character.setCurrentHp(character.getCurrentHp() + HEAL_AMOUNT); } } 

下面是一个如何在Effects类中使用enum而不是字符串的示例。 我冒昧地将您的Character类重命名为PlayerCharacter以避免与java.lang.Character冲突。

Effects.java:

 public class Effects { ... public static void changeStat(Stat status, int mod, PlayerCharacter target) { System.out.println(status + " + " + mod); status.effect(mod).accept(target); } } 

有点清洁,不是吗? 这个怎么运作? 魔法全部在enum

Stat.java:

 import java.util.function.Consumer; import java.util.function.IntUnaryOperator; import java.util.function.ObjIntConsumer; import java.util.function.ToIntFunction; public enum Stat { STRENGTH(PlayerCharacter::getStrength, PlayerCharacter::setStrength), CONSTITUTION(PlayerCharacter::getConstitution, PlayerCharacter::setStrength), DEXTERITY(PlayerCharacter::getDexterity, PlayerCharacter::setDexterity), INTELLIGENCE(PlayerCharacter::getIntelligence, PlayerCharacter::setIntelligence), WISDOM(PlayerCharacter::getWisdom, PlayerCharacter::setWisdom), CHARISMA(PlayerCharacter::getCharisma, PlayerCharacter::setCharisma), ARMORCLASS(PlayerCharacter::getArmorClass, PlayerCharacter::setArmorClass); Stat(ToIntFunction findcurrentvalue, ObjIntConsumer setnewvalue) { this.findcurrentvalue = findcurrentvalue; this.setnewvalue = setnewvalue; } private ToIntFunction findcurrentvalue; private ObjIntConsumer setnewvalue; Consumer effect(int mod) { return target -> { setnewvalue.accept(target, findcurrentvalue.applyAsInt(target) + mod); }; } } 

两个神秘类型ToIntFunctionObjIntConsumerfunction接口

  • ToIntFunction将某种对象作为输入(这里:一个PlayerCharacter )并返回一个int
  • ObjIntConsumer将某种对象(此处: PlayerCharacter )和int作为输入,并且不返回任何内容。

如果您愿意,也可以创建自己的function界面,如下所示:

Effect.java:

 @FunctionalInterface public interface Effect { void affect(T t); } 

Stat.java:

  ... Effect effect(IntUnaryOperator calculator) { return target -> { setnewvalue.accept(target, calculator.applyAsInt(findcurrentvalue.applyAsInt(target))); }; } ... 

然后你可以在changeStat执行此changeStat

 public class Effects { ... public static void changeStat(Stat status, int mod, PlayerCharacter target) { System.out.println(status + " + " + mod); status.effect(x -> x + mod).affect(target); } } 

这样你就可以在Effects类中决定会发生什么。 好吧,我不认为角色统计数据会从法术中改变很多,但类似的机制可以用于HP等等:)

x -> x + mod位也可能来自咒语本身。 它是一个函数,它接受一个int并返回一个int ,在Java中称为IntUnaryOperator

Effects.java:

 ... public static void boost(int dice, PlayerCharacter target) { int value = DiceRoller.roll(dice); changeStat(Stat.STRENGTH, x -> x + value, target); } public static void changeStat(Stat status, IntUnaryOperator change, PlayerCharacter target) { status.effect(change).affect(target); } ... 

这里的咒语(在这种情况下,我刚刚发明了!)将通过掷骰子增加玩家的力量( STRENGTH常数)。 它通过使用三个参数调用changeStat来实现此changeStat

  1. STRENGTH →告诉方法要更改的状态。
  2. 用于更改值的“公式”(请注意,您实际上不需要知道此处的值,只需公式!)。
  3. 影响的目标。

如您所见,这里不需要知道如何找到强度值,或者如何将其设置为其他值。 这全部由enum处理,因此您可以保持拼写代码清洁。

您甚至可以通过changeStat方式直接在拼写方法中内联changeStat方法,因为它中不再存在任何“真实”代码 – 该逻辑隐藏在enum

干净整洁:)

我认为你有一个SpellCaster接口(包括castSpell() )的castSpell()很好。 这定义了角色的行为或能力。

我会在Mage或Cleric类中包含可用法术列表作为实例字段。 想想看,也许创建一个名为SpellCaster的抽象类可能是一个好主意,它扩展了CharacterSpellCaster类可以声明法术和子类列表( MageCleric )可以为其添加特定法术。

我现在要放弃Effects类。 每个法术都可以照顾自己的行为。 因此,例如,当调用castSpell("spellName", hero, target)您可以将所需参数传递给法术对象,它可以处理伤害或更改统计数据。


此外,可能有多个Spell子类。 例如, DamageSpellBuffDebuff 。 超类Spell有一个方法apply() ,每个子类都可以用它自己的行为来实现它。 当调用castSpell()您将控件委托给Spell的特定子类,该子类已封装行为并确切知道它是否应该造成伤害或更改统计数据。 这基本上就是战略模式 。

为什么治疗法术与能力不同? 战斗机类可能没有法术作为魔法咒语,但它应该能够像旋风一样执行特定类别的动作。

类PlayableCharacter:抽象类,定义处理资源的抽象方法(regen rate,max,对角色的影响),能力,齿轮。 并实现所有基础知识。

类ManaCharacter:extends PlayableCharacter将其作为法术力处理。

Class Mage扩展了ManaCharacter:只是实现方法来定义它可以使用什么样的装备,它可以执行的特殊能力等等。