Создание 3D игр на C++
Часть1. Урок 3
Обработка команд консоля;
Обработка клавиш;
FullScreen;

В этом уроке мы придадим нашему консолю функциональность, а так же, с помощью DirectX, изменим разрешение экрана, после чего установим полноэкранный режим.

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

А пока наделим его основной функциональностью. Перво-наперво определимся, что консоль должен делать. Команды, вводимые пользователем, имеют определенный формат. В одну строку пользователь может ввести не одну, а сразу несколько команд, разделив их точкой с запятой (;). Сама же команда состоит из нескольких разделенных пробелами подстрок. Например, команда FOV 90 - это две подстроки: первая - это оператор FOV, вторая - это его операнд - 90 градусов.
Обработку строк мы зададим в две процедуры: первая разобьет строку на отдельные команды, а вторая займется обработкой самих команд.
Итак, первая:
void __fastcall TypeString (AnsiString S)
{
   int i = 1, Last = 1;
   while (i < S.Length())
   {
      // Пока текущий символ не равен точке с запятой, увеличиваем порядковый номер текущего символа
      while (S[i] != ';' && i < S.Length()) i++;
      if (i > 1 && (i < S.Length() || (i == S.Length() && S[i] == ';')))
      {// Если прочитано больше одного символа
       // И мы еще не дошли до конца строки, или дошли, но этот конец строки - точка с запятой
       // То можно утвержать, что текущий символ - точка с запятой, а потому мы уменьшаем
       // текущий символ, а потом, передав все следующей функции, увеличиваем на 2
         i--;
         TypeString1 (S.SubString(Last, i-Last+1));
         i += 2;
         Last = i;
      }
      else
      {// Иначе текущий символ - не точка с запятой! И мы передаем строку, не уменьшая номер
       // текущего символа, а потому увеличиваем только на один.
         TypeString1 (S.SubString(Last, i-Last+1));
         i ++;
         Last = i;
      }
      // После этого в i содержится первый символ после точки с запятой
   }
}
Немного пояснений: мы ищем первую точку с запятой. Если мы нашли ее, то мы уменьшаем порядковый номер текущего символа (чтоб не передать точку с запятой), а потом увеличиваем на 2, чтобы оказаться сразу за точкой с запятой (иначе при следующем поиске мы бы сразу нашли эту самую точку с запятой и попали бы тем самым в бесконечный цикл).
Если же мы не нашли точку с запятой, то мы обрабатываем последнюю (а возможно, единственную) команду, и потому уменьшать номер последнего символа не надо, так как он тоже значащий.

Теперь займемся непосредственно обработкой команд:
void __fastcall TypeString1 (AnsiString S)
{
   if (S == ";") return; // Если все же каким-то чудом
   // нам передали точку с запятой, выйти
   S = S + " "; // добавим в конец пробел (будет нужен)
   int _Start;
   // Всего нам надо будет не более пяти подстрок:
   AnsiString _Values[5] = {"", "", "", "", ""};
   int i = 1; int j = 0;
   // Читаем команду, так же, как в предыд. функции читали строку,
   // Но вместо точек с запятой разделителями ищем пробелы.
   // Здесь не надо предусматривать случай, когда мы дошли до конца,
   // так как мы добавили пробел в конец!
   while (j < 5 && i < S.Length())
   {
      while (S[i] == ' ' && i < S.Length()) i++;
      if (S[i] != ' ')
      {
         _Start = i;
         while (S[i] != ' ' && i < S.Length()) i++;
         if (S[i] != ' ') i++;
         _Values[j] = S.SubString(_Start, i-_Start);
      }
      j++;
   }

   // И обрабатываем ее. Тут, думаю, все понятно
   // Мы просто сравниваем оператор с поддерживаемыми операторами (их список будет расти)
   // и делаем соответсвующие действия!
   if (AnsiLowerCase(_Values[0]) == "exit" || AnsiLowerCase(_Values[0]) == "quit")
      Application->Terminate ();
   else if (AnsiLowerCase(_Values[0]) == "console")
      frmCon->Show ();
   else
      AddString ("Unknown command " + _Values[0], true);
}
Данная функция поддерживает лишь две команды: exit и quit, остальные мы дадим по ходу дела.
Основная суть данной функции похожа на предыдущую, и я надесь, в ней все останется понятным.

Теперь надо лишь сделать так, чтобы при вводе команды консоль не только выводил ее (AddString), но и обрабатывал (TypeString). В обработчике события OnKeyDown формы добавьте одну строку:
   if (Key == VK_RETURN)
   {  // Enter - перевод строки
      AddString (CurrentString, false);
  // Следующую строку надо добавить
      TypeString (CurrentString);
      CurrentString = "";
   }
Теперь займемся обработкой клавиш. Для начала нам надо инициализировать консоль. С первого урока у нас остался модуль col_main. Он у нас является основным (то есть при запуске программы показывается именно форма из col_main), а значит именно в инициализации данного модуля мы и будем писать все прочие инициализации.
Для начала переименуйте форму в frmMain (если не сделали этого раньше). Для этого перейдите в инспектор объектов и там измените свойство Name.

Дважды кликните по форме. Это создаст обработчик события OnCreate (можно было и через инспектор объектов, но так проще). Немного предисловия к инициализации. В нашем случае она будет проходить примерно так: первым будет открыт консоль. В этом процессе я почти исключаю ошибки. Потом вся остальная инициализация будет производиться с выводом всей информации в окно консоля. Таким образом мы сможем точно засечь ошибку.
Если вся инициализация пройдет успешно, то дальше все пойдет своим чередом (пока об этом не надо думать). Если же будут ошибки, то мы поступим так: мы скроем окно консоля и покажем его снова, но не функцией Show, а функцией ShowModal. Таким образом, пока окно консоля не будет закрыто, дальше программа не пойдет. А следующей строкой Application->Termianate (); мы закроем приложение.
Теперь подгурзим модуль col_con к col_main. Просто добавьте в начало строку:
#include "col_main.h"
// Следующую строку надо добавить
#include "col_con.h"
Итак, вот сам код функции OnCreate:
Не обращайте пока внимания на инициализацию DirectX. Об этом мы поговорим в конце урока.
Функцию extinInit мы тоже напишем сегодня.
//==============================================================================
// Инициализация
void __fastcall TfrmMain::FormCreate(TObject *Sender)
{
   bool CanRun = true;

   // Покажем консоль
   frmCon = new TfrmCon (Application);
   frmCon->Show();

   // Инициализируем поддержку DirectX
   CanRun = extdxInit (Handle) && CanRun;
   // Перепоказываем консоль с новыми координатами
   frmCon->Hide ();
   frmCon->Left = 0;
   frmCon->Top = 0;
   frmCon->Show ();
   frmCon->TControl::Refresh ();

   AddString ("Software Initializating...", true);

   AddString ("  Initializating Software Input...", true);
   extinInit ();

   AddString ("  Other Initializations...", true);
   // Запомним папку, из которой запущены
   fDir = ExtractFileDir (ParamStr(0)) + "\\";

   AddString ("Software Initializated successfully", true);

   AddString (EXT_SEPARATOR, true);

   // Обновим консоль
   frmCon->Refresh();

   if (!CanRun)
   {// Если были ошибки
      frmCon->Hide(); // Скроем окно консоля
      frmCon->ShowModal(); // И покажем его вновь, но модально!
      Application->Terminate();
   }
}
Здесь может быть немного непонятно в вызове незнакомых функций. Их мы еще объявим сегодня. Будут еще, их мы добавим в инициализацию по ходу создания.
В переменную fDir мы помещаем текущую папку. Для этого мы обрабатываем нулевой параметр, передаваемый программе (всегда полный путь к программе) и извлекаем из него папку. Переменную fDir надо объявить в начале col_main.cpp:
TfrmMain *frmMain;
AnsiString fDir;  // Объявление
И сразу же предстоит ее объявить как внешнюю в файле col_main.h:
extern AnsiString fDir;
Дальше у нас обработка клавиш:
Создайте новый модуь:
File->New->Unit
И сохраните его под именем col_input
Сразу подгрузите его к col_main
#include "col_main.h"
#include "col_con.h"
// Следующую строку надо добавить
#include "col_input.h"
Теперь в col_input.h напишите следующий код:
#ifndef ext_inputH
#define ext_inputH
#include <classes.hpp>
//==============================================================================
// Клавиша: ее имя и ее действие!
struct Key_t
{
   AnsiString Name;
   AnsiString Action;
};
extern Key_t _Keys[255];
void extinInit ();
int GetKey (AnsiString fName);
void BindKey (AnsiString fKey, AnsiString fAction);
//==============================================================================
const KeysNum = 222;
#endif
Мы будем хранить массив клавиш _Keys, каждый элемент которого будет хранить имя клавиши, которым мы его будем называть в консоле, и ее действие (команду консоля).
Так же будет три функции:
extinInit - инициализация модуля;
GetKey - возвращает номер клавиши по ее имени;
BindKey - устанавливает клавише новое значение.

Теперь приступим к модулю col_input.h Для начала напишем огромный код, который будет связывать клавиши с их номерами:
#pragma hdrstop
#include "col_input.h"
#include "col_con.h" // Обязательно подгрузить!!!
#pragma package(smart_init)
Key_t _Keys [255]; // Объявим массив клавиш
//==============================================================================
// Сопоставляем все клавиши с их кодами
void extinInit ()
{
   _Keys [  8].Name = "backspace";
   _Keys [ 13].Name = "enter";
   _Keys [ 16].Name = "shift";
   _Keys [ 17].Name = "control";
   _Keys [ 18].Name = "alt";
   _Keys [ 19].Name = "pause";
   _Keys [ 20].Name = "caps";
   _Keys [ 27].Name = "escape";
   _Keys [ 32].Name = "space";
   _Keys [ 33].Name = "pageup";
   _Keys [ 34].Name = "pagedown";
   _Keys [ 35].Name = "end";
   _Keys [ 36].Name = "home";
   _Keys [ 37].Name = "left";
   _Keys [ 38].Name = "up";
   _Keys [ 39].Name = "right";
   _Keys [ 40].Name = "down";
   _Keys [ 45].Name = "insert";
   _Keys [ 46].Name = "delete";
   _Keys [ 48].Name = "0";
   _Keys [ 49].Name = "1";
   _Keys [ 50].Name = "2";
   _Keys [ 51].Name = "3";
   _Keys [ 52].Name = "4";
   _Keys [ 53].Name = "5";
   _Keys [ 54].Name = "6";
   _Keys [ 55].Name = "7";
   _Keys [ 56].Name = "8";
   _Keys [ 57].Name = "9";
   _Keys [ 65].Name = "a";
   _Keys [ 66].Name = "b";
   _Keys [ 67].Name = "c";
   _Keys [ 68].Name = "d";
   _Keys [ 69].Name = "e";
   _Keys [ 70].Name = "f";
   _Keys [ 71].Name = "g";
   _Keys [ 72].Name = "h";
   _Keys [ 73].Name = "i";
   _Keys [ 74].Name = "j";
   _Keys [ 75].Name = "k";
   _Keys [ 76].Name = "l";
   _Keys [ 77].Name = "m";
   _Keys [ 78].Name = "n";
   _Keys [ 79].Name = "o";
   _Keys [ 80].Name = "p";
   _Keys [ 81].Name = "q";
   _Keys [ 82].Name = "r";
   _Keys [ 83].Name = "s";
   _Keys [ 84].Name = "t";
   _Keys [ 85].Name = "u";
   _Keys [ 86].Name = "v";
   _Keys [ 87].Name = "w";
   _Keys [ 88].Name = "x";
   _Keys [ 89].Name = "y";
   _Keys [ 90].Name = "z";
   _Keys [ 91].Name = "l_win";
   _Keys [ 92].Name = "r_win";
   _Keys [ 93].Name = "menu";
   _Keys [ 96].Name = "num_0";
   _Keys [ 97].Name = "num_1";
   _Keys [ 98].Name = "num_2";
   _Keys [ 99].Name = "num_3";
   _Keys [100].Name = "num_4";
   _Keys [101].Name = "num_5";
   _Keys [102].Name = "num_6";
   _Keys [103].Name = "num_7";
   _Keys [104].Name = "num_8";
   _Keys [105].Name = "num_9";
   _Keys [106].Name = "num_*";
   _Keys [107].Name = "num_+";
   _Keys [109].Name = "num_-";
   _Keys [110].Name = "num_del";
   _Keys [111].Name = "num_/";
   _Keys [112].Name = "f1";
   _Keys [113].Name = "f2";
   _Keys [114].Name = "f3";
   _Keys [115].Name = "f4";
   _Keys [116].Name = "f5";
   _Keys [117].Name = "f6";
   _Keys [118].Name = "f7";
   _Keys [119].Name = "f8";
   _Keys [120].Name = "f9";
   _Keys [121].Name = "f10";
   _Keys [122].Name = "f11";
   _Keys [123].Name = "f12";
   _Keys [144].Name = "num";
   _Keys [145].Name = "scroll";
   _Keys [186].Name = ":";
   _Keys [187].Name = "=";
   _Keys [188].Name = ",";
   _Keys [189].Name = "-";
   _Keys [190].Name = ".";
   _Keys [191].Name = "/";
   _Keys [192].Name = "`";
   _Keys [219].Name = "[";
   _Keys [220].Name = "\\"; // Обратная косая черта
   _Keys [221].Name = "]";
   _Keys [222].Name = "'";

   // Зададим самые стандартные клавиши!!!
   _Keys [192].Action = "console";
   _Keys [27].Action = "exit";
   _Keys [112].Action = "help; console";
}
Это простые объявления.
Теперь получение клавиши по имени:
//==============================================================================
// Номер клавиши по названию
int GetKey (AnsiString fName)
{
   for (int i = 0; i < KeysNum; i++)
      if (_Keys[i].Name == AnsiLowerCase (fName))
         return i;
   return -1;
}
Она вернет -1, если не найдет клавишу с таким именем, и номер клавиши в другом случае.

Перезадание клавиши:
//==============================================================================
// Связывание клавиши с действием
void BindKey (AnsiString fKey, AnsiString fAction)
{
   int _TEMP = GetKey (fKey);
   if (_TEMP == -1) AddString ("Unknown key " + fKey, true);
   else
      _Keys[_TEMP].Action = fAction;

}
Надеюсь, в этих функциях все понятно.
Вот и все. Модуль col_input готов.

Сама обработка клавиш будет через форму frmMain:
Добавьте ей обработчик события OnKeyDown (позже мы еще добавим OnKeyUp):
//==============================================================================
// Обрабокта клавиш
void __fastcall TfrmMain::FormKeyDown(TObject *Sender, WORD &Key,
      TShiftState Shift)
{
   TypeString (_Keys[Key].Action);
}
Все просто. Обработать строку, которая соответсвует данной строке.
Это крайне не оптимальный способ обработки клавиш. Не исключено, что мы его сменим.

В завершении добавим в консоль команду bind:
В функцию TypeString1 добавьте после обработки команд exit и quit:
   else if (AnsiLowerCase(_Values[0]) == "bind")
   {
      if (_Values[2] != "") // Если есть и команда, и клавиша
         BindKey (_Values [1], _Values [2] + " " + _Values [3] + " " + _Values [4]);
      else if (_Values[1] != "") // Если есть только клавиша
      { // Вывести ее текущее значение
         int _TEMP = GetKey (_Values[1]);
         if (_TEMP == -1) AddString ("Unknown key " + _Values [1], true);
         else
            AddString (_Keys[_TEMP].Name + "=" + _Keys[_TEMP].Action, true);
      }
      else // А если нет ничего, то просто выдать формат функции
         AddString ("BIND [KEY] [ACTION]", true);
   }
Все, пока с обработкой клавиш хватит. Можете запустить программу. Клавиша тильда (~) должна открывать консоль, если вы его, конечно, закроете.
Кстати, если наш консоль потеряет активность (если, скажем, кликнут по основной форме, когда он активен), то надо бы его вообзе закрывать. Добавьте ему обработчик события OnDeactivete
Hide ();
Вот так.

Приступим наконец к основам DirectX. Напомню, что мы будем использовать модуль NukeDX, который можно скачать с моего сайта (см. в конце).

Создайте новый модуль
File->New->Unit
И сразу сохраните его под именем col_dx
В файле col_dx.h добавьте строки
#define NDX_INC_LIBS
#include "NukeDX.h"
И дайте объявления двум функциям:
bool extdxInit (HWND Handle);
void extdxFree ();
Теперь перйдите к файлу cpp, и подгрузите к нему консоль и основной модуль:
#include "col_main.h"
#include "col_con.h"
ОБРАТИТЕ ВНИМАНИЕ!!!
Заголовочные файлы должны идти в таком порядке:
#include "col_main.h"
#include "col_dx.h"
#include "col_con.h"
Поскольку в модуле col_main подгружается заголовочный файл vcl.h, а он в свою очередь должен быть подгужен до загрузки заголовочных файлов NukeDX.

К сожалению, при работе с DirectX есть небольшая ошибка. В файле dinput.lib не объявлена одна переменная для работы с джойстиком. Мы ее объявим сами, пустой, так как джойстик нам не нужен.
Для начала вам надо несколько библиотек. Откройте папку (C++BUILDER)\Lib\Psdk
И скопируйте оттуда файлы dinput.lib, ddraw.lib и dsound.lib в папку (C++BUILDER)\Lib
Все готово.
Теперь объявите несколько глобальных переменных:
//==============================================================================
// Это нужно для устранения ошибки! В DirectInput почему-то не объявлено :о(
const DIDATAFORMAT c_dfDIJoystick={0,0,0,0,0,0}; // Объявим сами
//==============================================================================
// Глобальные переменные
NDX_Screen *GameScreen;
NDX_Midi *GameMusic;
NDX_Timer GameTimer;
NDX_Sound *GameSound;
//==============================================================================
// Сэмплы (экземпляры звуков)
NDX_Sample smpClick;
NDX_Sample smpWarning;
NDX_Sample smpCrash;
NDX_Sample smpDanger;
NDX_Sample smpAttGun;
NDX_Sample smpAttLaser;
NDX_Sample smpAttChain;
Эти переменные нам пригодятся. С курсом поставляется документация Сашнова Александра по NukeDX, где все описано.
Итак, инициализация:
Здесь есть небольшая поддержка музыки. Создайте в папке с игрой папку Data, скопируйте в нее любой MIDI файл и переименуйте его в Exterminators.mid
//==============================================================================
// *** extdxInit ***
// Инициализация
//  Возвращает true, если все прошло гладко
//             false, в случае каких либо ошибок
// Все ошибки автоматически будут отображениы в консоле
//==============================================================================
bool extdxInit (HWND Handle)
{
   AddString ("Initializatig DirectX...", true);
   AddString ("  Setting Full Screen Mode...", true);
   GameScreen = new NDX_Screen;
   GameScreen->Create ();
   // установка полноэкранного режима 800x600, 16 битовая глубина цвета
   // Можете поставить свое. Но не рекомендую менять размер экрана.
   GameScreen->SetFullScreen (0, Handle, 800, 600, 16);
   // И теперь мы мило забиваем на этот экранчик:о) Он нам был нужен лишь
   // для FullScreena, а потом поверх него появится форма с OpenGL!!!
   AddString ("  Initializating Sound...", true);
   GameSound = new NDX_Sound;
   GameSound->Create (Handle);
   AddString ("  Initializating Music...", true);
   GameMusic = new NDX_Midi;
   GameMusic->Create (Handle);
   GameMusic->Load((fDir + "Data\\Exterminators.mid").c_str());
   GameMusic->Play();
   AddString ("DirectX initializated successfully", false);
   AddString (EXT_SEPARATOR, true);
   return true;
}
Подробно о инициализации можно почитать в документации к NukeDX.
И функция выгрузки:
//==============================================================================
// *** extdxFree ***
// Деинициализация
//  Очищает все за собой
//==============================================================================
void extdxFree ()
{
   delete GameScreen;
   delete GameSound;
}
В модуле col_main добавьте обработчик события OnDestroy и напишите там:
//==============================================================================
// Деинициализация
void __fastcall TfrmMain::FormDestroy(TObject *Sender)
{
   extdxFree ();
}