Difference between revisions of "Programming Roguelike Magic"

From RogueBasin
Jump to navigation Jump to search
 
Line 222: Line 222:
</pre>
</pre>


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

Revision as of 11:59, 12 September 2006

1. A typical first-time approach

Magic is an important part of most roguelikes, in one way or another.
Partly due to adding some glitz and atmosphere to the game, and partly
due to the added tactical options and diversity. It was also the one 
that I had most difficulty in programming when I tried to make my first 
roguelike.

One method of creating spells is to hard-code all the spells into separate
functions, and select the correct function from a gigantic switch
statement. My first try was something along these lines:

switch (spell_num)
{
 case SPELL_FIREBALL:
    cast_fireball(y, x);
	break;
 case SPELL_MAGICMISSILE:
    cast_magicmissile(y, x);
	break;
 case SPELL_HEALSELF:
    cast_healself();
	break;
}

But once I started adding more spells, a lot of code needed to be
unnecessarily duplicated. For example, a magic missile spell and
a icebolt spell do not really differ very much, and neither do a 
fireball and a firebolt. 

By analyzing the different spells that you want to deal with in your 
game and deciding upon some properties that a lot of spells will 
share the work can be greatly simplified.

2. Properties of a spell

The basic properties of spells that I decided upon were the effect of
the spell (heal, teleport, fire, cold), the area that it acts on
(unit, beam, ball, level) and the possible targets of the spell 
(the square the player is on, any square adjacent to the player,
any square on the level).

typedef struct
{
	int (*ef)(int, int);
	int area;
	int target;
}
spell_s;
							
Wait, what is the first variable in the struct ? It's a function pointer,
a very powerful feature of C. Just like regular pointers point to the
memory location of a variable, function pointers point to the adress
of a function. Any function that matches the prototype of the pointer
(return int, accept 2 ints as arguments in this case) can be assigned to
the pointer, and called from it. Well that's neat, but how does this 
help us make a better magic system ?

The logical way to use the properties that we have is to go through
them one by one. First look at the target variable and prompt the
user for the coordinates of the target. If the target is any adjacent
square, we can just prompt for a direction and receive the x and y
coordinates. For a target of the square the player is on we need to
do even less.

Next, look at the area variable and find out all the squares that
will be affected by the spell. For a ball spell a ball of a
certain radius around the target coordinates, for a level spell
all the squares on the level. 

Finally, look at the effect variable and apply the effect to all
the squares affected. And naturally to any players, monsters or
items that are residing in the particular squares too.

3. An example

The last two stages are easier to do parallelly, by finding one square
to be affected, and applying the spell to it. Here is a small
example of the concept:

First, lets initialize a couple of spells:

int spell_init(void)
{
    /* a heal-player spell */
    /* The function pointer can be assigned to using the name of the
	   function without the parenthesis at the end*/
	spells[0].ef = spell_effect_heal; 	        /* Correct way */
	/* spells[0].ef = spell_effect_heal(); */     /* Wrong way */
	spells[0].area = AREA_SQUARE;
	spells[0].target = TARGET_SELF;

    /* a fireball spell */
	spells[1].ef = spell_effect_fire;
	spells[1].area = AREA_BIGSQUARE;   /* Well, it's almost a ball ;) */
	spells[1].target = TARGET_VISIBLE;

    return 0;
}

And of course we'll need the functions that are being pointed to:

int spell_effect_heal(int y, int x)
{
    monster_s* monster;
	
	/* Heal the player first */
    if (player_at(y, x) )
	  player.hp += 10;
	  
	/* If there is a monster in the square, heal it too */
    monster = monster_at(y, x);
	if (monster != NULL)
	  monster-&gthp += 10;

    return 0;
}

int spell_effect_fire(int y, int x)
{
    monster_s* monster;
 
    /* Do some visual effects to the square, to make our fireball look
	   more spectacular */
    draw_character(y, x, '*', RED);

    if (player_at(y, x) && player.fireresistance == 0) 
	  player.hp -= 10;
	  
    monster = monster_at(y, x);
	if (monster != NULL)
	  monster-&gthp -= 10;

    return 0;
}


And of course a function for actually casting the spells:

int spell_do(spell_s* sp)
{
    int y, x, y2, x2;
	
	/* Acquire a target for the spell */
	
    switch (sp-&gttarget)
	{
	 case TARGET_VISIBLE:
	    get_visible_target_coordinates(&y, &x);
		break;
	 case TARGET_ADJACENT:
	    get_adjacent_target_coordinates(&y, &x);
		break;
	 case TARGET_SELF:
	    y = player.y;  /* Y-coordinate of the player */
		x = player.x;  /* X-coordinate of the player */
		break;
	}

    /* Apply the spell to an area */

    switch (sp-&gtarea)
	{
	 /* Just one square */
	 case AREA_SQUARE:
	    sp-&gtef(y, x);
		break;
	 /* Lots of squares */
	 case AREA_BIGSQUARE:
	   for (y2 = -3; y2 <= 3; y2++)
	   	 for (x2 = -3; x2 <= 3; x2++)
		   sp-&gtef(y+y2, x+x2);
	   break;
	}	
	return 0;
}

Some nice things came out of this approach. Adding new spells that share 
properties with the old ones is now very easy. In my own game there 
are no static spells, the player can mix the different properties 
freely to create custom-made spells for any situation. Using 
function pointers also helps to avoid cut-and-paste programming.

4. Projects to continue with

Beyond adding weird areas of effect or new types of damage there are
a few things that you can work with to make your roguelike meet the
standards set by earlier games.

Obviously some improvements really need to be made into this system. 
Some method of telling the spell_effect functions a power level for
the spell. Another parameter that should be added is some concept
of range. It is constraining to have all ball-spells have a radius
of three squares, or beam-spells to reach 5 squares all the time.

The current method of supplying coordinates of the squares to be
affected is too constraining. For example you would often want to 
apply spells into objects in your inventory or equipment slots. 
One method is adding a new targeting method (TARGET_INVENTORY) 
returning an item from the inventory and a new area of effect 
(AREA_INVENTORY) that calls the spell_effect_* function with some 
extra parameters. Another approach is making a separate hierarchy
for spells that affect inventory items, one for those affecting objects
on the main map, and new hierarchies for other needs that arise.

Once you have spells it should be easy to add magical items like potions,
scrolls and wands, as well as adding spell activations to weapons and
armor.

And the biggest challenge that you will face is in all propability 
designing a balanced, interesting and maybe even remotely original 
set of spells.

If you disagree vehemently with something I said in this article, 
please let me know at jsnell@lyseo.edu.ouka.fi. If you think that 
the article was the most brilliant piece of writing ever, 
you really need to let me know. I could use the encouragement :-)


Juho Snellman