Designing a clean/flexible way for a "character" to cast different spells in a role playing game [closed]
开发者_C百科
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 3 years ago.
Improve this questionI am creating a role playing game for fun and as a learning experience. I am at the point where my character (a wizard) is cast spells. I am using a strategy pattern to set the spell they are going to cast before casting the spell. The reason I went with this approach is because I want to be able to add different spell types later on w/ out having to mess with the character/wizard class.
My question - is this a bad design? Is there a better/cleaner/easier approach for this?
I am trying to stay away from being "that guy" who tries to make everything fit into a design pattern. But in this case I feel like it's a decent fit.
Here is what my code looks like with 2 spells so far
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);
}
}
now to cast a spell we do something like this:
Wizard wizard = new Wizard();
wizard.Cast(new Spell.MageArmor(), wizard); // i am buffing myself
UPDATE: updated code with some suggestions from the answers below
Following what Willcodejavaforfood said, you could design a SpellEffect
class that describes a single effect your spell could have. You can create a "vocabulary" to use to describe:
Attributes for Spell:
- Name
- Mana cost
- Target restriction of the whole spell (player, npc, monster,...)
- Total duration of spell (highest of SpellEffect durations) (10 sec, 5 ticks,...)
- Casting time
- Spell range (5 meters, 65 units, ...)
- Fail rate (5%, 90%)
- Time to wait before this spell can be cast again (Recast time)
- Time to wait before ANY spell can be cast again (Recovery time)
- etc...
Attributes for SpellEffect:
- Type of effect (defense, offense, buff, debuff,...)
- Target of effect (self, party, target, area around target, line to target,...)
- Property or stat the effect acts on (hp, mana, max hp, strength, attack speed,...)
- How much the effect changes the stat (+10, -500, 5%,...)
- How long the effect lasts (10 sec, 5 ticks,...)
- etc.
I would imagine that your vocabulary (the words in parentheses above) would be defined in a set of enums. It might also be advisable to create a class hierarchy to represent the SpellEffect types, instead of using an enum for that particular attribute, because there might be a SpellEffect type that doesn't need all these attributes or perhaps there's some kind of custom logic for each basic SpellEffect type that I'm not thinking about. But that also might complicate things too much. KISS principle =).
Anyway, the point is that you are pulling out the specific information on a Spell's effect into a separate data structure. The beauty of this is that you can create 1 Spell
class and make it hold a List of SpellEffects to apply upon activation. Then the spell can perform multiple functions (damage enemy and heal player, aka life tap) in one shot. You create a new instance of Spell for each spell. Of course, at some point you will have to actually create the spells. You could easily throw together a spell editor utility to make that easier.
Furthermore, each SpellEffect you define can very easily be written to and loaded from XML by using System.Xml.Serialization's XmlSerializer class. It's a breeze to use on simple data classes like SpellEffect. You can even just serialize your final List of Spell to xml too. For example:
<?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>
You could also chose to put your data in a database instead of xml. Sqlite would be small, fast, easy, and free. You can also use LINQ to query your spell data from xml or sqlite.
Of course, you could do something similar for your monsters and such, too--at least for their data. I'm not sure about the logic part.
If you use this kind of system, you can get the added benefit of being able to use your Creature/Spell system for other games. You can't do that if you "hard code" your spells. It will also allow you to change the spells (class balancing, bugs, whatever) without having to rebuild and redistribute your game executable. Just a simple xml file.
Holy cow! I'm really excited about your project now and how something like I described could be implemented. If you need any help, let me know!!
It's not particularly clear why you'd want it to be a two stage process unless that's going to be exposed in the UI (i.e. if the user will set the "loaded spell" and can later change their mind).
Additionally, if you are going to have a property rather than just wizard.Cast(new Spell.MageArmor(), wizard)
, having a SetSpell method is a bit odd - why not just make the LoadedSpell
property public?
Finally, do spells actually have any mutable state? Could you just have a fixed set of instances (flyweight/enum pattern)? I'm not thinking of the memory usage here (which is the normal reason for the flyweight pattern) but just the conceptual nature of it. It feels like you want something which is really just like a Java enum - a set of values with custom behaviour. It's harder to do that in C# because there's no direct language support, but it's still possible.
The actual pattern within the spell (having a caster and a target) seems reasonable, although you may find it becomes inflexible if you want to be able to have area effect spells (with a target location rather than a specific creature) or spells which curse/bless items etc. You may also need to pass in the rest of the state of the game world - e.g. if you have a spell to create minions.
I probably would not use subclassing for each spell here. I would try and put it on disk by using XML or JSON and create them dynamically.
--Edit to clarify (hopefully)--
This approach would require to really plan out in advance as much as possible. You would have to defined attributes as:
- Name
- Description
- Duration
- Target (self, area, other)
- Type (bonus, damage, curse)
- Effect (ex: 1d6 frost damage, +2 Armor Class, -5 Damage Resistance)
Wrapping all this behaviour in a generic spell class should make it really flexible and more straight forward to test.
It's natural to encapsulate "Spells" with the Command Pattern (which is basically what you've done). But you run into two problems:-
1) You've got to recompile to add more spells
You can enumerate every possible action it is possible for a spell to take, then define the spells in some external format (XML, Database) which gets loaded into your application on startup. Western RPGs tend to be coded like this - a "spell" consists of "Apply spell effect #1234 with parameter 1000", "play animation #2345", etc.
You can expose your gamestate to a scripting language, and script your spells (you can also combine this with the first idea so that in most cases your scripted spells are just calling pre-defined effects in code). Duel of the Planeswalkers (the M:TG game on X-Box 360) was written broadly with this approach
Or you can just live with it (I do...)
2) What happens when your spell target isn't a creature?
If you're exposing your gamestate to your spell scripts, this isn't a problem because your scripts can do anything they like within the context of what you're exposing.
Otherwise, you'd be best making a generic type.
I generally do something like the following (and not just in games either, I've been using this kind of pattern to represent behaviours in 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.
The advantage of this pattern is that it's really flexible for doing those "odd" things that you'd often expect spells to be able to do that more fixed models won't be capable of. You can do things like chain them together. You can have an Effect which adds a TriggeredEffect to your target - good for doing something like a Thorns Aura. You can have an IReversibleEffect (with an extra Unapply method) good for representing buffs.
That article on Duel of the Planeswalkers is really excellent reading though. So good I'll link it twice!
For some reason, "spells" feel more like a command pattern to me. But I've never designed a game so...
The biggest problem I see with this pattern is that all spells have to remember to subtract their mana cost. How about:
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);
}
Also, should Wizard extend PlayerCharacter, which would extend Creature?
I think your design looks fine. Since each Spell class is basically a wrapper around a function (this is more properly the Command pattern, not Strategy), you could get rid of spell classes completely and just use functions with a little bit of reflection to find the spell methods and add some metadata to them. Like:
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);
}
}
First of all: There is always a better/cleaner/easier approach for everything.
But in my eyes you've made a decent abstraction of your challenge which can be a solid base for further improvements.
I might be missing something, but the trio WizardSpells, LoadedSpell, SetSpell seems like it could be clarified. Specifically, I don't see the list being used in your code so far. I would probably add spells available to the Wizard to the list, with LearnNewSpell(Spell newSpell), and check that LoadSpell uses a spell from that list.
Also, you might consider at some point adding some extra info about the type of caster on the Spell, if you are going to have multiple types of Casters.
I tend to think that your spells and items should really not be classes but a composition of effects.
Here's my take on it, feel free to expand. It's basically using a composite approach and a two-phase evaluation of spell effects, so each class can add specific 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);
}
}
精彩评论