Программирование игр на ассемблере (Часть 3)

Дата публикации 22 фев 2003

Программирование игр на ассемблере (Часть 3) — Архив WASM.RU

--> От переводчика...

По вашим многочисленным просьбам продолжаю перевод этой серии туториалов.
И начнем с того, что же такое DirectInput и для чего он нужен?

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

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

--> Так, и на чем же мы остановились в прошлой статье?

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

Давайте продолжим. Сначала нам нужно создать DirectInput library. После этого напишем несколько процедур для синхронизации. Затем разработаем код для меню.

--> Direct Input

Вы готовы? Отлично! Код DirectInput имеет почти тот же формат, что и код для DirectDraw.
Давайте рассмотрим код подпрограммы обработки чтения с клавиатуры. (В прилагаемом исходнике также имеется код для обработки мыши, но так как в нашей игре мы ее не используем, то в этих статьях я не буду затрагивать эту тему.)

Начнем с... думаю стоит начать с процедуры инициализации DirectInput.

Код (Text):
  1.  
  2. ;########################################################################
  3. ; DI_Init Procedure
  4. ;########################################################################
  5.  DI_Init PROC
  6.  
  7.     ;=======================================================
  8.     ; Эта функция установит Direct Input
  9.     ;=======================================================
  10.  
  11.     ;=============================
  12.     ; Создадим объект  DirectInput
  13.     ;=============================
  14.     INVOKE DirectInputCreate, hInst, DIRECTINPUT_VERSION, ADDR lpdi,0
  15.  
  16.     ;=============================
  17.     ; ошибки???
  18.     ;=============================
  19.     .IF EAX != DI_OK
  20.         JMP err
  21.     .ENDIF
  22.  
  23.     ;=============================
  24.     ; Инициализируем клавиатуру
  25.     ;=============================
  26.     INVOKE DI_Init_Keyboard
  27.  
  28.     ;=============================
  29.     ; ошибки???
  30.     ;=============================
  31.     .IF EAX == FALSE
  32.         JMP err
  33.     .ENDIF
  34.  
  35.     ;=============================
  36.     ; Инициализируем мышь
  37.     ;=============================
  38.     INVOKE DI_Init_Mouse
  39.  
  40.     ;=============================
  41.     ; ошибки???
  42.     ;=============================
  43.     .IF EAX == FALSE
  44.         JMP err
  45.     .ENDIF
  46.  
  47.  done:
  48.     ;===================
  49.     ; Выполнено успешно
  50.     ;===================
  51.     return TRUE
  52.  
  53.  err:
  54.     ;===================
  55.     ; Вывести сообщение об ошибке
  56.     ;===================
  57.     INVOKE MessageBox, hMainWnd, ADDR szNoDI, NULL, MB_OK
  58.  
  59.     ;===================
  60.     ; Упс!... Ошибка!
  61.     ;===================
  62.     return FALSE
  63.  
  64.  DI_Init    ENDP
  65. ;########################################################################
  66. ; END DI_Init
  67. ;########################################################################
  68.  

Этот код совсем не сложный. И начинается он с создания основного объекта DirectInput, вызовом DirectInputCreate(). Это объект, который используется для получения всех объектов устройств.
Вот ее описание:

DirectInputCreate( HINSTANCE hInst, DWORD dwVer, LPDIRECTINPUT *lplpDI, LPUNKNOWN pU);

  • hInst - Дескриптор экземпляра приложения или DLL.
  • dwVer - версия объекта DirectInput. Использование DIRECTINPUT_VERSION позволяет использовать версию по умолчанию.
  • lplpDI - указатель на указатель интерфейса, который примет в себя информацию.
  • pU - указатель наследования или агрегирования COM. Нас он не интересует - достаточно передать NULL.

Затем вызываем процедуры инициализации клавиатуры и мыши.
А теперь посмотрим на саму процедуру инициализации клавиатуры:

Код (Text):
  1.  
  2. ;########################################################################
  3. ; DI_Init_Keyboard   Procedure
  4. ;########################################################################
  5.  DI_Init_Keyboard    PROC
  6.  
  7.     ;=======================================================
  8.     ; Эта функция инициализирует клавиатуру
  9.     ;=======================================================
  10.  
  11.     ;===========================
  12.     ; Получение интерфейса устройства
  13.     ;===========================
  14.     DIINVOKE CreateDevice, lpdi, ADDR GUID_SysKeyboard, ADDR lpdikey, 0
  15.  
  16.     ;============================
  17.     ; ошибки???
  18.     ;============================
  19.     .IF EAX != DI_OK
  20.         JMP err
  21.     .ENDIF
  22.  
  23.     ;==========================
  24.     ; Установим уровень доступа
  25.     ;==========================
  26.     DIDEVINVOKE SetCooperativeLevel, lpdikey, hMainWnd, \
  27.               DISCL_NONEXCLUSIVE OR DISCL_BACKGROUND
  28.  
  29.     ;============================
  30.     ; ошибки???
  31.     ;============================
  32.     .IF EAX != DI_OK
  33.         JMP err
  34.     .ENDIF
  35.  
  36.     ;==========================
  37.     ; Установим формат данных
  38.     ;==========================
  39.     DIDEVINVOKE SetDataFormat, lpdikey, ADDR c_dfDIKeyboard
  40.  
  41.     ;============================
  42.     ; ошибки???
  43.     ;============================
  44.     .IF EAX != DI_OK
  45.         JMP err
  46.     .ENDIF
  47.  
  48.     ;===================================
  49.     ; Теперь попытаемся захватить устройство
  50.     ;===================================
  51.     DIDEVINVOKE Acquire, lpdikey
  52.  
  53.     ;============================
  54.     ; ошибки???
  55.     ;============================
  56.     .IF EAX != DI_OK
  57.         JMP err
  58.     .ENDIF
  59.  
  60.  done:
  61.     ;===================
  62.     ; Успешное завершение
  63.     ;===================
  64.     return TRUE
  65.  
  66.  err:
  67.     ;===================
  68.     ; Упс!... Ошибка!!! :(
  69.     ;===================
  70.     return FALSE
  71.  
  72.  DI_Init_Keyboard   ENDP
  73. ;########################################################################
  74. ; END DI_Init_Keyboard 
  75. ;########################################################################
  76.  

Первое, что делает эта процедура, это пытается "создать" устройство, от которого мы хотим получать данные (в нашем случае, это клавиатура, но может быть и мышь, и джойстик, и т.д.). Для его создания воспользуемся функцией - CreateDevice():

CreateDevice(REFGUID rg,LPDIRECTINPUTDEVICE *lplpDI,LPUNKNOWN pU);

  • rg - уникальный идентификатор устройства. Для стандартных устройств заранее определены значения: GUID_SysKeyboard для клавиатуры; GUID_SysMouse для мыши. Остальные идентификаторы устройств (например джойстики) можно получить вызовом EnumDevices().
  • lplpDI - указатель на указатель интерфейса, с помощью которого мы будем впоследствии работать с устройством.
  • pU - все то же агрегирование.

[Примечание переводчика]

Обратите внимание: Согласно описанию функции, она содержит 3 параметра, а мы передаем 4 !!! Это связано с тем, что DIINVOKE это макрос, которому первым параметром мы должны передать указатель на интерфейс DirectInput, да, да, тот самый, который мы получили функцией DirectInputCreate().
Более подробно о технологии COM можно почитать здесь... .

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

Уровни доступа:

  • Эксклюзивный (exclusive)/Совместный (non-exclusive) - первое чем хорош эксклюзивный режим, это тем, что он "подавляет" большинство системных комбинаций (кроме Alt+TAB и Alt+Ctrl+Del, кстати говоря - есть и отдельный флажок при установке уровня кооперации запрещающий системные комбинации Windows), позволяя обработать нажатие, скажем, Ctrl+Esc. Следует помнить, что ни одно приложение не сможет получить эксклюзивного доступа к устройству, если такой доступ уже был получен другим приложением, однако сможет получить совместный.
  • Активный (foreground)/Фоновый (background) - при фоновом доступе приложение сможет читать данные с устройства даже когда окно приложения не активно. В случае же активного доступа приложение сможет читать данные, только если его окно обладает "фокусом" (активно).

Уровень доступа устройства устанавливается вызовом SetCooperativeLevel().

HRESULT SetCooperativeLevel ( HWND hwnd, DWORD dwFlags );

  • hwnd - Дескриптор окна связанный с устройством.
  • flags - Флаг совместного доступа:
    • DISCL_BACKGROUND - Приложение использует устройство когда имеет фокус ввода
    • DISCL_FOREGROUND - Приложение может использовать устройство даже когда не активно
    • DISCL_EXCLUSIVE - Монопольный доступ к устройству
    • DISCL_NONEXCLUSIVE - Приложение не требует монопольного доступа

Приложение должно обязательно определить DISCL_FOREGROUND или DISCL_BACKGROUND и DISCL_EXCLUSIVE или DISCL_NONEXCLUSIVE.

После того как установлен уровень кооперации, обязательно нужно установить формат данных. Смысл установки формата данных в том, чтобы указать какие части устройства (например кнопки мыши) будут использоваться. Функция SetDataFormat() получает всего один параметр - указатель на структуру описывающую формат данных для устройства. Для стандартных устройств заранее определены глобальные переменные: c_dfDIKeyboard, c_dfDIMouse, c_dfDIMouse2, c_dfDIJoystick, c_dfDIJoystick2. Итак - формат данных установлен - DirectInput знает тип устройства.

И последнее - как и в случае с DirectDraw, когда приложение могло "потерять" память поверхностей - приложение DirectInput перед чтением данных с устройства должно "захватить"(Acquire) его. При чтении данных обязательно проверьте результат - приложение может "уступить" (Unacquire) устройство, например, при потере фокуса приложением (Кстати, это происходит автоматически, если приложение имеет "активный" доступ к устройству). Для захвата и освобождения устройств существуют функции Acquire() и Unacquire() соответственно.

А теперь мы готовы использовать то, что написали. Давайте теперь посмотрим на саму процедуру чтения клавиатуры:

Код (Text):
  1.  
  2. ;########################################################################
  3. ; DI_Read_Keyboard   Procedure
  4. ;########################################################################
  5. DI_Read_Keyboard     PROC
  6.  
  7.     ;================================================================
  8.     ; Эта процедура считает клавиатуру и установит входное состояние
  9.     ;================================================================
  10.  
  11.     ;============================
  12.     ; Считать, если существует
  13.     ;============================
  14.     .IF lpdikey != NULL
  15.         ;========================
  16.         ; Теперь считаем состояние
  17.         ;========================
  18.         DIDEVINVOKE GetDeviceState, lpdikey, 256, ADDR keyboard_state
  19.         .IF EAX != DI_OK
  20.             JMP err
  21.         .ENDIF
  22.     .ELSE
  23.         ;==============================================
  24.         ; клавиатура не включена, обнулить состояние
  25.         ;==============================================
  26.         DIINITSTRUCT ADDR keyboard_state, 256
  27.         JMP err
  28.  
  29.     .ENDIF
  30.  
  31. done:
  32.     ;===================
  33.     ; Все прошло успешно!
  34.     ;===================
  35.     return TRUE
  36.  
  37. err:
  38.     ;===================
  39.     ; Упс... ошибка!!! :(
  40.     ;===================
  41.     return FALSE
  42.  
  43. DI_Read_Keyboard    ENDP
  44. ;########################################################################
  45. ; END DI_Read_Keyboard
  46. ;########################################################################
  47.  

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

Ну а если, интерфейс устройства получен верно, то получаем данные с устройства (в нашем случае с клавиатуры).

По умолчанию клавиатура использует непосредственные (не буферизированные) данные - для того, чтобы прочитать состояние клавиатуры нужно воспользоваться функцией GetDeviceState(). Параметрами для этой функции в случае непосредственных данных, будет являться размер массива (равный 256) и указатель на массив из 256 байт для данных о всех клавишах. Для удобства работы, для каждого индекса массива, в файле "DInput.inc" определены константы с именами клавиш, например: DIK_J, DIK_N, DIK_ESCAPE, и т.д.
Для определения нажата ли клавиша, достаточно проверить соответствующий ей байт, и если он имеет значение TRUE (или не равен 0), то значит клавиша нажата:

Код (Text):
  1.  
  2.     .if keyboard_state[DIK_ESCAPE]
  3.         ; клавиша ESC нажата
  4.     .else
  5.         ; клавиша ESC не нажата
  6.     .endif

[от переводчика:]

Обратите внимание, как автор проверяет, нажата ли клавиша. Если значение не равно 0, то считается, что клавиша нажата.
Так, конечно, работать будет, но в официальной документации по DirectX, все же говорится, что мы должны проверять только старший (последний) бит байта, и только если он установлен, то считать, что клавиша нажата.
Таким образом, правильнее было бы написать вот так:
Код (Text):
  1.  
  2. .if keyboard_state[DIK_ESCAPE] & 80h
  3.     ; клавиша ESC нажата
  4. .else
  5.     ; клавиша ESC не нажата
  6. .endif

Ну ладно, не будем задерживаться на мелочах и пойдем дальше.
На очереди у нас процедура завершения DirectInput:

Код (Text):
  1.  
  2. ;########################################################################
  3. ; DI_ShutDown Procedure
  4. ;########################################################################
  5. DI_ShutDown PROC
  6.  
  7.     ;=======================================================
  8.     ; Эта процедура завершает DirectInput
  9.     ;=======================================================
  10.  
  11.     ;=============================
  12.     ; Освобождаем мышь
  13.     ;=============================
  14.     DIDEVINVOKE Unacquire, lpdimouse
  15.     DIDEVINVOKE Release, lpdimouse
  16.  
  17.     ;=============================
  18.     ; Освобождаем клавиатуру
  19.     ;=============================
  20.     DIDEVINVOKE Unacquire, lpdikey
  21.     DIDEVINVOKE Release, lpdikey
  22.  
  23.     ;==================================
  24.     ; Удаляем объект DirectInput
  25.     ;==================================
  26.     DIINVOKE Release, lpdi
  27.  
  28. done:
  29.     ;===================
  30.     ; Успешно завершено! <img src="styles/smiles_s/smile3.gif" class="mceSmilie" alt=":smile3:" title="Smile3    :smile3:">
  31.     ;===================
  32.     return TRUE
  33.  
  34. err:
  35.     ;===================
  36.     ; Упс!... Ошибка! :(
  37.     ;===================
  38.     return FALSE
  39.  
  40. DI_ShutDown ENDP
  41. ;########################################################################
  42. ; END DI_ShutDown
  43. ;########################################################################
  44.  

Что же собственно делает эта процедура?
По завершению приложения мы должны "убрать за собой" - "уступить" (освободить) все устройства (которые захватили при инициализации), и удалить объекты DirectInput вызовом Release().

В действительности, DirectInput - одна из самых простых частей DirectX. И мы разобрались, как с ней работать. Теперь нам не обязательно зависеть от "оконной" процедуры и сообщений Windows - мы можем в любом месте программы и в любой промежуток времени прочитать данные с устройства.

Вот и все, теперь у нас есть все, что нам нужно для создания системы меню. Но прежде, чем мы этим займемся, давайте разберемся с синхронизацией.

--> Синхронизация и Виндоуз

Тот, кто только что написал или начал писать свою первую игру обязательно обнаружит, что на компьютерах с разной конфигурацией или частотой процессора его игра будет работать по-разному. Кто-то сгоряча начнет добавлять циклы задержки. Ха! А теперь представьте, как это будет выглядеть на более медленных машинах? Кто-то будет определять или запрашивать частоту процессора... Все это неверно, ведь производительность зависит не только от ЦП. А как же тогда? Ответ прост, да и наверное вы его уже знаете - отталкиваться от пройденного времени.

Для определения времени в Windows можно воспользоваться следующими вызовами:

  • GetTickCount()
    Функция kernel32.dll (ядро) возвращает количество миллисекунд прошедших с момента старта Windows. Возвращаемое значение переполняется приблизительно через 49,71 дней и отсчет начинается снова (это надо учитывать!).

  • timeGetTime
    Функция winmm.dll (мультимедия-API) является копией функции GetTickCount. Возможно регулирование точности возвращаемого значения - снижение точности более 1 миллисекунды.
    [примечание]
    по утверждению автора эта функция более точная и надежная, чем GetTickCount

  • Ну а если нужна большая точность, то можно воспользоваться вызовами QueryPerformanceFrequency и QueryPerformanceCounter, или как их еще называют, счетчиком производительности. Но, к сожалению, он поддерживается не всеми системами, а точнее процессорами. По сути это 64-х битный счетчик тактовых импульсов процессора (по моему он называется TSC), который есть в последних x86-совместимых процессорах (в Celeron и Pentium точно есть). По Микрософтовской документации функция QueryPerformanceCounter считывает в предоставленную Вами 64-битную переменную значение TSC, если он есть, либо 0, если его нет. А если быть немножко поточнее, то функция QueryPerformanceCounter возвращает не количество тактов процессора, а количество тиков, каждый из которых равен примерно 0,838 мкс. Хотя перед тем, как пользоваться этой функцией, нужно с помощью QueryPerformanceFrequency узнать частоту инкремента этого счетчика для данного компьютера (также 64-х битное значение).
    [примечание]
    Автор этой статьи, называет его высокоэффективным таймером (High Performance), отсюда и в названии переменных, связанных с этим таймером, присутствует аббревиатура HP.

Итак, для начала нам потребуется процедура инициализации нашей системы синхронизации - Init_Time():

Код (Text):
  1.  
  2. ;########################################################################
  3. ; Init_Time Procedure
  4. ;########################################################################
  5. Init_Time   PROC   
  6.  
  7.     ;=======================================================
  8.     ; Эта функция определяет,
  9.     ; можем ли мы использовать счетчик производительности.
  10.     ; и устанавливает необходимые переменные.
  11.     ;=======================================================
  12.  
  13.     ;=============================================
  14.     ; извлекаем  частоту счетчика производительности,
  15.     ; если таковой существует.
  16.     ;=============================================
  17.     INVOKE QueryPerformanceFrequency, ADDR HPTimerVar
  18.  
  19.     .IF EAX == FALSE
  20.         ;====================
  21.         ; не использовать
  22.         ;====================
  23.         MOV UseHP, FALSE
  24.         JMP done
  25.  
  26.     .ENDIF
  27.  
  28.     ;========================================
  29.     ; Мы можем его использовать,
  30.     ; так что устанавливаем переменные и частоту.
  31.     ;========================================
  32.     MOV UseHP, TRUE
  33.     MOV EAX, HPTimerVar
  34.     MOV HPTimerFreq, EAX
  35.     MOV ECX, 1000
  36.     XOR EDX, EDX
  37.     DIV ECX
  38.     MOV HPTicksPerMS, EAX
  39.  
  40. done:
  41.     ;===================
  42.     ; Завершаем процедуру
  43.     ;===================
  44.     return TRUE
  45.  
  46. Init_Time   ENDP
  47. ;########################################################################
  48. ; END Init_Time
  49. ;########################################################################
  50.  

Эта процедура определяет, поддерживает ли установленное аппаратное обеспечение счетчик производительности, и в случае успеха, возвращает его частоту. Для чего и используется функция QueryPerformanceFrequency.
Вот ее описание:

BOOL QueryPerformanceFrequency ( LARGE_INTEGER *lpFrequency );

  • lpFrequency - указывает на 64-х битную переменную, значение которой, в отсчетах в секунду, функция устанавливает в текущую частоту счетчика производительности. Если установленное аппаратное обеспечение не поддерживает счетчик производительности, значение этого параметра может быть равно нулю.
  • Возвращаемое значение:
    В случае, если установленное аппаратное обеспечение поддерживает счетчик производительности, возвращается ненулевое значение (TRUE). В случае, если установленное аппаратное обеспечение не поддерживает счетчик производительности, возвращается нуль (FALSE).

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

Следующая функция - Start_Time(). Эта функция используется, для запуска таймера с переданным ей значением. Она будет использоваться, для управления скоростью смены кадров.
А вот собственно и сам код...

Код (Text):
  1.  
  2. ;########################################################################
  3. ; Start_Time Procedure
  4. ;########################################################################
  5. Start_Time  PROC    ptr_time_var:DWORD
  6.  
  7.     ;=======================================================
  8.     ; Эта функция запускает таймер и сохраняет значение,
  9.     ; по адресу переданному ей в качестве параметра.
  10.     ;=======================================================
  11.  
  12.     ;========================================
  13.     ; Доступен ли нам счетчик производительности?
  14.     ;========================================
  15.     .IF UseHP == TRUE
  16.         ;==================================
  17.         ; Да, доступен.
  18.         ;==================================
  19.         INVOKE QueryPerformanceCounter, ADDR HPTimerVar
  20.         MOV EAX, HPTimerVar
  21.         MOV EBX, ptr_time_var
  22.         MOV DWORD PTR [EBX], EAX
  23.  
  24.     .ELSE
  25.         ;==================================
  26.         ; Нет. Вместо него используем  timeGetTime.
  27.         ;==================================
  28.  
  29.         ;==================================
  30.         ; Получаем начальное время.
  31.         ;==================================
  32.         INVOKE timeGetTime
  33.  
  34.         ;=================================
  35.         ; Устанавливаем переменную
  36.         ;=================================
  37.         MOV EBX, ptr_time_var
  38.         MOV DWORD PTR [EBX], EAX
  39.    
  40.     .ENDIF
  41.  
  42. done:
  43.     ;===================
  44.     ; завершаем
  45.     ;===================
  46.     return TRUE
  47.  
  48. Start_Time  ENDP
  49. ;########################################################################
  50. ; END Start_Time
  51. ;########################################################################
  52.  

Этот код очень прост. Если счетчик производительности у нас доступен, то вызываем QueryPerformanceCounter(), а если недоступен, то вместо него вызываем timeGetTime().

Что же делают эти функции?
Функция QueryPerformanceCounter(), вот ее описание:

BOOL QueryPerformanceCounter ( LARGE_INTEGER *lpPerformanceCount );

  • lpPerformanceCount - указывает на 64-х битную переменную, которую функция устанавливает в текущее значение счетчика. Если установленное аппаратное обеспечение не поддерживает счетчик производительности, этот параметр может быть установлен в нуль.

Функция timeGetTime(), вот ее описание:

DWORD timeGetTime (VOID);

  • Функция возвращает текущее системное время в миллисекундах.

И еще одна, последняя процедура Wait_Time(). Это однотипная процедура для Start_Time(). И вместе они используются для управления скоростью смены кадров, нашей игры.
А вот и сам код:

Код (Text):
  1.  
  2. ;########################################################################
  3. ; Wait_Time Procedure
  4. ;########################################################################
  5. Wait_Time   PROC    time_var:DWORD, time:DWORD
  6.  
  7.     ;=========================================================
  8.     ; Эта функция ожидает прохождения определенного (в переменной time)
  9.     ;  промежутка времени от начального времени (time_var).
  10.     ; Функция возвращает время (в миллисекундах),
  11.     ; затраченное на ее выполнение.
  12.     ;=========================================================
  13.    
  14.     ;========================================
  15.     ; Используем ли мы счетчик производительности
  16.     ;========================================
  17.     .IF UseHP == TRUE
  18.         ;==================================
  19.         ; Да, используем
  20.         ;==================================
  21.    
  22.         ;==================================
  23.         ; Корректируем время для частоты
  24.         ;==================================
  25.         MOV EAX, 1000
  26.         MOV ECX, time
  27.         XOR EDX, EDX
  28.         DIV ECX
  29.         MOV ECX, EAX
  30.         MOV EAX, HPTimerFreq
  31.         XOR EDX, EDX
  32.         DIV ECX
  33.         MOV time, EAX
  34.  
  35.         ;================================
  36.         PUSH    EAX
  37.  
  38.     again1:
  39.  
  40.         ;================================
  41.         POP EAX
  42.  
  43.         ;======================================
  44.         ; Получаем текущее время
  45.         ;======================================
  46.         INVOKE QueryPerformanceCounter, ADDR HPTimerVar
  47.         MOV EAX, HPTimerVar
  48.  
  49.         ;======================================
  50.         ; Вычитаем из него начальное время
  51.         ;======================================
  52.         MOV ECX, time_var
  53.         MOV EBX, time
  54.         SUB EAX, ECX
  55.  
  56.         ;======================================
  57.         ; Сохраняем требуемое количество времени
  58.         ;======================================
  59.         PUSH    EAX
  60.  
  61.         ;======================================
  62.         ; Прыгаем наверх и выполняем цикл снова,
  63.         ; если значение в eax меньше или равно,
  64.         ; чем значение в переменной time
  65.         ;======================================
  66.         SUB EAX, EBX
  67.         JLE again1
  68.  
  69.         ;========================================
  70.         ; Востанавливаем конечное время из стека
  71.         ;========================================
  72.         POP EAX
  73.  
  74.         ;========================================
  75.         ; Переводим в миллисекунды (MS)
  76.         ;========================================
  77.         MOV ECX, HPTicksPerMS
  78.         XOR EDX, EDX
  79.         DIV ECX
  80.  
  81.     .ELSE
  82.         ;==================================
  83.         ; Нет. Счетчик производительности недоступен
  84.         ; вместо него используем timeGetTime.
  85.         ;==================================
  86.  
  87.         ;==================================
  88.         PUSH    EAX
  89.  
  90.     again:
  91.         ;======================================
  92.         POP EAX
  93.  
  94.         ;======================================
  95.         ; Получаем текущее время
  96.         ;======================================
  97.         INVOKE timeGetTime
  98.  
  99.         ;======================================
  100.         ; Вычитаем из него начальное время
  101.         ;======================================
  102.         MOV ECX, time_var
  103.         MOV EBX, time
  104.         SUB EAX, ECX
  105.  
  106.         ;======================================
  107.         ; Сохраняем, требуемое количество времени
  108.         ;======================================
  109.         PUSH    EAX
  110.  
  111.         ;======================================
  112.         ; Прыгаем наверх и выполняем цикл снова,
  113.         ; если значение в eax меньше или равно,
  114.         ; чем значение в переменной time
  115.         ;======================================
  116.         SUB EAX, EBX
  117.         JLE again
  118.    
  119.         ;========================================
  120.         ; Вытаскиваем конечное время из стека
  121.         ;========================================
  122.         POP EAX
  123.  
  124.     .ENDIF
  125.  
  126.     ;=======================
  127.     ; Возвращаемся
  128.     ;=======================
  129.     RET
  130.  
  131. Wait_Time   ENDP
  132. ;########################################################################
  133. ; END Wait_Time
  134. ;########################################################################
  135.  

Эта процедура возможно самая сложная из описанных ранее. Что же она делает? Она ожидает прохождения определенного промежутка времени (переданного вторым параметром), от начального времени (переданного первым параметром). Другими словами, например, начальное время у нас было 100мс, и мы указали ожидать 50мс, то процедура не вернет управление, пока текущее время не станет больше или равно 150мс. Процедура возвращает время, затраченное на ее выполнение.

Вот и все о синхронизации, теперь мы можем перейти к нашему меню.

--> Меню

Создание меню в игре, может быть довольно сложным занятием. Но, в нашем случае это будет легко. Основная идея состоит в том, чтобы обеспечить возможность выбора процесса в игре. В большинстве случаев, меню состоит из фонового битмапа и битмапа - "указателя", для выбора пункта меню. Я все же решил сделать это немного по-другому. Вместо "указателя", мы просто назначим соответствующие клавиши каждому пункту меню.

Есть еще одна важная вещь, это выбор типа системы меню. Вы хотите, чтобы код вошел в "цикл меню" и не возвращал управление, пока пользователь не сделает выбор? Или, Вы хотите вызывать функцию меню снова и снова? В случае с Windows, второй тип в миллион раз лучше, так как нам нужно обрабатывать сообщения. Если мы составим программу первым способом, то представьте, что произойдет, если пользователь нажмет "ALT+TAB". Возможно программа повиснет. (ПРИМЕЧАНИЕ: в игре пока нет поддержки "ALT+TAB".... но это будет реализовано в более поздних выпусках!). Так что, будем использовать второй тип системы.

В процедурах инициализации и завершения, нет ничего, чего бы вы не видели ранее. Все, что они делают, это загружают два фоновых битмапа для нашего меню. Один из них для главного меню, а второй для файлового меню. А процедура завершения просто освобождает выделенную под них память.

Интересный код находится в процедурах Process_XXXX_Menu(). Давайте подробно рассмотрим одну из них, процедуру Process_Main_Menu(). Ну, и как обычно, вот ее код:

Код (Text):
  1.  
  2. ;########################################################################
  3. ; Process_Main_Menu Procedure
  4. ;########################################################################
  5. Process_Main_Menu   PROC   
  6.  
  7.     ;===========================================================
  8.     ; Эта процедура обрабатывает главное меню нашей игры
  9.     ;===========================================================
  10.  
  11.     ;===================================
  12.     ; Блокируем DirectDraw back buffer
  13.     ;===================================
  14.     INVOKE DD_Lock_Surface, lpddsback, ADDR lPitch
  15.  
  16.     ;============================
  17.     ; Проверяем на ошибки.
  18.     ;============================
  19.     .IF EAX == FALSE
  20.         JMP err
  21.     .ENDIF
  22.  
  23.     ;===================================
  24.     ; Рисуем битмап на поверхность
  25.     ;===================================
  26.     INVOKE Draw_Bitmap, EAX, ptr_MAIN_MENU, lPitch, screen_bpp
  27.  
  28.     ;===================================
  29.     ; Разблокируем back buffer
  30.     ;===================================
  31.     INVOKE DD_Unlock_Surface, lpddsback
  32.  
  33.     ;============================
  34.     ; Проверяем на ошибки
  35.     ;============================
  36.     .IF EAX == FALSE
  37.         JMP err
  38.     .ENDIF
  39.  
  40.     ;=====================================
  41.     ; Все ОК. Так что переключаем поверхности
  42.     ; и делаем видимой поверхность, на которой,
  43.     ; только что, нарисовали битмап.
  44.     ;======================================
  45.     INVOKE DD_Flip
  46.  
  47.     ;============================
  48.     ; Проверяем на ошибки
  49.     ;============================
  50.     .IF EAX == FALSE
  51.         JMP err
  52.     .ENDIF
  53.  
  54.     ;========================================================
  55.     ; Теперь читаем клавиатуру и проверяем нажаты ли
  56.     ; клавиши, соответствующие нашему меню.
  57.     ;========================================================
  58.     INVOKE DI_Read_Keyboard
  59.  
  60.     .IF keyboard_state[DIK_N]
  61.         ;======================
  62.         ; Новая игра.
  63.         ;======================
  64.         return  MENU_NEW
  65.  
  66.     .ELSEIF keyboard_state[DIK_G]
  67.         ;======================
  68.         ; Файлы игры.
  69.         ;======================
  70.         return MENU_FILES
  71.  
  72.     .ELSEIF keyboard_state[DIK_R]
  73.         ;======================
  74.         ; Возврат.
  75.         ;======================
  76.         return MENU_GAME
  77.  
  78.     .ELSEIF keyboard_state[DIK_E]
  79.         ;======================
  80.         ; Выход.
  81.         ;======================
  82.         return MENU_EXIT
  83.  
  84.     .ENDIF
  85.  
  86. done:
  87.     ;===================
  88.     ; Выходим ничего не делая
  89.     ;===================
  90.     return MENU_NOTHING
  91.  
  92. err:
  93.     ;===================
  94.     ; В процессе выполнения
  95.     ; возникли ошибки. :(
  96.     ;===================
  97.     return MENU_ERROR
  98.  
  99. Process_Main_Menu   ENDP
  100. ;########################################################################
  101. ; END Process_Main_Menu
  102. ;########################################################################
  103.  

Интересная процедура, не так ли? Ну хорошо... возможно и нет. И что же она делает?

Начинается она с блокировки фоновой поверхности (back buffer), и прорисовки на ней битмапа меню. Затем разблокируем фоновую поверхность, и переключаем поверхности, для того, чтобы мы могли ее увидеть.

Вот мы и добрались до вызова одной из наших DirectInput процедур, до вызова процедуры DI_Read_Keyboard(). Эта процедура, как вы помните, получает состояние всех клавиш на клавиатуре. После ее вызова, мы проверяем, были ли нажаты интересующие нас клавиши. Если да, то возвращаем значение соответствующее нажатой клавише. Например, если пользователь нажимает клавишу 'N' для новой игры, то мы возвращаем значение MENU_NEW вызывающей программе. Эти значения известны и определены в начале модуля.

А если же ничего нажато не было, то мы просто возвращаем значение MENU_NOTHING. А если при выполнении кода произошли ошибки, то возвращаем значение MENU_ERROR.

Тот же метод используется и для процедуры Process_File_Menu(). Вот мы и связали код DirectInput с нашей системой меню. Все, что нам осталось сделать, это связать меню с кодом таймера.

--> Связываем все вместе

Вот мы почти и закончили. Пришло время связать все, что мы написали и поместить в один пакет. Так что давайте начнем с процедуры инициализации игры.

Код (Text):
  1.  
  2. ;########################################################################
  3. ; Game_Init Procedure
  4. ;########################################################################
  5. Game_Init   PROC
  6.  
  7.     ;=========================================================
  8.     ; Процедура инициализации игры
  9.     ;=========================================================
  10.    
  11.     ;============================================
  12.     ; Инициализация Direct Draw -- 640, 480, bpp
  13.     ;============================================
  14.     INVOKE DD_Init, 640, 480, screen_bpp
  15.  
  16.     ;============================
  17.     ; Проверяем на ошибки
  18.     ;============================
  19.     .IF EAX == FALSE
  20.         JMP err
  21.     .ENDIF
  22.  
  23.     ;============================================
  24.     ; Читаем битмап и создаем буфер
  25.     ;============================================
  26.     INVOKE Create_From_SFP, ADDR ptr_BMP_LOAD, ADDR szLoading, screen_bpp
  27.  
  28.     ;============================
  29.     ; Проверяем на ошибки
  30.     ;============================
  31.     .IF EAX == FALSE
  32.         JMP err
  33.     .ENDIF
  34.  
  35.     ;============================================
  36.     ; Блокируем фоновый буфер DirectDraw
  37.     ;============================================
  38.     INVOKE DD_Lock_Surface, lpddsback, ADDR lPitch
  39.  
  40.     ;============================
  41.     ; Проверяем на ошибки
  42.     ;============================
  43.     .IF EAX == FALSE
  44.         JMP err
  45.     .ENDIF
  46.  
  47.     ;============================================
  48.     ; Прорисовываем битмап на поверхности
  49.     ;============================================
  50.     INVOKE Draw_Bitmap, EAX, ptr_BMP_LOAD, lPitch, screen_bpp
  51.  
  52.     ;============================================
  53.     ; Разблокируем фоновый буфер
  54.     ;============================================
  55.     INVOKE DD_Unlock_Surface, lpddsback
  56.  
  57.     ;============================
  58.     ; Проверяем на ошибки
  59.     ;============================
  60.     .IF EAX == FALSE
  61.         JMP err
  62.     .ENDIF
  63.  
  64.     ;============================================
  65.     ; Все ОК! Так что переключаем поверхности
  66.     ; и делаем видимой поверхность, на которой,
  67.     ; только что, нарисовали битмап.
  68.     ;============================================
  69.     INVOKE DD_Flip
  70.  
  71.     ;============================
  72.     ; Проверка на ошибки
  73.     ;============================
  74.     .IF EAX == FALSE
  75.         JMP err
  76.     .ENDIF
  77.  
  78.     ;============================================
  79.     ; Инициализация Direct Input
  80.     ;============================================
  81.     INVOKE DI_Init
  82.  
  83.     ;============================
  84.     ; Проверка на ошибки
  85.     ;============================
  86.     .IF EAX == FALSE
  87.         JMP err
  88.     .ENDIF
  89.  
  90.     ;============================================
  91.     ; Инициализация системы синхронизации
  92.     ;============================================
  93.     INVOKE Init_Time
  94.  
  95.     ;============================================
  96.     ; Инициализация наших меню
  97.     ;============================================
  98.     INVOKE Init_Menu
  99.  
  100.     ;============================
  101.     ; Проверка на ошибки
  102.     ;============================
  103.     .IF EAX == FALSE
  104.         JMP err
  105.     .ENDIF
  106.  
  107.     ;============================================
  108.     ; Устанавливаем режим меню
  109.     ;============================================
  110.     MOV GameState, GS_MENU
  111.  
  112.     ;============================================
  113.     ; Освобождаем память битмапа
  114.     ;============================================
  115.     INVOKE GlobalFree, ptr_BMP_LOAD
  116.  
  117. done:
  118.     ;============================
  119.     ; Успешное завершение
  120.     ;============================
  121.     return TRUE
  122.  
  123. err:
  124.     ;============================
  125.     ; В процессе выполнения
  126.     ; возникли ошибки :(
  127.     ;============================
  128.     return FALSE
  129.  
  130. Game_Init   ENDP
  131. ;########################################################################
  132. ; END Game_Init
  133. ;########################################################################
  134.  

Эта процедура претерпела некоторые изменения, с тех пор, как вы ее последний раз видели. Сначала мы добавили несколько вызовов: один для инициализации нашей системы синхронизации, один для инициализации нашей системы меню, и еще один для инициализации нашей DirectInput библиотеки. А также, в конце процедуры, мы освобождаем память, выделенную под битмап загрузочного экрана. Это сделано для того, чтобы не использовать зря память, под битмап, который нам больше не понадобится. И еще одна вещь, на которую стоит обратить внимание, это то, что мы добавили глобальную переменную GameState, которая хранит текущее состояние игры, а также указывает главному игровому циклу, какое состояние обрабатывать. В конце процедуры инициализации игры, мы устанавливаем эту переменную в значение GS_MENU.

Вот собственно и все, что было изменено в процедуре инициализации. А в код завершения были добавлены вызовы процедур завершения для модуля DirectInput, и для модуля меню. А еще, мы должны сделать изменения в процедуре главного игрового цикла. Фактически, нам достаточно просто заменить старую процедуру новой, так как, с прошлых уроков она была пустой (помните, мы ее просто зарезервировали, но не использовали?).

А вот и сам новый код процедуры главного игрового цикла:

Код (Text):
  1. ;########################################################################
  2. ; Game_Main Procedure
  3. ;########################################################################
  4. Game_Main   PROC
  5.  
  6.     ;============================================================
  7.     ; Это процедура - сердце игры (основа игры),
  8.     ; она получает управление снова и снова,
  9.     ; даже если мы обрабатываем сообщение!
  10.     ;============================================================
  11.  
  12.     ;=========================================
  13.     ; Локальные переменные
  14.     ;=========================================
  15.     LOCAL   StartTime   :DWORD
  16.  
  17.     ;====================================
  18.     ; Получает стартовое время для цикла
  19.     ;====================================
  20.     INVOKE Start_Time, ADDR StartTime
  21.  
  22.     ;==============================================================
  23.     ; Выбирает нужное действие(я), исходя из значения переменной GameState
  24.     ;==============================================================
  25.     .IF GameState == GS_MENU
  26.         ;=================================
  27.         ; Мы находимся в режиме главного меню
  28.         ;=================================
  29.         INVOKE Process_Main_Menu
  30.        
  31.         ;=================================
  32.         ; Что хочет сделать пользователь
  33.         ;=================================
  34.         .IF EAX == MENU_NOTHING
  35.             ;=================================
  36.             ; пользователь ничего не выбирал, так что,
  37.             ; соответственно, ничего и не делаем
  38.             ;=================================
  39.  
  40.         .ELSEIF EAX == MENU_ERROR
  41.             ;==================================
  42.             ; А тут мы получили код ошибки
  43.             ;==================================
  44.  
  45.         .ELSEIF EAX == MENU_NEW
  46.             ;==================================
  47.             ; Пользователь решил начать новую игру
  48.             ;==================================
  49.  
  50.         .ELSEIF EAX == MENU_FILES
  51.             ;==================================
  52.             ; Пользователю понадобилось файловое меню
  53.             ;==================================
  54.             MOV GameState, GS_FILE
  55.  
  56.         .ELSEIF EAX == MENU_GAME
  57.             ;==================================
  58.             ; Пользователь хочет вернуться
  59.             ;==================================
  60.  
  61.         .ELSEIF EAX == MENU_EXIT
  62.             ;==================================
  63.             ; пользователь надумал покинуть игру
  64.             ;==================================
  65.             MOV GameState, GS_EXIT
  66.  
  67.         .ENDIF
  68.  
  69.  
  70.     .ELSEIF GameState == GS_FILE
  71.         ;=================================
  72.         ; Мы находимся в состоянии файлового меню
  73.         ;=================================
  74.         INVOKE Process_File_Menu
  75.        
  76.         ;=================================
  77.         ; И чего же хочет пользователь?
  78.         ;=================================
  79.         .IF EAX == MENU_NOTHING
  80.             ;=================================
  81.             ; Пользователь ничего пока не выбрал (думает! <img src="styles/smiles_s/smile3.gif" class="mceSmilie" alt=":smile3:" title="Smile3    :smile3:">
  82.             ; так что, ничего не делаем.
  83.             ;=================================
  84.  
  85.         .ELSEIF EAX == MENU_ERROR
  86.             ;==================================
  87.             ; А тут мы окажемся в случае ошибки
  88.             ;==================================
  89.  
  90.         .ELSEIF EAX == MENU_LOAD
  91.             ;==================================
  92.             ; Пользователь решил загрузить игру
  93.             ;==================================
  94.  
  95.         .ELSEIF EAX == MENU_SAVE
  96.             ;==================================
  97.             ; Пользователь захотел сохранить игру
  98.             ;==================================
  99.  
  100.  
  101.         .ELSEIF EAX == MENU_MAIN
  102.             ;==================================
  103.             ; Пользователь хочет вернуться
  104.             ;==================================
  105.             MOV GameState, GS_MENU
  106.  
  107.         .ENDIF
  108.  
  109.  
  110.     .ELSEIF GameState == GS_PLAY
  111.         ;=================================
  112.         ; А здесь мы находимся в режиме игры
  113.         ;=================================
  114.  
  115.     .ELSEIF GameState == GS_DIE
  116.         ;=================================
  117.         ; мы проиграли :(
  118.         ;=================================
  119.  
  120.     .ENDIF
  121.  
  122.     ;===================================
  123.     ; Ожидаем синхронизации времени
  124.     ;===================================
  125.     INVOKE Wait_Time, StartTime, sync_time
  126.  
  127. done:
  128.     ;===================
  129.     ; Выполнено без ошибок!
  130.     ;===================
  131.     return TRUE
  132.  
  133. err:
  134.     ;===================
  135.     ; В процессе выполнения
  136.     ; возникли ошибки :(
  137.     ;===================
  138.     return FALSE
  139.  
  140. Game_Main   ENDP
  141. ;########################################################################
  142. ; END Game_Main
  143. ;########################################################################
  144.  

Первое, на что вы должны обратить внимание, это то, что код расположен между вызовами процедур Start_Time() - наверху, и Wait_Time() - внизу. Эти процедуры управляют скоростью кадров в нашей игре. Я сделал так, чтобы было 25 FPS (т.е. 25 кадров в секунду), или 40 миллисекунд.

Далее мы имеем одну большую конструкцию IF-ELSE, которая выбирает нужные действия, исходя из значения глобальной переменной GameState, которую мы специально и определили для этой цели.

Новый игровой цикл, это просто менеджер состояний. Он выполняет соответствующий текущему состоянию код. Все игры имеют нечто похожее в их ядре.

--> До следующего раза ...

Ура! Вот у нас и готов хороший, чистый каркас для игры, в котором фактически не хватает самого игрового кода. Но, для этого вам придется ждать следующей статьи.

--> В следующей статье...

В следующей статье мы охватим еще некоторый расширенный материал. Также сделаем упор непосредственно на сам игровой код, включая: основу анимации, "цикл", и, ... как обычно, ... что-нибудь еще, что придет в голову.

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

--> Готовый исходник для этой статьи, можно взять здесь...

А для его компиляции вам также понадобятся LIB'ы DirectX, которые можно найти в DirectX SDK, либо скачать здесь... или здесь....

[от переводчика]
P.S.
Вот и все, наконец-то я выкроил время и перевел 3-ю часть (а всего их 6 !!!) данного туториала, и в будущем, планирую перевести остальные 3. Я также, хотел бы поблагодарить всех, кто откликнулся на прошлые статьи.

P.S.S. В интернете очень много статей о том, как писать вирусы, и почти совсем нет информации о том, как создавать красивые игры, демо-сцены (демки), заставки и т.д., как реализовать некоторые алгоритмы, например: вода (Java апплет - Lake помните такой? А как такое же сделать на ассемблере под windows? Никто не задумывался?), как реализовать jpeg/mpeg/mp3 декодер, как реализовать огонь и многое другое (имеется в виду под windows и на чистом ассемблере)? Нет, есть конечно некоторые исходники реализации плазмы, огня и т.д., но ведь в них почти нет никаких комментариев, и начинающему демо-мэйкеру разобраться в огромных листингах кода очень тяжело.
Так почему же такой информации очень и очень мало, по сравнению с информацией о создании вирусов??? Да потому, что вирусы гораздо легче писать, правда я не понимаю для чего? Неужели вам станет легче, если кто-то пострадает от вашего вируса??? У всех вирусов почти один и тот же алгоритм: открыть файл, дописать в него тело вируса, закрыть файл, ну и возможно выполнить какие-нибудь деструктивные действия (типа убить инфу на винте), или стырить чей-нибудь пароль к интернету и т.д. А вот чтобы реализовать какой-нибудь видеоэффект, то тут надо сильно постараться, возможно даже подучить (или вспомнить) математику, геометрию, физику... к тому же, это намного интереснее! Это надо как-то исправлять!!! Надо учить начинающих ассемблерщиков не созданию вирусов, а созданию чего-нибудь более стоящего. Ох, что-то меня на философию потянуло... надо завязывать. И к чему я все это рассказываю? Ах да, если у вас есть свои разработки, свои программы, свои игры, которыми вы могли бы поделиться с людьми, то присылайте их мне (мой e-mail ниже), желательно с подробным (или хотя бы с кратким) описанием.
А также по всем вопросам (для вопросов относящихся к программированию есть форум!), предложениям, пожеланиям и т.д. пишите:
e-mail: unis2@mail.ru,
Andrey aka UniSoft

Вот теперь все!!! До скорых встреч!!!

Счастливого кодирования!!!

Особая благодарность CyberManiac за помощь в оформлении статьи и за исправление ошибок!!!


© Chris Hobbs, пер. UniSoft

0 1.284
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532