Программирование игр на ассемблере (Часть 3) — Архив WASM.RU
--> От переводчика...
По вашим многочисленным просьбам продолжаю перевод этой серии туториалов.
И начнем с того, что же такое DirectInput и для чего он нужен?
DirectInput был создан с целью решить проблемы со скоростью ввода. То есть это компонент, который получает данные ввода от пользователя с различных устройств (клавиатура, мышь, джойстик и т.д.) в обход операционной системы.
Итак, основные преимущества:
- Максимальное быстродействие
- Поддержка дополнительных устройств (например, с обратной связью)
- Получение текущего состояния даже без фокуса ввода
- Получения информации из драйвера в обход настроек операционной системы (например, автоповтор)
--> Так, и на чем же мы остановились в прошлой статье?
Если я правильно помню, то в прошлой статье, мы написали код, который выводит загрузочный экран игры. Это значит, что у нас уже есть DirectDraw и Bitmap библиотеки, но это еще не все. А для обработки ввода с клавиатуры, вместо DirectInput, мы использовали сообщение WM_KEYDOWN.
Давайте продолжим. Сначала нам нужно создать DirectInput library. После этого напишем несколько процедур для синхронизации. Затем разработаем код для меню.
--> Direct Input
Вы готовы? Отлично! Код DirectInput имеет почти тот же формат, что и код для DirectDraw.
Давайте рассмотрим код подпрограммы обработки чтения с клавиатуры. (В прилагаемом исходнике также имеется код для обработки мыши, но так как в нашей игре мы ее не используем, то в этих статьях я не буду затрагивать эту тему.)Начнем с... думаю стоит начать с процедуры инициализации DirectInput.
Код (Text):
;######################################################################## ; 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.
Затем вызываем процедуры инициализации клавиатуры и мыши.
А теперь посмотрим на саму процедуру инициализации клавиатуры:
Код (Text):
;######################################################################## ; 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() соответственно.
А теперь мы готовы использовать то, что написали. Давайте теперь посмотрим на саму процедуру чтения клавиатуры:
Код (Text):
;######################################################################## ; 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), то значит клавиша нажата:Код (Text):
.if keyboard_state[DIK_ESCAPE] ; клавиша ESC нажата .else ; клавиша ESC не нажата .endif[от переводчика:]
Обратите внимание, как автор проверяет, нажата ли клавиша. Если значение не равно 0, то считается, что клавиша нажата.
Так, конечно, работать будет, но в официальной документации по DirectX, все же говорится, что мы должны проверять только старший (последний) бит байта, и только если он установлен, то считать, что клавиша нажата.
Таким образом, правильнее было бы написать вот так:Код (Text):
.if keyboard_state[DIK_ESCAPE] & 80h ; клавиша ESC нажата .else ; клавиша ESC не нажата .endifНу ладно, не будем задерживаться на мелочах и пойдем дальше.
На очереди у нас процедура завершения DirectInput:Код (Text):
;######################################################################## ; DI_ShutDown Procedure ;######################################################################## DI_ShutDown PROC ;======================================================= ; Эта процедура завершает DirectInput ;======================================================= ;============================= ; Освобождаем мышь ;============================= DIDEVINVOKE Unacquire, lpdimouse DIDEVINVOKE Release, lpdimouse ;============================= ; Освобождаем клавиатуру ;============================= DIDEVINVOKE Unacquire, lpdikey DIDEVINVOKE Release, lpdikey ;================================== ; Удаляем объект DirectInput ;================================== DIINVOKE Release, lpdi done: ;=================== ; Успешно завершено! <img src="styles/smiles_s/smile3.gif" class="mceSmilie" alt=":smile3:" title="Smile3 :smile3:"> ;=================== 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():
Код (Text):
;######################################################################## ; 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(). Эта функция используется, для запуска таймера с переданным ей значением. Она будет использоваться, для управления скоростью смены кадров.
А вот собственно и сам код...Код (Text):
;######################################################################## ; 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(). И вместе они используются для управления скоростью смены кадров, нашей игры.
А вот и сам код:
Код (Text):
;######################################################################## ; 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(). Ну, и как обычно, вот ее код:
Код (Text):
;######################################################################## ; 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 с нашей системой меню. Все, что нам осталось сделать, это связать меню с кодом таймера.
--> Связываем все вместе
Вот мы почти и закончили. Пришло время связать все, что мы написали и поместить в один пакет. Так что давайте начнем с процедуры инициализации игры.
Код (Text):
;######################################################################## ; 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, и для модуля меню. А еще, мы должны сделать изменения в процедуре главного игрового цикла. Фактически, нам достаточно просто заменить старую процедуру новой, так как, с прошлых уроков она была пустой (помните, мы ее просто зарезервировали, но не использовали?).
А вот и сам новый код процедуры главного игрового цикла:
Код (Text):
;######################################################################## ; 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 ;================================= ; Пользователь ничего пока не выбрал (думает! <img src="styles/smiles_s/smile3.gif" class="mceSmilie" alt=":smile3:" title="Smile3 :smile3:"> ; так что, ничего не делаем. ;================================= .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
Вот теперь все!!! До скорых встреч!!!Счастливого кодирования!!!
Особая благодарность CyberManiac за помощь в оформлении статьи и за исправление ошибок!!!
© Chris Hobbs, пер. UniSoft
Программирование игр на ассемблере (Часть 3)
Дата публикации 22 фев 2003