Programming Roguelike Magic

From RogueBasin
(Difference between revisions)
Jump to: navigation, search
(wikified)
Line 1: Line 1:
<pre>
+
== A typical first-time approach ==
1. A typical first-time approach
+
  
 
Magic is an important part of most roguelikes, in one way or another.
 
Magic is an important part of most roguelikes, in one way or another.
Line 12: Line 11:
 
statement. My first try was something along these lines:
 
statement. My first try was something along these lines:
  
switch (spell_num)
+
switch (spell_num)
{
+
{
case SPELL_FIREBALL:
+
  case SPELL_FIREBALL:
    cast_fireball(y, x);
+
    cast_fireball(y, x);
break;
+
  break;
case SPELL_MAGICMISSILE:
+
  case SPELL_MAGICMISSILE:
    cast_magicmissile(y, x);
+
    cast_magicmissile(y, x);
break;
+
  break;
case SPELL_HEALSELF:
+
  case SPELL_HEALSELF:
    cast_healself();
+
    cast_healself();
break;
+
  break;
}
+
}
  
 
But once I started adding more spells, a lot of code needed to be
 
But once I started adding more spells, a lot of code needed to be
Line 34: Line 33:
 
share the work can be greatly simplified.
 
share the work can be greatly simplified.
  
2. Properties of a spell
+
== Properties of a spell ==
  
 
The basic properties of spells that I decided upon were the effect of
 
The basic properties of spells that I decided upon were the effect of
Line 42: Line 41:
 
any square on the level).
 
any square on the level).
  
typedef struct
+
typedef struct
{
+
{
int (*ef)(int, int);
+
  int (*ef)(int, int);
int area;
+
  int area;
int target;
+
  int target;
}
+
}
spell_s;
+
spell_s;
 
 
 
Wait, what is the first variable in the struct ? It's a function pointer,
 
Wait, what is the first variable in the struct ? It's a function pointer,
Line 74: Line 73:
 
items that are residing in the particular squares too.
 
items that are residing in the particular squares too.
  
3. An example
+
== An example ==
  
 
The last two stages are easier to do parallelly, by finding one square
 
The last two stages are easier to do parallelly, by finding one square
Line 82: Line 81:
 
First, lets initialize a couple of spells:
 
First, lets initialize a couple of spells:
  
int spell_init(void)
+
int spell_init(void)
{
+
{
    /* a heal-player spell */
+
  /* a heal-player spell */
    /* The function pointer can be assigned to using the name of the
+
  /* The function pointer can be assigned to using the name of the
  function without the parenthesis at the end*/
+
      function without the parenthesis at the end*/
spells[0].ef = spell_effect_heal;         /* Correct way */
+
  spells[0].ef = spell_effect_heal;       /* Correct way */
/* spells[0].ef = spell_effect_heal(); */    /* Wrong way */
+
  /* spells[0].ef = spell_effect_heal(); */    /* Wrong way */
spells[0].area = AREA_SQUARE;
+
  spells[0].area = AREA_SQUARE;
spells[0].target = TARGET_SELF;
+
  spells[0].target = TARGET_SELF;
 
+
    /* a fireball spell */
+
  /* a fireball spell */
spells[1].ef = spell_effect_fire;
+
  spells[1].ef = spell_effect_fire;
spells[1].area = AREA_BIGSQUARE;  /* Well, it's almost a ball ;) */
+
  spells[1].area = AREA_BIGSQUARE;  /* Well, it's almost a ball ;) */
spells[1].target = TARGET_VISIBLE;
+
  spells[1].target = TARGET_VISIBLE;
 
+
    return 0;
+
  return 0;
}
+
}
  
 
And of course we'll need the functions that are being pointed to:
 
And of course we'll need the functions that are being pointed to:
  
int spell_effect_heal(int y, int x)
+
int spell_effect_heal(int y, int x)
{
+
{
    monster_s* monster;
+
  monster_s* monster;
+
 
/* Heal the player first */
+
  /* Heal the player first */
    if (player_at(y, x) )
+
  if (player_at(y, x) )
  player.hp += 10;
+
    player.hp += 10;
 
+
 
/* If there is a monster in the square, heal it too */
+
  /* If there is a monster in the square, heal it too */
    monster = monster_at(y, x);
+
  monster = monster_at(y, x);
if (monster != NULL)
+
  if (monster != NULL)
  monster-&gthp += 10;
+
    monster-&gthp += 10;
 
+
    return 0;
+
  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
+
int spell_effect_fire(int y, int x)
  more spectacular */
+
{
    draw_character(y, x, '*', RED);
+
  monster_s* monster;
 
+
    if (player_at(y, x) && player.fireresistance == 0)  
+
  /* Do some visual effects to the square, to make our fireball look
  player.hp -= 10;
+
      more spectacular */
 
+
  draw_character(y, x, '*', RED);
    monster = monster_at(y, x);
+
if (monster != NULL)
+
  if (player_at(y, x) && player.fireresistance == 0)  
  monster-&gthp -= 10;
+
    player.hp -= 10;
 
+
 
    return 0;
+
  monster = monster_at(y, x);
}
+
  if (monster != NULL)
 
+
    monster-&gthp -= 10;
 +
 +
  return 0;
 +
}
  
 
And of course a function for actually casting the spells:
 
And of course a function for actually casting the spells:
  
int spell_do(spell_s* sp)
+
int spell_do(spell_s* sp)
{
+
{
    int y, x, y2, x2;
+
  int y, x, y2, x2;
+
/* Acquire a target for the spell */
+
  /* Acquire a target for the spell */
+
    switch (sp-&gttarget)
+
  switch (sp-&gttarget)
{
+
  {
case TARGET_VISIBLE:
+
    case TARGET_VISIBLE:
    get_visible_target_coordinates(&y, &x);
+
      get_visible_target_coordinates(&y, &x);
break;
+
    break;
case TARGET_ADJACENT:
+
    case TARGET_ADJACENT:
    get_adjacent_target_coordinates(&y, &x);
+
      get_adjacent_target_coordinates(&y, &x);
break;
+
    break;
case TARGET_SELF:
+
    case TARGET_SELF:
    y = player.y;  /* Y-coordinate of the player */
+
      y = player.y;  /* Y-coordinate of the player */
x = player.x;  /* X-coordinate of the player */
+
      x = player.x;  /* X-coordinate of the player */
break;
+
    break;
}
+
  }
 
+
    /* Apply the spell to an area */
+
  /* Apply the spell to an area */
 
+
    switch (sp-&gtarea)
+
  switch (sp-&gtarea)
{
+
  {
/* Just one square */
+
    /* Just one square */
case AREA_SQUARE:
+
    case AREA_SQUARE:
    sp-&gtef(y, x);
+
      sp-&gtef(y, x);
break;
+
    break;
/* Lots of squares */
+
    /* Lots of squares */
case AREA_BIGSQUARE:
+
    case AREA_BIGSQUARE:
  for (y2 = -3; y2 <= 3; y2++)
+
      for (y2 = -3; y2 <= 3; y2++)
  for (x2 = -3; x2 <= 3; x2++)
+
        for (x2 = -3; x2 <= 3; x2++)
  sp-&gtef(y+y2, x+x2);
+
          sp-&gtef(y+y2, x+x2);
  break;
+
      break;
}
+
  }
return 0;
+
  return 0;
}
+
}
  
 
Some nice things came out of this approach. Adding new spells that share  
 
Some nice things came out of this approach. Adding new spells that share  
Line 183: Line 181:
 
function pointers also helps to avoid cut-and-paste programming.
 
function pointers also helps to avoid cut-and-paste programming.
  
4. Projects to continue with
+
== Projects to continue with ==
  
 
Beyond adding weird areas of effect or new types of damage there are
 
Beyond adding weird areas of effect or new types of damage there are
Line 214: Line 212:
  
 
If you disagree vehemently with something I said in this article,  
 
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  
+
please let me know at jsnell[at]lyseo[dot]edu[dot]ouka[dot]fi. If you think that  
 
the article was the most brilliant piece of writing ever,  
 
the article was the most brilliant piece of writing ever,  
 
you really need to let me know. I could use the encouragement :-)
 
you really need to let me know. I could use the encouragement :-)
  
 +
''Written by Juho Snellman.''
  
Juho Snellman
+
[[Category:Magic]]
</pre>
+
[[Category:Articles]]
 
+
[[Category:Magic]][[Category:Articles]]
+

Revision as of 20:06, 29 September 2006

Contents

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.

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.

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.

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[at]lyseo[dot]edu[dot]ouka[dot]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 :-)

Written by Juho Snellman.

Personal tools