Designing a clean/flexible way for a “character” to cast different spells in a role playing game
我正在创建一个角色扮演游戏,以获得乐趣和学习体验。我的角色(巫师)正处于施法阶段。我使用策略模式来设置他们在施法之前要施法的法术。我使用这种方法的原因是因为我希望以后能够添加不同的拼写类型,而不必再去处理字符/向导类。
我的问题是,这是一个糟糕的设计吗?有没有更好/更干净/更简单的方法?
我尽量避免成为"那个试图使一切都符合设计模式的人"。但在这种情况下,我觉得这是一个体面的适合。
以下是到目前为止我的代码和2个拼写的情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | 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); } } |
现在要施法,我们要做如下的事情:
号
更新:用下面的答案中的一些建议更新了代码
按照willcodejavaforfood所说的,您可以设计一个
拼写属性:
- 名字
- 法力消耗
- 整个法术的目标限制(玩家,NPC,怪物,…)
- 法术总持续时间(法术效果持续时间的最高值)(10秒,5个滴答…)
- 铸造时间
- 拼写范围(5米,65单位,…)
- 不合格率(5%,90%)
- 等待该法术再次施展的时间(重铸时间)
- 等待任何法术再次施放的时间(恢复时间)
- 等。。。
- 效果类型(防御,进攻,buff,debuff,…)
- 效果目标(自我、团体、目标、目标周围区域、目标线…)
- 属性或属性效果作用于(生命值、法力值、最大生命值、力量值、攻击速度…)
- 效果如何改变统计(+10,-500,5%,…)
- 效果持续多长时间(10秒,5个滴答…)
- 等。
你可以列举所有可能的动作一个法术可以接受,然后定义一些咒语外部格式(XML、数据库),其中在上加载到应用程序启动。西方RPG的编码方式这个-一个"咒语"由"应用"组成拼写效果1234,带参数1000","播放动画2345"等。
你可以让你的游戏状态暴露在脚本中语言,编写拼写脚本(也可以把这个和第一个想法结合起来,这样在大多数情况下你的脚本法术只是在代码中调用预先定义的效果)。吞刨子的决斗(x-box 360上的m:tg游戏)是用这种方法
或者你可以就这样生活(我是……)
如果你将你的游戏状态暴露给你的拼写脚本,这不是问题,因为你的脚本可以在你所暴露的上下文中做任何他们喜欢的事情。
否则,最好是生成一个泛型类型。
- 名字
- 说明
- 持续时间
- 目标(自身、区域、其他)
- 类型(奖励、伤害、诅咒)
- 效果(例如:1d6霜冻伤害,+2装甲等级,-5伤害抗性)
小精灵
拼写效果的属性:
小精灵
我可以想象你的词汇表(上面括号中的单词)将在一组枚举中定义。最好创建一个类层次结构来表示拼写效果类型,而不是对该特定属性使用枚举,因为可能有一个不需要所有这些属性的拼写效果类型,或者可能对于我没有考虑的每个基本拼写效果类型都有某种自定义逻辑。但这也可能使事情变得过于复杂。亲吻原则=)。
不管怎样,关键是要将拼写效果的特定信息提取到单独的数据结构中。这样做的好处是,您可以创建1个
此外,通过使用System.xml.Serialization的XmlSerializer类,可以非常容易地将定义的每个拼写效果写入XML并从XML加载。在像spelleffect这样的简单数据类上使用是轻而易举的。您甚至可以将最后的拼写列表序列化为XML。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 | <?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查询拼写数据。
当然,你也可以为你的怪物做类似的事情,至少对他们的数据也是这样。我不确定逻辑部分。
如果你使用这种系统,你可以得到额外的好处,可以使用你的生物/法术系统进行其他游戏。如果你"硬编码"你的拼写,你就不能这样做。它还允许你改变咒语(类平衡,bug,随便什么),而不必重建和重新分配你的游戏可执行文件。只是一个简单的XML文件。
天哪!我现在对你的项目以及我描述的一些东西是如何实现感到非常兴奋。如果你需要帮助,请告诉我!!
不太清楚为什么你希望它是一个两阶段的过程,除非它将在用户界面中公开(也就是说,如果用户设置了"加载的咒语",然后可以改变他们的想法)。
另外,如果您要拥有一个属性而不仅仅是wizard.cast(新的
最后,拼写是否有任何可变的状态?你能不能有一组固定的实例(flyweight/enum模式)?我不考虑这里的内存使用(这是flyweight模式的正常原因),只考虑它的概念性质。感觉像是想要一个真正像JavaEnUM的东西——一组带有自定义行为的值。在C语言中很难做到这一点,因为没有直接的语言支持,但这仍然是可能的。
法术中的实际模式(拥有施法者和目标)似乎是合理的,尽管你可能会发现如果你想拥有区域效果法术(目标位置而不是特定生物)或诅咒/祝福物品的法术,它会变得不灵活。你可能还需要在游戏世界的其他状态中通过-例如。。如果你有一个法术来创造奴仆。
用命令模式(基本上就是你所做的)封装"咒语"是很自然的。但你遇到了两个问题:
1)你必须重新编译才能添加更多的法术。
小精灵
2)当你的法术目标不是生物时会发生什么?
小精灵
我通常会做如下的事情(不仅仅是在游戏中,我一直在使用这种模式来表示多智能代理系统中的行为):-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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. |
这种模式的优点是它非常灵活,可以做那些你经常希望法术能够做到的"奇怪"的事情,而更多的固定模式是不可能做到的。你可以做一些像把它们连在一起的事情。你可以有一个效果,这会给你的目标增加一个触发效果,这对你做一些类似荆棘光环的事情很有好处。你可以有一个IReversibleeffect(用一个非常不实用的方法)来表示buff。
不过,那篇关于"吃刨子的人的决斗"的文章读得很好。太好了,我会链接两次!
我可能不会在这里为每个咒语使用子类化。我将尝试使用XML或JSON将其放到磁盘上,并动态地创建它们。
--编辑以澄清(希望)--
这种方法需要尽可能提前进行真正的计划。您必须将属性定义为:
小精灵
将所有这些行为包装在一个通用的拼写类中应该能够使它真正灵活,更直接地进行测试。
我觉得你的设计看起来不错。因为每个spell类基本上都是一个函数的包装器(这更恰当地说是命令模式,而不是策略),所以您可以完全摆脱spell类,只需使用带有少许反射的函数来查找spell方法并向其添加一些元数据。像:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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,哪个可以扩展生物?
出于某种原因,"咒语"对我来说更像是一种命令模式。但我从未设计过一款游戏,所以…
我倾向于认为你的咒语和物品不应该是类,而应该是效果的组合。
这是我的看法,请随意扩大。它基本上是使用复合方法和对法术效果的两阶段评估,因此每个类都可以添加特定的抵抗力。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | [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); } } |
号
你的单元测试是什么样子的?
设计是否使您编写所需测试变得容易?
我可能遗漏了一些东西,但是三个巫师的咒语,loadedspell,setspell似乎可以被澄清。具体来说,到目前为止,我看不到您的代码中使用的列表。我可能会使用learnnewspell(拼写newspell)将向导可用的拼写添加到列表中,并检查loadspell是否使用该列表中的拼写。另外,如果你要有多种类型的施法者,你可能会考虑在某个时候添加一些关于施法者类型的额外信息。
首先:总是有更好/更干净/更简单的方法来处理所有事情。
但在我看来,你对自己的挑战做了一个很好的抽象,这是进一步改进的坚实基础。