Создание ролевой компьютерной игры 36) Отыгрыш партией
36) Отыгрыш партией
Партия
До сих пор мы обходились одним героем, хотя в программу везде заложена потенциальная возможность управления группой (говорят - партией) персонажей. Покажем, как от одного героя перейти к произвольному числу, не исключено, и объемной армии "своих" персонажей (что, впрочем, может перевести игру из разряда ролевых в разряд стратегических). Пусть в игре у нас сможет действовать три персонажа. Для этого прежде всего увеличим до 3 значение константы-размера массива игроков Heroes:
const MaxHeroes = 3; var Heroes: array[1..MaxHeroes] of THero;
...
После этого, надо отметить, попытка восстановить игру из ранее созданного сэйв-файла приведет к неудаче, потому что размер игровых данных изменился (выросло число элементов в массиве Heroes). Эту особенность сохранения-восстановления всегда надо иметь в виду, и не реализовывать ее рано, когда большая часть игровой информации не формализована.
В момент генерации героя необходимо заполнить все элементы массива Heroes. В принципе, делать это можно и в другие игровые моменты, как нередко происходит в коммерческих играх - персонажи присоединяются к герою не сразу. Для этого каким-то несложным способом достаточно определить, существует ли i-й герой (элемент массива Heroes) или же его можно не учитывать. Можно в типе THero ввести специальное поле, можно обойтись, например, условным признаком (если значение Level меньше нуля, значит, соответствующий слот
массива Heroes пуст).
Мы в демонстрационных целях поступим более просто. После того, как будет сгенерирован основной персонаж, двое других просто будут его клонами, повторяя все параметры оригинала один в один. В профессиональных играх персонажи, дополняющие героя в партии, обычно как раз обладают чертами, которые у героя отсутствуют, что позволяет вести игру максимально гибко. Такая возможность реализуема достаточно очевидно.
В процедуре GenerateHero создаем клоны:
...
for i := 2 to MaxHeroes do begin
Heroes[i] := Heroes[1];
SetHeroVisible(i); end;
SetHeroVisible(CurHero); end;
Визуализацию всех героев мы делаем, чтобы они были исходно видны на карте.
Вместо одного героя выведем на карте всех персонажей сразу. Сейчас единственный главный персонаж выводится с помощью процедуры ShowHero с номером нужного персонажа. Создадим в процедуре ShowGame дополнительный цикл с проверкой видимости i-го героя с точки зрения текущего персонажа:
{ ----------------- } procedure ShowGame; var i: Integer; begin
ShowMap;
ShowItems;
ShowMonsters; for i := 1 to MaxHeroes do if VisiblePoint(Heroes[i].x,Heroes[i].y) then
ShowHero(i);
ShowHeroInfo(CurHero); end;
Только, чтобы различить "текущего" героя (определяемого значением переменной CurHero) от всех остальных, откорректируем процедуру ShowHero.
{ ----------------- } procedure ShowHero(HeroNum: Integer); begin
GoToXY(
WINDOW_LEFT+Heroes[HeroNum].x-GameMap[CurMap].LocalMapLeft,
if HeroNum = CurHero then TextColor( White ) else TextColor( Yellow );
Write( ' X ' ) end;
Главный на данный момент герой покажется на карте белым, любой другой партиец - желтым.
Единственная неточность в текущем коде, которую, впрочем, можно считать, как в известном анекдоте, не "багом", а "фичей" - все клоны главного героя будут расположены на одной клетке с ним. Поэтому они будут видны (точнее, один из них, за которым будет "скрываться" третий) только когда главный выделенный белым герой сделает шаг в сторону из своей исходной позиции. Возможно, это и правильно, так как персонажи могут свободно вставать на одну клетку друг с другом.
Чтобы режим партийной игры стал действовать, остается реализовать возможность переключения от одного персонажа к другому. Потенциально это сделать легок (достаточно лишь изменить значение переменной CurHero!), а "привяжем" мы такое изменение к клавише Tab.
case k of
...
#9: NextHero;
Процедура NextHero (модуль Game)
{ ----------------- } procedure NextHero; begin
end;
должна просто корректно изменить значение текущего игрового персонажа - перейти к следующему герою. Только для прокрутки карты к новому персонажу понадобится вызов процедуры ScrollMap.
{ ----------------- } procedure NextHero; begin
inc(CurHero); if CurHero > MaxHeroes then
CurHero := 1;
ScrollMap; end;
Внесем изменения и в процедуру MonstersStep, где возможность атаки пока рассматривается лишь в отношении лишь текущего персонажа. Мы расширим спектр рассматриваемых монстрами в качестве потенциальных жертв объектов всеми живыми героями:
{ ----------------- } procedure MonstersStep; var i, k: Integer; begin for i := 1 to MaxMonsters do for k := 1 to MaxHeroes do if (Monsters[i].HP > 0) and
(Heroes[k].HP > 0) and
CanTrace(i, k) then begin
MonsterStep(i, k);
break end;
MonstersAttack; end;
Вложенный цикл со счетчиком k перебирает всех персонажей и проверяет, может ли за ним охотиться i-й монстр. Схожую корректировку надо внести и в процедуру атаки:
{ --------------------------- } procedure MonstersAttack; var i,k: Integer; begin for i := 1 to MaxMonsters do for k := 1 to MaxHeroes do if (Monsters[i].HP > 0) and
(Heroes[k].HP > 0) and
CanAttack(i,k) then begin
MonsterAttack(i, Heroes[k]);
break end;
end;
Также нам придется откорректировать процедуру HeroDied, в которой сообщается о смерти героя. Пока в таком печальном случае игра сразу прекращается, мы же вставим в нее проверку, которая будет завершать игру лишь в том случае, если убиты все персонажи в партии. Для этого достаточно проверить уровень здоровья каждого персонажа - пока жив хотя бы один герой, игру надо продолжать:
{ ----------------- } procedure HeroDied; var I: Integer; begin
ShowInfo(STR_HERO_DIED);
for i := 1 to MaxHeroes do if Heroes[i].HP > 0 then Exit;
ClrScr;
Halt; end;
Отметим, что возможность игры, когда один из героев (в частности, текущий) погиб, у нас полноценно не поддерживается. Дело в том, что ранее мы ошибочно предполагали, что если герой уничтожен, то игра закончена и действие продолжается. Теперь же нам надо дополнить игру учетом того факта, что некоторый герой в ней участия не принимает (физически отсутствует). Прежде всего это необходимо отметить в процедуре передачи управления следующему за текущим герою NextHero - не просто увеличить значение CurHero, а дополнительно
анализировать, не является ли новый герой на самом деле мертвым?
{ ----------------- } procedure NextHero; begin
repeat
inc(CurHero); if CurHero > MaxHeroes then
CurHero := 1;
until Heroes[CurHero].HP > 0;
ScrollMap; end;
Цикл будет продолжаться до тех пор, пока не будет найден здоровый персонаж. Кроме того, после уничтожения одного из героев можно например указать в качестве его координат достаточно удаленную позицию, чтобы труп не привлекал внимание монстров. Более корректно, впрочем, откорректировать процедуру поиска монстром жертвы - атаковать имеет смысл только живого героя. Функция CanTrace, ответственная за это, модифицируется так:
{ ----------------- } function CanTrace(mi, hi: Integer): Boolean; var d, dd: Integer; begin
d := Distance( Monsters[mi].x,Monsters[mi].y,
Heroes[hi].x,Heroes[hi].y );
CanTrace := false;
if Heroes[hi].HP <= 0 then Exit;
dd := RollDice(3,6); if dd <= Heroes[hi].Chars[chrCHA] then
Exit;
CanTrace := (d <= Monsters[mi].ViewZone) and (d > 1); end;
В середину ее вставлена проверка того, жив ли вообще анализируемый персонаж.
В завершение этой модификации должна быть изменена процедура ShowHero, отображающая персонаж на карте. Делать это имеет смысл лишь тогда, если герой жив:
{ ----------------- } procedure ShowHero(HeroNum: Integer); begin
GoToXY(
WINDOW_LEFT+Heroes[HeroNum].x-GameMap[CurMap].LocalMapLeft,
if HeroNum = CurHero then TextColor( White ) else TextColor( Yellow );
if Heroes[HeroNum].HP > 0 then
Write( ' X ' ) end;
И, наконец, после уничтожения некоторого героя сдвинем номер текущего героя к следующему - чтобы исключить ситуацию, когда убит текущий герой, а значение CurHero по прежнему указывает на несуществующий персонаж:
{ ----------------- } procedure HeroDied; var I: Integer; begin
ShowInfo(STR_HERO_DIED);
for i := 1 to MaxHeroes do if Heroes[i].HP > 0 then begin
NextHero;
Exit; end;
ClrScr;
Halt; end;
Теперь наша игра дополнилась возможностью играть группой-партией, а при увеличении числа персонажей она приблизится к жанру стратегических игр!
Далее - добавляем в программу режим реального времени.
Исходный код текущей версии для Turbo Pascal (всегда проверен и работоспособен, главный файл - main.pas):