суббота, 3 марта 2012 г.

Давайте сделаем рогалик. Глава 03: Главное меню

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


Мы рисуем фон точно так же, как мы делали это для экрана заставки, просто берем цвет из массива и выводим ASCII символ 219. Но, поскольку мы используем одним и тем же кодом дважды, то вступает наше правило обобщения, напишем подпрограмму для вывода какой либо цветовой карты на экран. Добавим новую процедуру DrawBackgound в файл utils.bi

'Рисуем фоновое изображения используя карту цветов
Sub DrawBackground(cmap() As UInteger)
  'Перебор значений массива, рисуем символ блока используя цвет из массива.
  For x As Integer = 0 To txcols - 1
    For y As Integer = 0 To txrows - 1
        'Получим значение цвета используя формулу.
        Dim clr As UInteger = cmap(x + y * txcols)
        'Используем draw string т. к. это быстрее и нам не нужно заботится об расположении.
        Draw String (x * charw, y * charh), acBlock, clr
     Next
  Next
End Sub

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

dod.bas
'Отображает игровую заствку.
Sub DisplayTitle
  Dim As String txt
  Dim As Integer tx, ty
  
  'Установим значения для копирайтов.
  txt = "Copyright (C) 2010, by Richard D. Clark"
  tx = CenterX(txt)
  ty = txrows - 2
  'Заблокируем экран перед выводом.
  ScreenLock
  'Нарисуем фон.
  DrawBackground title()
  'Выведем информацию о копирайтах.
  Draw String (tx * charw, ty * charh), txt, fbYellow
  ScreenUnlock
  Sleep
  'Очистим буфер клавиатуры.
  ClearKeys
End Sub

Теперь для отображения массива с картой цветов мы используем код DrawBackground title(). Этот же код вы найдете в процедуре отображения меню.

Есть еще одно изменение в этой процедуре. Убрана команда очистки экрана Cls. Так как мы все равно рисуем фон на весь экран, то, в данном месте, команда CLS вызывалась впустую. Когда вы в разгаре программирования — легко пропустить такие вещи. Поэтому, возвращение к ранее написанному коду для внесения изменений — хороший шанс обнаружить и почистить ненужный код. Теперь мы готовы для написания меню. Для начала создадим файл mmenu.bi

mmenu.bi
/'****************************************************************************
*
* Name: mmenu.bi
*
* Synopsis: Main menu file.
*
* Description: This is the main menu routines that display and return the menu
*              selection to the main program.  
*
* Copyright 2010, Richard D. Clark
*
*                          The Wide Open License (WOL)
*
* Permission to use, copy, modify, distribute and sell this software and its
* documentation for any purpose is hereby granted without fee, provided that
* the above copyright notice and this license appear in all source copies. 
* THIS SOFTWARE IS PROVIDED "AS IS" WITHOUT EXPRESS OR IMPLIED WARRANTY OF
* ANY KIND. See http://www.dspguru.com/wol.htm for more information.
*
*****************************************************************************'/

'Обернем весь код в пространство имен, т.к. Этот код нужен нам только в начале программы.
Namespace mmenu

'подключаем массив с картой цветов.
#Include "menuback.bi"

'Перечисление возвращаемых значений нашего меню.
Enum mmenuret
  mNew
  mLoad
  mInstruction
  mQuit
End Enum

'Нарисуем меню.
Sub DrawMenu(m() As String, midx As Integer, mx As Integer, my As Integer)

  Dim As Integer x = mx, y = my

  'Нарисуем элементы меню из массива.
  For i As Integer = mNew To mQuit
    If midx = i Then
      Draw String (x * charw, y * charh), m(i), fbWhite
    Else
      Draw String (x * charw, y * charh), m(i), fbGray
    EndIf
    y += 2
  Next

End Sub

'Здесь рисуется меню и возвращается выбранное значение.
Function MainMenu() As mmenuret
  Dim As mmenuret idx = mNew
  Dim menuitems(mNew To mQuit) As String
  Dim As Integer mx, my, done = FALSE, tx, ty
  Dim  As String mkey, mtitle 
  'Настроим пункты меню.
  menuitems(mNew) = "New Game    "
  menuitems(mLoad) = "Load Game   "   
  menuitems(mInstruction) = "Instructions"   
  menuitems(mQuit) = "Quit        "
  'Установим x, y для элементов меню
  mx = CenterX(menuitems(3))
  my = CenterY(UBound(menuitems) * 2)
  ScreenLock
  'Нарисуем фон.
  DrawBackground menuback()
  'Нарисуем заголовок с отбрасываемой тенью.
  mtitle = "Dungeon of Doom v." & dodver
  tx = CenterX(mtitle) * charw
  ty = (10 * charh)
  Draw String (tx + 1, ty + 1), mtitle, fbBlack
  Draw String (tx, ty), mtitle, fbYellow
  'Нарисуем пункты меню.
  DrawMenu menuitems(), idx, mx, my
  ScreenUnLock
  Do
    'Получим текущую клавишу.
    mkey = InKey
    'Пользователь нажал клавишу?
    If mkey <> "" Then
      'Eсли пользователь нажал escape или кнопку выхода, выйдем с результатом mQuit.
      If (mkey = key_esc) Or (mkey = key_close) Then
        idx = mQuit
        done = TRUE
      EndIf
      'Пользователь нажал стрелку вверх.
      If mkey = key_up Then
        'Уменьшим индекс меню.
        idx -= 1
        'Переместимся вниз меню, если необходимо.
        If idx < mNew Then idx = mQuit
        'Перерисуем меню.
        ScreenLock
        DrawMenu menuitems(), idx, mx, my
        ScreenUnLock
      EndIf
      'Пользователь нажал стрелку вниз.
      If mkey = key_dn Then
        'Увеличим индекс меню.
        idx += 1
        'Переместимся вверх меню, если необходимо.
        If idx > mQuit Then idx = mNew
        'Перерисуем меню.
        ScreenLock
        DrawMenu menuitems(), idx, mx, my
        ScreenUnLock
      EndIf
      'Пользователь нажал клавишу Enter.
      If mkey = key_enter Then
        'Выход из меню.
        done = TRUE
      EndIf
    EndIf
    Sleep 10
  Loop Until done = TRUE
  'Очистим буфер клавиатуры.
  ClearKeys
  Return idx
End Function

End Namespace

На самом деле код достаточно прост, но тут есть некоторые новинки, которые мы рассмотрим подробнее. Первое что бросается в глаза — весь код «завернут» в пространство имен. Пространство имен изолирует код от остальной части программы. Пространство имен задает какое либо имя (в нашем случае mmenu), которое используется для доступа к элементам, описанным внутри. Как это реализуется, вы увидите в основном файле проекта, когда мы до него доберемся.

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

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

mmenu.bi
'Перечисление возвращаемых значений нашего меню.
Enum mmenuret
  mNew
  mLoad
  mInstruction
  mQuit
End Enum

Перечисление, это набор значений, которые компилятор создаст для вас, когда программа компилируется. В нашем коде, mNew будет присвоено значение 0, mLoad значение 1, и так далее. Также вы можете сами установить начальное значение для перечисления или же установить значения для всех элементов. Перечисления являются наиболее полезными, когда его значения идут по очереди. Вы увидите это при выводе на экран пунктов меню.

Данное перечисление поможет нам решить сразу несколько задач. Оно будут задаваться возвращаемые значения из функции меню, указывать — какой из пунктов меню в данный момент активен, также оно позволит определить границы нашего меню. Это еще один способ оптимизации, который часто упускают из виду. Нам придется написать меньше кода. Меньше кода — меньше мест для ошибок, меньше кода придется пересмотреть при поиске ошибки, меньше правок при внесении изменений. В нашем случае, как можно увидеть далее, перечисление еще и улучшает читаемость кода.

Следующий блок кода выводит на экран текст меню:

mmenu.bi
'Нарисуем меню.
Sub DrawMenu(m() As String, midx As Integer, mx As Integer, my As Integer)
  Dim As Integer x = mx, y = my
  'Нарисуем элементы меню из массива.
  For i As Integer = mNew To mQuit
    If midx = i Then
      Draw String (x * charw, y * charh), m(i), fbWhite
    Else
      Draw String (x * charw, y * charh), m(i), fbGray
    EndIf
    y += 2
  Next
End Sub

Процедура DrawMenu отображает на экране пункты меню из массива m() переданного ей в качестве параметра. Параметр midx указывает на выделенный пункт меню, чтобы мы могли нарисовать его другим цветом. Сейчас активный пункт меню рисуется белым цветом, а все другие — серым. Это позволит пользователю узнать, какой из пунктов меню выбран в данное время. Параметры mx и my задают координаты первого пункта меню. В нашем случае это mNew. Цикл For-Next пробегает по всем идентификаторам перечисления для отображения каждого пункта. Обратите внимание, что при использовании перечисления в цикле, код становится гораздо более понятным, чем если бы мы использовали обычные цифры от 0 до 3. Это само документирующее свойство является одним из преимуществ использования символьных констант перед обычными цифрами (На мой взгляд, более важно, что если нам понадобится добавить в центр меню еще один пункт, то мы просто добавляем его в перечисление, а вышеописанной функции не нужно будет менять ни одной строчки кода. (примечание переводчика)).

Все пункты меню имеют одинаковую координату x, но координата y должна меняться для каждого пункта, если мы хотим отобразить меню по вертикали. Инструкция y += 2 увеличивает координату y для каждого пункта на 2. Это нужно, чтобы добавить пустую строку между пунктами меню и сделать его более читабельным.

Следующая часть кода описывают функцию MainMenu, вызываемую из основной программы:

mmenu.bi
'Здесь рисуется меню и возвращается выбранное значение.
Function MainMenu() As mmenuret
  Dim As mmenuret idx = mNew
  Dim menuitems(mNew To mQuit) As String
  Dim As Integer mx, my, done = FALSE, tx, ty
  Dim  As String mkey, mtitle
...

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

Затем мы инициализируем переменные нашего меню, для того чтобы им управлять. Переменная idx отвечает за текущий выделенный пункт меню и по умолчанию устанавливается в mNew. Т.е. у нас есть выбранный пункт меню по умолчанию. Строки пунктов меню у нас содержатся в массиве строк. Еще раз мы используем идентификаторы перечисления для инициализации массива. Это необходимо не только для улучшения читаемости кода, но и для упрощения управления программным кодом. Если нам нужно будет добавить новый пункт меню в список, то нам не нужно менять инициализацию массива пунктов меню, если мы не меняли позиции mNew и mQuit в списке перечисления. Идентификаторы перечисления будут автоматически перенумерованы, что сделает наш массив необходимого размера. Остальные переменные, это внутренние рабочие переменные, которые мы увидим в действии при дальнейшем изучении кода.

mmenu.bi
...
  'Настроим пункты меню.
  menuitems(mNew) = "New Game    "
  menuitems(mLoad) = "Load Game   "   
  menuitems(mInstruction) = "Instructions"   
  menuitems(mQuit) = "Quit        "
  'Установим x, y для элементов меню
  mx = CenterX(menuitems(3))
  my = CenterY(UBound(menuitems) * 2)
  ScreenLock
  'Нарисуем фон.
  DrawBackground menuback()
  'Нарисуем заголовок с отбрасываемой тенью.
  mtitle = "Dungeon of Doom v." & dodver
  tx = CenterX(mtitle) * charw
  ty = (10 * charh)
  Draw String (tx + 1, ty + 1), mtitle, fbBlack
  Draw String (tx, ty), mtitle, fbYellow
  'Нарисуем пункты меню.
  DrawMenu menuitems(), idx, mx, my
  ScreenUnLock
...

После того как мы инициализировали наши переменные, мы должны настроить строки пунктов меню, рассчитать его координаты, нарисовать фон и заголовок меню. Мы добавили в программу новую функцию: CenterY которую опишем в defs.bi

defs.bi
#Define CenterY(ni)((txrows / 2) - (ni / 2))

Мы определили ее как макрос, т. к. ее функциональность аналогична макросу CenterX. В этот макрос мы передаем количество элементов по вертикали, которое должно быть отрисовано в центре экрана. Но так как мы не просто рисуем только пункты меню, но еще и добавляем одну пустую строку между пунктами, то мы удваевам значение передаваемое в макрос, чтобы наше меню было точно по центру. В результате, вычисление Y позициии меню выглядит как my = CenterY(UBound(menuitems) * 2).

Как только мы вычислили x и y координаты меню, мы рисуем фон используя нашу подпрограмму DrawBackground, рисуем заголовок и сами пункты меню. Обратите внимание, что заголовок мы выводим на экран дважды: первый раз черным цветом, второй — желтым. Когда мы рисуем заголовок черным цветом, то смешаем позицию заголовка на 1 пиксель по вертикали и по горизонтали. Это даст на эффект тени, падающей от заголовка, и заголовок будет выделяться на общем фоне. Хотя это всего 1 пиксель, но это заметно. Попробуйте закомментировать строку Draw String (tx + 1, ty + 1), mtitle, fbBlack и откомпилировать программу. Вы сразу увидите разницу.

После отображения меню, мы входим в цикл Do-Loop, который ждет ввода данных от пользователя.

mmenu.bi
...
  Do
    'Получим текущую клавишу.
    mkey = InKey
    'Пользователь нажал клавишу?
    If mkey <> "" Then
      'Eсли пользователь нажал escape или кнопку выхода, выйдем с результатом mQuit.
      If (mkey = key_esc) Or (mkey = key_close) Then
        idx = mQuit
        done = TRUE
      EndIf
      'Пользователь нажал стрелку вверх.
      If mkey = key_up Then
        'Уменьшим индекс меню.
        idx -= 1
        'Переместимся вниз меню, если необходимо.
        If idx < mNew Then idx = mQuit
        'Перерисуем меню.
        ScreenLock
        DrawMenu menuitems(), idx, mx, my
        ScreenUnLock
      EndIf
      'Пользователь нажал стрелку вниз.
      If mkey = key_dn Then
        'Увеличим индекс меню.
        idx += 1
        'Переместимся вверх меню, если необходимо.
        If idx > mQuit Then idx = mNew
        'Перерисуем меню.
        ScreenLock
        DrawMenu menuitems(), idx, mx, my
        ScreenUnLock
      EndIf
      'Пользователь нажал клавишу «ввод».
      If mkey = key_enter Then
        'Выход из меню.
        done = TRUE
      EndIf
    EndIf
    Sleep 10
  Loop Until done = TRUE
  'Очистим буфер клавиатуры.
  ClearKeys
  Return idx

End Function

Мы используем функцию InKey для того, чтобы узнать, какую клавишу нажал пользователь. Мы могли бы использовать MultiKey, но с InKey работать проще, также она возвращает нам клавиши в той последовательности, в которой они были нажаты. При помощи MultiKey мы не смогли бы определить, когда пользователь нажал на кнопку «закрыть окно», поэтому нам в любом случае пришлось бы использовать InKey. В результате, весь ввод с клавиатуры мы будем получить при помощи InKey (функция MultiKey проверяет, нажата ли клавиша, скан код которой передан ей в качестве параметра, в текущий момент, а функция InKey проверяет буфер клавиатуры, и возвращает пустую строку, если буфер пуст, или строку из одного или двух символов, если в буфере сохранено нажатие обычной или специальной клавиши. (примечание переводчика)).

В файле defs.bi определим некоторые коды клавиш, возвращаемые функцией InKey.

defs.bi
'Клавиашные константы
Const xk = Chr(255)
Const key_up = xk + "H"
Const key_dn = xk + "P"
Const key_rt = xk + "M"
Const key_lt = xk + "K"
Const key_close = xk + "k"
Const key_esc = Chr(27)
Const key_enter = Chr(13)

Когда нажимаются дополнительные (специальные) клавиши, то InKey возвращает строку из 2-х символов. Дополнительными клавишами являются курсорные клавиши, клавиши редактирования и функциональные клавиши. Для дополнительных клавиш первый символ, в возвращаемой строке, будет равен 255, поэтому мы определяем его в константу xk и будем использовать для задания других констант. Сравнение строк — достаточно медленная процедура, поэтому использование MultiKey предпочтительнее, т. к. там сравниваются числовые коды клавиш, но мы просто ждем ввод от пользователя и ничего не рисуем в реальном времени, поэтому, в нашем случае, скорость не важна.

После вызова InKey мы проверяем, вернула ли она не пустое значение, и если это так, то сравниваем полученный код с кодами клавиш, которые нам нужно обработать. Это клавиши стрелок вверх и вниз, клавиши enter, escape и закрытия приложения. Если сравнение успешно, то мы выполняем соответствующую часть кода, связанную с этой клавишей.

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

mmenu.bi
'Пользователь нажал стрелку вверх.
      If mkey = key_up Then
        'Уменьшим индекс меню.
        idx -= 1
        'Переместимся вниз меню, если необходимо.
        If idx < mNew Then idx = mQuit
        'Перерисуем меню.
        ScreenLock
        DrawMenu menuitems(), idx, mx, my
        ScreenUnLock
      EndIf

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

Для выбора пункта меню, пользователю необходимо нажать клавишу enter.

mmenu.bi
'Пользователь нажал клавишу «ввод».
      If mkey = key_enter Then
        'Выход из меню.
        done = TRUE
      EndIf

Все что нам нужно сделать, это просто выйти из цикла Do-Loop. Для этого мы устанавливаем переменную-флаг done в значение TRUE — истина. Значения TRUE и FALSE, это новые константы, которые нам нужно добавить в defs.bi. Их мы будем использовать для логических операций.

defs.bi
'Константы для ИСТИНА и ЛОЖЬ
#Ifndef FALSE
  #Define FALSE 0
#EndIf
#Ifndef TRUE
  #Define TRUE -1
#EndIf

Мы используем значение 0 для FALSE (ЛОЖЬ) и значение -1 для TRUE (ИСТИНА). Мы могли бы использовать 1 для TRUE, но при -1 мы сможем использовать в коде конструкцию Not FALSE (Not 0 эквивалентно -1), если нам нужно. На самом деле нам это не нужно, но по крайней мере такая возможность есть, и FreeBasic оператор Not будет работать с нашей константой так, как и задумывалось. Почему бы не определить значение TRUE, как (Not FALSE)? Потому что это потребует дополнительных вычислений каждый раз, когда мы будем проверять TRUE, что нам не нужно. Можно было бы использовать Const определиен для TRUE, но тогда бы каждый раз при сравнении с TRUE программа бы обращалась к таблице символьных имен и уже после получала значение константы, что тоже неприемлимо. Поэтому при сравнении переменной done с константой описанной в #define избавит нас от одного обращения к таблице имен.

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

dod.bas
'Получим выбранный пункт меню
Dim mm As mmenu.mmenuret
'Повторить, пока пользователь не выберет New, Load или Quit.
Do
  'Нарисуем главное меню.
   mm = mmenu.MainMenu
  'Обработаем выбранный пункт меню.
  If mm = mmenu.mNew Then
    'Создание персонажа.
  ElseIf mm = mmenu.mLoad Then
    'Загрузка/Сохранение игры.
  ElseIf mm = mmenu.mInstruction Then
    'Показать инструкцию.
  EndIf
Loop Until mm <> mmenu.mInstruction

'Главный игровой цикл.
If mm <> mmenu.mQuit Then
  'Сгенерировать карту.
  'Нарисовать главный экран.
  'Получить ввод с клавиатуры пока пользователь не вышел.
EndIf

Это код нашего меню в основной программе. Первое что нам нужно сделать, это создать переменную для хранения пункта меню, который выбрал пользователь Dim mm As mmenu.mmenuret. Так как код меню у нас «завернут» в пространство имен, то мы должны комбинировать имя пространства имен (mmenu) со значениями в нем через оператор (.) точка. Т.е., для доступа к любому объекту или значению, описанному в пространстве имен, используется формат mmenu.объект. Мы могли бы использовать команду Using <имя пространства имен>, и, после нее, использовать все переменные и объекты из пространства имен напрямую, без указания его названия, но это бы перечеркнуло все плюсы использования пространства имен, о которых мы говорили выше.

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

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

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


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

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