Урок 20.
Обходим препятствия
Продолжаем
обучать монстров преследованию жертвы :)
Выходов из
логического тупика два, который обсуждался в прошлом выпуске, два. Классические
подходы позволяют определять полный путь из одной точки до другой на карте со
сложно проходимым рельефом. Однако процедура эта весьма ресурсоемкая. Поэтому
реализации таких алгоритмов выполняются как правило не для всей карты, а для
определенного ее участка.
Так, в
предыдущем примере можно искать оптимальный путь из точки М в точку Г по всей
карте, а можно взять для анализа небольшой прямоугольник. Для поиска пути на
таком небольшом участке процессорного времени требуется значительно меньше,
хотя возникает опасность такой путь не найти, если прямоугольник будет мал.
Существующие алгоритмы в таких случаях постепенно увеличивают просматриваемую
зону в надежде рано или поздно найти маршрут обхода препятствия.
Но, повторим,
эти подходы достаточно требовательны к производительности. Хотя в случае пошаговых
игр поиск оптимального маршрута не вызовет серьезного замедления.
Мы рассмотрим
другую возможность быстрого обхода возникшей проблемы. Во многих играх поведение
прекрасно имитируется случайным (сложное похоже на …, как сказал кто-то из
великих кибернетиков). Ведь пользователю неважно как монстр сделал тот или иной
маневр, внешне выглядящий вполне разумно. Поэтому можно определять расстояние
от монстра до героя просто случайным образом! С вероятностью 50% станем
сокращать расстояние по вертикали, и с такой же вероятностью по горизонтали. Ну, а если по выбранному направлению
двигаться и на пути встретилось препятствие, а по другому направлению путь
открыт, то конечно возможно перемещение по единственному маршруту. Конечно,
всех проблем это не решит и нужно задать корректный алгоритм поиска маршрута),
но эта тема не совсем в тему данной статьи.
Мы конечно
описали алгоритм выбора направления движения монстра. В нем явно видны
повторяемые элементы - проверка, свободно ли некоторое направление, а также
движение монстра. Анализ таила карты на незанятость у нас уже есть - это
функции
… и IsMonsterOnTile. А
вот перемещение монстра на новое место - вещь, явно в других отношениях -
напрашивается для реализации в виде отдельной процедуры.
Неизвестно
насколько она будет востребована в таком виде в будущем, но для повышения реальности
реализуем ее в виде процедуры StepMonster:
Procedure StepMonster ( mi,
dx, dy: Integer
);
Begin
… (Monsters[mi].x,dx);
… (Monsters[mi].y,dy);
End;
Процедура
элементарна в реализации, в дальнейшем при расширении возможностей в нее могут
быть внесены весьма серьезные усовершенствования. Так, если мы побеждая
монстров будем подбирать разбросанные по карте предметы, то выполнить такой алгоритм
можно именно в данной процедуре - по аналогии с процедурой MoveHero у главного
героя, где происходят вес проверки попадания героя в ловушки, борьба с
монстрами и так далее. Процедура выбора направления движения монстров запишется
следующим образом:
Procedure MonsterStep(mi, hi:
Integer);
Var dx,dy:integer;
Begin
If Monsters[hi].x>Monsters[mi].x
then dx:=+1; else
If Monsters[hi].x<Monsters[mi].x
then dx:=-1;
…dy:=0;
If Monsters[hi].y>Monsters[mi].y
then dy:=+1; else
If Monsters[hi].y<Monsters[mi].y
then dy:=-1;
…dx:=0;
If dx=0 then
If not FreeTile( GameMap[CurMap].Cells[
Monsters[mi].x, Monsters[mi].y+dy ].Tile ) or
IsMonstersOnTile( GameMap[CurMap].Cells[
Monsters[mi].x+dx, Monsters[mi].y ]>0 ) then
Dx:=0;
If dy=0 then
If not FreeTile( GameMap[CurMap].Cells[
Monsters[mi].x, Monsters[mi].y+dy ].Tile ) or
IsMonstersOnTile( GameMap[CurMap].Cells[
Monsters[mi].x+dx, Monsters[mi].y ]>0 ) then
Dy:=0;
If (dx=c) and (dy=c) then exit;
if dx =
0 then
StepMonster( mi,
0, dy )
else
if
dy = 0 then
StepMonster( mi,
dx, 0 )
else
if
random(2) = 0 then
StepMonster( mi,
0, dy ) else
StepMonster( mi,
dx, 0 );
end;
Логика этого алгоритма тривиальна. Вначале мы заносим в
переменные dx и dy возможные сдвиги координаты текущего монстра (параметр mi -
индекс в массиве Monsters) в зависимости от взаимного положения героя (параметр
hi- индекс персонажа в массиве Heroes). Гхли герой дальше монстра по какой-то
из осей, координату монстра надо будет увеличить. Если герой ближе к началу
координат, чем монстр - координату последнего надо уменьшить. Наконец, если они
на одном уровне, то перемешать монстра по этой координате не надо.
Последнее
утверждение, впрочем, не совсем верно. Возможны ситуации, когда сдвигать
монстра необходимо именно по координате, где уже достигнут минимум. Иногда по
оси X и монстр, и персонаж вроде бы находятся на одном уровне и сокращать
горизонтальную разницу невозможно. Но по оси Y перемещение невозможно из-за
препятствия, поэтому желательно двигать монстра именно по оси X, хотя общее
расстояние до цели у него при этом не уменьшится, а увеличится. Наш алгоритм не
учитывает таких нюансов, как впрочем, и ряда других тонкостей поиска
оптимального пути. Решить эту проблему предоставляется уважаемым читателям самостоятельно
:)
Теперь наша
игра превратилась в достаточно целостную систему. Герой может передвигаться по
карте, находить и подбирать предметы, обнаруживать и обезвреживать ловушки,
вступать в схватку с монстрами, получать пункгы опыта за победу и восстанавливать
здоровье в живительных источниках. Однако небольшая практика в игровом процессе
покажет, что игре пока не хватает как минимум двух важнейших вещей. Во-первых,
невозможно посмотреть детальные характеристики героя (уровни навыков, опыт).
Во-вторых, сеанс игры невозможно сохранить и загрузить. Существует немало игр,
где возможность сохранения не предусмотрена - в основном это аркады, однако в
случае РПГ такие режимы должны быть реализованы обязательно. Их
программированием мы займемся попозже.
Отмстим еще
одно неудобство в игровом процессе. Сейчас предупреждения почти во всех игровых
моментах выдаются не после перерисовки экрана, а до нее. Поэтому возникает
парадоксальная ситуация - герой еще не встал на новую клетку карты, а на экране
уже появилось сообщение о происшествии. Например, герой перемещается на клетку,
соседствующую клетке с монстром. Тот наносит удар первым, о чем выдается
сообщение. Однако на карте герой но прежнему показан на старом месте, в двух
тайлах от монстра. Схожая ситуация и при попадании героя на таил с ловушкой,
при обнаружении предметов и так далее.
В чем причина
такого недоразумения? В нашей попытке создать универсальный метод обработки
игровых событий в главном модуле программы. Мы решили полностью перерисовывать
все игровое поле перед ожиданием очередного действия пользователя (вызов
ShovvGame перед оператором отслеживания нажатой клавиши "k :=
ReadKey"). Однако необходимость перерисовки возникает, как оказалось, и
внутри некоторых процедур в разные моменты развития игрового мира. Прежде всего
потребность в перерисовке возникает в процедуре MoveHero, ответственной за
перемещение персонажа. Ведь именно здесь он попадает на тайлы с ловушками и
перемещается к опасному соседству с монстрами.
Обойти такое
неудобство можно весьма просто. Достаточно добавить дополнительные вызовы
процедуры перерисовки ShowGame непосредственно перед точками, где потенциально
возникает необходимость уведомления пользователя с помощью сообщений. Это координаты
героя меняются на новые:
…(Heroes[CurHero].x,dx);
…(Heroes[CurHero].y,dy);
На
программном уровне изменение местоположения персонажа произошло, логично
отобразилось это изменение на карте. Следующая команда отмечает те тайлы карты,
которые видимы в результате перемещения:
…(CurHero);
Также можно
поместить команду отображения нового положения героя. Всю карту перерисовывать
необязательно - ведь герой в любом случае будет находиться в ее границах, выделим
видимую часть каймой длинной в три таила (SCROLL_DELTA). Если это находится в
безопасной зоне, то расходы на повторную перерисовку одного таила не
значительны. А если он вошел в трехтайловую зону, то все равно останется в
видимой части. И после того как человек нажмет ENTER, закрывая строку
сообщения, карта будет активирована и перерисована полностью. … выполняется
процедурой ShowHero. Вставим ее сразу после вызова …:
…(Heroes[CurHero].x,dx);
…(Heroes[CurHero].y,dy);
…Table(CurHero);
…(CurHero);
Теперь герой
действительно стал показываться на новом месте, а прошлый тайл не обновляется,
в результате чего на экране возникают два героя. Предварительно требуется еще
вызывать процедуру, отрисовывающую таил, с которого герой уходит. Только вызывать ее удобнее до обращения к
командам изменения координат персонажа. Ведь нам надо знать координаты старого
таила - пока они соответствуют координатам
героя.
…( GameMap[CurMap].Cells[
Heroes[CurHero].x, Heroes[CurHero].y),
Heroes[CurHero].x, Heroes[CurHero].y);
…( Heroes[CurHero].x, dx);
…( Heroes[CurHero].y, dy);
…Table(CurHero);
…(CurHero);
Все работает
прекрасно! Конечно, самым простым способом был бы вызов полной отрисовки экрана:
…( Heroes[CurHero].x, dx);
…( Heroes[CurHero].y, dy);
…Table(CurHero);
…;
Это было бы
более корректно - ведь мы в оригинальном варианте перерисовываем только то где
может быть предмет, который надо показывать процедурой Showltem, или … вариант
мы не обрабатываем. Возможны и другие игровые ситуации. В большинстве случаев,
вызывать перерисовку всего экрана дважды подряд ресурсоемко, хотя для современных
компьютеров это на первый взгляд не проблема). Это еще допустимо в играх, где
реализуется пошаговый режим, и скорость
и скорость прорисовки не играет
роли. Но если создается стратегия реального времени,
то дополнительных перерисовок экрана надо всячески избегать. Здесь было
рассмотрено одно из возможных решений.
Можно также
поместить обращение к функции ShowGame непосредственно в процедуру вывода
сообщений Showlnfo. добавив в нее еще одни параметр, указывающий, требуется ли
выполнять перерисовку карты. Реализовать эту идею вы можете самостоятельно.
Неплохо в дополнение к перерисовке героя перед комбатом перерисовывать и
положение монстров.
Далее - Герой
готовится к жизни.
Вопрос,
вводить ли в игру понятия рас и классов, давно и яростно обсуждается
поклонниками ролевых игр. Мы эти понятия введем :)
Источник: delgame.at.ua
|