Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Создание ролевой компьютерной игры 17) Вступаем в схватку


Информационный Канал Subscribe.Ru

Разработка ролевой игры

17) Вступаем в схватку

Павел прислал исходники своего варианта игры на Си++.

Базы данных по характеристикам предметов, монстров и тайлов хранятся в отдельных файлах с расширением .dat , что позволяет менять их даже обычному пользователю, не знакомому с языком программирования. Краткое писание формата этих файлов смотрите в readme.txt
В архив, кроме того, я поместил фоновый рисунок, своеобразную "доску", на которой идет игра. Пришлось его сделать в формате bmp, так как стандартная функция LoadImage не понимает других :( . Но, даже заархивированный, этот файл тянет на 200 кБ, вместе с тем остальные файлы всего на 30. Поэтому я перевел картинку в jpg формат, но перед запуском откомпилированного exe файла надо её перевести обратно в bmp, например, с помощью GIMP, иначе программа просто "не пойдет"
В архиве находятся исходники игры на C++ вместе с файлами проекта, документацией, необходимыми dat файлами и фоновым рисунком в jpg формате. Перед компиляцией желательно почитать ForDevelop.txt ;)

Архив брать тут:

http://russianenterprisesolutions.com/sbo/download/rog.rar 42 kb


Вступаем в схватку.

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

  procedure MoveHero( dx,dy: Integer );
  var m, dam: Integer;
  begin
  if not FreeTile( GameMap[CurMap].Cells[
  Heroes[CurHero].x+dx,Heroes[CurHero].y+dy].Tile ) then
     Exit;

  m := IsMonsterOnTile(Heroes[CurHero].x+dx, Heroes[CurHero].y+dy);
  if m > 0 then
     begin

     Exit
     end;

  inc(Heroes[CurHero].x,dx);
  inc(Heroes[CurHero].y,dy);
  SetHeroVisible(CurHero);

  ...

В процедуру MoveHero (перемещение героя, модуль Game) добавлена проверка тайла, на который тот должен встать - имеется ли на тайле монстр. Если да (функция IsMonsterOnTile возвращает индекс монстра в массиве Monsters; эту функцию мы реализуем позже), то надо выполнить определенные действия по нападению на монстра, после чего завершить выполнение MoveHero - ведь передвигать героя на тайл монстра не надо. Функцию IsMonsterOnTile разместим, конечно, в модуле Monster:

  function IsMonsterOnTile(x,y: Integer): Integer;
  var i: Integer;
  begin
  IsMonsterOnTile := 0;
  for i := 1 to MaxMonsters do
    if (Monsters[i].HP > 0) and
       (Monsters[i].x = x) and
       (Monsters[i].y = y) then
       begin
       IsMonsterOnTile := i;
       Exit
       end;
  end;

Программа пробегает по элементам массива Monsters, проверяя, какой из монстров живой (его здоровье - поле HP, имеет положительное значение), и находится ли на он на тайле с предложенными координатами. Уже в таком виде игра будет приближена к реальности - теперь герой не сможет проходить "сквозь" монстров. Это связано с местом вызова функции IsMonsterOnTile в процедуре MoveHero. Вызов происходит до фактического перемещения героя, когда выполняется изменение его координат. Поэтому, в случае столкновения с монстром герой остается на месте. Все, что от него требуется - это нанести удар и отразить ответную атаку. Отметим, что после того, как герой проведет атаку, на него может нападать не только атакуемый монстр, но и все другие находящиеся поблизости монстры, даже если они и не атакованы.

Для программирования схваток создадим новый модуль нашего приложения. Назовем этот модуль, конечно, Combat. Все, что в нем пока будет - это единственная процедура нападения персонажа на конкретного монстра HeroAttack, вызов которой надо разместить в процедуре MoveHero:

  ...
  m := IsMonsterOnTile(Heroes[CurHero].x+dx, Heroes[CurHero].y+dy);
  if m > 0 then
     begin
     HeroAttack(Heroes[CurHero], m);
     Exit
     end;
  ...

Это заготовка модуля Combat:

  unit Combat;

  interface uses Hero;

  procedure HeroAttack( var H: THero; m: Integer );

  implementation

  { --------------------------- }
  procedure HeroAttack( var H: THero; m: Integer );
  begin

  end;

  end.

Первоначально в ходе схватки нам надо проверить успешность нападения героя. Для этого предназначен навык skillHandWeapon. Пока практика его применения не реализована, поэтому запрограммируем ее следующим образом (напомним, что проверка успешности навыков выполняется в процедурах SkillTest и SuccessSkillTest модуля Hero):

  function SkillTest( var H: THero; skl: Integer ): Boolean;
  var xp: Integer;
  begin
  SkillTest := false;
  if random(100)+1 > H.Skills[skl] then Exit;
  SkillTest := true;

  case skl of

     skillHandWeapon:
         begin
         SuccessSkillTest(H, skillHandWeapon);
         end;

     skillTrapSearch:
         begin
         ShowInfo(STR_TRAPOK);
         IncXP(H, H.Level + random(H.Level));
         SuccessSkillTest(H, skillTrapSearch);
         end;

  end;
  end;

В данной процедуре никаких специальных действий по обработке успеха или неудачи атаки в отличие от, например, проверки навыка обнаружения ловушек, не происходит. Дело в том, что проверка успешности атаки представляет собой не законченный в смысловом плане этап программы (обнаружил ловушку - она обезврежена, получен опыт, и на этом все). Атака является лишь одним шагом в последовательности взаимных нападений и защит, поэтому обработку ее успешности будем выполнять на более высоком уровне, в процедуре HeroAttack. Здесь же внесем еще одно дополнение в процедуру SuccessSkillTest, где происходит повышение соответствующего навыка. Посмотрим, чему равняется базовое значение навыка атаки (модуль Tables, константа BaseSkill_Table)? Оно равняется 30. Теперь попробуем рассчитать, на сколько может вырасти заданный навык. В среднем на уровне может находиться число монстров, равное 50 (MaxMonsters). Сколько успешных ударов для уничтожения одного монстра потребуется, сказать сложно. Но, по всей видимости оно вряд ли будет больше десяти. Ведь здоровье монстров на первых уровнях составляет 1-2 пункта, а на старших локациях - до 35 единиц. Сила одного удара не моет быть меньше единицы, значит, слабые монстры будут гибнуть как минимум от одного удара. С учетом того, что сила удара оружия также будет расти, среднее число ударов, равное десяти, можно считать явно избыточным.

Итак, для уничтожения всех монстров в локации нам потребуется нанести 50 * 10 = 500 успешных ударов. Всего на четырех локациях пещеры герой сможет ударить врага 500 * 4 = 2000 раз. До какого значения при этом возрастет навык атаки? Давайте возьмем величину 80%. То есть после нанесения 2000 ударов значение элемента Skills[skillHandWeapon] увеличится с 30 до 80. Другими словами, каждый пятидесятый (2000 / (80 - 30)) удар можно считать вносящим свой вклад в увеличение навыка рукопашного боя. Выше уже описывался способ повышения навыка skillTrapSearch, когда это повышение происходило случайным образом. Таким же образом мы реализуем рост мастерства атаки:

  procedure SuccessSkillTest( var H: THero; skl: Integer);
  var rnd: Integer;
  begin
  case skl of

     skillHandWeapon:
         begin
         if random(50) = 0 then
            begin
            ShowInfo(STR_HANDWEAPONSKILL_OK);
            inc(H.Skills[skillHandWeapon]);
            end;
         end;

     skillTrapSearch:
         begin
         rnd := round(20 / MaxDungeonLevel*100);
         if random(100)+1 <= rnd then
            begin
            ShowInfo(STR_TRAPSKILL_OK);
            inc(H.Skills[skillTrapSearch]);
            end;
         end;
  end;
  end;

Константа STR_HANDWEAPONSKILL_OK (модуль Texts) может быть записана так:

  const STR_HANDWEAPONSKILL_OK = ' Навык ручного боя повышен. ' ;

Вернемся к процедуре сражения. Проверка неудачного удара (фактически означающего, что персонаж промазал) запишется так:

  procedure HeroAttack( var H: THero; m: Integer );
  begin
  if not SkillTest(Heroes[CurHero], skillHandWeapon) then
        begin
        ShowInfo(STR_BAD_ATTACK);
        Exit
        end;

  end;

Константу STR_BAD_ATTACK опишем в модуле Texts:

  const STR_BAD_ATTACK = ' Вы промазали... ' ;

Чтобы программа собиралась нормально, в заголовок реализации модуля Combat надо добавить ссылки на следующие модули:

  implementation uses Game, LowLevel, Texts;

Если же удар достиг своей цели, в дело вступает монстр. Его шкура (поля Dd1, Dd2 структуры TMonster) принимает на себя определенную часть поражающего воздействия. Величина же этого воздействия будет вычисляться на основе параметров оружия. Расчет величины воздействия для оружия вынесем в отдельную функцию:

  function WeaponDamage( Itm: TGameItem ): Integer;
  begin
  WeaponDamage := 0;
  if random(100)+1 > Itm.Ints[intAttackHit] then Exit;
  WeaponDamage := RollDice(Itm.Ints[intAttack_d1], Itm.Ints[intAttack_d2]);
  end;

Если оружие бьет неточно, повреждений, очевидно, нет. Видно, что эта характеристика оружия практически идентична навыку skillHandWeapon, и реальная вероятность попадания по монстру будет равна произведению вероятности, связанной с навыком рукопашного боя, на вероятность попадания, связанная с характеристиками самого оружия. Так, для топора, у которого поле Ints[intAttackHit] равно 50 (см. массив ItemTypes), и героя с базовым навыком рукопашного боя, равным 30 (таблица BaseSkill_Table), вероятность успешного удара будет равна 0,5 * 0,3 = 0,15, что на самом деле весьма невелико. Правильно ли такое дополнительное снижение суммарной вероятности попадания? Да, правильно. Очень важно разделять эту вероятность на две составляющие, связанные как с личными навыками персонажа, и потому достаточно стабильные, так и связанные с характеристиками оружия, и по ходу игры сильно изменяющиеся. Нашел герой меч - сразу стал попадать по монстру чаще. Купил дорогой топор - каждый удар может завалить героя, а вот вероятность попадания существенно снизилась. Кроме того, учет вероятности попадания необходим, когда в игру будет добавлено дальнобойное оружие. Вероятность поражения цели из лука обычно значительно ниже, чем вероятность попадания с помощью, например, кинжала.

Расчет величины поражения выполняется функцией RollDice. Эта функция имитирует бросание игровых кубиков и возвращает сумму выпавших на них значений. RollDice может быть весьма полезной и обращаться к ней нам понадобится из разных модулей, поэтому поместим ее реализацию в игровой модуль Game:

  function RollDice( d1, d2: Integer ): Integer;
  var i,s: Integer;
  begin
  s := 0;
  for i := 1 to d1 do
    s := s + random(d2)+1;
  RollDice := s;
  end;

В цикле происходит бросок кубика с числом граней, равным d2. Продолжительность цикла задается параметром d1 (число кубиков).

Дополним процедуру сражения проверкой величины наносимого поражения. Для этого нам потребуется определить, какое оружие в данный момент использует герой. Такую проверку вынесем в отдельную функцию GetHeroWeapon, так как она не так проста для реализации, как может показаться. Разместим такую функцию в модуле Hero:

  function GetHeroWeapon( var H: THero ): Integer;
  begin
  GetHeroWeapon := 0;
  if H.Slots[slotHands].IType = itemNone then Exit;
  GetHeroWeapon := slotHands;
  end;

Эта функция существенно зависит от количества слотов персонажа, а также от их назначения. В нашем случае проверять на наличие оружия надо только один слот (slotHands). Если предмета в нем нет, значит, герой не может вести сражение (ему нечем это делать). Функция возвращает номер слота, в котором хранится предмет-оружие, или ноль, если подходящего оружия нет. Вот как будет выглядеть очередная версия процедуры HeroAttack:

  procedure HeroAttack( var H: THero; m: Integer );
  var i, dam, skin: Integer;
  begin
  if not SkillTest(Heroes[CurHero], skillHandWeapon) then
        begin
        ShowInfo(STR_BAD_ATTACK);
        Exit
        end;

  i := GetHeroWeapon(H);
  if i = 0 then
        begin
        ShowInfo(STR_NONE_WEAPONS);
        Exit
        end;

  dam := WeaponDamage( H.Slots[i] );
  skin := RollDice( Monsters[m].dd1, Monsters[m].dd2 );
  if skin >= dam then
        begin
        ShowInfo(STR_BIG_SKIN);
        Exit
        end;
  end;

Текстовые константы описаны в модуле Texts:

  const STR_NONE_WEAPONS = ' У вас нет оружия. ' ;
  const STR_BIG_SKIN = ' Шкура монстра выдержала удар... ' ;

Сначала определяется, есть ли у героя оружие. Если его нет, выдается сообщение (STR_NONE_WEAPONS) и работа процедуры заканчивается. В противном случае рассчитывается величина поражения, наносимого оружием героя, а также определяется, какой удар может выдержать шкура монстра (это поля dd1, dd2 в структуре TMonster). Если размер повреждения меньше или равен толщине шкуры, значит, шкуру монстра пробить не удалось (сообщение STR_BIG_SKIN).

В данном коде видно, что толщина шкуры (значение переменной skin) случайная величина и будет меняться при каждом новом ударе. Это не ошибка. Такая переменчивость толщины имитирует удар оружием в разные точки тела монстра. Ведь удар по толстому спинному панцирю нанесет ему очень слабое повреждение, а вот удар в незащищенное горло может быть смертельным. Такую задачу и решает колеблющееся значение переменной skin.

Продолжение комбата следует.


Исходный код текущей версии (всегда проверен и работоспособен, главный файл- main.pas):

http://russianenterprisesolutions.com/sbo/download/15115.zip 8758 байтов


(c) 2004-2005 Сергей Бобровский bobrovsky@russianenterprisesolutions.com

Все предыдущие выпуски базового курса тут:
http://russianenterprisesolutions.com/sbo/

Дизайн рассылки: Алексей Голубев - Web-дизайн и web-программирование


Subscribe.Ru
Поддержка подписчиков
Другие рассылки этой тематики
Другие рассылки этого автора
Подписан адрес:
Код этой рассылки: comp.soft.prog.prognull.game
Архив рассылки
Отписаться
Вспомнить пароль

В избранное