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

时间:2010-02-04 07:15:09

标签: c# design-patterns

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

我的问题 - 这是一个糟糕的设计吗?对此有更好/更清洁/更简单的方法吗?

我试图远离那个试图让一切都融入设计模式的“那个人”。但在这种情况下,我觉得它很合适。

到目前为止,我的代码看起来像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 

更新:更新代码,并提供以下答案中的一些建议

11 个答案:

答案 0 :(得分:6)

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

法术属性:

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

SpellEffect的属性:

  • 效果类型(防御,进攻,buff,debuff,......)
  • 效果的目标(自我,派对,目标,目标周围区域,目标线,......)
  • 效果的属性或属性(hp,mana,max hp,力量,攻击速度......)
  • 效果会改变统计数据(+ 10,-500,5%,...)
  • 效果持续多长时间(10秒,5个刻度,......)

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

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

此外,您定义的每个SpellEffect都可以通过使用System.Xml.Serialization的XmlSerializer类轻松地将非常写入XML并从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文件。

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

答案 1 :(得分:3)

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

此外,如果你 将拥有一个属性而不仅仅是wizard.Cast(new Spell.MageArmor(), wizard),拥有一个SetSpell方法有点奇怪 - 为什么不只是制作{{ 1}} property public?

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

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

答案 2 :(得分:2)

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

- 编辑澄清(希望) -

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

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

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

答案 3 :(得分:2)

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

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

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

  • 您可以将您的游戏状态暴露给脚本 语言和脚本你的法术(你也可以 将此与第一个想法相结合,以便在大多数情况下 你的脚本法术只是在代码中调用预定义的效果。 Duel of the Planeswalkers (the M:TG game on X-Box 360) was written broadly with this approach

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

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方法)来表示增益效果。

That article on Duel of the Planeswalkers is really excellent reading though. So good I'll link it twice!

答案 4 :(得分:1)

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

答案 5 :(得分:1)

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

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?

答案 6 :(得分:1)

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

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);
    }
}

答案 7 :(得分:0)

首先:对所有事情总是有更好/更清洁/更容易的方法。

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

答案 8 :(得分:0)

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

答案 9 :(得分:0)

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

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

答案 10 :(得分:0)

我倾向于认为你的咒语和物品应该不是类,而是效果的组合。

这是我对它的看法,随意扩展。它基本上使用复合方法和法术效果的两阶段评估,因此每个类都可以添加特定的resitance。

[Serializable]
class Spell
{
    string Name { get; set; }
    Dictionary<PowerSource, double> PowerCost { get; set; }
    Dictionary<PowerSource, TimeSpan> CoolDown { get; set; }
    ActionProperty[] Properties { get; set; }
    ActionEffect Apply(Wizzard entity)
    {
        // evaluate
        var effect = new ActionEffect();
        foreach (var property in Properties)
        {
            entity.Defend(property,effect);
        }

        // then apply
        entity.Apply(effect);

        // return the spell total effects for pretty printing
        return effect;
    }
}

internal class ActionEffect
{
    public Dictionary<DamageKind,double> DamageByKind{ get; set;}       
    public Dictionary<string,TimeSpan> NeutralizedActions{ get; set;}       
    public Dictionary<string,double> EquipmentDamage{ get; set;}
    public Location EntityLocation{ get; set;} // resulting entity location
    public Location ActionLocation{ get; set;} // source action location (could be deflected for example)
}

[Serializable]
class ActionProperty
{
    public DamageKind DamageKind { get;  set; }
    public double? DamageValue { get; set;}
    public int? Range{ get; set;}
    public TimeSpan? duration { get; set; }
    public string Effect{ get; set}
}

[Serializable]
class Wizzard
{
    public virtual void Defend(ActionProperty property,ActionEffect totalEffect)
    {
        // no defence   
    }
    public void Apply(ActionEffect effect)
    {
        // self damage
        foreach (var byKind in effect.DamageByKind)
        {
            this.hp -= byKind.Value;
        }
        // let's say we can't move for X seconds
        foreach (var neutralized in effect.NeutralizedActions)
        {
            Actions[neutralized.Key].NextAvailable += neutralized.Value;
        }

        // armor damage?
        foreach (var equipmentDamage in effect.EquipmentDamage)
        {
            equipment[equipmentDamage.Key].Damage += equipmentDamage.Value;
        }
    }
}

[Serializable]
class RinceWind:Wizzard
{
    public override void Defend(ActionProperty property, ActionEffect totalEffect)
    {
        // we have resist magic !
        if(property.DamageKind==DamageKind.Magic)
        {
            log("resited magic!");
            double dmg = property.DamageValue - MagicResistance;
            ActionProperty resistedProperty=new ActionProperty(property);
            resistedProperty.DamageValue = Math.Min(0,dmg);                
            return;
        }           
        base.Receive(property, totalEffect);
    }
}