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

Все статьи

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


--> На чем мы остановились?

В прошлой статье были рассмотрены основы 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. Вот несколько примеров объявления прототипов для ваших функций:

	;==================================
	; Main Program Procedures
	;==================================
	WinMain PROTO  		:DWORD,:DWORD,:DWORD,:DWORD
	WndProc PROTO  		:DWORD,:DWORD,:DWORD,:DWORD 

Вышеупомянутый код сообщает ассемблеру, о двух новых процедурах WinMain и WndProc. Каждая из них имеет список параметров связанных с ними. В каждой функции задаются 4 параметра (DWORD). Для тех кто использует пакет MASM32, то в нем уже есть прототипы всех Windows API функций, вам просто нужно подключить соответствующий файл. Но вам надо убедиться, что все пользовательские процедуры определены вышеупомянутым способом.

Породив прототип, мы можем породить и саму функцию с помощью ключевого слова PROC:

;########################################################################
; 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().

  .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. Этот код вы можете использовать во всех программах, которые будете писать, по крайней мере, мне никогда не приходилось его изменять.

А теперь сам код:

;########################################################################
; 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, мы выходим из цикла и соответственно завершаем программу.

--> Связь с Direct Draw

Мы не будем рассматривать сам DirectX на уровне асм-а, а рассмотрим его на уровне основных концепций.

Прежде всего, необходимо понять саму концепцию Таблицы Виртуальных Функций. Делается запрос в нее в форме смещения, и получается АДРЕС расположения функции. Т.е. под вызовом функции понимается обращение к таблице, которая УЖЕ существует. Адреса функций имеются в DirectX-библиотеке.

Далее, необходимо определить адрес объекта, для которого вызывается функция. Вычисляем виртуальный адрес и сохраняем в стеке все параметры. Для этой цели существуют различные макросы, в частности, DD4INVOKE еще из 4-го DirextX.

Сначала определяем имя функции, затем имя объекта и параметры:

    ;========================================
    ; Создадим primary surface
    ;========================================
      DD4INVOKE CreateSurface, lpdd, ADDR ddsd, ADDR lpddsprimary, NULL

В этом примере создается поверхность, вызовом функции CreateSurface(), передавая ей в качестве параметров: указатель на объект, адрес структуры Direct Draw Surface Describe (ddsd), адрес переменной для хранения указателя на поверхность, и наконец NULL. Теперь, когда мы увидели, как делать запросы к DirectX, давайте построим небольшую библиотеку.

--> Наша Direct Draw Library

Нам понадобятся функции для инициализации и выхода из игры, для определения формата пикселя, создания и прорисовки поверхностей, и загрузки в нее битмапа.

Далее код функции инициализации:

;########################################################################
; 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:
        ;===================
        ; Все Ок!  :)
        ;===================
        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.

После, вызывается процедура для определения формата пикселя для нашей поверхности.

В следующей процедуре мы получаем формат пикселя:

;########################################################################
; 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:

;########################################################################
; 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.

--> Наша Bitmap Library

Нам нужны 2 процедуры: для загрузки битмапа и его прорисовки. В данном случае рассматривается уникальный формат файла.

Этот формат, вероятно один из самых простых, с которым вы когда-либо столкнетесь. Он состоит из 5 основных частей: Width, Height, BPP, Size of Buffer, Buffer. Первые 3 дают информацию о самом образе. В данном случае применяется режим 16 bpp.

;########################################################################
; 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:

;########################################################################
; 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 раз - по количеству строк.

Теперь осталось все это вывести на экран.

--> A Game ...

Итак, функции библиотек мы написали, и теперь готовы приступать к основному коду игры. Начнем с инициализации, так как она выполняется в начале нашей программы:

;########################################################################
; 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, которая, как известно, обрабатывает сообщения. Если мы захотим добавить обработку еще какого-либо сообщения, то код его обработчика войдет именно в эту функцию.

;########################################################################
; 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. Вы только должны вызвать ее всякий раз, когда не обрабатываете сообщение.

И далее процедура завершения:

;########################################################################
; 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. А также создадим свое меню.

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

2002-2013 (c) wasm.ru