執行個體:FreakOut
在沉溺于所讨論的有關Windows、DirectX 和3D 圖形之前,應當暫停一下,先給你看一個完整的遊戲——雖然簡單了一點,但毫無疑問是一個完整的遊戲。你會看到一個實際的遊戲循環和一些圖形功能調用,最後一霎那就可以通過編譯。不錯吧?跟我來吧!
問題是我們現在才講到第一章。我不應該使用後面章節中的内容……這有點像作弊,對吧?是以,我決定要做的是讓你習慣于使用黑黑(black box)API來進行遊戲程式設計。基于這個要求,我要提一個問題“要制作一個類似Breakout(打磚塊)的2D遊戲,其最低要求是什麼?”我們真正所需要的是下面的功能:
? 在任意圖像模式中切換
? 在螢幕上畫各種顔色的矩形
? 擷取鍵盤輸入
? 使用一些定時函數同步遊戲循環
? 在螢幕上畫彩色文字串
是以我建了一個名為BLACKBOX.CPP|H的庫。它封裝了一套DirectX函數集(限于DirectDraw),并且包含實作所需功能的支援代碼。妙處是,讀者根本不需要看這些代碼,隻需依照函數原型來使用這些函數就可以了,并與BLACKBOX.CPP|H連接配接來産生.EXE可執行檔案。
以BLACKBOX庫為基礎,我編寫了一個名字為FreakOut的遊戲,這個遊戲示範了本章中所讨論的許多概念。FreakOut 遊戲包含真正遊戲的全部主要組成部分,包括:遊戲循環、計分、關卡,甚至還有為球而寫的迷你實體模型。真是可愛。圖1.9 是一幅遊戲運作中的螢幕畫面。顯然它比不上Arkanoid(經典的打磚塊類遊戲),但4 個小時的工作有此成果也不賴!
圖1.9 FreakOut遊戲的截屏
在閱讀遊戲源代碼之前,我希望讀者能看一下工程和遊戲各組成部分是如何協調一緻的。參見圖1.10。
圖1.10 FreakOut 的結構
從圖中可以看到,遊戲由下面檔案構成:
FREAKOUT.CPP——遊戲的主要邏輯,使用BLACKBOX.CPP,建立一個最小化的Win32應用程式。
BLACKBOX.CPP——遊戲庫(請不要偷看:)。
BLACKBOX.H——遊戲庫的頭檔案。
DDRAW.LIB——用于生成應用程式的DirectDraw輸入庫。其中并不含有真正的DirectX代碼。它主要是用作讓使用者調用的中間庫,然後輪流調用進行實際工作的DDRAW.DLL動态連結庫。它可以在DirectX SDK 安裝目錄下的LIB子目錄内被找到。
DDRAW.DLL——運作時(Runtime)的DirectDraw 庫,實際上含有通過DDRAW.LIB 輸入庫調用DirectDraw 接口函數的COM 執行程式。不必為此擔心;隻要确認已經安裝了DirectX運作時檔案即可。
為了通過編譯,需要将BLACKBOX.CPP和FREAKOUT.CPP加入工程裡面,連接配接上DDRAW.LIB庫檔案,并確定BLACKBOX.H在頭檔案搜尋路徑或工作目錄裡,以便編譯器可以正确地找到它。
現在我們已大緻了解了FreakOut的結構。讓我們看一下BLACKOUT.H頭檔案,看看它包含了哪些函數。
程式清單1.2 BLACKOUT.H 頭檔案
// BLACKBOX.H - Header file for demo game engine library
// watch for multiple inclusions
#ifndef BLACKBOX
#define BLACKBOX
// DEFINES
// default screen size
#define SCREEN_WIDTH 640 // size of screen
#define SCREEN_HEIGHT 480
#define SCREEN_BPP 8 // bits per pixel
#define MAX_COLORS 256 // maximum colors
// MACROS /
// these read the keyboard asynchronously
#define KEY_DOWN(vk_code) ((GetAsyncKeyState(vk_code) & 0x8000) ? 1 : 0)
#define KEY_UP(vk_code) ((GetAsyncKeyState(vk_code) & 0x8000) ? 0 : 1)
// initializes a direct draw struct
#define DD_INIT_STRUCT(ddstruct) {memset(&ddstruct,0,sizeof(ddstruct));
ddstruct.dwSize=sizeof(ddstruct); }
// TYPES //
// basic unsigned types
typedef unsigned short USHORT;
typedef unsigned short WORD;
typedef unsigned char UCHAR;
typedef unsigned char BYTE;
// EXTERNALS //
extern LPDIRECTDRAW7 lpdd; // dd object
extern LPDIRECTDRAWSURFACE7 lpddsprimary; // dd primary surface
extern LPDIRECTDRAWSURFACE7 lpddsback; // dd back surface
extern LPDIRECTDRAWPALETTE lpddpal; // a pointer dd palette
extern LPDIRECTDRAWCLIPPER lpddclipper; // dd clipper
extern PALETTEENTRY palette[256]; // color palette
extern PALETTEENTRY save_palette[256]; // used to save palettes
extern DDSURFACEDESC2 ddsd; // a ddraw surface description struct
extern DDBLTFX ddbltfx; // used to fill
extern DDSCAPS2 ddscaps; // a ddraw surface capabilities struct
extern HRESULT ddrval; // result back from dd calls
extern DWORD start_clock_count; // used for timing
// these defined the general clipping rectangle
extern int min_clip_x, // clipping rectangle
max_clip_x,
min_clip_y,
max_clip_y;
// these are overwritten globally by DD_Init()
extern int screen_width, // width of screen
screen_height, // height of screen
screen_bpp; // bits per pixel
// PROTOTYPES /
// DirectDraw functions
int DD_Init(int width, int height, int bpp);
int DD_Shutdown(void);
LPDIRECTDRAWCLIPPER DD_Attach_Clipper(LPDIRECTDRAWSURFACE7 lpdds,
int num_rects, LPRECT clip_list);
int DD_Flip(void);
int DD_Fill_Surface(LPDIRECTDRAWSURFACE7 lpdds,int color);
// general utility functions
DWORD Start_Clock(void);
DWORD Get_Clock(void);
DWORD Wait_Clock(DWORD count);
// graphics functions
int Draw_Rectangle(int x1, int y1, int x2, int y2,
int color,LPDIRECTDRAWSURFACE7 lpdds=lpddsback);
// gdi functions
int Draw_Text_GDI(char *text, int x,int y,COLORREF color,
LPDIRECTDRAWSURFACE7 lpdds=lpddsback);
int Draw_Text_GDI(char *text, int x,int y,int color,
LPDIRECTDRAWSURFACE7 lpdds=lpddsback);
#endif
現在,不要花費太多時間絞盡腦汁研究這裡的程式代碼,搞清楚那些神秘的全局變量究竟表示什麼并不重要。讓我們來看一看這些函數。如你所想,這裡有實作我們的簡單圖形界面所需的全部函數。基于這個圖形界面和最小化的Win32 應用程式(我們要做的Windows 程式設計工作越少越好)的基礎上,我建立了遊戲FREAKOUT.CPP,如清單1.3 所示。請認真地看一看,尤其是遊戲主循環和對遊戲處理功能的調用。
程式清單1.3 FREAKOUT.CPP 源檔案
// INCLUDES ///
#define WIN32_LEAN_AND_MEAN // include all macros
#define INITGUID // include all GUIDs
#include // include important windows stuff
#include
#include
#include // include important C/C++ stuff
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include // directX includes
#include "blackbox.h" // game library includes
// DEFINES
// defines for windows
#define WINDOW_CLASS_NAME "WIN3DCLASS" // class name
#define WINDOW_WIDTH 640 // size of window
#define WINDOW_HEIGHT 480
// states for game loop
#define GAME_STATE_INIT 0
#define GAME_STATE_START_LEVEL 1
#define GAME_STATE_RUN 2
#define GAME_STATE_SHUTDOWN 3
#define GAME_STATE_EXIT 4
// block defines
#define NUM_BLOCK_ROWS 6
#define NUM_BLOCK_COLUMNS 8
#define BLOCK_WIDTH 64
#define BLOCK_HEIGHT 16
#define BLOCK_ORIGIN_X 8
#define BLOCK_ORIGIN_Y 8
#define BLOCK_X_GAP 80
#define BLOCK_Y_GAP 32
// paddle defines
#define PADDLE_START_X (SCREEN_WIDTH/2 - 16)
#define PADDLE_START_Y (SCREEN_HEIGHT - 32);
#define PADDLE_WIDTH 32
#define PADDLE_HEIGHT 8
#define PADDLE_COLOR 191
// ball defines
#define BALL_START_Y (SCREEN_HEIGHT/2)
#define BALL_SIZE 4
// PROTOTYPES /
// game console
int Game_Init(void *parms=NULL);
int Game_Shutdown(void *parms=NULL);
int Game_Main(void *parms=NULL);
// GLOBALS
HWND main_window_handle = NULL; // save the window handle
HINSTANCE main_instance = NULL; // save the instance
int game_state = GAME_STATE_INIT; // starting state
int paddle_x = 0, paddle_y = 0; // tracks position of paddle
int ball_x = 0, ball_y = 0; // tracks position of ball
int ball_dx = 0, ball_dy = 0; // velocity of ball
int score = 0; // the score
int level = 1; // the current level
int blocks_hit = 0; // tracks number of blocks hit
// this contains the game grid data
UCHAR blocks[NUM_BLOCK_ROWS][NUM_BLOCK_COLUMNS];
// FUNCTIONS //
LRESULT CALLBACK WindowProc(HWND hwnd,
UINT msg,
WPARAM wparam,
LPARAM lparam)
{
// this is the main message handler of the system
PAINTSTRUCT ps; // used in WM_PAINT
HDC hdc; // handle to a device context
// what is the message
switch(msg)
{
case WM_CREATE:
{
// do initialization stuff here
return(0);
} break;
case WM_PAINT:
{
// start painting
hdc = BeginPaint(hwnd,&ps);
// the window is now validated
// end painting
EndPaint(hwnd,&ps);
return(0);
} break;
case WM_DESTROY:
{
// kill the application
PostQuitMessage(0);
return(0);
} break;
default:break;
} // end switch
// process any messages that we didn't take care of
return (DefWindowProc(hwnd, msg, wparam, lparam));
} // end WinProc
// WINMAIN
int WINAPI WinMain(HINSTANCE hinstance,
HINSTANCE hprevinstance,
LPSTR lpcmdline,
int ncmdshow)
{
// this is the winmain function
WNDCLASS winclass; // this will hold the class we create
HWND hwnd; // generic window handle
MSG msg; // generic message
HDC hdc; // generic dc
PAINTSTRUCT ps; // generic paintstruct
// first fill in the window class structure
winclass.style = CS_DBLCLKS | CS_OWNDC |
CS_HREDRAW | CS_VREDRAW;
winclass.lpfnWndProc = WindowProc;
winclass.cbClsExtra = 0;
winclass.cbWndExtra = 0;
winclass.hInstance = hinstance;
winclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
winclass.hCursor = LoadCursor(NULL, IDC_ARROW);
winclass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
winclass.lpszMenuName = NULL;
winclass.lpszClassName = WINDOW_CLASS_NAME;
// register the window class
if (!RegisterClass(&winclass))
return(0);
// create the window, note the use of WS_POPUP
if (!(hwnd = CreateWindow(WINDOW_CLASS_NAME, // class
"WIN3D Game Console", // title
WS_POPUP | WS_VISIBLE,
0,0, // initial x,y
GetSystemMetrics(SM_CXSCREEN), // initial width
GetSystemMetrics(SM_CYSCREEN), // initial height
NULL, // handle to parent
NULL, // handle to menu
hinstance, // instance
NULL))) // creation parms
return(0);
// hide mouse
ShowCursor(FALSE);
// save the window handle and instance in a global
main_window_handle = hwnd;
main_instance = hinstance;
// perform all game console specific initialization
Game_Init();
// enter main event loop
while(1)
{
if (PeekMessage(&msg,NULL,0,0,PM_REMOVE))
{
// test if this is a quit
if (msg.message == WM_QUIT)
break;
// translate any accelerator keys
TranslateMessage(&msg);
// send the message to the window proc
DispatchMessage(&msg);
} // end if
// main game processing goes here
Game_Main();
} // end while
// shutdown game and release all resources
Game_Shutdown();
// show mouse
ShowCursor(TRUE);
// return to Windows like this
return(msg.wParam);
} // end WinMain
// T3DX GAME PROGRAMMING CONSOLE FUNCTIONS
int Game_Init(void *parms)
{
// this function is where you do all the initialization
// for your game
// return success
return(1);
} // end Game_Init
///
int Game_Shutdown(void *parms)
{
// this function is where you shutdown your game and
// release all resources that you allocated
// return success
return(1);
} // end Game_Shutdown
///
void Init_Blocks(void)
{
// initialize the block field
for (int row=0; row < NUM_BLOCK_ROWS; row++)
for (int col=0; col < NUM_BLOCK_COLUMNS; col++)
blocks[row][col] = row*16+col*3+16;
} // end Init_Blocks
///
void Draw_Blocks(void)
{
// this function draws all the blocks in row major form
int x1 = BLOCK_ORIGIN_X, // used to track current position
y1 = BLOCK_ORIGIN_Y;
// draw all the blocks
for (int row=0; row < NUM_BLOCK_ROWS; row++)
{
// reset column position
x1 = BLOCK_ORIGIN_X;
// draw this row of blocks
for (int col=0; col < NUM_BLOCK_COLUMNS; col++)
{
// draw next block (if there is one)
if (blocks[row][col]!=0)
{
// draw block
Draw_Rectangle(x1-4,y1+4,
x1+BLOCK_WIDTH-4,y1+BLOCK_HEIGHT+4,0);
Draw_Rectangle(x1,y1,x1+BLOCK_WIDTH,
y1+BLOCK_HEIGHT,blocks[row][col]);
} // end if
// advance column position
x1+=BLOCK_X_GAP;
} // end for col
// advance to next row position
y1+=BLOCK_Y_GAP;
} // end for row
} // end Draw_Blocks
///
void Process_Ball(void)
{
// this function tests if the ball has hit a block or the paddle
// if so, the ball is bounced and the block is removed from
// the playfield note: very cheesy collision algorithm :)
// first test for ball block collisions
// the algorithm basically tests the ball against each
// block's bounding box this is inefficient, but easy to
// implement, later we'll see a better way
int x1 = BLOCK_ORIGIN_X, // current rendering position
y1 = BLOCK_ORIGIN_Y;
int ball_cx = ball_x+(BALL_SIZE/2), // computer center of ball
ball_cy = ball_y+(BALL_SIZE/2);
// test of the ball has hit the paddle
if (ball_y > (SCREEN_HEIGHT/2) && ball_dy > 0)
{
// extract leading edge of ball
int x = ball_x+(BALL_SIZE/2);
int y = ball_y+(BALL_SIZE/2);
// test for collision with paddle
if ((x >= paddle_x && x <= paddle_x+PADDLE_WIDTH) &&
(y >= paddle_y && y <= paddle_y+PADDLE_HEIGHT))
{
// reflect ball
ball_dy=-ball_dy;
// push ball out of paddle since it made contact
ball_y+=ball_dy;
// add a little english to ball based on motion of paddle
if (KEY_DOWN(VK_RIGHT))
ball_dx-=(rand()%3);
else
if (KEY_DOWN(VK_LEFT))
ball_dx+=(rand()%3);
else
ball_dx+=(-1+rand()%3);
// test if there are no blocks, if so send a message
// to game loop to start another level
if (blocks_hit >= (NUM_BLOCK_ROWS*NUM_BLOCK_COLUMNS))
{
game_state = GAME_STATE_START_LEVEL;
level++;
} // end if
// make a little noise
MessageBeep(MB_OK);
// return
return;
} // end if
} // end if
// now scan thru all the blocks and see if ball hit blocks
for (int row=0; row < NUM_BLOCK_ROWS; row++)
{
// reset column position
x1 = BLOCK_ORIGIN_X;
// scan this row of blocks
for (int col=0; col < NUM_BLOCK_COLUMNS; col++)
{
// if there is a block here then test it against ball
if (blocks[row][col]!=0)
{
// test ball against bounding box of block
if ((ball_cx > x1) && (ball_cx < x1+BLOCK_WIDTH) &&
(ball_cy > y1) && (ball_cy < y1+BLOCK_HEIGHT))
{
// remove the block
blocks[row][col] = 0;
// increment global block counter, so we know
// when to start another level up
blocks_hit++;
// bounce the ball
ball_dy=-ball_dy;
// add a little english
ball_dx+=(-1+rand()%3);
// make a little noise
MessageBeep(MB_OK);
// add some points
score+=5*(level+(abs(ball_dx)));
// that's it -- no more block
return;
} // end if
} // end if
// advance column position
x1+=BLOCK_X_GAP;
} // end for col
// advance to next row position
y1+=BLOCK_Y_GAP;
} // end for row
} // end Process_Ball
///
int Game_Main(void *parms)
{
// this is the workhorse of your game it will be called
// continuously in real-time this is like main() in C
// all the calls for your game go here!
char buffer[80]; // used to print text
// what state is the game in?
if (game_state == GAME_STATE_INIT)
{
// initialize everything here graphics
DD_Init(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP);
// seed the random number generator
// so game is different each play
srand(Start_Clock());
// set the paddle position here to the middle bottom
paddle_x = PADDLE_START_X;
paddle_y = PADDLE_START_Y;
// set ball position and velocity
ball_x = 8+rand()%(SCREEN_WIDTH-16);
ball_y = BALL_START_Y;
ball_dx = -4 + rand()%(8+1);
ball_dy = 6 + rand()%2;
// transition to start level state
game_state = GAME_STATE_START_LEVEL;
} // end if
else
if (game_state == GAME_STATE_START_LEVEL)
{
// get a new level ready to run
// initialize the blocks
Init_Blocks();
// reset block counter
blocks_hit = 0;
// transition to run state
game_state = GAME_STATE_RUN;
} // end if
///
else
if (game_state == GAME_STATE_RUN)
{
// start the timing clock
Start_Clock();
// clear drawing surface for the next frame of animation
Draw_Rectangle(0,0,SCREEN_WIDTH-1, SCREEN_HEIGHT-1,200);
// move the paddle
if (KEY_DOWN(VK_RIGHT))
{
// move paddle to right
paddle_x+=8;
// make sure paddle doesn't go off screen
if (paddle_x > (SCREEN_WIDTH-PADDLE_WIDTH))
paddle_x = SCREEN_WIDTH-PADDLE_WIDTH;
} // end if
else
if (KEY_DOWN(VK_LEFT))
{
// move paddle to right
paddle_x-=8;
// make sure paddle doesn't go off screen
if (paddle_x < 0)
paddle_x = 0;
} // end if
// draw blocks
Draw_Blocks();
// move the ball
ball_x+=ball_dx;
ball_y+=ball_dy;
// keep ball on screen, if the ball hits the edge of
// screen then bounce it by reflecting its velocity
if (ball_x > (SCREEN_WIDTH - BALL_SIZE) || ball_x < 0)
{
// reflect x-axis velocity
ball_dx=-ball_dx;
// update position
ball_x+=ball_dx;
} // end if
// now y-axis
if (ball_y < 0)
{
// reflect y-axis velocity
ball_dy=-ball_dy;
// update position
ball_y+=ball_dy;
} // end if
else
// penalize player for missing the ball
if (ball_y > (SCREEN_HEIGHT - BALL_SIZE))
{
// reflect y-axis velocity
ball_dy=-ball_dy;
// update position
ball_y+=ball_dy;
// minus the score
score-=100;
} // end if
// next watch out for ball velocity getting out of hand
if (ball_dx > 8) ball_dx = 8;
else
if (ball_dx < -8) ball_dx = -8;
// test if ball hit any blocks or the paddle
Process_Ball();
// draw the paddle and shadow
Draw_Rectangle(paddle_x-8, paddle_y+8,
paddle_x+PADDLE_WIDTH-8,
paddle_y+PADDLE_HEIGHT+8,0);
Draw_Rectangle(paddle_x, paddle_y,
paddle_x+PADDLE_WIDTH,
paddle_y+PADDLE_HEIGHT,PADDLE_COLOR);
// draw the ball
Draw_Rectangle(ball_x-4, ball_y+4, ball_x+BALL_SIZE-4,
ball_y+BALL_SIZE+4, 0);
Draw_Rectangle(ball_x, ball_y, ball_x+BALL_SIZE,
ball_y+BALL_SIZE, 255);
// draw the info
sprintf(buffer,"F R E A K O U T Score %d //
Level %d",score,level);
Draw_Text_GDI(buffer, 8,SCREEN_HEIGHT-16, 127);
// flip the surfaces
DD_Flip();
// sync to 33ish fps
Wait_Clock(30);
// check if user is trying to exit
if (KEY_DOWN(VK_ESCAPE))
{
// send message to windows to exit
PostMessage(main_window_handle, WM_DESTROY,0,0);
// set exit state
game_state = GAME_STATE_SHUTDOWN;
} // end if
} // end if
///
else
if (game_state == GAME_STATE_SHUTDOWN)
{
// in this state shut everything down and release resources
DD_Shutdown();
// switch to exit state
game_state = GAME_STATE_EXIT;
} // end if
// return success
return(1);
} // end Game_Main
哈哈,酷吧?這就是一個完整的Win32/DirectX遊戲了,至少幾乎是完整的了。BLACKOUT.CPP源檔案中有好幾百行代碼,但是我們可以将其視為某人(我!)編寫的DirectX的一部分。不管怎樣說,還是讓我們迅速浏覽一下程式清單1.3的内容吧。
首先,Windows 需要一個事件循環。這是所有Windows程式的标準結構,因為Windows幾乎完全是事件驅動的。但是遊戲卻不是事件驅動的,無論使用者在幹什麼,它們都在一直運作。是以,我們至少需要支援小型事件循環以配合Windows。執行這項功能的代碼位于WinMain()中。WinMain() 是所有Windows 程式的主要入口點,就好比main()是所有DOS/UNIX 程式中的入口點一樣。FreakOut 的WinMain()建立一個視窗并進入事件循環。當Windows需要作某些工作時,就随它去。當所有的基本事件處理都結束時,調用Game_Main()。Game_Main是實際運作遊戲程式的部分。
如果願意的話,你可以不停地在Game_Main()中循環,而不釋放回到WinMain()主事件循環體中。但這樣做不是件好事,因為Windows會得不到任何資訊。哎,我們該做的是讓遊戲在運作一幀時間的動畫和邏輯之後,傳回到WinMain()。這樣的話,Windows可以繼續響應和處理資訊。如果所有這些聽起來像是幻術的話,請不要擔心——在下一章中情況還會更糟。
進入Game_Main()後,FreakOut的遊戲邏輯開始被執行。遊戲圖像被渲染到一個不直接顯示出來的工作緩沖區,爾後通過調用DD_FLIP()而在循環結束時在顯示屏上顯示出來。是以我希望你閱讀一下全部的遊戲狀态,一行一行地過一遍一遍遊戲循環的每一部分,了解工作原理。要啟動遊戲,隻須輕按兩下FREAKOUT.EXE,遊戲程式會立即啟動。遊戲控制方式如下:
右箭頭鍵——向右移動擋闆。
左箭頭鍵——向左移動擋闆。
Esc鍵——退回Windows。
還有,如果你錯過一個球的話,将被罰掉100分,可要仔細盯緊啊!
如果你已經明白了遊戲代碼和玩法,不妨試着修改一下遊戲。你可以增加不同的背景顔色(0~255 是有效的顔色)、增加更多的球、可以改變擋闆的大小以及加上更多的聲音效果(目前我隻用到了Win32 API 中的MessageBeep()函數)
總結
這大概是我所寫的最快的一章遊戲程式設計入門教程了!我們提及了大量的基礎内容,但是還隻能算作是本書的縮略版本(就像印在封底的那樣)。我隻想讓讀者對本書中我們将學習和讨論的内容有一個感性認識。另外,閱讀一個完整的遊戲總是有益的,因為這帶來許多需要讀者思考的問題。
在進入第二章之前,請先確定你能夠輕松編譯FreakOut遊戲。如果還不行的話,請立即翻開編譯器的書并且RTFM(閱讀那惱人的使用手冊!)。我等着你們。