游戏中自定义角色状态的设计模式

5

我决定使用Java实现一个扎实的RPG风格的游戏结构,以练习设计模式。

基本上,我的游戏中有不同类型的角色,它们都被视为“游戏对象”,具有一些共同的特征:

public abstract class Character extends GameObject {
  Status status;
  //fields, methods, etc.
}

public abstract class Monster extends Character{
  //fields, methods, etc
}

public class Hero extends Character {
  //fields, methods, etc
}

这里的“Status”是一种枚举:

public enum Status {
  NORMAL,
  BURNT,
  POISONED,
  HEALED,
  FROZEN
}

我希望让我的代码灵活易修改,并且想要遵循SOLID原则,有效地运用必要的设计模式。
假设我想要自定义我的角色,允许创建自定义角色扩展来仅具有某些状态更改。例如,我将创建一个名为“Monster”的怪物。
public class FireGolem extends Monster{...}

该技术与IT相关,涉及内容如下:

创建能够抵御高温(因此不会燃烧)的类。我有两个想法:

1) 为Character类创建一个Set,其中指定Character可以具有哪些状态更改。

2) 创建不同的接口(Burnable、Freezable等),需要时再实现它们。

你认为哪种方法更好?为什么?是否有更好和更干净的选项?

提前感谢您的回答。

5个回答

2
< p > FireGolem 可能会覆盖方法setStatus,并在给定的状态不能应用于其实例时抛出一个IllegalArgumentException异常。

最初的回答可能是覆盖了setStatus方法,并在无法将给定状态应用于其实例时抛出异常。
class FireGolem extends Monster {

    @Override
    public void setStatus(Status status) {
         if (Status.BURNT.equals(status)) {
             throw new IllegalArgumentException("FireGolem can't be burnt!");
         }

         super.setStatus(status);
    }

}

如@Vince Emigh所指出的那样,这不是一个纯粹的SOLID示例:前置条件不应在子类中加强。最初的回答。

1
但这违反了LSP,因为它添加了一个前置条件。他提到了他想遵守SOLID原则。它可以工作,但不符合SOLID原则。 - Dioxin
@VinceEmigh 违反了LSP的精神是什么? - Andrew Tobilko
@VinceEmigh 我明白你的意思。尽管super.setStatus的前提条件可能会被Status.BURNT.equals(status)加强,但这不会影响程序的正确性,这是这个原则的主要思想。仅仅因为一个程序没有(完全)遵循SOLID或者“我将写这个程序只是为了遵循SOLID”而说它是不正确或者设计不良是有点愚蠢的。顺便说一下,我的代码片段并不是纯粹的SOLID演示,它只是我认为合理的一种方法。 - Andrew Tobilko
这并不是一个错误的设计或任何东西 - 你的解决方案确实有效。只是OP的要求是遵守SOLID原则:“我想有效地使用必要的设计模式来遵循SOLID原则”。至于通过LSP确保正确性,如果您有一个clearStatus(List<Monster>)试图清除在Monster级别支持的所有潜在状态呢?控制流将退出,这可能是不希望的,因为它会使其他尚未处理其状态的怪物仍然启用。您需要跳过障碍以确保正确性。 - Dioxin
@VinceEmigh monsters.forEach(m -> m.setStatus(null));,有什么问题吗? - Andrew Tobilko
显示剩余6条评论

1

为什么不看一下状态模式呢?

基本上,每个状态都是一个类,并且它们都有相同的基类。然后你会有一个上下文(例如你的角色),它保存当前状态并使用它。

当然,你可以控制状态X是否可以转换为状态Y等,因为每个状态都持有对其上下文的引用。


1
一般情况下,您不应该通过扩展来限制功能/功能。这违反了Liskov替换原则,它是SOLID的一部分,您说您想使用它。
在您的特定情况中,您首先说每个Character都可以拥有其中一个状态,然后尝试引入一个无法具有给定状态的字符。
对于这种特殊情况,我的第一想法(我不能在不知道所有细节的情况下提出解决方案)是遵循接口隔离原则并引入提供isBurntisFrozen等的接口。我可以将它们聚合到例如Fragile接口中,如果大多数Character应该实现所有接口。也可以使用具有公共逻辑的FragileCharacter抽象类。

看起来会引入很多instanceof和类型转换。 - Andrew Tobilko
或者你必须分别处理每种类型,这确实不是一个好选择。 - Andrew Tobilko
这取决于其余架构的外观。这就是为什么我说“不知道所有细节就不能提出解决方案”。 - Milen Dyankov
我也不确定,只是好奇... 假设你有一个 Character 对象,并且需要将其状态更改为 BURNT,你会在其上使用 instanceof 吗? - Andrew Tobilko
我们现在深入到了假设的领域;) 我们说的任何话可能与主题无关。话虽如此,我可以想象一个“动作”,比如 setOnFire (Burnable ...) 而不是 setOnFire (Character ...),这样可以在编译时为您提供良好的保障(您不能点燃不可燃物)。相反地(检查 Character 是否可以处于给定状态并在不行时引发异常)只能在运行时起作用。 - Milen Dyankov

1
我假设你的范围不仅仅是一个玩具项目,因此遵循SOLID原则是必要的,而不仅仅是一种练习。否则,你完全可以采用你的方法。
我的选择不是使用继承,而是封装,因为这样你的代码具有更好的模块化和可维护性。有关深入讨论的参考,请在此处查看 因此,在你的情况下,避免使用GameObject,然后扩展Character,再扩展Monster和hero,因为对GameObject的每一个小改变都会影响到你游戏中的每一个实体。
你可以使用另一种方法:实体组件系统(参考资料这里)。
因此,在你的情况下,你将会:
  • 基本组件:位置、移动、图形
  • 创建基本系统:例如取出所有带有图形组件的实体并在屏幕上显示
  • 根据模板构建实体:英雄、怪物

如果想了解使用它的经验,请参考这里

注意:像Unity这样的大公司也在使用Ecs。


1
我认为你可能想考虑改变方向。
一般来说,大多数角色都可以被燃烧、冻结等等。
因此,不要创建一个适用于角色所有状态的集合,而是创建一个免疫性的集合。
这将使您能够在父类(Character)中处理免疫力,因此当创建新的怪物时,您只需在其构造函数中添加免疫力即可,无需覆盖任何方法。
让我们看看在您的示例中如何工作。
哦,但在此之前,请注意:我将称您的状态为BURNING而不是BURNT,只是因为我假设拥有该状态的角色实际上仍在燃烧;)
public abstract class Character extends GameObject {
  Status status;
  ArrayList<Status> immunities = new ArrayList<>();
  //fields, methods, etc.

  public void addImmunity(Status immunity) {
    immunities.add(immunity);
  }

  // return false if the status couldn't be set in case you want to do something
  // like show an "Immune!" message or something like that
  public boolean setStatus(Status status) {
    if (immunities.contains(status)) {
      return false;
    }
    this.status = status;
    return true;
  }
}

class FireGolem extends Monster {
  public FireGolem() {
    addImmunity(Status.BURNING);
  }
}

这种方法的好处是长期来看可以节省相当多的内存。而且你不需要过度设计任何东西。现在...无论你是否使用 ArrayList 或其他内容,当然都有争议,这只是一个简单的例子。
此外,setStatus 方法在这里返回一个布尔值作为结果。我之所以不抛出异常,是因为我根本不认为它是异常。为什么玩家不会试图点燃火元素?当然,它不应该起作用,但这仍然是预期情况之一。另一方面,不同的人使用不同的方法,在这里抛出异常当然没有完全错误,只是对我个人来说,感觉不太对。如果你想要比简单的 true 或 false 更多的信息以进行可视化,请返回更复杂的对象,但我想让示例尽可能简单。
还有一件事要补充:也许你应该考虑给角色一个状态列表,而不是单个状态,因为虽然冰冻和燃烧可能会相互抵消,但我认为同时被燃烧和中毒是可能的,但这只是一个观点问题。有很多游戏只允许同时存在一个状态。

1
这个答案非常棒,既涵盖了异常使用,又没有强制应用任何标准设计模式。所需的只是更好地理解程序要求,并加入一些设计理论以保证完整性。 - Dioxin
1
很好的回答,但我不同意异常部分。 setStatus 应该只设置状态。如果我想将那个东西烧毁,我会这样做,我不必检查任何返回值。然而,之后仍然保持它的存在是非常意外的,并且会极大地影响新的动作:这就是为什么我认为抛出异常更合适的原因... - Andrew Tobilko
@AndrewTobilko 当发生意外错误时,应该抛出异常,以便控制流转移到错误处理。在运行时异常的情况下,没有错误处理:它只是使线程崩溃。如果不应该烧毁FireGolem,尝试将状态设置为燃烧不是意外行为-而是不需要的行为,应该通过条件进行处理,但不是意外行为。一个“什么也不做”的调用会更合理(只需删除抛出的异常),但两者都可以通过坚固的设计避免。 - Dioxin
@AndrewTobilko,“之后还活着,这是非常出乎意料的”-我强烈不同意这个观点。您可能从玩家的角度来看待此问题,但这并不应该影响您的代码设计。如果每次玩家遇到意料之外的事情就抛出异常,则游戏将变得无法玩耍(或极其无聊)。作为开发人员,火焰魔像不开始燃烧正是我所期望的。实际上,如果它开始燃烧,我会认为这才是异常情况。 - Mark
@CsongiNagy 听起来很合理...但我不明白那个-5是什么意思 ;) 90%的免疫并不代表你会受到多少伤害。100%-90%才是实际受到的伤害量,也就是说如果你想治愈魔像,我会期望一个105%的值而不是-5。 - Mark
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接