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

Создание ролевой компьютерной игры 20) Обходим препятствия


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

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

20) Обходим препятствия

Письма.

Я тут неделю назад подписался на рассылку и заинтересовался вашим проектом и начал вместе с вами делать эту игру. У меня есть не большой опыт в программирование на паскале и Делфи, поэтому я быстро прошел почти все выпуски. Дошел я до 77 и там вы пишете: при выбрасывании предмета функция GetFreeItemNum: Integer; может возвратить нулевое значение из-за того, что свободного места нет. Можно поступить по-другому: при распределении предметов на карте массив Items может быть заполнен максимально до MaxItems, можно размер этого массива увеличить на количество максимальных предметов у героя, т.е. Items: array [1..MaxItems+MaxHeroItems] of TGameItem; И при работе функции GetFreeItemNum: Integer; цикл изменить до MaxItems+MaxHeroItems и теперь при переборе у нас всегда будет свободны ,для первого раза, последние MaxHeroItems элементов, т.к при распределении предметов на карте максимально массив будет заполнен до MaxItems элементов. Но при прописывании модуля Hero в модуле GameItem возникнет ошибка и для исправления этого можно константу MaxHeroItems прописать в модуле GameItem и тогда все будет в порядке
Денис


Продолжаем обучать монстров преследованию жертвы :)

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

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

Но, повторим, эти подходы достаточно требовательны к производительности. Хотя в случае пошаговых игр поиск оптимального маршрута не вызовет серьезного замедления.

Но мы рассмотрим другую возможность быстрого обхода возникшей проблемы. Во многих случаях сложное поведение прекрасно имитируется случайным (сложное похоже на случайное, как сказал кто-тоо из великих кибернетиков). Ведь пользователю неважно, почему монстр сделал тот или иной маневр, внешне выглядящий вполне разумно. Поэтому мы будем сокращать расстояние от монстра до героя просто случайным образом!

То есть с вероятностью 50% станем сокращать расстояние по вертикали, и с такой же вероятностью - по горизонтали. Ну а если по выбранному направлению двигаться невозможно - встретилось препятствие, а по другому направлению путь открыт, то конечно выполним перемещение по единственному маршруту. Конечно, всех проблем это не решит (лучше все же взять корректный алгоритм поиска маршрута), но эта тема не совсем в тему нашей рассылки.

Сейчас мы словесно описали алгоритм выбора направления движения монстра. В нем явно выделяются повторяемые элементы - проверка, свободно ли некоторое направление, а также само перемещение монстра. Анализ тайла карты на незанятость у нас уже есть - это функции FreeTile и IsMonsterOnTile. А вот перемещение монстра на новое место - вещь, явно полезная во всех отношениях - напрашивается для реализации в виде отдельной процедуры. Пока неясно, насколько она будет востребована в таком виде в будущем, но для повышения гибкости все же реализуем ее в виде процедуры StepMonster:

  procedure StepMonster( mi, dx, dy: Integer );
  begin
  inc( Monsters[mi].x, dx );
  inc( Monsters[mi].y, dy );
  end;

Хотя она пока элементарна в реализации, в дальнейшем при расширении возможностей игры в этот код могут быть внесены весьма серьезные усовершенствования. Так, если мы захотим научить монстров подбирать разбросанные по карте предметы, то выполнить такой подбор удобнее всего именно в данной процедуре - по аналогии с процедурой MoveHero перемещения главного героя, где происходят все проверки попадания героя в ловушки, столкновения с монстрами и так далее.

Сам код процедуры выбора направления движения монстров запишется следующим образом:

   procedure MonsterStep(mi, hi: Integer);
    var dx, dy: Integer;
    begin

    if Heroes[hi].x > Monsters[mi].x then
         dx := +1 else
    if Heroes[hi].x < Monsters[mi].x then
         dx := -1
    else dx := 0;

    if Heroes[hi].y > Monsters[mi].y then
         dy := +1 else
    if Heroes[hi].y < Monsters[mi].y then
         dy := -1
    else dy := 0;

   if dx <> 0 then
       if (not FreeTile( GameMap[CurMap].Cells[
    Monsters[mi].x+dx,Monsters[mi].y].Tile)) or
          (IsMonsterOnTile( 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
          (IsMonsterOnTile( Monsters[mi].x,Monsters[mi].y+dy ) > 0) then
          dy := 0;
    if (dx = 0) and (dy = 0) 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, хотя общее расстояние до цели у него при этом не уменьшится, а увеличится. Наш алгоритм не учитывает таких нюансов, как впрочем, и ряда других тонкостей поиска оптимального пути.
Решить эту проблему предоставляется уважаемым читателям самостоятельно :)


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

Во-первых, невозможно посмотреть детальные характеристики героя (уровни навыков, опыт). Во-вторых, сеанс игры невозможно сохранить и загрузить. Существует немало игр, где возможность сохранения не предусмотрена - в основном это аркады, однако в случае РПГ такие режимы должны быть реализованы обязательно. Их программированием мы займемся попозже.

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

Схожая ситуация и при попадании героя на тайл с ловушкой, при обнаружении предметов и так далее.

В чем причина такого недоразумения? В нашей попытке создать универсальный метод обработки игровых событий в главном модуле программы. Мы решили полностью перерисовывать все игровое поле перед ожиданием очередного действия пользователя (вызов ShowGame перед оператором отслеживания нажатой клавиши "k := ReadKey"). Однако необходимость перерисовки возникает, как оказалось, и внутри некоторых процедур в разные моменты развития игрового мира. Прежде всего потребность в перерисовке возникает в процедуре MoveHero, ответственной за перемещение персонажа. Ведь именно здесь он попадает на тайлы с ловушками и перемещается к опасному соседству с монстрами.

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

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

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

SetHeroVisible(CurHero);

Сразу за ней можно поместить команду отображения нового положения героя. Всю карту перерисовывать необязательно - ведь герой в любом случае будет находиться в ее границах, так как мы выделили видимую часть каймой длинной в три тайла (SCROLL_DELTA). Если герой останется в безопасной зоне, то расходы на повторную перерисовку одного тайла не страшны. А если он вошел в трехтайловую зону, то все равно останется в видимой части карты. Сразу после того, как человек нажмет ENTER, закрывая строку сообщения, карта будет проскроллирована и перерисована полностью.

Отрисовка героя выполняется процедурой ShowHero. Вставим ее сразу после вызова SetHeroVisible:

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

Работает? Нет. Потому что теперь герой действительно стал показываться на новом месте, однако его старый тайл не обновляется, в результате чего на экране возникают два героя.

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

  ShowCell( GameMap[CurMap].Cells[ Heroes[CurHero].x,Heroes[CurHero].y ],
            Heroes[CurHero].x,Heroes[CurHero].y);
  inc(Heroes[CurHero].x,dx);
  inc(Heroes[CurHero].y,dy);
  SetHeroVisible(CurHero);
  ShowHero(CurHero);

Теперь все работает прекрасно! Конечно, самым простым способом был бы вызов полной перерисовки экрана:

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

Это было бы и более корректно - ведь мы в оригинальном варианте перерисовываем только тайл, а на нем может быть предмет, который надо показывать процедурой ShowItem, или источник, а такой вариант мы не обрабатываем. Возможны и другие игровые ситуации. Однако в большинстве случаев вызывать перерисовку всего экрана дважды подряд нежелательно (хотя для современных компьютеров это на первый взгляд не проблема). Такое решение допустимо в играх, где реализуется пошаговый режим, и скорость графического вывода не играет роли. Но если создается стратегия реального времени, подобных дополнительных перерисовок экрана надо всячески избегать. Здесь было продемонстрировано одно из возможных решений.

Можно также поместить обращение к функции ShowGame непосредственно в процедуру вывода сообщений ShowInfo, добавив в нее еще один параметр, указывающий, требуется ли выполнять перерисовку карты. Реализовать эту идею вы можете самостоятельно.
Неплохо в дополнение к перерисовке героя перед комбатом перерисовывать и положение монстров.


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

http://russianenterprisesolutions.com/sbo/download/6125.zip 9732 байта


Далее - Герой готовится к жизни.

Вопрос, вводить ли в игру понятия рас и классов, давно и яростно обсуждается поклонниками ролевых игр. Мы эти понятия введем :)


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

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

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


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

В избранное