Программирование системы магии в рогаликах

Материал из Клуб любителей рогаликов
Перейти к: навигация, поиск

Типичный новый метод

Магия - так или иначе важная часть большинства roguelikes. Частично из-за дополнения некоторых фишек и атмосферы игры, и частично из-за дополнительного тактического разнообразия. Это также то, с чем я имел много трудностей в программировании, когда я попытался сделать свою первую roguelike.

Один метод создания заклинаний - жесткий код всех заклинаний в отдельных функциях и выбор правильной функции из гигантского оператора выбора. Моя первая попытка была что-то вроде этого:

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;
}

Но как только я начал добавлять больше заклинаний, много кода должно было быть сдублировано. Например, заклинания магической стрелы (magic missile) и удара льда (icebolt) не сильно отличаются, и незачем делать шаровую молнию (fireball) и огненный удар (firebolt).

Анализируя другие заклинания, с которыми Вы хотите иметь дело в своей игре и решая некоторые свойства, можно обнаружить, что многие заклинания могут разделять работу и могут быть существенно упрощены.

Свойства заклинаний

Основные свойства заклинаний которые Я решил сделать: эффект заклинаний (лечение, телепорт, огонь, холод (heal, teleport, fire, cold)), область, на которую он (эффект) воздействует (существо, луч, сфера, уровень (unit, beam, ball, level)) и возможные цели заклинания (квадрат игрока, любой квадрат смежный игроку, любой квадрат на уровне).

typedef struct
{
int (*ef)(int, int);
int area;
int target;
}
spell_s;

Что такое первая переменная в struct? Это указатель на функцию, подобно указателям на адрес в памяти. Любая функция, которая соответствует прототипу указателя (обратный int, принимает 2 ints как аргументы в этом случае) может присваиваться указателю и вызываться из него. Хорошо, но что это дает?

Логический путь использовать свойства, которые мы имеем, как аргументы функции. Первый взгляд на целевую переменную подсказывает координаты цели. Если цель - любой смежный квадрат, мы можем просто взять и получить x и y координаты. Для квадрата, на котором находится игрок нам нужно сделать даже меньше.

Затем, взглянем на переменную области и обнаружим все квадраты, которые будут поражаться заклинанием. Для заклинания шара, очерчиваем круг определенного радиуса вокруг целевых координат, для уровня - все квадраты на уровне.

Наконец, взгляд на переменную эффекта и прилагает эффект ко всем взятым квадратам. И, естественно, любым игрокам, чудовищам или предметам, которые находятся в этих конкретных квадратах.

Пример

Последние два этапа легче сделать параллельно, обнаруживая один квадрат для воздействия, и прилагая к нему заклинание. Вот небольшой пример понятия:

Сначала, инициализируем пару заклинаний:

int spell_init(void)
{
    /* заклинание лечения игрока */
spells[0].ef = spell_effect_heal;         
spells[0].area = AREA_SQUARE;
spells[0].target = TARGET_SELF;

    /* заклинание огненного шара */
spells[1].ef = spell_effect_fire;
spells[1].area = AREA_BIGSQUARE;   /* Хорошо, это почти шар ;) */
spells[1].target = TARGET_VISIBLE;

    return 0;
}

И конечно нам нужны функции воздействия эффектов:

int spell_effect_heal(int y, int x)
{
    monster_s* monster;

/* Лечение первым игрока */
    if (player_at(y, x) )
  player.hp += 10;

/* Если есть чудовище в квадрате, излечить и его */
    monster = monster_at(y, x);
if (monster != NULL)
  monster->hp += 10;

    return 0;
}

int spell_effect_fire(int y, int x)
{
    monster_s* monster;

    /* Сделать некоторые визуальные эффекты в квадрате, чтобы заставить нашу 
       шаровую молнию выглядеть более грандиозной  */
    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->hp -= 10;

    return 0;
}

И конечно функция действительного заклинания:

int spell_do(spell_s* sp)
{
    int y, x, y2, x2;

/* Определить цель для заклинания */

    switch (sp->target)
{
 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-координата игрока */
x = player.x;  /* X-координата игрока */
break;
}

    /* Применить заклинание в область */

    switch (sp->area)
{
 /* Только один квадрат */
 case AREA_SQUARE:
    sp->ef(y, x);
break;
 /* Масса квадратов */
 case AREA_BIGSQUARE:
   for (y2 = -3; y2 <= 3; y2++)
    for (x2 = -3; x2 <= 3; x2++)
   sp->ef(y+y2, x+x2);
   break;
}
return 0;
}

Некоторые хорошие вещи получаются из этого метода. Добавлять свойства новых заклинаний к старым теперь очень легко. В моей собственной игре нет статических заклинаний, игрок может смешать свойства свободно, чтобы создать изготовленное на заказ заклинание для любой ситуации. Использование функциональных указателей также помогает избегать дублирования кода.

Проекты продолжения

За дополнением странных областей эффекта или новыми типами ущерба есть несколько вещей, с которыми Вы можете работать чтобы заставить свою roguelike использовать стандартный набор более ранних игр.

Очевидно некоторые улучшения действительно должны быть сделаны в этой системе. Некоторый метод сообщающий spell_effect функциям силовой уровень заклинаний. Другой параметр, который должен быть добавлен - понятие дипазона. Это ограничение, чтобы все заклинания шара имели радиус трех квадратов, или заклинания луча достигающие 5 квадратов всегда.

Текущий метод поставки координат квадратов является слишком ограниченным. Например Вы можете захотеть приложить заклинание на объекты в своем инвентаре или слотах персонажа.

Один метод - добавить новую цель метода (TARGET_INVENTORY), возвращающую предмет из инвентаря и новой области эффекта (AREA_INVENTORY) который вызывает spell_effect_* функцию с некоторыми дополнительными параметрами. Другой метод - сделать отдельную иерархию для заклинаний, которые влияют на предметы инвентаря, влияющих на объекты на карте, и новые иерархии для других потребностей.

Как только Вы сделаете заклинания, станет легким добавить магические предметы, зелья, свитки и палочки, заклинания активизации в оружии и броне.

И большой вызов с которым Вы столкнетесь - разработка сбалансированного, интересного и может быть даже оригинального набора заклинаний.

Если Вы горячо не согласны стем, что Я сказал в этой статье, пожалуйста сообщите по адресу jsnell@lyseo.edu.ouka.fi. Если Вы думаете, что статья удалась на славу, Вам действительно нужно сообщить это. Я мог бы использовать одобрение :-)



Автор: Juho Snellman.
Источник: Programming Roguelike Magic.
Перевел: Сергей В. Ждановских [Alchemist], 14.05.2005.