Урок X1. Улучшенная обработка ввода с использованием DirectInput и Windows

Вы должны использовать самые современные технологии, чтобы конкурировать с такими играми как Quake и Unreal. В этом уроке я научу вас, как подключить и использовать DirectInput и как использовать мышь в OpenGL под Windows. Код этого урока базируется на коде урока 10. Начнем.

 

Мышь

  Первое, что нам понадобиться, это переменная для хранения X и Y позиции мыши.

 

typedef struct tagSECTOR

{

  int numtriangles;

  TRIANGLE* triangle;

} SECTOR;

 

SECTOR sector1;            // Наша модель

 

POINT mpos;                // Позиция мыши (Новое)

 

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);  // Объявление WndProc

 

Отлично, как вы видите, мы добавили новую переменную mpos. Структура POINT состоит из двух переменных – x и y, мы будем использовать их для того, чтобы вычислить вращение сцены. Далее мы изменим, часть функции CreateGLWindow() так, как показано ниже.

 

ShowCursor(FALSE);             // Убрать указатель мыши (Изменено)

if (fullscreen)                // Если полноэкранный режим?

{

  dwExStyle=WS_EX_APPWINDOW;  

  dwStyle=WS_POPUP;

}

  Выше мы переместили вызов ShowCursor(FALSE) так, чтобы курсора мыши не было видно не только в полноэкранном режиме, но и в оконном тоже. Теперь нам нужно получить и установить координаты мыши каждый кадр, поэтому измените функцию WinMain() так как показано ниже:

 

SwapBuffers(hDC);              // Смена буферов (двойная буферизация)

GetCursorPos(&mpos);           // Получить текущую позицию мыши (Новое)

SetCursorPos(320,240);         // Установить мышь в центр окна (Новое)

heading += (float)(320 - mpos.x)/100 * 5;//Обновить направление движения (Новое)

yrot = heading;                // Обновить вращение вокруг оси Y (Новое)

lookupdown -= (float)(240 - mpos.y)/100 * 5;//Обновить вращение вокруг X (Новое)

  Сначала мы получили позицию мыши при помощи функции GetCursorPos(POINT p). Смещение от центра окна даст нам информацию о том, куда и насколько нужно вращать камеру. Затем мы устанавливаем мышь в центр окна для следующего прохода используя SetCursorPos(int X, int Y).

  Замечание: Не устанавливайте позицию мыши в 0,0! Если вы сделаете это, то не сможете обработать перемещение мыши вверх и влево, потому что 0,0 это левый верхний угол окна. 320, 240 – это центр окна для режима 640х480.

  После того как мы позаботились о мыши, нужно изменить часть кода для выполнения перемещения.

 

float = (P - CX) / U * S;

 

P – точка, в которую мы устанавливаем мышь каждый кадр

CX – текущая позиция мыши

U – единицы

S – скорость мыши (будучи истинным квакером я люблю в этом месте значение 12).

  По этой формуле вычисляются значения переменных heading и lookupdown.

  С мышью вроде как разобрались. Идем дальше.

 

Клавиатура (DirectX 7)

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

  Сначала я расскажу, как использовать DirectX7. Первый шаг – настройка компилятора. Я покажу, как сделать это на примере Visual C++, настройка других компиляторов может отличаться от предложенного способа.

  Если у вас еще нет DirectX SDK, то вам придется его заиметь, например, скачать с сайта MicroSoft, и проинсталлировать его.

  После этого, в VisualStudio зайдите в меню Project->Settings. Выберите закладку Link и в строке Object/libraty modules в начало строки добавьте dinput.lib dxguid.lib winmm.lib. Библиотеки DirectInput, DirectX GUID и Windows Multimedia соответственно, последняя необходима для таймера. Возможно, вам также понадобиться войти в меню Tools->Options и на закладке Directories добавить пути (Include files и Library files) к DirectX SDK и переместить их наверх списка.

  Теперь DirectInput готов к использованию, можно начинать программировать!

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

 

#include <stdio.h>             // Заголовочный файл стандартного ввода/вывода

#include <gl\gl.h>             // Заголовочный файл библиотеки OpenGL32

#include <gl\glu.h>            // Заголовочный файл библиотеки GLu32

#include <gl\glaux.h>          // Заголовочный файл библиотеки Glaux

#include <dinput.h>            // DirectInput функции  (Новое)

 

LPDIRECTINPUT7       g_DI;     // DirectInput (Новое)

LPDIRECTINPUTDEVICE7 g_KDIDev; // Устройство клавиатуры (Новое)

 

 

В последних две строчках объявляются  переменные для DirectInput (g_DI) и для устройства клавиатуры (g_KDIDev), последнее будет получать данные и обрабатывать их. Константы DirectInput не сильно отличаются от стандартных констант Windows.

 

Windows       DirectInput

VK_LEFT        DIK_LEFT

VK_RIGHT      DIK_RIGHT

... и так далее

  Основное отличие в замене VK на DIK. Хотя некоторые названия изменили существенно. Все DIK константы объявлены в файле dinput.h.

  Теперь нужно написать функцию инициализации DirectInput’а и устройства клавиатуры. Под CreateGLWindow() добавьте следующее:

 

 

// Инициализация DirectInput (Новое)

int DI_Init()

{

  // Создание DirectInput

  if ( DirectInputCreateEx( hInstance,    // Instance окна

        DIRECTINPUT_VERSION,              // Версия DirectInput

        IID_IDirectInput7,

        (void**)&g_DI,                    // DirectInput

        NULL ) )                          // NULL параметр

  {

    return(false);                        // Не создался DirectInput

  }

 

  // Создание устройства клавиатуры

  if ( g_DI->CreateDeviceEx(  GUID_SysKeyboard, 

               // Какое устройство создается (клавиатура, мышь или джойстик)

        IID_IDirectInputDevice7, 

        (void**)&g_KDIDev,       // Устройство клавиатуры

        NULL ) )                 // NULL параметр

  {

    return(false);               // Не создалось устройство клавиатуры

  }

 

  // Установка формата данных для клавиатуры

  if ( g_KDIDev->SetDataFormat(&c_dfDIKeyboard) )

  {

    return(false);            // Не удалось установить формат данных

    // здесь не хватает функций уничтожения устройства клавиатуры и DirectInput

  }

 

  // Установка уровня кооперации

  if ( g_KDIDev->SetCooperativeLevel(hWnd, DISCL_FOREGROUND | DISCL_EXCLUSIVE) )

  {

    return(false);            // Не удалось установить режим

    // здесь не хватает функций уничтожения устройства клавиатуры и DirectInput

  }

 

  if (g_KDIDev)              // Создано устройство клавиатуры? (лишняя проверка)

    g_KDIDev->Acquire();     // Взять его под контроль

  else                       // если нет

    return(false);           // возвращаем false

 

  return(true);              // все отлично

}

 

// Уничтожение DirectInput

void DX_End()

{

  if (g_DI)

  {

    if (g_KDIDev)

    {

      g_KDIDev->Unacquire();

      g_KDIDev->Release();

      g_KDIDev = NULL;

    }

 

    g_DI->Release();

    g_DI = NULL;

  }

}

  Этот код в достаточной мере снабжен комментариями и должен быть понятен. Первое – мы инициализируем DirectInput и при помощи него создаем устройство клавиатуры, которое затем берем под контроль. Можно также использовать DirectInput для мыши, но на данном этапе средств Windows вполне достаточно.

  Теперь нужно заменить старый код обработки ввода с клавиатуры на новый, использующий DirectInput. Изменить предстоит много, приступим.

 

Удалите следующий код из WndProc():

 

  case WM_KEYDOWN:          // Клавиша была нажата?

  {

    keys[wParam] = TRUE;    // Пометить ее как нажатую

    return 0;               // Вернуться

  }

 

  case WM_KEYUP:            // Клавиши была отпущена?

  {

    keys[wParam] = FALSE;   // Отменить пометку

    return 0;               // Вернуться

  }

 

В начале программы необходимо сделать следующие изменения:

 

BYTE  buffer[256];       // Новый буфер вместо Keys[] (Изменено)

bool  active=TRUE;       // Флаг активности окна

bool  fullscreen=TRUE;   // Флаг полноэкранного режима

bool  blend;             // Смешивание ON/OFF

bool  bp;                // Состояние кнопки смешивания

bool  fp;                // Состояние F1 (Изменено)

 

...

 

GLfloat  lookupdown = 0.0f;

GLfloat  z=0.0f;         // Глубина в экран

GLuint  filter;          // Фильтр (Удалено)

 

GLuint texture[5];       // Для текстур (Изменено)

 

В функции WinMain()

 

  // Создание окна OpenGL

  if (!CreateGLWindow("Justin Eslinger's & NeHe's Advanced DirectInput Tutorial",640,480,16,fullscreen))  // (Изменено)

  {

    return 0;            // Выйти, если не удалось создать окно

  }

 

  if (!DI_Init())        // Инициализация DirectInput (Новое)

  {

    return 0;

  }

 

  ...

 

  // Отрисовка сцены, пока окно активно и не была нажата клавиша Esc

  if ((active && !DrawGLScene())) // (Изменено)

 

      ...

  // Обновить состояние клавиатуры (Новое)

  HRESULT hr = g_KDIDev->GetDeviceState(sizeof(buffer), &buffer); 

  if ( buffer[DIK_ESCAPE] & 0x80 )  // Тест клавиши Escape (Изменено)

  {

    done=TRUE;

  }

 

  if ( buffer[DIK_B] & 0x80)    // Нажата клавиша B? (Изменено)

  {

    if (!bp)

    {

      bp = true;                // Нажата клавиша смешения (Новое)

      blend=!blend;

      if (!blend)

      {

        glDisable(GL_BLEND);

        glEnable(GL_DEPTH_TEST);

      }

      else

      {

        glEnable(GL_BLEND);

        glDisable(GL_DEPTH_TEST);

      }

    }

  }

  else

  {

    bp = false;

  }

 

  if ( buffer[DIK_PRIOR] & 0x80 )   // Page Up? (Изменено)

  {

    z-=0.02f;

  }

 

  if ( buffer[DIK_NEXT] & 0x80 )    // Page Down? (Изменено)

  {

    z+=0.02f;

  }

 

  if ( buffer[DIK_UP] & 0x80 )      // Вверх? (Изменено)

  {

    xpos -= (float)sin(heading*piover180) * 0.05f;

    zpos -= (float)cos(heading*piover180) * 0.05f;

    if (walkbiasangle >= 359.0f)

    {

      walkbiasangle = 0.0f;

    }

    else

    {

      walkbiasangle+= 10;

    }

 

    walkbias = (float)sin(walkbiasangle * piover180)/20.0f;

  }

 

  if ( buffer[DIK_DOWN] & 0x80 )    // Вниз? (Изменено)

  {

    xpos += (float)sin(heading*piover180) * 0.05f;

    zpos += (float)cos(heading*piover180) * 0.05f;

    if (walkbiasangle <= 1.0f)

    {

      walkbiasangle = 359.0f;

    }

    else

    {

      walkbiasangle-= 10;

    }

 

    walkbias = (float)sin(walkbiasangle * piover180)/20.0f;

  }

 

  if ( buffer[DIK_LEFT] & 0x80 )    // Влево? (Изменено)

  {

    xpos += (float)sin((heading - 90)*piover180) * 0.05f;  ( Modified )

    zpos += (float)cos((heading - 90)*piover180) * 0.05f;  ( Modified )

    if (walkbiasangle <= 1.0f)

    {

      walkbiasangle = 359.0f;

    }

    else

    {

      walkbiasangle-= 10;

    }

    walkbias = (float)sin(walkbiasangle * piover180)/20.0f;

  }

 

  if ( buffer[DIK_RIGHT] & 0x80 )    // Вправо? (Изменено)

  {

    xpos += (float)sin((heading + 90)*piover180) * 0.05f;  ( Modified )

    zpos += (float)cos((heading + 90)*piover180) * 0.05f;  ( Modified )

    if (walkbiasangle <= 1.0f)

    {

      walkbiasangle = 359.0f;

    }

    else

    {

      walkbiasangle-= 10;

    }

    walkbias = (float)sin(walkbiasangle * piover180)/20.0f;

  }

 

  if ( buffer[DIK_F1] & 0x80)    // F1 нажато? (Изменено)

  {

    if (!fp)                     // Если не была нажата (Новое)

    {

      fp = true;                 // F1 нажата (Новое)

      KillGLWindow();            // Уничтожить текущее окно (Изменено)

      fullscreen=!fullscreen;    // Переключить режим (Изменено)

 

      // Пересоздать окно

      if (!CreateGLWindow("Justin Eslinger's & NeHe's Advanced Direct Input Tutorial",640,480,16,fullscreen))  (Изменено)

      {

         return 0;       // Выйти, если не удалось создать окно (Изменено)

      }

 

      if (!DI_Init())    // Переинициализировать DirectInput (Новое)

      {

        return 0;        // Выйти, если не удалось

      }

    }

  }

  else

  {

    fp = false;          // F1 отпущена

  }

 

  // Shutdown

  // Выход

  DX_End();              // Уничтожить DirectInput (Новое)

  KillGLWindow();        // Уничтожить окно

  return (msg.wParam);   // Выйти из программы

}

 

Функция DrawGLScene() изменилась следующим образом.

 

  glTranslatef(xtrans, ytrans, ztrans);

  numtriangles = sector1.numtriangles;

 

  // Для каждого треугольника

  for (int loop_m = 0; loop_m < numtriangles; loop_m++)

  {

    glBindTexture(GL_TEXTURE_2D, texture[sector1.triangle[loop_m].texture]);  // (Изменено)

 

    glBegin(GL_TRIANGLES);

 

 

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

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

  Добавим переменную для корректировки и структуру для работы с таймером.

 

POINT  mpos;               

int  adjust = 5;            // Корректировка скорости (Новое)

 

// информация для таймера (Новое)

struct

{

  __int64    frequency;              // Частота

  float    resolution;               // Точность

  unsigned long mm_timer_start;      // Стартовое значение

  unsigned long mm_timer_elapsed;    // Прошедшее время

  bool performance_timer;            // Использовать эффективный таймер

  __int64 performance_timer_start;   // Стартовое значение эффективного таймера

  __int64 performance_timer_elapsed; // Прошедшее время по эффективному таймеру

} timer;

 

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

 

Этот код обсуждался в уроке 21. Рекомендую просмотреть его.

 

// Инициализация таймера (Новое)

void TimerInit(void)

{

  memset(&timer, 0, sizeof(timer));  // Очистить структуру

  // Проверить есть ли возможность использования эффективного таймера

  if (!QueryPerformanceFrequency((LARGE_INTEGER *) &timer.frequency))

  {

    // нет эффективного таймера

    timer.performance_timer = FALSE;     

    timer.mm_timer_start = timeGetTime(); // Использовать timeGetTime()

    timer.resolution = 1.0f/1000.0f;      // Установить точность .001f

    timer.frequency = 1000;               // Установить частоту 1000

    // Установить прошедшее время равным стартовому

    timer.mm_timer_elapsed = timer.mm_timer_start;

  }

  else

  {

    // доступен эффективный таймер

    QueryPerformanceCounter((LARGE_INTEGER *) &timer.performance_timer_start);

    timer.performance_timer = TRUE;       

 

    // Вычисление точности и частоты

    timer.resolution = (float) (((double)1.0f)/((double)timer.frequency));

    // Установить прошедшее время равным стартовому

    timer.performance_timer_elapsed = timer.performance_timer_start;

  }

}

 

// Получить время в миллисекундах (Новое)

float TimerGetTime()

{

  __int64 time;                // Время храниться в 64-битном целом

  if (timer.performance_timer) // Если используется эффективный таймер

  {

    // Получить текущее время по эффективному таймеру

    QueryPerformanceCounter((LARGE_INTEGER *) &time);

    // вернуть текущее время мину стартовое с данной точностью и в миллисекундах

    return ( (float) ( time - timer.performance_timer_start) * timer.resolution)*1000.0f;

  }

  else

  {

    // вернуть текущее время минус стартовое с данной точностью и в миллисекундах

    return( (float) ( timeGetTime() - timer.mm_timer_start) * timer.resolution)*1000.0f;

  }

}

  Напомню, что пояснения кода таймера есть в 21-ом уроке. Убедитесь, что к проекту добавлена библиотека winmm.lib.

  Теперь мы добавим кое-что в функцию WinMain().

 

  if (!DI_Init())           // Инициализация DirectInput  (Новое)

  {

    return 0;

  }

 

  TimerInit();              // Инициализация таймера (Новое)

 

  ...

 

      float start=TimerGetTime();

      // Получить время перед отрисовкой (Новое)

 

      // Отрисовка сцены. Esc – выход.

      // Если окно активно и был выход (Изменено)

      if ((active && !DrawGLScene()))

      {

        done=TRUE;        // ESC DrawGLScene сигнализирует о выходе

      }

      else            // обновить сцену

      {

        // Цикл ожидания для быстрых систем (Новое)

        while(TimerGetTime()<start+float(adjust*2.0f)) {} 

 

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

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

 

Изменения структуры tagTriangle.

 

typedef struct tagTRIANGLE

{

  int  texture; // (Новое)

  VERTEX  vertex[3];

} TRIANGLE;

 

Изменения кода SetupWorld

 

for (int loop = 0; loop < numtriangles; loop++)

{

  readstr(filein,oneline); // (Новое)

  sscanf(oneline, "%i\n", &sector1.triangle[loop].texture);  // (Новое)

  for (int vert = 0; vert < 3; vert++)

  {

 

Изменения кода DrawGLScene

 

  // Для каждого треугольника

  for (int loop_m = 0; loop_m < numtriangles; loop_m++)

  {

    // (Модифицировано)

    glBindTexture(GL_TEXTURE_2D, texture[sector1.triangle[loop_m].texture]);

 

    glBegin(GL_TRIANGLES);

 

В функцию LoadGLTextures добавлена загрузка дополнительных текстур

 

int LoadGLTextures()          // Загрузка картинок и конвертирование их в текстуры

{

  int Status=FALSE;           // Статус

  AUX_RGBImageRec *TextureImage[5];         // Массив текстур

  memset(TextureImage,0,sizeof(void *)*2);  // Инициализация указателей NULL

  if( (TextureImage[0]=LoadBMP("Data/floor1.bmp")) &&  // Загрузка текстуры пола

    (TextureImage[1]=LoadBMP("Data/light1.bmp"))&&     // Загрузка текстуры освещения

    (TextureImage[2]=LoadBMP("Data/rustyblue.bmp"))&&  // Стены

    (TextureImage[3]=LoadBMP("Data/crate.bmp")) &&     // решетки

    (TextureImage[4]=LoadBMP("Data/weirdbrick.bmp")))  // потолок

  {

    Status=TRUE;           

    glGenTextures(5, &texture[0]);        // Создание текстур

    for (int loop1=0; loop1<5; loop1++)  

    {

      glBindTexture(GL_TEXTURE_2D, texture[loop1]);

      glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[loop1]->sizeX,
      TextureImage[loop1]->sizeY, 0,

        GL_RGB, GL_UNSIGNED_BYTE, TextureImage[loop1]->data);

      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);

      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);

    }

    for (loop1=0; loop1<5; loop1++)       

    {

      if (TextureImage[loop1]->data)   

      {

         free(TextureImage[loop1]->data);

      }

      free(TextureImage[loop1]);   

    }

  }

  return Status; // Вернуть статус

}

 

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

© Justin Eslinger (BlackScar)
blackscar@ticz.com
http://members.xoom.com/Blackscar/
 

 25 октября 2002 (c)  Cupper