Программирование игр на ассемблере (Часть 2) — Архив WASM.RU
- На чем мы остановились?
- Синтаксис MASM
- Основной игровой цикл
- Связь с Direct Draw
- Наша Direct Draw Library
- Наша Bitmap Library
- A Game ...
- В следующей статье...
В прошлой статье были рассмотрены основы Win32 ASM программирования, основы создания игр, и сам процесс разработки. Пришло время зайти немного дальше. Сначала я расскажу о высокоуровневых конструкциях MASM, которые являются удобоваримыми в сравнительном смысле с аналогичными конструкциями на Си. Затем рассмотрим основной цикл и главные оконные процедуры. После чего обратим внимание на Direct Draw и вызовы связанные с ним. Поняв, как это работает, мы сможем построить свою собственную Direct Draw Library, после чего построим свою bitmap file library и в конце напишем программу, которая отображает экран 'Loading Game' и выходит из нее по нажатию клавиши Esc.
Для компиляции вам потребуется пакет MASM32, или по крайней мере MASM 6.11+.[Можно взять здесь http://wasm.ru/tools/7/masm32v7.zip и здесь http://wasm.ru/tools/7/masm615.zip- прим. ред.]
--> Синтаксис MASM [рекомендуем обратиться к источникам на http://www.wasm.ru, в частности, посмотреть туториалы Iczelion'a и др.- прим. ред.]
Досовские варианты ассемблерных листингов представляют в своем большинстве собрание весьма неудобоваримых сочинений, порой непонятных даже квалифицированным программистам. Очень много меток, jmp-ов и прочей нечисти. Но asm не стоит на месте, и в MASM 6.0 макро-конструкции становятся неотъемлемым инструментом разработки.
MASM ныне - такой же легкий в чтении язык, что и Си. Это, конечно, только мое мнение. Давайте теперь рассмотрим некоторые Си-шные конструкции и их аналоги в MASM.
- IF - ELSE IF - ELSE
The C version: if ( var1 == var2 ) { // Code goes here } else if ( var1 == var3 ) { // Code goes here } else { // Code goes here }The MASM version: .if ( var1 == var2 ) ; Code goes here .elseif ( var1 == var3 ) ; Code goes here .else ; Code goes here .endif- DO - WHILE
The C version: do { // Code goes here } while ( var1 == var2 );The MASM version: .repeat ; Code goes here .until ( var1 != var2 )- WHILE
The C version: while ( var1 == var2 ) { // Code goes here }The MASM version: .while ( var1 == var2 ) ; Code goes here .endwЭто все - примеры рабочих конструкций, и как вы видите они чрезвычайно просты. При компиляции, MASM скомпилирует в коде, все те же нужные метки, jmp-ы, cmp-конструкции.
Есть еще и другие вещи, которые нам следует обсудить, это псевдо-операторы которые позволяют нам легко определить процедуры/функции, такие как PROTO и PROC. Использовать их очень легко. Для начала, также как и в Си, вам нужно иметь прототип. В MASM это делается с помощью ключевого слова PROTO. Вот несколько примеров объявления прототипов для ваших функций:
Код (Text):
;================================== ; Main Program Procedures ;================================== WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD WndProc PROTO :DWORD,:DWORD,:DWORD,:DWORDВышеупомянутый код сообщает ассемблеру, о двух новых процедурах WinMain и WndProc. Каждая из них имеет список параметров связанных с ними. В каждой функции задаются 4 параметра (DWORD). Для тех кто использует пакет MASM32, то в нем уже есть прототипы всех Windows API функций, вам просто нужно подключить соответствующий файл. Но вам надо убедиться, что все пользовательские процедуры определены вышеупомянутым способом.
Породив прототип, мы можем породить и саму функцию с помощью ключевого слова PROC:
Код (Text):
;######################################################################## ; WinMain Function ;######################################################################## WinMain PROC hInstance :DWORD, hPrevInst :DWORD, CmdLine :DWORD, CmdShow :DWORD ;=========================== ; We are through ;=========================== return msg.wParam WinMain endp ;######################################################################## ; End of WinMain Procedure ;########################################################################При такой записи имеется доступ ко всем параметрам функции. Не правда ли - слишком просто для Winmain?
Теперь, когда мы все знаем, как писать на ассемблере, давайте приступим к написанию основного игрового цикла.
Начнем с WinMain().
Код (Text):
.CODE start: ;================================== ; Получим экземпляр ; приложения ;================================== INVOKE GetModuleHandle, NULL MOV hInst, EAX ;================================== ; Как насчет командной строки? ;================================== INVOKE GetCommandLine MOV CommandLine, EAX ;================================== ; Вызов WinMain ;================================== INVOKE WinMain,hInst,NULL,CommandLine,SW_SHOWDEFAULT ;================================== ; Выход ;================================== INVOKE ExitProcess,EAXЕдинственное, что здесь может оказаться немного странным [ можно подумать, что данный случай - исключение, а не правило. Это не странно, а естественно - прим. ред.], так это пересылка регистра EAX в переменную (MOV ...,EAX) в конце INVOKE. Причина в том, что все функции Windows (и Си функции в том числе), возвращают значение результата функции/процедуры в регистре EAX. Этот код вы можете использовать во всех программах, которые будете писать, по крайней мере, мне никогда не приходилось его изменять.
А теперь сам код:
Код (Text):
;######################################################################## ; WinMain Function ;######################################################################## WinMain PROC hInstance :DWORD, hPrevInst :DWORD, CmdLine :DWORD, CmdShow :DWORD ;========================= ; локальные переменные (размещаются в стеке) ;========================= LOCAL wc :WNDCLASS ;================================================== ; Заполнение структуры WNDCLASS требуемыми переменными ;================================================== MOV wc.style, CS_OWNDC MOV wc.lpfnWndProc,OFFSET WndProc MOV wc.cbClsExtra,NULL MOV wc.cbWndExtra,NULL m2m wc.hInstance,hInst ;<< замечание: это макрос INVOKE GetStockObject, BLACK_BRUSH MOV wc.hbrBackground, EAX MOV wc.lpszMenuName,NULL MOV wc.lpszClassName,OFFSET szClassName INVOKE LoadIcon, hInst, IDI_ICON ; icon ID MOV wc.hIcon,EAX INVOKE LoadCursor,NULL,IDC_ARROW MOV wc.hCursor,EAX ;================================ ; Регистрация класса, который мы создали ;================================ INVOKE RegisterClass, ADDR wc ;=========================================== ; Создание главного экрана ;=========================================== INVOKE CreateWindowEx,NULL, ADDR szClassName, ADDR szDisplayName, WS_POPUP OR WS_CLIPSIBLINGS OR \ WS_MAXIMIZE OR WS_CLIPCHILDREN, 0,0,640,480, NULL,NULL, hInst,NULL ;=========================================== ; сохранить указатель на окно (хэндл) ;=========================================== MOV hMainWnd, EAX ;==================================== ; Скрыть курсор ;==================================== INVOKE ShowCursor, FALSE ;=========================================== ; Вывод на экран нашего окна, которое мы создали ;=========================================== INVOKE ShowWindow, hMainWnd, SW_SHOWDEFAULT ;================================= ; Инициализация игры ;================================= INVOKE Game_Init ;======================================== ; проверка на ошибки и выход в случае чего ;======================================== .IF EAX != TRUE JMP shutdown .ENDIF ;=================================== ; Цикл, пока не будет послано PostQuitMessage ;=================================== .WHILE TRUE INVOKE PeekMessage, ADDR msg, NULL, 0, 0, PM_REMOVE .IF (EAX != 0) ;=================================== ; Выход из цикла ;=================================== MOV EAX, msg.message .IF EAX == WM_QUIT ;====================== ; Выход ;====================== JMP shutdown .ENDIF ;=================================== ; Translate and Dispatch the message ;=================================== INVOKE TranslateMessage, ADDR msg INVOKE DispatchMessage, ADDR msg .ENDIF ;================================ ; вызов главного игрового цикла ;================================ INVOKE Game_Main .ENDW shutdown: ;================================= ; Завершение игры ;================================= INVOKE Game_Shutdown ;================================= ; Показать курсор ;================================= INVOKE ShowCursor, TRUE getout: ;=========================== ; Завершение ;=========================== return msg.wParam WinMain endp ;######################################################################## ; End of WinMain Procedure ;########################################################################Давайте проанализируем. Обратите внимание: при инициализации локальной переменной (в нашем случае это структура WNDCLASS), в начале функции не нужно никакой возни со стеком push/pop, также как и в ее конце, за вас все сделает компилятор. Вы должны только объявить локальные переменные, как в Си. Далее, заполняем структуру значениями. Обратите внимание на использование макроса m2m. Это потому, что ASM не позволяет напрямую копировать из памяти в память, без использования регистра или стека в качестве посредника.
Далее, создаем окно и прячем курсор, так как он нам в игре не нужен. Показываем окно и вызываем Game_Init(). Если внутри процедуры Game_Init() произошла ошибка, то ее результат будет FALSE. Проверяем результат процедуры Game_Init() и в случае ошибки выходим из программы (прыгаем на метку shutdown). Вообще, при отладке asm-процедур всегда нужно помнить, что должна быть одна точка входа и одна точка выхода.
Дальше идет цикл сообщений (message loop), которые могут поступать откуда угодно. Если бы это была обычная программа, а не игра, то мы бы использовали GetMessage() для приема сообщений из очереди. Но здесь есть одна проблема, если нет никаких сообщений, то функция будет ждать, пока придет какое-нибудь сообщение. Это совершенно не подходит для игры. Нам нужно постоянно выполнять главный игровой цикл, независимо от того, придут ли какие-либо сообщения или нет. Есть один путь обойти это - использовать PeekMessage(). PeekMessage() возвращает ноль, если нет никаких сообщений, иначе вернет сообщение из очереди.
Обратите внимание, что главный игровой цикл (Game_Main), будет вызываться всегда, независимо от того, пришло ли какое-либо сообщение или нет. Если бы мы этого не сделали, то Windows мог бы обработать кучу сообщений, в то время, как главный игровой цикл - ни разу.
И в конце, когда мы получаем сообщение quit, мы выходим из цикла и соответственно завершаем программу.
Мы не будем рассматривать сам DirectX на уровне асм-а, а рассмотрим его на уровне основных концепций.
Прежде всего, необходимо понять саму концепцию Таблицы Виртуальных Функций. Делается запрос в нее в форме смещения, и получается АДРЕС расположения функции. Т.е. под вызовом функции понимается обращение к таблице, которая УЖЕ существует. Адреса функций имеются в DirectX-библиотеке.
Далее, необходимо определить адрес объекта, для которого вызывается функция. Вычисляем виртуальный адрес и сохраняем в стеке все параметры. Для этой цели существуют различные макросы, в частности, DD4INVOKE еще из 4-го DirextX.
Сначала определяем имя функции, затем имя объекта и параметры:
Код (Text):
;======================================== ; Создадим primary surface ;======================================== DD4INVOKE CreateSurface, lpdd, ADDR ddsd, ADDR lpddsprimary, NULLВ этом примере создается поверхность, вызовом функции CreateSurface(), передавая ей в качестве параметров: указатель на объект, адрес структуры Direct Draw Surface Describe (ddsd), адрес переменной для хранения указателя на поверхность, и наконец NULL. Теперь, когда мы увидели, как делать запросы к DirectX, давайте построим небольшую библиотеку.
Нам понадобятся функции для инициализации и выхода из игры, для определения формата пикселя, создания и прорисовки поверхностей, и загрузки в нее битмапа.
Далее код функции инициализации:
Код (Text):
;######################################################################## ; DD_Init Procedure ;######################################################################## DD_Init PROC screen_width:DWORD, screen_height:DWORD, screen_bpp:DWORD ;======================================================= ; Устанавливаем полноэкранный режим ;======================================================= ;================================= ; Локальные переменные ;================================= LOCAL lpdd_1 :LPDIRECTDRAW ;============================= ; Создаем объект ;============================= INVOKE DirectDrawCreate, 0, ADDR lpdd_1, 0 ;============================= ; Обработка ошибок ;============================= .IF EAX != DD_OK ;====================== ; Ошибка ;====================== INVOKE MessageBox, hMainWnd, ADDR szNoDD, NULL, MB_OK ;====================== ; Выход ;====================== JMP err .ENDIF ;========================================= ; Получим DirectDraw 4 object ;========================================= DDINVOKE QueryInterface, lpdd_1, ADDR IID_IDirectDraw4, ADDR lpdd ;========================================= ; Получили ??? ;========================================= .IF EAX != DD_OK ;============================== ; Нет ;============================== INVOKE MessageBox, hMainWnd, ADDR szNoDD4, NULL, MB_OK ;====================== ; Выход ;====================== JMP err .ENDIF ;=================================================== ; Установка cooperative level ;=================================================== DD4INVOKE SetCooperativeLevel, lpdd, hMainWnd, \ DDSCL_ALLOWMODEX OR DDSCL_FULLSCREEN OR \ DDSCL_EXCLUSIVE OR DDSCL_ALLOWREBOOT ;========================================= ; Получили ??? ;========================================= .IF EAX != DD_OK ;============================== ; Нет ;============================== INVOKE MessageBox, hMainWnd, ADDR szNoCoop, NULL, MB_OK ;====================== ; Выход ;====================== JMP err .ENDIF ;=================================================== ; Установка Display Mode ;=================================================== DD4INVOKE SetDisplayMode, lpdd, screen_width, \ screen_height, screen_bpp, 0, 0 ;========================================= ; Установили ??? ;========================================= .IF EAX != DD_OK ;============================== ; Нет ;============================== INVOKE MessageBox, hMainWnd, ADDR szNoDisplay, NULL, MB_OK ;====================== ; Выход ;====================== JMP err .ENDIF ;================================ ; screen info ;================================ m2m app_width, screen_width m2m app_height, screen_height m2m app_bpp, screen_bpp ;======================================== ; Зададим параметры для поверхности (surface) ;======================================== DDINITSTRUCT OFFSET ddsd, SIZEOF(DDSURFACEDESC2) MOV ddsd.dwSize, SIZEOF(DDSURFACEDESC2) MOV ddsd.dwFlags, DDSD_CAPS OR DDSD_BACKBUFFERCOUNT; MOV ddsd.ddsCaps.dwCaps, DDSCAPS_PRIMARYSURFACE OR \ DDSCAPS_FLIP OR DDSCAPS_COMPLEX MOV ddsd.dwBackBufferCount, 1 ;======================================== ; Создадим первичную поверхность (primary surface) ;======================================== DD4INVOKE CreateSurface, lpdd, ADDR ddsd, ADDR lpddsprimary, NULL ;========================================= ; Создали ??? ;========================================= .IF EAX != DD_OK ;============================== ; Нет ;============================== INVOKE MessageBox, hMainWnd, ADDR szNoPrimary, NULL, MB_OK ;====================== ; Выход ;====================== JMP err .ENDIF ;========================================== ; Попробуем получить backbuffer ;========================================== MOV ddscaps.dwCaps, DDSCAPS_BACKBUFFER DDS4INVOKE GetAttachedSurface, lpddsprimary, ADDR ddscaps, ADDR lpddsback ;========================================= ; Получили ??? ;========================================= .IF EAX != DD_OK ;============================== ; Нет ;============================== INVOKE MessageBox, hMainWnd, ADDR szNoBackBuffer, NULL, MB_OK ;====================== ; Выход ;====================== JMP err .ENDIF ;========================================== ; Получим RGB format для surface ;========================================== INVOKE DD_Get_RGB_Format, lpddsprimary done: ;=================== ; Все Ок! <img src="styles/smiles_s/smile3.gif" class="mceSmilie" alt=":smile3:" title="Smile3 :smile3:"> ;=================== return TRUE err: ;=================== ; Ничего не Ок! :( ;=================== return FALSE DD_Init ENDP ;######################################################################## ; END DD_Init ;########################################################################Рассмотрим подробнее.
Сначала создаем так называемый default Direct Draw object с помощью функции DirectDrawCreate(). Это не более, чем простой вызов функции с несколькими параметрами. Это еще не виртуальная функция. Поэтому мы можем вызывать ее с помощью INVOKE. Также, обратите внимание, что мы потом проверяем на ошибку. Это очень важно в DirectX!!! В случае ошибки, мы просто выводим сообщение, и переходим на метку err: в конце процедуры.
Далее делаем запрос на получение DirectDraw4 object. После чего устанавливаем режимы экрана с помощью SetCooperativeLevel() и SetDisplayMode(). Не забывайте проверять на ошибки, после каждого вызова функций.
На следующем шаге создаем первичную поверхность (primary surface), и в случае успеха создаем back buffer. При этом полученную структуру надо очистить с помощью макроса DDINITSTRUCT, который я включил в файл Ddraw.inc.
После, вызывается процедура для определения формата пикселя для нашей поверхности.
В следующей процедуре мы получаем формат пикселя:
Код (Text):
;######################################################################## ; DD_Get_RGB_Format Procedure ;######################################################################## DD_Get_RGB_Format PROC surface:DWORD ;========================================================= ; Установим несколько глобальных переменных ;========================================================= ;==================================== ; Локальные переменные ;==================================== LOCAL shiftcount :BYTE ;================================ ; получим surface despriction ;================================ DDINITSTRUCT ADDR ddsd, sizeof(DDSURFACEDESC2) MOV ddsd.dwSize, sizeof(DDSURFACEDESC2) MOV ddsd.dwFlags, DDSD_PIXELFORMAT DDS4INVOKE GetSurfaceDesc, surface, ADDR ddsd ;============================== ; маски ;============================== m2m mRed, ddsd.ddpfPixelFormat.dwRBitMask ; Red Mask m2m mGreen, ddsd.ddpfPixelFormat.dwGBitMask ; Green Mask m2m mBlue, ddsd.ddpfPixelFormat.dwBBitMask ; Blue Mask ;==================================== ; определим red mask ;==================================== MOV shiftcount, 0 .WHILE (!(ddsd.ddpfPixelFormat.dwRBitMask & 1)) SHR ddsd.ddpfPixelFormat.dwRBitMask, 1 INC shiftcount .ENDW MOV AL, shiftcount MOV pRed, AL ;======================================= ; определим green mask ;======================================= MOV shiftcount, 0 .WHILE (!(ddsd.ddpfPixelFormat.dwGBitMask & 1)) SHR ddsd.ddpfPixelFormat.dwGBitMask, 1 INC shiftcount .ENDW MOV AL, shiftcount MOV pGreen, AL ;======================================= ; определим blue mask ;======================================= MOV shiftcount, 0 .WHILE (!(ddsd.ddpfPixelFormat.dwBBitMask & 1)) SHR ddsd.ddpfPixelFormat.dwBBitMask, 1 INC shiftcount .ENDW MOV AL, shiftcount MOV pBlue, AL ;=========================================== ; определим специальную переменную для 16 bit mode ;=========================================== .IF app_bpp == 16 .IF pRed == 10 MOV Is_555, TRUE .ELSE MOV Is_555, FALSE .ENDIF .ENDIF done: ;=================== ; все ;=================== return TRUE DD_Get_RGB_Format ENDP ;######################################################################## ; END DD_Get_RGB_Format ;########################################################################Сначала, инициализируем структуру description, а затем делаем вызов из Direct Draw для получения surface description. Возвращенные маски мы разместим в глобальных переменных для их дальнейшего использования. Маска это значение, которое мы можем использовать для установки или очистки некоторых бит в переменной или регистре. В нашем случае маски используются для того, чтобы получить доступ к red, green, и blue - битам пикселя.
Следующие три секции кода используется для определения числа битов для каждой цветовой компоненты. Например, если нам нужен цветовой режим 24 bpp, то на каждую компоненту нужно отводить по 8 бит. Делается это путем битового сдвига вправо и операции AND.
В случае установки 16-битного режима, переменная Is_555 становится TRUE для режима 5-5-5, или FALSE для режима 5-6-5.
И еще одна функция - для прорисовки текста. Она использует GDI:
Код (Text):
;######################################################################## ; DD_Draw_Text Procedure ;######################################################################## DD_Draw_Text PROC surface:DWORD, text:DWORD, num_chars:DWORD, x:DWORD, y:DWORD, color:DWORD ;======================================================= ; Эта функция будет рисовать текст ; с помощью GDI ;======================================================= ;=========================================== ; Для начала получим DC ;=========================================== DDS4INVOKE GetDC, surface, ADDR hDC ;=========================================== ; установим цвет текста ;=========================================== INVOKE SetTextColor, hDC, color INVOKE SetBkMode, hDC, TRANSPARENT ;=========================================== ; запишем текст в позицию ;=========================================== INVOKE TextOut, hDC, x, y, text, num_chars ;=========================================== ; release DC ;=========================================== DDS4INVOKE ReleaseDC, surface, hDC done: ;=================== ; все ;=================== return TRUE DD_Draw_Text ENDP ;######################################################################## ; END DD_Draw_Text ;########################################################################Далее, получаем контекст устройства (DC) для нашей поверхности - это первая вещь, которую нужно получить при рисовании. Устанавливаем background mode и цвет текста с помощью все той же Windows GDI - и мы готовы рисовать текст с помощью вызова TextOut(). После чего освобождаем DC.
Далее, напишем код для наложения bitmap.
Нам нужны 2 процедуры: для загрузки битмапа и его прорисовки. В данном случае рассматривается уникальный формат файла.
Этот формат, вероятно один из самых простых, с которым вы когда-либо столкнетесь. Он состоит из 5 основных частей: Width, Height, BPP, Size of Buffer, Buffer. Первые 3 дают информацию о самом образе. В данном случае применяется режим 16 bpp.
Код (Text):
;######################################################################## ; Create_From_SFP Procedure ;######################################################################## Create_From_SFP PROC ptr_BMP:DWORD, sfp_file:DWORD, desired_bpp:DWORD ;========================================================= ; битмап будет загружаться из SFP file. ;========================================================= ;================================= ; Local Variables ;================================= LOCAL hFile :DWORD LOCAL hSFP :DWORD LOCAL Img_Left :DWORD LOCAL Img_Alias :DWORD LOCAL red :DWORD LOCAL green :DWORD LOCAL blue :DWORD LOCAL Dest_Alias :DWORD ;================================= ; Создадим этот SFP file ;================================= INVOKE CreateFile, sfp_file, GENERIC_READ,FILE_SHARE_READ, \ NULL,OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,NULL MOV hFile, EAX ;=============================== ; ошибка ;=============================== .IF EAX == INVALID_HANDLE_VALUE JMP err .ENDIF ;=============================== ; Получим размер файла ;=============================== INVOKE GetFileSize, hFile, NULL PUSH EAX ;================================ ; ошибка ;================================ .IF EAX == -1 JMP err .ENDIF ;============================================== ; получим память ;============================================== INVOKE GlobalAlloc, GMEM_FIXED, EAX MOV hSFP, EAX ;=================================== ; ошибка ;=================================== .IF EAX == 0 JMP err .ENDIF ;=================================== ; положим файл в память ;=================================== POP EAX INVOKE ReadFile, hFile, hSFP, EAX, OFFSET Amount_Read, NULL ;==================================== ; ошибка ;==================================== .IF EAX == FALSE ;======================== ; failed ;======================== JMP err .ENDIF ;=================================== ; Определим размер ;=================================== MOV EBX, hSFP MOV EAX, DWORD PTR [EBX] ADD EBX, 4 MOV ECX, DWORD PTR [EBX] MUL ECX PUSH EAX ;====================================== ; Разберемся с типом буфера ;====================================== .IF desired_bpp == 16 ;============================ ; просто установим 16-bit ;============================ POP EAX SHL EAX, 1 INVOKE GlobalAlloc, GMEM_FIXED, EAX MOV EBX, ptr_BMP MOV DWORD PTR [EBX], EAX MOV Dest_Alias, EAX ;==================================== ; ошибка ;==================================== .IF EAX == FALSE ;======================== ; облом ;======================== JMP err .ENDIF .ELSE ;======================================== ; код для 24 bit ;======================================== ;============================ ; ошибка ;============================ JMP err .ENDIF ;==================================== ; подготовим чтение ;==================================== MOV EBX, hSFP ADD EBX, 10 MOV EAX, DWORD PTR[EBX] MOV Img_Left, EAX ADD EBX, 4 MOV Img_Alias, EBX ;==================================== ; конвертация ;==================================== .WHILE Img_Left > 0 ;================================== ; создадим color word ;================================== .IF desired_bpp == 16 ;========================================== ; прочтем по байту для blue, green , red ;========================================== XOR ECX, ECX MOV EBX, Img_Alias MOV CL, BYTE PTR [EBX] MOV blue, ECX INC EBX MOV CL, BYTE PTR [EBX] MOV green, ECX INC EBX MOV CL, BYTE PTR [EBX] MOV red, ECX ;======================= ; Img_Alias ;======================= ADD Img_Alias, 3 ;================================ ; 555 или 565 ? ;================================ .IF Is_555 == TRUE ;============================ ; 555 ;============================ RGB16BIT_555 red, green, blue .ELSE ;============================ ; 565 ;============================ RGB16BIT_565 red, green, blue .ENDIF ;================================ ; перевод в buffer ;================================ MOV EBX, Dest_Alias MOV WORD PTR [EBX], AX ;============================ ; делим на 2 ;============================ ADD Dest_Alias, 2 .ELSE ;======================================== ; код для 24 bit ;======================================== ;============================ ; ошибка ;============================ JMP err .ENDIF ;===================== ; Sub amount left by 3 ;===================== SUB Img_Left, 3 .ENDW ;==================================== ; Почистим память ;==================================== INVOKE GlobalFree, hSFP done: ;=================== ; Вроде все хорошо ;=================== return TRUE err: ;==================================== ; Почистим SFP Memory ;==================================== INVOKE GlobalFree, hSFP ;=================== ; Не получилось ;=================== return FALSE Create_From_SFP ENDP ;######################################################################## ; END Create_From_SFP ;########################################################################Сначала создаем файл, а потом выделяем для него память и читаем данные. После размещения файла в памяти определяем размер буфера.
Далее функция загрузки. Читаем 3 байта и определяем значение переменной ( 5-6-5 или 5-5-5 ) для буфера, после чего сохраняем ее там. Каждый пиксель битмапа конвертируем, для чего используется макрос.
После конвертации мы возвращаем буфер с отконвертированными пикселами.
После загрузки в память битмап можно нарисовать в back buffer:
Код (Text):
;######################################################################## ; Draw_Bitmap Procedure ;######################################################################## Draw_Bitmap PROC surface:DWORD, bmp_buffer:DWORD, lPitch:DWORD, bpp:DWORD ;========================================================= ; Эта функция рисует BMP . ; используются width и height экрана ;========================================================= ;=========================== ; Локальные переменные ;=========================== LOCAL dest_addr :DWORD LOCAL source_addr :DWORD ;=========================== ; инициализация ;=========================== MOV EAX, surface MOV EBX, bmp_buffer MOV dest_addr, EAX MOV source_addr, EBX MOV EDX, 480 ;================================= ; 16 bit mode ;================================= copy_loop1: ;============================= ; Setup num of bytes in width ; 640*2/4 = 320. ;============================= MOV ECX, 320 ;============================= ; установим source и dest ;============================= MOV EDI, dest_addr MOV ESI, source_addr ;====================================== ; Move с помощью dwords ;====================================== REP movsd ;============================== ; variables ;============================== MOV EAX, lPitch MOV EBX, 1280 ADD dest_addr, EAX ADD source_addr, EBX ;======================== ; декремент ;======================== DEC EDX ;======================== ; Конец ? ;======================== JNE copy_loop1 done: ;=================== ; Да ;=================== return TRUE err: ;=================== ; Нет ;=================== return FALSE Draw_Bitmap ENDP ;######################################################################## ; END Draw_Bitmap ;########################################################################Общеизвестно, что обращение к регистрам осуществляется намного быстрее, чем к памяти. Поэтому адреса источника и приемника мы размещаем в регистрах.
Затем вычисляем количество WORD-значений, которое делим на 2, и получаем количество DWORD-значений. Вообще, используем 640 x 480 x 16. Число 320 разместим в регистре ECX. Делаем классическое REP MOVSD. Двигаем DWORD-ми, вычитаем из ECX по 1, сравнивая с ZERO, если нет, то MOVE A DWORD, до тех пор пока ECX не станет равен 0. Все это здорово смахивает на Си-шный for со счетчиком в ECX. Повторяем 480 раз - по количеству строк.
Теперь осталось все это вывести на экран.
Итак, функции библиотек мы написали, и теперь готовы приступать к основному коду игры. Начнем с инициализации, так как она выполняется в начале нашей программы:
Код (Text):
;######################################################################## ; Game_Init Procedure ;######################################################################## Game_Init PROC ;========================================================= ; setup the game ;========================================================= ;============================================ ; инициализация 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 back buffer ;=================================== INVOKE DD_Lock_Surface, lpddsback, ADDR lPitch ;============================ ; ошибка ;============================ .IF EAX == FALSE ;=================== ; облом ;=================== JMP err .ENDIF ;=================================== ; рисуем битмап ;=================================== INVOKE Draw_Bitmap, EAX, ptr_BMP_LOAD, lPitch, screen_bpp ;=================================== ; back buffer ;=================================== INVOKE DD_Unlock_Surface, lpddsback ;============================ ; ошибка ;============================ .IF EAX == FALSE ;=================== ; облом ;=================== JMP err .ENDIF ;===================================== ; loading ;====================================== INVOKE DD_Flip ;============================ ; ошибка ;============================ .IF EAX == FALSE ;=================== ; облом ;=================== JMP err .ENDIF done: ;=================== ; да ;=================== return TRUE err: ;=================== ; нет ;=================== return FALSE Game_Init ENDP ;######################################################################## ; END Game_Init ;########################################################################Эта функция играет важную роль в нашей игре. В этой функции мы делаем вызов процедуры инициализации Direct Draw и в случае успеха загружаем с диска наш битмап. Далее беремся за back buffer и рисуем в него наш битмап. После чего делаем флиппинг видимого и невидимого буферов.
Далее у нас на пути функция WndProc, которая, как известно, обрабатывает сообщения. Если мы захотим добавить обработку еще какого-либо сообщения, то код его обработчика войдет именно в эту функцию.
Код (Text):
;######################################################################## ; Main Window Callback Procedure -- WndProc ;######################################################################## WndProc PROC hWin :DWORD, uMsg :DWORD, wParam :DWORD, lParam :DWORD .IF uMsg == WM_COMMAND ;=========================== ; без меню ;=========================== .ELSEIF uMsg == WM_KEYDOWN ;======================================= ; не будем программировать Direct input ;======================================= MOV EAX, wParam .IF EAX == VK_ESCAPE ;=========================== ; закрыть программу ;=========================== INVOKE PostQuitMessage,NULL .ENDIF ;========================== ; processed it ;========================== return 0 .ELSEIF uMsg == WM_DESTROY ;=========================== ; закрыть программу ;=========================== INVOKE PostQuitMessage,NULL return 0 .ENDIF ;================================================= ; procedure handle the message ;================================================= INVOKE DefWindowProc,hWin,uMsg,wParam,lParam RET WndProc endp ;######################################################################## ; End of Main Windows Callback Procedure ;########################################################################Я думаю этот код не требует пояснений. Пока мы имеем дело только с 2-мя сообщениями - WM_KEYDOWN и WM_DESTROY. Мы обрабатываем сообщение WM_KEYDOWN для того, чтобы пользователь мог выйти из игры по нажатию клавиши escape. Обратите внимание, что те сообщения, которые мы не обрабатываем, обрабатываются функцией DefWindowProc(). Эта функция уже определена в Windows. Вы только должны вызвать ее всякий раз, когда не обрабатываете сообщение.
И далее процедура завершения:
Код (Text):
;######################################################################## ; Game_Shutdown Procedure ;######################################################################## Game_Shutdown PROC ;=========================== ; Shutdown DirectDraw ;=========================== INVOKE DD_ShutDown ;========================== ; освобождаем память битмап'а ;========================== INVOKE GlobalFree, ptr_BMP_LOAD done: ;=================== ; Завершено успешно ;=================== return TRUE err: ;=================== ; Мы не завершились ;=================== return FALSE Game_Shutdown ENDP ;######################################################################## ; END Game_Shutdown ;########################################################################Итак, здесь мы выгружаем Direct Draw library, и освобождаем память, которую выделяли под битмап.
В следующей статье мы познакомимся с программированием Direct Input. А также создадим свое меню.
Счастливого кодирования!!! © Chris Hobbs, пер. UniSoft
Программирование игр на ассемблере (Часть 2)
Дата публикации 29 окт 2002