Difference between revisions of "Representing Magic Skills"

From RogueBasin
Jump to navigation Jump to search
 
(heavy edits, code fixes, and further explanations; note that I am the original author of the post)
 
(4 intermediate revisions by 3 users not shown)
Line 1: Line 1:
<pre>
This article describes the basics of representing and using magic spells in roguelikes and many other games. The general techniques are very similar to those used in my article on object
This article describes the basics of representing and using Magick Spells in roguelikes.
The general techniques are very similar to those used in my article on Object
representation.
representation.


For starts, we need a class to hold the information for a specific spell.
The basic concept is to create a data-driven definition of a spell, including the effects the spell has.  This can be done with a simple set of data structure and a hard-coded table to start.  More complete versions will want to use an external data-file to allow more rapid iteration of spells.


class Spell {
For starts, we need a class to hold the information for a specific spell.  We will call this the SpellEffect.  Spell effects can be defined with a few simple properties.  One of these is an enumeration to define the specific effect in place, which would be used in code to select which set of game logic to apply when the spell effect is used.  Different spell types will need additional parameters.  We can for many simpler magic systems simply keep this as an array of integers, the specific interpretation of which is up to the spell effect.
public:


OK.  Now, let's give the spell a name.
First, we create an enumeration of the different spell effects the game has support for.


char *Name;
    enum ESpellEffectType {
        kEffectNone,
        kEffectHeal,
        kEffectDamage,
        kEffectTickle,
        kEffectCreateObject,
    };
   
Now we can create a simple definition for a spell effect.  All we need is the type of effect and the parameter array.  We'll create a macro for the number of items to ensure we have as few magic numbers (no pun intended) as possible.


There.  Now, every magick spell costs Mana Points (well, if you're using a magick system
    #define NUM_SPELL_EFFECT_PARAMS 5
similar to most others).  So we need to define the spell's cost in MP.
    struct SpellEffect {
        enum ESpellEffectType type;
        int params[NUM_SPELL_EFFECT_PARAMS];
    };


int MPCost;
At this point we can define an entire spell.  A spell can have multiple effects, so we need an array of SpellEffect values in our spell.  We'd also like some additional data about the spell, such as its name, its cost in mana points, and its level.  This metadata will vary based on how exactly the magic system in a particular game is meant to function from a gameplay perspective.  Not all games use levels or mana points, for instance.
   
    #define NUM_SPELL_EFFECTS 5
    struct Spell {
        char* name;
        int mpCost;
        int level;
        struct SpellEffect effects[NUM_SPELL_EFFECTS];
    };


Also, every spell has a level: how difficult a spell it is.  Only more powerful casters can
Casting the spell is just as simple.  In order to cast a spell, the system needs to know which spell is being cast, who is casting the spell, the target (if any), and so on.
use more powerful spells.


int Level;
    void CastSpell ( const struct Spell* spell, struct Actor* source, struct Actor* target );


There.  Now all we need to know is what in Gehenna the spell does.  We'll make a
An implementation could be as simple as a switch statement on the effec type.
sub-class called Effect to store this information.


class Effect: {
    void CastSpell ( const struct Spell* spell, struct Actor* source, struct Actor* target ) {
friend Spell;
        int effectIndex;
        PrintLog( "%s cast %s on %s", source->name, spell->name, target->name );
        for ( effectIndex = 0 ; effectIndex < NUM_SPELL_EFFECTS &&
                spell->effects[ effectIndex ].type != kEffectNone; ++ effectIndex ) {
            const struct SpellEffect* effect = &spell->effects[ effectIndex ];
            switch ( effect->type ) {
                case kEffectHeal:
                    Heal( target, effect->params[0] + source->level * effect->params[1] );
                    break;
                case kEffectDamage:
                    Damage( target, effect->params[0] + source->level * effect->params[1] );
                    break;
                case kEffectCreateObject:
                    CreateObjectAt( effect->params[0], target->position );
                    break;
            }
        }
    }


OK, so what does a specific spell do? We'll make a simple int to describe what a specific
Notice how the params of each effect are used.  For both heal and damage, the parameters are defined as a constant value plus a scalar applied to the caster's level.  The other values in the params array are unused.  For the create object effect, the first param is used as an object type (likely taken from another enumeration) and the other values are unused. These parameters can be used however is most appropriate for the desired effect.  Also notice how kEffectNone is used as a sentinel, which must be 0 to make easy initialization in a C table work.
Effect describes.


int Type;
For instance, the heal effect could instead be defined as:


So what does Type mean?  We'll use some #define's to specify.
    Heal( target, random_dice( effect->params[0] , effect->params[1] ) + effects->params[2] );
   
That definition would mean that the first three parameters are used to define an expression similar to the common XdY+Z pattern, meaning to roll X number of Y-sided dice and add Z to the result.
   
An example of definining a couple of really simple spells with this system in C without a data file would be:


#define HEAL 0
    const struct Spell spells[] = {
#define SCORCH 1
        {
#define TICKLE 2
            "Cure Light Wounds", 10, 1, {
#define CREATE_OBJ 3
                { kEffectHeal, { 10, 1, } },
            }
        },
        {
            "Cure Moderate Wounds", 30, 2, {
                { kEffectHeal, { 15, 4, } },
            }
        },
        {
            "Summon Duck", 15, 5, {
                { kEffectCreateObject, { kObjectDuckActor } },
            }
        },
    };
   
Note the aggregate initialize syntax of C.  Only the parameters in use need to be specified.  Since the definition of the heal effect only uses the first two parameters, that's all we fill in.  The kEffectNone spell type is used as a sentinel value so our loop above knows when it's done with all effects in a spell.


You can of course add as many as you want.  Now, we know what an Effect does, but
It may be necessarily to add some additional fields to SpellEffect, such as whether the effect affects the caster/source or the target, timing information, and so on.
that's not enough.  For example, for a HEAL Effect, how much HP does it heal?  We
could base everything of level (making a rigid and uniform magick system, which may
be what you want: predictability), or we could give each Effect a set of arguments to
define in more detial what it does.  We'll do this through the use of 5 int's.
 
int Args[5];
 
What do the fields of Args mean?  That's based on what Type is equal to.  For an Effect
with Type HEAL, we might say that:
 
Args[0] is Number of Dice
Args[1] is Sides per Die
Args[3] is Roll Mod
 
So an Effect of type HEAL with Args[0] = 2, Args[1] = 6, Args[3] = 2 would heal 2d6+2
HP.  Pretty simple, eh?
 
Anyways, we can close of the Effect class.  We want each Spell to have 5 Effect's (so
every spell can have lots of flexibility).
 
} Effects[5];
 
We can now close of the Spell class.
 
};
 
So that's all there is to a basic magick class.
 
Casting the spell is just as simple.  Make a function called Cast or whatever (I used an
object method inside the Spell class, since I'm a C++ OOP freak).  The function would
take as arguments the spell to cast, the target, etc.
 
void Cast ( Spell *SpellToCast, int TX, int TY );
 
Then we go with a big, evil, switch statement based on effect.  This actually works
VERY well.  The flexibility is astounding...
 
Of course, how each spell takes effect is based on how you've programmed the rest
of your roguelike.  Because of the OO nature of WotR, I found it very easy to create
simple object Methods for spell effects.
 
For the HEAL Spell Effect Type, you might to do something as simple as loop through
all the Characters (NPC's and Players) in the target loc (defined by TX and TY), and
heal them based on the arguments listed above... 2d6+2, or whatever.
 
Anyways, this is just the basics.  The advanced stuff all depends on your magick
system and how you programmed the rest of the game.
 
The complete source for the Spell class is:
 
#define HEAL 0
#define SCORCH 1
#define TICKLE 2
#define CREATE_OBJ 3
 
class Spell {
public:
char *Name;
 
int MPCost;
int Level;
 
class Effect: {
friend Spell;
 
int Type;
 
int Args[5];
} Effects[5];
};
 
Any questions, comments, threats, etc., e-mail me at
sean.middleditch@iname.com
Well, I don't really want any threats.
 
The End


Any questions, comments, threats, etc., e-mail me at s_middleditch@wargaming.net


Sean Middleditch
Sean Middleditch
Wargaming Seattle
</pre>
</pre>


[[Category:Articles]][[Category:Magic]]
[[Category:Articles]][[Category:Magic]]

Latest revision as of 04:29, 16 August 2013

This article describes the basics of representing and using magic spells in roguelikes and many other games. The general techniques are very similar to those used in my article on object representation.

The basic concept is to create a data-driven definition of a spell, including the effects the spell has. This can be done with a simple set of data structure and a hard-coded table to start. More complete versions will want to use an external data-file to allow more rapid iteration of spells.

For starts, we need a class to hold the information for a specific spell. We will call this the SpellEffect. Spell effects can be defined with a few simple properties. One of these is an enumeration to define the specific effect in place, which would be used in code to select which set of game logic to apply when the spell effect is used. Different spell types will need additional parameters. We can for many simpler magic systems simply keep this as an array of integers, the specific interpretation of which is up to the spell effect.

First, we create an enumeration of the different spell effects the game has support for.

   enum ESpellEffectType {
       kEffectNone,
       kEffectHeal,
       kEffectDamage,
       kEffectTickle,
       kEffectCreateObject,
   };
   

Now we can create a simple definition for a spell effect. All we need is the type of effect and the parameter array. We'll create a macro for the number of items to ensure we have as few magic numbers (no pun intended) as possible.

   #define NUM_SPELL_EFFECT_PARAMS 5
   struct SpellEffect {
       enum ESpellEffectType type;
       int params[NUM_SPELL_EFFECT_PARAMS];
   };

At this point we can define an entire spell. A spell can have multiple effects, so we need an array of SpellEffect values in our spell. We'd also like some additional data about the spell, such as its name, its cost in mana points, and its level. This metadata will vary based on how exactly the magic system in a particular game is meant to function from a gameplay perspective. Not all games use levels or mana points, for instance.

   #define NUM_SPELL_EFFECTS 5
   struct Spell {
       char* name;
       int mpCost;
       int level;
       struct SpellEffect effects[NUM_SPELL_EFFECTS];
   };

Casting the spell is just as simple. In order to cast a spell, the system needs to know which spell is being cast, who is casting the spell, the target (if any), and so on.

   void CastSpell ( const struct Spell* spell, struct Actor* source, struct Actor* target );

An implementation could be as simple as a switch statement on the effec type.

   void CastSpell ( const struct Spell* spell, struct Actor* source, struct Actor* target ) {
       int effectIndex;
       PrintLog( "%s cast %s on %s", source->name, spell->name, target->name );
       for ( effectIndex = 0 ; effectIndex < NUM_SPELL_EFFECTS &&
               spell->effects[ effectIndex ].type != kEffectNone; ++ effectIndex ) {
           const struct SpellEffect* effect = &spell->effects[ effectIndex ];
           switch ( effect->type ) {
               case kEffectHeal:
                   Heal( target, effect->params[0] + source->level * effect->params[1] );
                   break;
               case kEffectDamage:
                   Damage( target, effect->params[0] + source->level * effect->params[1] );
                   break;
               case kEffectCreateObject:
                   CreateObjectAt( effect->params[0], target->position );
                   break;
           }
       }
   }

Notice how the params of each effect are used. For both heal and damage, the parameters are defined as a constant value plus a scalar applied to the caster's level. The other values in the params array are unused. For the create object effect, the first param is used as an object type (likely taken from another enumeration) and the other values are unused. These parameters can be used however is most appropriate for the desired effect. Also notice how kEffectNone is used as a sentinel, which must be 0 to make easy initialization in a C table work.

For instance, the heal effect could instead be defined as:

   Heal( target, random_dice( effect->params[0] , effect->params[1] ) + effects->params[2] );
   

That definition would mean that the first three parameters are used to define an expression similar to the common XdY+Z pattern, meaning to roll X number of Y-sided dice and add Z to the result.

An example of definining a couple of really simple spells with this system in C without a data file would be:

   const struct Spell spells[] = {
       {
           "Cure Light Wounds", 10, 1, {
               { kEffectHeal, { 10, 1, } },
           }
       },
       {
           "Cure Moderate Wounds", 30, 2, {
               { kEffectHeal, { 15, 4, } },
           }
       },
       {
           "Summon Duck", 15, 5, {
               { kEffectCreateObject, { kObjectDuckActor } },
           }
       },
   };
   

Note the aggregate initialize syntax of C. Only the parameters in use need to be specified. Since the definition of the heal effect only uses the first two parameters, that's all we fill in. The kEffectNone spell type is used as a sentinel value so our loop above knows when it's done with all effects in a spell.

It may be necessarily to add some additional fields to SpellEffect, such as whether the effect affects the caster/source or the target, timing information, and so on.

Any questions, comments, threats, etc., e-mail me at s_middleditch@wargaming.net

Sean Middleditch Wargaming Seattle