问题 为角色扮演游戏中的“角色”设计一个干净/灵活的方式来施放不同的法术


我正在创建一个角色扮演游戏,以获得乐趣和学习体验。我正处于我的角色(巫师)施放法术的地步。我正在使用战略模式来设置他们在施放法术之前施放的法术。我采用这种方法的原因是因为我希望以后能够添加不同的法术类型,而不必使用字符/向导类。

我的问题 - 这是一个糟糕的设计吗?这有更好/更清洁/更容易的方法吗?

我试图远离那个试图让一切都融入设计模式的“那个人”。但在这种情况下,我觉得这是一个不错的选择。

这是我的代码到目前为止使用2个法术的样子

public class Wizard : Creature
{
   public List<Spell> Spells { get; set; }

   public void Cast(Spell spell, Creature targetCreature)
   {
      spell.Cast(this, targetCreature);
   }
}

public abstract class Spell
{
   public string Name { get; set; }
   public int ManaCost { get; set; }
   public Spell(string name, int manaCost)
   {
      Name = name;
      ManaCost = manaCost;
   }
   public void Cast(Creature caster, Creature targetCreature)
   {
      caster.SubtractMana(ManaCost);
      ApplySpell(caster, targetCreature);
   }
   public abstract void ApplySpell(Creature caster, Creature targetCreature);
}

// increases the target's armor by 4
public class MageArmor : Spell
{
   public MageArmor() : base("Mage Armor", 4);
   public override void ApplySpell(caster, targetCreature)
   {
      targetCreature.AddAC(4);
   }
}

// target takes 7 damage
public class FireBall : Spell
{
   public FireBall() : base("Fire Ball", 5);
   public override void ApplySpell(caster, targetCreature)
   {
      targetCreature.SubtractHealth(7);
   }
}

现在施放一个咒语,我们做这样的事情:

Wizard wizard = new Wizard();
wizard.Cast(new Spell.MageArmor(), wizard); // i am buffing myself 

更新:更新的代码与以下答案中的一些建议


5565
2018-02-04 07:15


起源

offtopic:据我所知,这个代码,没有法术力的玩家可以施放法术:) - erelender
我可能会将“WizardSpells”重命名为“Spells”或“SpellBook”。 “Wizard.WizardSpells”不是很干。 #主观 - Rob Fonseca-Ensor
你如何管理不以生物为目标的法术,但是,例如,一个位置(“火墙”,“调用”)或一组目标(“群体治疗”)? - PATRY Guillaume
@PATRY耶,乔恩也提到了。但我认为这将是一个不同的问题? - mikedev
你如何申请项目属性? wizard.Equip(wizard.Inventory, “背部”, “帽子”); - Florian Doyon


答案:


按照Willcodejavaforfood的说法,你可以设计一个 SpellEffect 描述你的咒语可能具有的单一效果的类。您可以创建一个“词汇表”来用来描述:

法术属性:

  • 名称
  • 法力消耗
  • 目标限制 整个 法术(玩家,全天候,怪物......)
  • 法术总持续时间(法术效果持续时间最高)(10秒,5个刻度,......)
  • 施法时间
  • 法术范围(5米,65个单位......)
  • 失败率(5%,90%)
  • 在此法术可以再次施放之前等待的时间(重建时间)
  • 在任何法术可以再次施放之前等待的时间(恢复时间)
  • 等等...

SpellEffect的属性:

  • 效果类型(防守,进攻,buff,debuff ......)
  • 目标 影响 (自我,党,目标,目标周围区域,线到目标,......)
  • 属性或属性作用于(马力,法力,最大马力,力量,攻击速度......)
  • 效果改变了多少(+ 10,-500,5%,...)
  • 效果持续多长时间(10秒,5个滴答,......)
  • 等等

我想你的词汇(上面括号中的单词)将在一组枚举中定义。也许建议创建一个类层次结构来表示SpellEffect类型,而不是使用该特定属性的枚举,因为那里 威力 是一个不需要所有这些属性的SpellEffect类型,或者对于我没有想到的每个基本SpellEffect类型,都有某种自定义逻辑。但这也可能使事情复杂化太多。 KISS原则=)。

无论如何,关键是你要将关于法术效果的特定信息提取到一个单独的数据结构中。这样做的好处就是你可以创造1 Spell class并使其保持一个SpellEffects列表,以便在激活时应用。然后该法术可以一次执行多种功能(伤害敌人和治疗玩家,又名生命攻击)。您为每个法术创建一个新的法术实例。当然,在某些时候你  必须要 创建 法术。您可以轻松地将拼写编辑器实用程序放在一起以使其更容易。

此外,您定义的每个SpellEffect都可以 非常 使用System.Xml.Serialization的XmlSerializer类可以轻松地从XML写入和加载。在SpellEffect这样的简单数据类上使用它是一件轻而易举的事。您甚至可以将最终的拼写列表序列化为xml。例如:

<?xml header-blah-blah?>
<Spells>
  <Spell Name="Light Healing" Restriction="Player" Cost="100" Duration="0s"
         CastTime="2s" Range="0" FailRate="5%" Recast="10s" Recovery="5s">
    <SpellEffect Type="Heal" Target="Self" Stat="Hp" Degree="500" Duration="0s"/>
  </Spell>
  <Spell Name="Steal Haste" Restriction="NPC" Cost="500" Duration="120s"
         CastTime="10s" Range="100" FailRate="10%" Recast="15s" Recovery="8s">
    <SpellEffect Type="Buff" Target="Self" Stat="AttackSpeed" Degree="20%" Duration="120s"/>
    <SpellEffect Type="Debuff" Target="Target" Stat="AttackSpeed" Degree="-20%" Duration="60s"/>
  </Spell>
  ...
</Spells>

您还可以选择将数据放在数据库中而不是xml中。 Sqlite会小巧,快速,简单且免费。您还可以使用LINQ从xml或sqlite查询拼写数据。

当然,你也可以为你的怪物做类似的事情 - 至少对于他们的数据而言。我不确定逻辑部分。

如果你使用这种系统,你可以获得额外的好处,即能够将你的生物/法术系统用于其他游戏。如果你“硬编码”你的法术,你就不能这样做。它还可以让你改变法术(类平衡,错误,等等)  必须重建和重新分发您的游戏可执行文件。只是一个简单的xml文件。

天啊!我现在对你的项目感到非常兴奋,以及我所描述的内容是如何实现的。如果您需要任何帮助,请告诉我!


6
2018-02-04 11:18



我喜欢“那么法术可以执行多种功能”。 TheElderScroll允许你这样做,使用游戏中的编辑器:) - PATRY Guillaume
哦,是的,那将是甜蜜的...你可以在游戏中制作自己独特的法术,而不仅仅是从一个列表中选择。每种效果的成本都可以累计到总法术费用。 - Benny Jobigan
ElderScroll有一个错误:如果你创造了一个花费超过2 * 16点法力值的法术,那么只考虑溢出。你能说“orbital nuke spell”:) - PATRY Guillaume
非常感谢你的回答。我将尝试使用这种方法进行原型设计 - mikedev
@mikedev我很高兴。 =) - Benny Jobigan


特别清楚为什么你希望它是一个两阶段的过程,除非它将在UI中公开(即如果用户将设置“加载的咒语”并且可以在以后改变他们的想法)。

另外,如果你  将拥有一个属性而不仅仅是wizard.Cast(新的 Spell.MageArmor(), wizard)有一个SetSpell方法有点奇怪 - 为什么不只是做 LoadedSpell 财产公众?

最后,法术实际上有任何可变状态吗?你能拥有一组固定的实例(flyweight / enum模式)吗?我不是在考虑这里的内存使用(这是flyweight模式的正常原因),而只是它的概念性。感觉就像你想要的东西真的就像一个Java枚举 - 一组具有自定义行为的值。在C#中更难做到这一点,因为没有直接的语言支持,但它仍然是可能的。

法术中的实际模式(有施法者和目标)似乎是合理的,但是如果你想要能够拥有区域效果法术(使用目标位置而不是特定生物)或诅咒法术,你会发现它变得不灵活/祝福物品等。您可能还需要传递游戏世界的其他状态 - 例如如果你有一个咒语来创建小兵。


3
2018-02-04 07:21



暗黑破坏神风格的法术,我喜欢它:) - erelender
感谢Jon的反馈。我使用SetSpell方法的唯一原因是因为我学会了如何进行策略模式。我已经使用你的建议清理了我的代码(在我的来源中,而不是在我的SO问题中)并且它更有意义。至于你最后一段......你给了我很多思考:) - mikedev
我认为法术可能确实具有一种状态:如何使用再生法术或具有充电时间的东西进行咒语? - Ed James
@Ed:好点。我不确定我是否会以同样的方式对它们进行建模,但它们肯定是要考虑的事情。 - Jon Skeet
不,可能不是,更好的方法是拥有一个'Effect'对象或其他一些非相互关联的类,但绝对是一个选择。我可以想到有一个Spell类来处理效果的例子就像Chain Lightning一样,它可以简单地只有一个.Jump方法,而不是每次你想跳它时都必须实例化新的Spell和Effect对象! - Ed James


我可能不会在这里为每个法术使用子类。我会尝试使用XML或JSON将其放在磁盘上并动态创建它们。

- 想澄清(希望) -

这种方法需要尽可能提前计划。您必须将属性定义为:

  • 名称
  • 描述
  • 持续时间
  • 目标(自我,区域,其他)
  • 类型(奖金,伤害,诅咒)
  • 效果(例如:1d6冰霜伤害,+ 2护甲等级,-5伤害抗性)

在通用法术类中包含所有这些行为应该使它非常灵活,更直接地进行测试。


2
2018-02-04 08:09



如何对法术有不同的行为呢?例如,MageArmor会增加目标护甲,而FireBall会对目标造成伤害。 - mikedev
那么显然需要一些仔细的计划。看看笔和纸RPG,你会发现一切都是经过精心定义的。有不同类型的奖金,损坏等。 - willcodejavaforfood
如果您在字符上有通用属性,那么大多数效果可以表示为property_name,property_change对。按名称查找属性,将更改添加到该属性。适用于治疗,伤害,护甲等等。聚合这些可能占你的法术的90%。其他10%可以通过继承完成,也可以是更复杂的通用方案。 - Kylotan
我同意willcodejavaforfood。我写了详细描述这可能如何工作。我根本无法抑制自己的兴奋。 - Benny Jobigan
@Kylotan&Benny - 是的,这正是我的意图:) - willcodejavaforfood


用命令模式封装“法术”是很自然的(这基本上就是你所做的)。但是你遇到两个问题: -

1)你必须重新编译才能添加更多法术

  • 你可以列举所有可能的 行动有可能是一个咒语 拿,然后定义一些法术 外部格式(XML,Database) 加载到您的应用程序中 启动。西方角色扮演游戏往往被编码为 这 - “咒语”包括“申请” 法术效果#1234,参数1000“, “播放动画#2345”等

  • 您可以将您的游戏状态暴露给脚本 语言和脚本你的法术(你也可以 将此与第一个想法相结合,以便在大多数情况下 你的脚本法术只是在代码中调用预定义的效果。 Planeswalkers的决斗 (X-Box 360上的M:TG游戏)写得很广泛 这种方法

  • 或者你可以忍受它(我...)

2)当你的法术目标不是生物时会发生什么?

  • 如果您将游戏状态暴露给您的拼写脚本,这不是问题,因为您的脚本可以在您所暴露的内容中执行他们喜欢的任何操作。

  • 否则,你最好制作一个通用类型。

我通常做类似下面的事情(不仅仅是在游戏中,我一直使用这种模式来表示mutli-agent-systems中的行为): -

public interface IEffect<TContext>
{
  public void Apply(TContext context);
}

public class SingleTargetContext
{
  public Creature Target { get; set; }
}
public class AoEContext
{
  public Point Target { get; set; }
}
// etc.

这种模式的优势在于它可以非常灵活地执行那些你经常期望法术能够完成的更多固定模型无法做到的“奇怪”事情。你可以把它们连在一起做。你可以有一个效果,它可以为你的目标添加一个TriggeredEffect - 这对于做一些像Thorns Aura这样的事情很有帮助。您可以使用IReversibleEffect(使用额外的Unapply方法)来表示增益效果。

关于Planeswalkers决斗的那篇文章虽然非常出色。太好了,我会连接两次!


2
2018-02-04 11:47





出于某种原因,“咒语”对我来说更像是一种命令模式。但我从来没有设计过这样的游戏......


1
2018-02-04 07:20





我看到这个模式的最大问题是所有法术必须记住减去他们的法术力费用。怎么样:

public abstract class Spell
{
   public string Name { get; set; }
   public int ManaCost { get; set; }
   public Spell(string name, int manaCost)
   {
      Name = name;
      ManaCost = manaCost;
   }

   public void Cast(Creature caster, Creature targetCreature)
   {
       caster.SubtractMana(ManaCost); //might throw NotEnoughManaException? 
       ApplySpell(caster, targetCreature);
   }

   protected abstract void ApplySpell(Creature caster, Creature targetCreature);
}

另外,应该向导扩展PlayerCharacter,这将扩展Creature?


1
2018-02-04 08:57



谢谢你。在我的源代码中,我实际上有前后投射方法。我的预制方法是我检查是否有足够的法力值,但我喜欢你做的方式。是的,向导扩展了Creature ...更新的源代码。 - mikedev


我觉得你的设计很好看。由于每个Spell类基本上都是一个函数的包装器(这更适合Command模式,而不是策略),你可以完全摆脱法术类,只需使用一些反射函数来找到拼写方法并添加一些他们的元数据。喜欢:

public delegate void Spell(Creature caster, Creature targetCreature);

public static class Spells
{
    [Spell("Mage Armor", 4)]
    public static void MageArmor(Creature caster, Creature targetCreature)
    {
        targetCreature.AddAC(4);
    }

    [Spell("Fire Ball", 5)]
    public static void FireBall(Creature caster, Creature targetCreature)
    {
        targetCreature.SubtractHealth(7);
    }
}

1
2018-02-05 20:26



当您需要添加其他行为时会发生什么,例如触发动画或将精灵渲染到法术? - Rob Fonseca-Ensor


首先:对于一切事物总是有更好/更清洁/更容易的方法。

但是在我看来,你已经对你的挑战做了很好的抽象,这可以成为进一步改进的坚实基础。


0
2018-02-04 07:21





我可能会遗漏一些东西,但三重奏WizardSpells,LoadedSpell,SetSpell似乎可以澄清一下。具体来说,到目前为止,我没有看到您的代码中使用的列表。我可能会使用LearnNewSpell(Spell newSpell)将向导可用的法术添加到列表中,并检查LoadSpell是否使用该列表中的一个咒语。
此外,如果您要使用多种类型的脚轮,您可能会考虑在某些时候在法术上添加一些关于施法者类型的额外信息。


0
2018-02-04 07:58



不需要LoadedSpell和SetSpell,但我在那里有它们,因为我最近学会了如何制定策略模式。它已从我的源代码中删除(但不是从问题中删除)。按照你的想法, WizardSpells 是角色当前知道的法术列表。但是在这个例子中没有使用它 - mikedev
如果您需要进一步的建议,可能会更新问题 - Rob Fonseca-Ensor
好的,更新了问题中的源代码 - mikedev


你的单元测试是什么样的?

设计是否使您可以轻松编写所需的测试?


0
2018-02-04 08:44



-1“......角色扮演游戏 为了娱乐“。到底为什么要编写单元测试以获得乐趣?!?! - Ed James
实际上,TDD也是我正在练习的东西:)为了回答Ian,是的,测试很容易写,但是正如人们已经指出的那样,如果我希望法术的目标不是另一个,那我就麻烦了生物(物品,效果区域等) - mikedev
@ed me。如果你的游戏崩溃了,这并不好玩。无论如何,TDD更多的是设计而不是测试覆盖,OP希望改进他的设计。更容易测试==更好的设计,毫无疑问。 - Rob Fonseca-Ensor
@Rob有时候我觉得我的职业生涯不对:对我来说,测试就是为了向客户和开发团队的其他成员证明你的产品在算法上合理,而不是花费你周日下午的事情! :) - Ed James
@Ed好,所以没有客户,这个项目没有开发团队的其余部分,但你怎么证明 你自己 该产品在算法上是否健全?运行应用程序并手动测试?我打算假设迈克甚至还没有用户界面 - 所以这很难。你会写一个小的(5分钟的工作,最大)控制台应用程序?将单元测试视为可重复,可保持的控制台应用程序片段可能会让您付出代价... - Rob Fonseca-Ensor