воскресенье, 24 февраля 2013 г.

Давайте сделаем рогалик. Глава 30: Допиливаем игру 1

Хорошая практика — время от времени остановиться и детально взглянуть на игру которая у нас получается. Что в ней должно быть переделано для того чтобы улучшить ее, исправить кукую либо ошибку, или сделать программу немного более отзывчивой. «Допиливанием» я называю незначительные исправления, а не капитальный ремонт. Обычно такие исправления заключаются в добавлении или удалении всего лишь одной либо двух строк кода. Если для внесения изменений требуется переработать достаточно много кода, то к такому исправлению следует подходить не как к быстрой правке, а как к основательному пересмотру кода программы. Мелкие исправления не должны затрагивать основной игровой процесс вообще. Если мы увидим, что внесенные изменения не работают так как нам бы хотелось, то всегда должна быть возможность быстро вернуть все назад. После пересмотра текущей версии игры я нашел несколько мест, которые могут быть переработаны для ее улучшения.

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

dod.bas
Sub DrawMainScreen (db As Integer = FALSE)
    Dim As Integer x, y, j, pct, row, col
    Dim As Double pctd
    Dim As UInteger clr
    Dim As String txt, idesc
    Dim As terrainids terr
    Dim inv As invtype
    Dim spl As spelltype
   
    ScreenLock
    'Нарисовать фон главного экрана
    If db = TRUE Then
      DrawBackground mainback()
    End If
  ...

Мы добавили для процедуры DrawMainScreen параметр, который указывает — нужно ли перерисовывать фон. Так как мы добавили параметр со значением по умолчанию, то нам не нужно вносить изменения везде, где вызывается эта подпрограмма, а только в том месте, где персонаж перемещается по лестницам.

dod.bas: Main Game Loop
...
  'Главный цикл игры.
  If mm <> mmenu.mQuit Then
   'Установим первый ровень.
   level.LevelID = 1
        'Построим первый уровень подземелья
        level.GenerateDungeonLevel
   'Отобразим главный экран.
   DrawMainScreen TRUE

  ...
         'Проверим на передвижение вглубь по лестнице.
         If ckey = ">" Then
            'Убедимся, что лестница есть.
            If level.GetTileID(pchar.Locx, pchar.Locy) = tstairdn Then
               'Сменим номер уровня.
               level.LevelID = level.LevelID + 1
               'Построим новый уровень.
                   level.GenerateDungeonLevel
               'Отобразим главный экран.
               DrawMainScreen TRUE
            End If
         EndIf
  ...

Мы должны обновить вызов процедуры в двух местах. В самом начале, после старта игры из главного меню, и при передвижении на персонажем по лестницам. Мы просто добавляем к вызову дополнительный параметр TRUE, в результате чего фоновое изображение будет также перерисовываться. Как видите — изменения в коде минимальны.

Следующее обновление связано с расходом опыта персонажа. Ткущая цена улучшения атрибутов, это 10 Опыта за 1 пункт атрибута. В результате персонаж может развиваться очень быстро, что позволяет ему «обмануть» игру. (я написал «обмануть» в кавычках потому, что на самом деле это не настоящий обман, а ошибка в игре, которой может воспользоваться проницательный игрок). Если игрок не будет тратить очки опыта до перехода на следующий уровень, а после перехода улучшить атрибуты персонажа, то его герой станет значительно сильнее монстров и запросто со всеми ими разделаться. Происходит это из за того, что мы масштабируем монстров, во время их создания, относительно текущих атрибутов персонажа.

Исправить это мы можем просто изменив соотношение опыта к атрибутам то 10 к 1 до 100 к 1. Это замедлит развитие персонажа и будет удерживать его характеристики в соответствии с характеристиками монстров. Игрок все еще может накапливать опыт до перехода на новый уровень, но теперь его превосходство над монстрами не будет таким большим.

dod.bas
'Применить опыт к атрибутам персонажа.
  Sub ImproveCharacter ()
    Dim As Integer sel, xp, reqamt = 100
   
    'Убедимся что у нас достаточно опыта.
    If pchar.CurrXP > reqamt Then
      Do
         If pchar.CurrXP > reqamt Then
            'Print Current stats.
            pchar.PrintStats
            PutTextShadow "Cost = " & reqamt & " XP for 1 point improvement.", 49, 10
            PutTextShadow "Enter attribute 1 to 5 to improve, enter to exit: ", 51, 10
            'Позиция для ввода.
            Locate 53, 10
            'Ввод номера атрибута.
            Input sel
            'Изменим выбранный атрибут. 
            If sel = 1 Then
               pchar.ChangeStrength 1
               'Subtract the xp amount.
               xp = pchar.CurrXP
               xp -= reqamt
               pchar.CurrXP = xp 
            EndIf
            If sel = 2 Then
               pchar.ChangeStamina 1
               'Subtract the xp amount.
               xp = pchar.CurrXP
               xp -= reqamt
               pchar.CurrXP = xp 
            EndIf
            If sel = 3 Then
               pchar.ChangeDexterity 1
               'Subtract the xp amount.
               xp = pchar.CurrXP
               xp -= reqamt
               pchar.CurrXP = xp 
            EndIf      
            If sel = 4 Then
               pchar.ChangeAgility 1
               'Subtract the xp amount.
               xp = pchar.CurrXP
               xp -= reqamt
               pchar.CurrXP = xp 
            EndIf
            If sel = 5 Then
               pchar.ChangeIntelligence 1
               'Subtract the xp amount.
               xp = pchar.CurrXP
               xp -= reqamt
               pchar.CurrXP = xp 
            EndIf
         Else
            sel = 0
         End If
      Loop Until sel = 0
    Else
      PrintMessage "Not enough XP to spend."
    End If
  End Sub

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

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

character.bi
...
    Declare Sub ChangeStrength(change As Integer, docurrhp As Integer = FALSE)  
    Declare Sub ChangeStamina(change As Integer, docurrhp As Integer = FALSE) 
  ...
  'Изменение атрибута и обновление связанных параметров.
  Sub character.ChangeStrength(change As Integer, docurrhp As Integer = FALSE)
    'Обновим аттрибут
    _cinfo.stratt(idxAttr) = _cinfo.stratt(idxAttr) + change
    If _cinfo.stratt(idxAttr) < 1 Then _cinfo.stratt(idxAttr) = 1
    'Обновим HP. 
    _cinfo.maxhp = _cinfo.stratt(idxAttr) + _cinfo.staatt(idxAttr)
    If docurrhp <> FALSE Then 
      _cinfo.currhp = _cinfo.maxhp
    End If 
    _cinfo.ucfsk(idxAttr) = _cinfo.stratt(idxAttr) + _cinfo.aglatt(idxAttr)
    _cinfo.acfsk(idxAttr) = _cinfo.stratt(idxAttr) + _cinfo.dexatt(idxAttr)
    _cinfo.cdfsk(idxAttr) = _cinfo.stratt(idxAttr) + _cinfo.aglatt(idxAttr)
  End Sub
  
  'Изменение атрибута и обновление связанных параметров.
  Sub character.ChangeStamina(change As Integer, docurrhp As Integer = FALSE)
    'Обновим аттрибут
    _cinfo.staatt(idxAttr) = _cinfo.staatt(idxAttr) + change
    If _cinfo.staatt(idxAttr) < 1 Then _cinfo.staatt(idxAttr) = 1
    'Обновим HP. 
    _cinfo.maxhp = _cinfo.stratt(idxAttr) + _cinfo.staatt(idxAttr) 
    If docurrhp <> FALSE Then 
      _cinfo.currhp = _cinfo.maxhp
    End If 
    _cinfo.currmana = _cinfo.intatt(idxAttr) + _cinfo.staatt(idxAttr) 
    _cinfo.maxmana = _cinfo.currmana
    _cinfo.mcfsk(idxAttr) = _cinfo.intatt(idxAttr) + _cinfo.staatt(idxAttr)
  End Sub

В процедуры ChangeStength и ChangeStamina мы добавили флаг docurrhp со значением по умолчанию FALSE. И добавили условие, что если он равен TRUE (не равен FALSE), то только тогда изменяем также и текущий уровень здоровья. Больше никаких изменений в коде не требуется. Теперь, при увеличении атрибутов персонажа, его текущее здоровье не измениться.

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

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

dod.bas: DrawInventoryScreen
...
         'Покажем игроку что предмет неопознан.
         If ret = FALSE Then
            txt &= "(*)"
         Else
            'Если содержит магию.
            If inv.classid = clWeapon Then
               If inv.weapon.spell.id <> splNone Then
                  txt &= " (M)"
               End If
            EndIf
         EndIf
  ...

Мы проверяем, опознан ли предмет, и если нет, то, как обычно, рисуем звездочку. Если же предмет опознан, то мы проверяем, оружие ли это и содержит ли оно магию, и, если это так, то мы отметим предмет буквой M. Это даст игроку возможность быстро узнать, без необходимости заходить в описание предмета, содержит ли он магию. Конечно, если предмет не опознан игроком, то он, все еще, не узнает, содержаться ли в нем какие либо магические свойства.

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

dod.bas: DrawMainScreen
  ...
    If pchar.BonStr > -1 Then
      txt = " +"
    Else
      txt = " -"
    EndIf
    PutText "Str: " & pchar.CurrStr & txt & pchar.BonStr & " (" & pchar.BonStrCnt & ")", row, col
  ...

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

Как вы можете видеть, все внесенные изменения очень просты в реализации, но имеют огромное влияние на игру. Изменение влияния опыта само по себе меняет всю динамику игры, будем надеяться, к лучшему. Это только первый этап настроек игрового процесса, в будущем, мы будем снова к этому возвращаться. Настройка игрового процесса это непрерывный процесс, и всегда есть что либо, что можно улучшить.

Комментариев нет:

Отправить комментарий