Давайте сделаем рогалик (Движение монстров)
Сейчас у нас уже есть монстры в игре, но это не очень интересно, если они
будут все время стоять на месте, пока игрок не запинает их досмерти. Монстры
должны двигаться, преследовать персонажа, атаковать его и отступать в случае
необходимости. В этой главе мы научим их догонять персонажа и убегать от
него.
Передвижение монстра можно рассматривать как конечный автомат, в
котором действие движения вытекают из его текущего состояния. Есть несколько
состояний, в котором чудовище может находится в текущий момент.
- Монстр видит персонажа
- Монстр потерял персонажа из виду, но запомнил его последнее местонахождение.
- Монстр потерял персонажа из виду, но слышит его
- Монстр бежит от персонажа
- Монстр не встречал или потерял персонажа полностью.
Для каждого из перечисленных состояний монстра мы должны закодировать его поведение соответствующим образом.
- Монстр видит персонажа
- Установить флаг видимости персонажа в TRUE
- Запомнить местоположение персонажа
- Проверить возможность атаки и атаковать если возможно
- Если не атакует, то перемещаемся в сторону персонажа
- Монстр потерял персонажа из виду, но запомнил его последнее
местонахождение.
- Перемешаться к последней позиции персонажа, пока она не достигнута
- Монстр потерял персонажа из виду, но слышит его.
- Перемешаться в сторону где звуки слышаться более отчетливо
- Монстр бежит от персонажа
- Отойти от персонажа
- Монстр не встречал или потерял персонажа полностью.
- Ничего не делать
Каждый монстр будет двигаться после того. Как переместился персонаж. Поэтому, после каждого передвижения персонажа, нам нужно проверить состояние монстра. Самое первое состояние — монстр видит персонажа, проверяется всегда, и является аналогом того, что монстр осматривает территорию вокруг себя в поисках персонажа. Если персонажа нет на линии прямой видимости, то монстр будет двигаться к точке где видел персонажа в последний раз, в надежде на то, что сможет вновь его найти. Если это не удается, то он будет прислушиваться к звукам вокруг, так как при своем передвижении персонаж будет производить шум. Это позволит монстрам преследовать персонажа, даже если они его не видят в данный момент. Если здоровье монстра упадет ниже определенного уровня, то он не будет стремиться преследовать персонажа, а попытается убегать, но отступать он будет с боем. И наконец, если монстр никогда не видел персонажа, или же потерял его окончательно (навряд ли, учитывая предыдущие условия), то он будет просто отдыхать, оставаясь на месте.
Эта простая машина состояний позволяет достигнуть потрясающих результатов. После того, как монстр заметит персонажа, он будет его все время преследовать, пока не умрет в бою или не скроется от персонажа, если ему придется обратиться в бегство. В большинстве случаев игроку придется стоять и сражаться с монстром, так как убежать от него будет достаточно проблематично. Конечно, когда в игре появится магия, то персонаж сможет телепортироваться в другое место, но нет никакой гарантии, что он не попадет в еще более трудную ситуацию. Сражения в рогаликах, это неотъемлемая часть игровой механики и безжалостность монстров будет хорошо это подчеркивать.
Обратите внимание, что среди состояний отсутствует состояние патрулирования территории. Так как мы разместили монстров внутри комнат, то игрок так или иначе с ними столкнется, когда будет исследовать подземелье, по крайней мере для поиска лестницы на следующий уровень. В этом случае монстрам не нужно бродить по подземелью, персонаж сам придет к ним. Это упрощает создание игры, а также позволит игроку несколько вариантов действий на выбор — убить монстра сейчас или бежать собирая за собой толпы преследователей. Подобные тактические решения сделают игру более интересной.
Код для передвижения монстров является частью объекта уровня, и процедура, его реализующая, называется, что не удивительно, MoveMonsters.
map.bi
'Передвижения для всех живых монстров. Sub levelobj.MoveMonsters () Dim As mcoord nxt Dim As Integer pdist 'Переберем всех монстров на карте For i As Integer = 1 To _level.nummon 'Make sure monster is not dead. If _level.moninfo(i).isdead = FALSE Then 'Убегает ли монстр? If _level.moninfo(i).flee = FALSE Then 'Персонаж на линии прямой видимости? If _level.lmap(_level.moninfo(i).currcoord.x, _level.moninfo(i).currcoord.y).visible = TRUE Then 'Установим флаг видимости персонажа. _level.moninfo(i).psighted = TRUE 'Запомним позицию персонажа. _level.moninfo(i).plastloc.x = pchar.Locx _level.moninfo(i).plastloc.y = pchar.Locy 'Проверим расстояние до персонажа. pdist = CalcDist(_level.moninfo(i).currcoord.x, pchar.Locx, _level.moninfo(i).currcoord.y, pchar.Locy) 'Проверим дальнось атаки монстра. If pdist <= _level.moninfo(i).atkrange Then 'Атакуем персонажа. Else 'Получтм следующу. Бдижайшую к игроку ячейку карты. nxt = _GetNextTile(_level.moninfo(i).currcoord.x, _level.moninfo(i).currcoord.y, _ pchar.Locx, pchar.Locy) 'Установим новые координаты монстра. _level.lmap(_level.moninfo(i).currcoord.x, _level.moninfo(i).currcoord.y).monidx = 0 _level.lmap(nxt.x, nxt.y).monidx = i _level.moninfo(i).currcoord = nxt End If Else 'Персонаж не на линии видимости. Персонаж был замечен. If _level.moninfo(i).psighted = TRUE Then 'Убедимся что есть запомненные координаты. If (_level.moninfo(i).plastloc.x > -1) And (_level.moninfo(i).plastloc.y > -1) Then 'Двигаемся к этим координатам. nxt = _GetNextTile(_level.moninfo(i).currcoord.x, _level.moninfo(i).currcoord.y, _ _level.moninfo(i).plastloc.x, _level.moninfo(i).plastloc.y) 'Установим новые координаты монстра. _level.lmap(_level.moninfo(i).currcoord.x, _level.moninfo(i).currcoord.y).monidx = 0 _level.lmap(nxt.x, nxt.y).monidx = i _level.moninfo(i).currcoord = nxt 'Достигли координат где видели персонажа в последний раз? If (nxt.x = _level.moninfo(i).plastloc.x) And (nxt.y = _level.moninfo(i).plastloc.y) Then 'Обнулим цель преследования. _level.moninfo(i).psighted = FALSE _level.moninfo(i).plastloc.x = -1 _level.moninfo(i).plastloc.y = -1 Endif Else 'Потеряли персонажа, обнулим цель преследования. _level.moninfo(i).psighted = FALSE _level.moninfo(i).plastloc.x = -1 _level.moninfo(i).plastloc.y = -1 End If Else 'Тут проверяем звуки на карте. If _level.lmap(_level.moninfo(i).currcoord.x, _level.moninfo(i).currcoord.y).sndvol > 0 Then nxt = _GetNextSndTile(_level.moninfo(i).currcoord.x, _level.moninfo(i).currcoord.y) 'Установим новые координаты монстра. _level.lmap(_level.moninfo(i).currcoord.x, _level.moninfo(i).currcoord.y).monidx = 0 _level.lmap(nxt.x, nxt.y).monidx = i _level.moninfo(i).currcoord = nxt Endif Endif Endif Else If (_level.moninfo(i).psighted = TRUE) Or (_level.lmap(_level.moninfo(i).currcoord.x, _level.moninfo(i).currcoord.y).visible = TRUE) Then 'Проверим расстояние до персонажа. pdist = CalcDist(_level.moninfo(i).currcoord.x, pchar.Locx, _level.moninfo(i).currcoord.y, pchar.Locy) 'Проверим дальность атаки монстра. If pdist <= _level.moninfo(i).atkrange Then 'Атакуем. Else 'Убегаем от персонажа. nxt = _GetNextTile(_level.moninfo(i).currcoord.x, _level.moninfo(i).currcoord.y, pchar.Locx, pchar.Locy, TRUE) 'Установим новые коррдинаты монстра. _level.lmap(_level.moninfo(i).currcoord.x, _level.moninfo(i).currcoord.y).monidx = 0 _level.lmap(nxt.x, nxt.y).monidx = i _level.moninfo(i).currcoord = nxt Endif End If End If Endif Next End Sub
Первое, что необходимо сделать, это убедиться что монстр все еще жив. Мертвое чудовище, очевидно, не может двигаться. Далее мы проверяем — убегает ли монстр, если убегает то переходим к «отступлению с боем», иначе — двигаемся в торону персонажа в зависимости от текущего состояния:
If _level.lmap(_level.moninfo(i).currcoord.x, _level.moninfo(i).currcoord.y).visible = TRUE Then
Первое состояние из нашего списка которое мы проверяем, это то что монстр
видит персонажа. Для этого нам просто необходимо проверить, находится ли клетка
карты на которой находится монстр на линии прямой видимости персонажа. Мы можем
это сделать без дополнительных расчетов из за того, что, как мы увидим чуть
позже, мы позволяем игроку двигаться первым и ячейки карты, которые персонаж
видит уже помечены в массиве карты уровня. Мы используем правило «я вижу тебя
если ты видишь меня». Это позволяет нам избежать дополнительных вычислений
сохраняя производительность и позволяет реализовать симметричную модель
видимости.
Если проверка видимости персонажа успешна, то мы обновляем данные монстра, которые представляют его память.
map.bi
'Установим флаг видимости персонажа. _level.moninfo(i).psighted = TRUE 'Запомним позицию персонажа. _level.moninfo(i).plastloc.x = pchar.Locx _level.moninfo(i).plastloc.y = pchar.Locy
Мы устанавливаем значение флага видимости персонажа в TRUE и запоминаем
его текущие координаты для дальнейшего использования.
Затем мы проверяем — может ли монстр атаковать персонажа, сравнивая расстояние до него и дальность атаки монстра.
map.bi
... 'Проверим расстояние до персонажа. pdist = CalcDist(_level.moninfo(i).currcoord.x, pchar.Locx, _level.moninfo(i).currcoord.y, pchar.Locy) 'Проверим дальнось атаки монстра. If pdist <= _level.moninfo(i).atkrange Then 'Атакуем персонажа. Else ...
Обратите внимание, что монстр не двигается, если персонаж находится в
радиус поражения. Атака имеет больший приоритет чем передвижение и позволяет
монстру многократно атаковать на дальней дистанции. Если монстр не может
атаковать, то он будет двигаться в сторону игрока.
map.bi
... Else 'Получаем следующую ближайшую к игроку ячейку карты. nxt = _GetNextTile(_level.moninfo(i).currcoord.x, _level.moninfo(i).currcoord.y, _ pchar.Locx, pchar.Locy) 'Установим новые координаты монстра. _level.lmap(_level.moninfo(i).currcoord.x, _level.moninfo(i).currcoord.y).monidx = 0 _level.lmap(nxt.x, nxt.y).monidx = i _level.moninfo(i).currcoord = nxt End If ...
Здесь мы используем вспомогательную функцию, которая возвращает нам
координаты соседней с монстром ячейки карты по направлению к персонажу. Как
только мы их получили мы обновляем координаты монстра и ячейки массива
уровня.
map.bi
'Возвращает координаты ячейки карты по направлению к x, y. Function levelobj._GetNextTile(mx As Integer, my As Integer, x As Integer, y As Integer, flee As Integer = FALSE) As mcoord Dim As Integer pdist, mdist, xx, yy Dim v As vec Dim As mcoord ret 'Установим текущие координаты. xx = mx yy = my 'Установим начальное значение для рассчитанного расстояния. If flee = FALSE Then pdist = 1000 Else pdist = 0 End If 'Переберем все клетки вокруг позиции и посчитаем расстояние до цели. For j As compass = north To nwest v.vx = mx v.vy = my v += j 'Убедимся что клетка не заблокирована. If (_BlockingTile(v.vx, v.vy) = FALSE) And (_level.lmap(v.vx, v.vy).monidx = 0) Then 'Посчитаем расстояние. mdist = CalcDist(v.vx, x, v.vy, y) 'Двигаемся к персонажу. If flee = FALSE Then 'Ксли расстояние меньше чем в прошлый раз, то запомним новые координаты. If mdist <= pdist Then xx = v.vx yy = v.vy pdist = mdist Endif Else 'Двигаемся от персонажа (убегаем). If mdist >= pdist Then xx = v.vx yy = v.vy pdist = mdist Endif End If Endif Next ret.x = xx ret.y = yy Return ret End Function
Функция GetNextTile используется как для движения монстра к персонажу,
так и от него. Так как для получения координат ячейки на которую должен
переместится монстр используется одни и те же вычисления, разные только
направления движения. Процесс довольно прост. Мы задаем начальные координаты
векторному объекту и прибавляем к нему одно из направлений сторон света —
получая координаты одной из 8 ближайших ячеек карты. Если ячейка карты в
полученных координатах не заблокирована стеной или монстром, то рассчитываем
расстояние от нее до ячейки по координатам x, y. И если монстр двигается к
персонажу и расстояние меньше ранее рассчитанного, то запоминаем ткущую ячейку,
если нет то пропускаем. Если монстр убегает от персонажа, то запоминаем ячейку
если расстояние между ней и персонажем больше полученного ранее.
В случае, если новые координаты не найдены (монстр может быть заблокирован станами и другими монстрами) то функция возвращает его текущие координаты. т. е. монстр не двигается.
Если монстр не видит персонажа, то он двигается к координатам в которых он последний раз его наблюдал. В результате у монстра появляется хороший шанс его найти.
map.bi
... 'Персонаж не на линии видимости. Персонаж был замечен. If _level.moninfo(i).psighted = TRUE Then 'Убедимся что есть запомненные координаты. If (_level.moninfo(i).plastloc.x > -1) And (_level.moninfo(i).plastloc.y > -1) Then 'Двигаемся к этим координатам. nxt = _GetNextTile(_level.moninfo(i).currcoord.x, _level.moninfo(i).currcoord.y, _ _level.moninfo(i).plastloc.x, _level.moninfo(i).plastloc.y) 'Установим новые координаты монстра. _level.lmap(_level.moninfo(i).currcoord.x, _level.moninfo(i).currcoord.y).monidx = 0 _level.lmap(nxt.x, nxt.y).monidx = i _level.moninfo(i).currcoord = nxt 'Достигли координат где видели персонажа в последний раз? If (nxt.x = _level.moninfo(i).plastloc.x) And (nxt.y = _level.moninfo(i).plastloc.y) Then 'Обнулим цель преследования. _level.moninfo(i).psighted = FALSE _level.moninfo(i).plastloc.x = -1 _level.moninfo(i).plastloc.y = -1 Endif Else 'Потеряли персонажа, обнулим цель преследования. _level.moninfo(i).psighted = FALSE _level.moninfo(i).plastloc.x = -1 _level.moninfo(i).plastloc.y = -1 End If ...
Здесь действия точно такие же, как и движение в сторону персонажа, только
двигаемся в сохраненные ранее координаты. Первое что нужно сделать, это убедится
что у нас эти координаты есть, если нет, то мы сбрасываем флаг видимости
персонажа и, на всякий случай, координаты где его видели, монстр остается ждать
на месте. Если же координаты есть, то мы получаем координаты ближайшей ячейки
карты в их сторону и передвигаем монстра. Если мы достигли точки где видели
персонажа в последний раз и не нашли его, то также сбрасываем данные
преследования и остаемся ждать.
Если ни одно из двух предыдущих условий не выполнено, то проверяем — слышит ли монстр что нибудь поблизости.
map.bi
... 'Тут проверяем звуки на карте. If _level.lmap(_level.moninfo(i).currcoord.x, _level.moninfo(i).currcoord.y).sndvol > 0 Then nxt = _GetNextSndTile(_level.moninfo(i).currcoord.x, _level.moninfo(i).currcoord.y) 'Установим новые координаты монстра. _level.lmap(_level.moninfo(i).currcoord.x, _level.moninfo(i).currcoord.y).monidx = 0 _level.lmap(nxt.x, nxt.y).monidx = i _level.moninfo(i).currcoord = nxt Endif ...
Каждый раз когда персонаж перемещается создается звуковая карта в
зависимости от того, что персонаж несет с собой. Если монстр окажется в пределах
карты звука, то он перемешается на ячейку где звук слышится громче при помощи
функции GetNextSndTile.
map.bi
'Возвращает координаты ячейки карты с более сильным звуком. Function levelobj._GetNextSndTile(x As Integer, y As Integer) As mcoord Dim As mcoord ret Dim As Integer xx, yy, msnd = 0, psnd = 0 Dim v As vec 'Установим текущие координаты. xx = x yy = y 'Переберем соседние ячейки карты для поиска большего значения шума. For j As compass = north To nwest v.vx = x v.vy = y v += j 'Проверим что в ячейку можно переместиться. If (_BlockingTile(v.vx, v.vy) = FALSE) And (_level.lmap(v.vx, v.vy).monidx = 0) Then 'Получим текущее значение шума. msnd = _level.lmap(v.vx, v.vy).sndvol 'Если шум в ячейке больше чем у ранее проверенной. If msnd >= psnd Then xx = v.vx yy = v.vy psnd = msnd Endif Endif Next ret.x = xx ret.y = yy Return ret End Function
Здесь все также, как и в функции GetNextTile, за исключением того, что
проверяется не расстояние до цели а значение из звуковой карты уровня. Звуковая
карта является частью карты уровня и описана в информации о ячейке карты.
map.bi
'Информация о ячейке карты Type mapinfotype terrid As terrainids 'Тип местности. monidx As Integer 'Индекс монстра в массиве монстров. visible As Integer 'Персонаж видит ячейку. seen As Integer 'Персонаж уже видел ячейку. doorinfo As doortype 'Информация о двери. sndvol As Integer 'Громкость звука в ячейке. End Type
Поле sndvol укзывает на громкость звука в данной ячейке карты.
Большинство ячеек карты будут содержать в этом поле значение 0, за исключением
ячеек находящихся недалеко от персонажа. Звуковая карта будет генерироваться при
каждом движении персонажа.
dod.bas:MoveChar
... 'Перемещение персонажа. If block = FALSE Then 'Установим новые координаты. pchar.Locx = vc.vx pchar.Locy = vc.vy ret = TRUE 'Сгенерируем карту звуков. level.ClearSoundMap snd = pchar.GetNoise() level.GenSoundMap(pchar.Locx, pchar.Locy, snd) ...
Если персонаж перемещается, то необходимо создать карту звуков. Для
этого, вначале, очистим предыдущие звуковые данные.
map.bi
'Очищает текущую звуковую карту. Sub levelobj.ClearSoundMap() For x As Integer = 1 To mapw For y As Integer = 1 To maph _level.lmap(x, y).sndvol = 0 Next Next End Sub
Просто перебираем массив всех ячее карты и устанавливаем значение шума
равным 0. Следующим шагом мы должны определить общее значение шума, создаваемое
персонажем, в зависимости от одетых на него предметов и предметов из его
инвентаря. Делает это функция GetNoise.
character.bi
'Возвращает текущее значение шума. Function character.GetNoise() As Integer Dim As Integer ret = 0 'Получим шум от количество золота. ret = _cinfo.currgold / 10 'Поучим шум от предметов в инвентаре. For i As Integer = Lbound(_cinfo.cinv) To Ubound(_cinfo.cinv) ret += GetItemNoise(_cinfo.cinv(i)) Next 'Шум от используемых предметов. For i As Integer = Lbound(_cinfo.cwield) To Ubound(_cinfo.cwield) ret += GetItemNoise(_cinfo.cwield(i)) Next Return ret End Function
Функция перебирает все вещи в инвентаре персонажа и используемые им и
возвращает сумм величин шумов которые производит каждый предмет. Поскольку
величина шума, генерируемого предметом принадлежит предмету, то необходимо
добавить метод позволяющий получить эту величину.
inv.bi
'Возфращает фактор шума предмета. Function GetItemNoise(inv As invtype) As Integer Dim As Integer ret = 0 'Убедимся что предмет существует. If inv.classid <> clNone Then 'Выберем тип предмета. Select Case inv.classid 'Получим фактор шума конкретного предмета. Case clSupplies ret = inv.supply.noise Case clArmor ret = inv.armor.noise Case clShield ret = inv.shield.noise Case clWeapon ret = inv.weapon.noise End Select Endif Return ret End Function
Как мы делали во всех процедурах работы с инвентарем, вначале мы
проверяем, что предмет существует, потом в зависимости от его типа получаем
значение создаваемого предметом шума из соответствующего поля.
Как только мы получили общее количество шума, создаваемого персонажем, мы вызываем подпрограмму GenSoundMap для генерации его карты.
map.bi
'Создает карту шума используя фактор затухания. Sub levelobj.GenSoundMap(x As Integer, y As Integer, sndvol As Integer) Dim csound As Integer Dim sdist As Integer 'Проверим выход за пределы карты. If x < 0 Or x > mapw Then Exit Sub If y < 0 Or y > mapw Then Exit Sub If sndvol <= 0 Then Exit Sub If _BlockingTile(x, y) Then Exit Sub If _level.lmap(x, y).sndvol > 0 Then Exit Sub 'Ослабление звука от значения квадрата расстояния. sdist = CalcDist(pchar.Locx, x, pchar.Locy, y) csound = sndvol - (sdist * sdist) 'Если нет звука, то выходим. If csound <= 0 Then Exit Sub 'Рекурсивно вызываем функцию для построения карты распространения звука. _level.lmap(x, y).sndvol = csound GenSoundMap x+1, y, csound GenSoundMap x, y+1, csound GenSoundMap x-1, y, csound GenSoundMap x, y-1, csound GenSoundMap x+1, y+1, csound GenSoundMap x-1, y+1, csound GenSoundMap x-1, y-1, csound GenSoundMap x+1, y-1, csound End Sub
Как вы можете видеть, эта рекурсивная функция, которая прекращает свою
работу как только значение шума равно нулю или встречается заблокированная
ячейка карты. На самом деле это обычный алгоритм «заливки» (flood-fill алгоритм.
http://ru.wikipedia.org/wiki/Заливка
(прим. переводчика)), который был изменен для получения громкости звука. Для
определения величины затухания звука, используется квадрат расстояния. И хотя
это не очень реалистично, но позволяет избежать заполнения звуком всего
подземелья. Даже при таком сильном затухании звука, если персонаж будет под
завязку загружен, то он будет оставлять приличный звуковой след который на
ближайших монстров будет действовать как магнит.
Последнее активное состояние монстра, это когда он убегает. Реализация его такая же как и как и движение к персонажу, но теперь он двигается в противоположном направлении.
map.bi:MoveMonsters
... Else If (_level.moninfo(i).psighted = TRUE) Or (_level.lmap(_level.moninfo(i).currcoord.x, _level.moninfo(i).currcoord.y).visible = TRUE) Then 'Проверим расстояние до персонажа. pdist = CalcDist(_level.moninfo(i).currcoord.x, pchar.Locx, _level.moninfo(i).currcoord.y, pchar.Locy) 'Проверим дальность атаки монстра. If pdist <= _level.moninfo(i).atkrange Then 'Атакуем. Else 'Убегаем от персонажа. nxt = _GetNextTile(_level.moninfo(i).currcoord.x, _level.moninfo(i).currcoord.y, pchar.Locx, pchar.Locy, TRUE) 'Установим новые коррдинаты монстра. _level.lmap(_level.moninfo(i).currcoord.x, _level.moninfo(i).currcoord.y).monidx = 0 _level.lmap(nxt.x, nxt.y).monidx = i _level.moninfo(i).currcoord = nxt Endif Endif End If ...
Сначала мы проверяем, по прежнему ли персонаж находится в поле зрения
монстра и если это так то проверяем возможность атаки и если атака возможна, то
атакует, если нет, то монстр двигается в противоположном от персонажа
направлении, пока последний не скроется из виду.
Как вы видите, действия монстра основываются на списке его текущих состояний. Эти состояния представляют собой основную форму поведенческого моделирования, аналогичный тому, что вы можете найти в природе. Например лев может почувствовать запах животного и подойти к той точке где оно находилось чтобы устроить засаду или, если он заметит добычу то может попробовать незаметно к ней подкрасться достаточно близко чтобы напасть. Каждое из этих действий основывается на текущем состоянии льва: отслеживании запаха, визуальное слежение, скрытое приближение, атака. Мы не используем все нюансы поведения настоящих хищников в нашей игре, но используя поведенческое моделирование на основе состояний мы можем создать монстров которые будут эффективно преследовать персонажа или бежать, если необходимо.
Следующим нашим шагом будет реализация боя, в которой мы расширим искусственный интеллект нашим монстрам, опять основываясь на состояниях, что позволит нашим монстрам выбирать между несколькими вариантами атаки.
Перевод на русский: Fantik
содержание | назад | вперед