allasm.ru

    Меню

 

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

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

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

  • Максимальное быстродействие

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

  • Получение текущего состояния даже без фокуса ввода

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

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

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

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

--> Direct Input

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

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

;########################################################################
; DI_Init Procedure
;########################################################################
 DI_Init PROC

	;=======================================================
	; Эта функция установит Direct Input
	;=======================================================

	;=============================
	; Создадим объект  DirectInput
	;=============================
	INVOKE DirectInputCreate, hInst, DIRECTINPUT_VERSION, ADDR lpdi,0

	;=============================
	; ошибки???
	;=============================
	.IF EAX != DI_OK
		JMP	err
	.ENDIF

	;=============================
	; Инициализируем клавиатуру
	;=============================
	INVOKE DI_Init_Keyboard

	;=============================
	; ошибки???
	;=============================
	.IF EAX == FALSE
		JMP	err
	.ENDIF

	;=============================
	; Инициализируем мышь
	;=============================
	INVOKE DI_Init_Mouse

	;=============================
	; ошибки???
	;=============================
	.IF EAX == FALSE
		JMP	err
	.ENDIF

 done:
	;===================
	; Выполнено успешно
	;===================
	return TRUE

 err:
	;===================
	; Вывести сообщение об ошибке
	;===================
	INVOKE MessageBox, hMainWnd, ADDR szNoDI, NULL, MB_OK

	;===================
	; Упс!... Ошибка!
	;===================
	return FALSE

 DI_Init	ENDP
;########################################################################
; END DI_Init
;########################################################################

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

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

  • hInst - Дескриптор экземпляра приложения или DLL.

  • dwVer - версия объекта DirectInput. Использование DIRECTINPUT_VERSION позволяет использовать версию по умолчанию.

  • lplpDI - указатель на указатель интерфейса, который примет в себя информацию.

  • pU - указатель наследования или агрегирования COM. Нас он не интересует - достаточно передать NULL.

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

;########################################################################
; DI_Init_Keyboard	 Procedure
;########################################################################
 DI_Init_Keyboard	 PROC 

	;=======================================================
	; Эта функция инициализирует клавиатуру
	;=======================================================

	;===========================
	; Получение интерфейса устройства 
	;===========================
	DIINVOKE CreateDevice, lpdi, ADDR GUID_SysKeyboard, ADDR lpdikey, 0

	;============================
	; ошибки???
	;============================
	.IF EAX != DI_OK
		JMP	err
	.ENDIF

	;==========================
	; Установим уровень доступа
	;==========================
	DIDEVINVOKE SetCooperativeLevel, lpdikey, hMainWnd, \
	          DISCL_NONEXCLUSIVE OR DISCL_BACKGROUND

	;============================
	; ошибки???
	;============================
	.IF EAX != DI_OK
		JMP	err
	.ENDIF

	;==========================
	; Установим формат данных
	;==========================
	DIDEVINVOKE SetDataFormat, lpdikey, ADDR c_dfDIKeyboard

	;============================
	; ошибки???
	;============================
	.IF EAX != DI_OK
		JMP	err
	.ENDIF

	;===================================
	; Теперь попытаемся захватить устройство
	;===================================
	DIDEVINVOKE Acquire, lpdikey

	;============================
	; ошибки???
	;============================
	.IF EAX != DI_OK
		JMP	err
	.ENDIF

 done:
	;===================
	; Успешное завершение
	;===================
	return TRUE

 err:
	;===================
	; Упс!... Ошибка!!! :(
	;===================
	return FALSE

 DI_Init_Keyboard	ENDP
;########################################################################
; END DI_Init_Keyboard	
;########################################################################

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

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

;########################################################################
; DI_Read_Keyboard	 Procedure
;########################################################################
DI_Read_Keyboard	 PROC 

	;================================================================
	; Эта процедура считает клавиатуру и установит входное состояние
	;================================================================

	;============================
	; Считать, если существует
	;============================
	.IF lpdikey != NULL 
		;========================
		; Теперь считаем состояние
		;========================
		DIDEVINVOKE GetDeviceState, lpdikey, 256, ADDR keyboard_state
		.IF EAX != DI_OK
			JMP	err
		.ENDIF
	.ELSE
		;==============================================
		; клавиатура не включена, обнулить состояние
		;==============================================
		DIINITSTRUCT ADDR keyboard_state, 256
		JMP	err

	.ENDIF

done:
	;===================
	; Все прошло успешно!
	;===================
	return TRUE

err:
	;===================
	; Упс... ошибка!!! :(
	;===================
	return FALSE

DI_Read_Keyboard	ENDP
;########################################################################
; END DI_Read_Keyboard
;########################################################################

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

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

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

	.if keyboard_state[DIK_ESCAPE]
	    ; клавиша ESC нажата
	.else
	    ; клавиша ESC не нажата
	.endif

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

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

;########################################################################
; DI_ShutDown Procedure
;########################################################################
DI_ShutDown PROC

	;=======================================================
	; Эта процедура завершает DirectInput
	;=======================================================

	;=============================
	; Освобождаем мышь
	;=============================
	DIDEVINVOKE Unacquire, lpdimouse
	DIDEVINVOKE Release, lpdimouse

	;=============================
	; Освобождаем клавиатуру
	;=============================
	DIDEVINVOKE Unacquire, lpdikey
	DIDEVINVOKE Release, lpdikey

	;==================================
	; Удаляем объект DirectInput
	;==================================
	DIINVOKE Release, lpdi

done:
	;===================
	; Успешно завершено! :)
	;===================
	return TRUE

err:
	;===================
	; Упс!... Ошибка! :(
	;===================
	return FALSE

DI_ShutDown	ENDP
;########################################################################
; END DI_ShutDown
;########################################################################

Что же собственно делает эта процедура?
По завершению приложения мы должны "убрать за собой" - "уступить" (освободить) все устройства (которые захватили при инициализации), и удалить объекты 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():

;########################################################################
; Init_Time Procedure
;########################################################################
Init_Time	PROC	

	;=======================================================
	; Эта функция определяет, 
	; можем ли мы использовать счетчик производительности.
	; и устанавливает необходимые переменные.
	;=======================================================

	;=============================================
	; извлекаем  частоту счетчика производительности, 
	; если таковой существует.
	;=============================================
	INVOKE QueryPerformanceFrequency, ADDR HPTimerVar

	.IF EAX == FALSE
		;====================
		; не использовать
		;====================
		MOV	UseHP, FALSE
		JMP	done

	.ENDIF

	;========================================
	; Мы можем его использовать, 
	; так что устанавливаем переменные и частоту.
	;========================================
	MOV	UseHP, TRUE
	MOV	EAX, HPTimerVar
	MOV	HPTimerFreq, EAX
	MOV	ECX, 1000
	XOR	EDX, EDX
	DIV	ECX
	MOV	HPTicksPerMS, EAX

done:
	;===================
	; Завершаем процедуру
	;===================
	return TRUE

Init_Time	ENDP
;########################################################################
; END Init_Time
;########################################################################

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

BOOL QueryPerformanceFrequency ( LARGE_INTEGER *lpFrequency );

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

  • Возвращаемое значение:
    В случае, если установленное аппаратное обеспечение поддерживает счетчик производительности, возвращается ненулевое значение (TRUE). В случае, если установленное аппаратное обеспечение не поддерживает счетчик производительности, возвращается нуль (FALSE).

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

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

;########################################################################
; Start_Time Procedure
;########################################################################
Start_Time	PROC	ptr_time_var:DWORD

	;=======================================================
	; Эта функция запускает таймер и сохраняет значение,
	; по адресу переданному ей в качестве параметра.
	;=======================================================

	;========================================
	; Доступен ли нам счетчик производительности?
	;========================================
	.IF UseHP == TRUE
		;==================================
		; Да, доступен.
		;==================================
		INVOKE QueryPerformanceCounter, ADDR HPTimerVar
		MOV	EAX, HPTimerVar
		MOV	EBX, ptr_time_var
		MOV	DWORD PTR [EBX], EAX

	.ELSE
		;==================================
		; Нет. Вместо него используем  timeGetTime.
		;==================================

		;==================================
		; Получаем начальное время.
		;==================================
		INVOKE timeGetTime

		;=================================
		; Устанавливаем переменную
		;=================================
		MOV	EBX, ptr_time_var
		MOV	DWORD PTR [EBX], EAX
	
	.ENDIF

done:
	;===================
	; завершаем
	;===================
	return TRUE

Start_Time	ENDP
;########################################################################
; END Start_Time
;########################################################################

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

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

BOOL QueryPerformanceCounter ( LARGE_INTEGER *lpPerformanceCount );

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

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

DWORD timeGetTime (VOID);

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

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

;########################################################################
; Wait_Time Procedure
;########################################################################
Wait_Time	PROC	time_var:DWORD, time:DWORD

	;=========================================================
	; Эта функция ожидает прохождения определенного (в переменной time)
	;  промежутка времени от начального времени (time_var).
	; Функция возвращает время (в миллисекундах), 
	; затраченное на ее выполнение.
	;=========================================================
	
	;========================================
	; Используем ли мы счетчик производительности
	;========================================
	.IF UseHP == TRUE
		;==================================
		; Да, используем
		;==================================
	
		;==================================
		; Корректируем время для частоты
		;==================================
		MOV	EAX, 1000
		MOV	ECX, time
		XOR	EDX, EDX
		DIV	ECX
		MOV	ECX, EAX
		MOV	EAX, HPTimerFreq
		XOR	EDX, EDX
		DIV	ECX
		MOV	time, EAX

		;================================
		PUSH	EAX

	again1:

		;================================
		POP	EAX

		;======================================
		; Получаем текущее время
		;======================================
		INVOKE QueryPerformanceCounter, ADDR HPTimerVar
		MOV	EAX, HPTimerVar

		;======================================
		; Вычитаем из него начальное время
		;======================================
		MOV	ECX, time_var
		MOV	EBX, time
		SUB	EAX, ECX

		;======================================
		; Сохраняем требуемое количество времени
		;======================================
		PUSH	EAX

		;======================================
		; Прыгаем наверх и выполняем цикл снова, 
		; если значение в eax меньше или равно,
		; чем значение в переменной time
		;======================================
		SUB	EAX, EBX
		JLE	again1

		;========================================
		; Востанавливаем конечное время из стека
		;========================================
		POP	EAX

		;========================================
		; Переводим в миллисекунды (MS)
		;========================================
		MOV	ECX, HPTicksPerMS
		XOR	EDX, EDX
		DIV	ECX

	.ELSE
		;==================================
		; Нет. Счетчик производительности недоступен
		; вместо него используем timeGetTime.
		;==================================

		;==================================
		PUSH	EAX

	again:
		;======================================
		POP	EAX

		;======================================
		; Получаем текущее время
		;======================================
		INVOKE timeGetTime

		;======================================
		; Вычитаем из него начальное время
		;======================================
		MOV	ECX, time_var
		MOV	EBX, time
		SUB	EAX, ECX

		;======================================
		; Сохраняем, требуемое количество времени
		;======================================
		PUSH	EAX

		;======================================
		; Прыгаем наверх и выполняем цикл снова, 
		; если значение в eax меньше или равно,
		; чем значение в переменной time
		;======================================
		SUB	EAX, EBX
		JLE	again
	
		;========================================
		; Вытаскиваем конечное время из стека
		;========================================
		POP	EAX

	.ENDIF

	;=======================
	; Возвращаемся
	;=======================
	RET	

Wait_Time	ENDP
;########################################################################
; END Wait_Time
;########################################################################

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

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

--> Меню

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

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

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

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

;########################################################################
; Process_Main_Menu Procedure
;########################################################################
Process_Main_Menu	PROC	

	;===========================================================
	; Эта процедура обрабатывает главное меню нашей игры
	;===========================================================

	;===================================
	; Блокируем DirectDraw back buffer
	;===================================
	INVOKE DD_Lock_Surface, lpddsback, ADDR lPitch

	;============================
	; Проверяем на ошибки.
	;============================
	.IF EAX == FALSE
		JMP	err
	.ENDIF

	;===================================
	; Рисуем битмап на поверхность
	;===================================
	INVOKE Draw_Bitmap, EAX, ptr_MAIN_MENU, lPitch, screen_bpp

	;===================================
	; Разблокируем back buffer
	;===================================
	INVOKE DD_Unlock_Surface, lpddsback

	;============================
	; Проверяем на ошибки
	;============================
	.IF EAX == FALSE
		JMP	err
	.ENDIF

	;=====================================
	; Все ОК. Так что переключаем поверхности
	; и делаем видимой поверхность, на которой,
	; только что, нарисовали битмап.
	;======================================
	INVOKE DD_Flip

	;============================
	; Проверяем на ошибки
	;============================
	.IF EAX == FALSE
		JMP	err
	.ENDIF

	;========================================================
	; Теперь читаем клавиатуру и проверяем нажаты ли
	; клавиши, соответствующие нашему меню.
	;========================================================
	INVOKE DI_Read_Keyboard

	.IF keyboard_state[DIK_N]
		;======================
		; Новая игра.
		;======================
		return	MENU_NEW

	.ELSEIF keyboard_state[DIK_G]
		;======================
		; Файлы игры.
		;======================
		return MENU_FILES

	.ELSEIF keyboard_state[DIK_R]
		;======================
		; Возврат.
		;======================
		return MENU_GAME

	.ELSEIF keyboard_state[DIK_E]
		;======================
		; Выход.
		;======================
		return MENU_EXIT

	.ENDIF

done:
	;===================
	; Выходим ничего не делая
	;===================
	return MENU_NOTHING

err:
	;===================
	; В процессе выполнения
	; возникли ошибки. :(
	;===================
	return MENU_ERROR

Process_Main_Menu	ENDP
;########################################################################
; END Process_Main_Menu
;########################################################################

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

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

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

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

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

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

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

;########################################################################
; Game_Init Procedure
;########################################################################
Game_Init	PROC

	;=========================================================
	; Процедура инициализации игры
	;=========================================================
	
	;============================================
	; Инициализация Direct Draw -- 640, 480, bpp
	;============================================
	INVOKE DD_Init, 640, 480, screen_bpp

	;============================
	; Проверяем на ошибки
	;============================
	.IF EAX == FALSE
		JMP	err
	.ENDIF

	;============================================
	; Читаем битмап и создаем буфер
	;============================================
	INVOKE Create_From_SFP, ADDR ptr_BMP_LOAD, ADDR szLoading, screen_bpp

	;============================
	; Проверяем на ошибки
	;============================
	.IF EAX == FALSE
		JMP	err
	.ENDIF

	;============================================
	; Блокируем фоновый буфер DirectDraw
	;============================================
	INVOKE DD_Lock_Surface, lpddsback, ADDR lPitch

	;============================
	; Проверяем на ошибки
	;============================
	.IF EAX == FALSE
		JMP	err
	.ENDIF

	;============================================
	; Прорисовываем битмап на поверхности
	;============================================
	INVOKE Draw_Bitmap, EAX, ptr_BMP_LOAD, lPitch, screen_bpp

	;============================================
	; Разблокируем фоновый буфер
	;============================================
	INVOKE DD_Unlock_Surface, lpddsback

	;============================
	; Проверяем на ошибки
	;============================
	.IF EAX == FALSE
		JMP	err
	.ENDIF

	;============================================
	; Все ОК! Так что переключаем поверхности
	; и делаем видимой поверхность, на которой,
	; только что, нарисовали битмап.
	;============================================
	INVOKE DD_Flip

	;============================
	; Проверка на ошибки
	;============================
	.IF EAX == FALSE
		JMP	err
	.ENDIF

	;============================================
	; Инициализация Direct Input
	;============================================
	INVOKE DI_Init

	;============================
	; Проверка на ошибки
	;============================
	.IF EAX == FALSE
		JMP	err
	.ENDIF

	;============================================
	; Инициализация системы синхронизации
	;============================================
	INVOKE Init_Time

	;============================================
	; Инициализация наших меню
	;============================================
	INVOKE Init_Menu

	;============================
	; Проверка на ошибки
	;============================
	.IF EAX == FALSE
		JMP	err
	.ENDIF

	;============================================
	; Устанавливаем режим меню 
	;============================================
	MOV	GameState, GS_MENU

	;============================================
	; Освобождаем память битмапа
	;============================================
	INVOKE GlobalFree, ptr_BMP_LOAD

done:
	;============================
	; Успешное завершение
	;============================
	return TRUE

err:
	;============================
	; В процессе выполнения
	; возникли ошибки :(
	;============================
	return FALSE

Game_Init	ENDP
;########################################################################
; END Game_Init
;########################################################################

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

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

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

;########################################################################
; Game_Main Procedure
;########################################################################
Game_Main	PROC

	;============================================================
	; Это процедура - сердце игры (основа игры), 
	; она получает управление снова и снова,
	; даже если мы обрабатываем сообщение!
	;============================================================

	;=========================================
	; Локальные переменные
	;=========================================
	LOCAL	StartTime	:DWORD

	;====================================
	; Получает стартовое время для цикла
	;====================================
	INVOKE Start_Time, ADDR StartTime

	;==============================================================
	; Выбирает нужное действие(я), исходя из значения переменной GameState
	;==============================================================
	.IF GameState == GS_MENU
		;=================================
		; Мы находимся в режиме главного меню
		;=================================
		INVOKE Process_Main_Menu
		
		;=================================
		; Что хочет сделать пользователь
		;=================================
		.IF EAX == MENU_NOTHING
			;=================================
			; пользователь ничего не выбирал, так что, 
			; соответственно, ничего и не делаем
			;=================================

		.ELSEIF EAX == MENU_ERROR
			;==================================
			; А тут мы получили код ошибки
			;==================================

		.ELSEIF EAX == MENU_NEW
			;==================================
			; Пользователь решил начать новую игру
			;==================================

		.ELSEIF EAX == MENU_FILES
			;==================================
			; Пользователю понадобилось файловое меню
			;==================================
			MOV	GameState, GS_FILE

		.ELSEIF EAX == MENU_GAME
			;==================================
			; Пользователь хочет вернуться
			;==================================

		.ELSEIF EAX == MENU_EXIT
			;==================================
			; пользователь надумал покинуть игру
			;==================================
			MOV	GameState, GS_EXIT

		.ENDIF


	.ELSEIF GameState == GS_FILE
		;=================================
		; Мы находимся в состоянии файлового меню
		;=================================
		INVOKE Process_File_Menu
		
		;=================================
		; И чего же хочет пользователь?
		;=================================
		.IF EAX == MENU_NOTHING
			;=================================
			; Пользователь ничего пока не выбрал (думает! :)
			; так что, ничего не делаем.
			;=================================

		.ELSEIF EAX == MENU_ERROR
			;==================================
			; А тут мы окажемся в случае ошибки
			;==================================

		.ELSEIF EAX == MENU_LOAD
			;==================================
			; Пользователь решил загрузить игру
			;==================================

		.ELSEIF EAX == MENU_SAVE
			;==================================
			; Пользователь захотел сохранить игру
			;==================================


		.ELSEIF EAX == MENU_MAIN
			;==================================
			; Пользователь хочет вернуться
			;==================================
			MOV	GameState, GS_MENU

		.ENDIF


	.ELSEIF GameState == GS_PLAY
		;=================================
		; А здесь мы находимся в режиме игры
		;=================================

	.ELSEIF GameState == GS_DIE
		;=================================
		; мы проиграли :(
		;=================================

	.ENDIF

	;===================================
	; Ожидаем синхронизации времени
	;===================================
	INVOKE Wait_Time, StartTime, sync_time

done:
	;===================
	; Выполнено без ошибок!
	;===================
	return TRUE

err:
	;===================
	; В процессе выполнения
	; возникли ошибки :(
	;===================
	return FALSE

Game_Main	ENDP
;########################################################################
; END Game_Main
;########################################################################

Первое, на что вы должны обратить внимание, это то, что код расположен между вызовами процедур 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

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

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