| 订阅 | 在线投稿
分享
 
 
 

《Windows游戏编程大师技巧》(第二版)第1章(下)

来源:互联网网民  宽屏版  评论
2006-12-17 09:28:23

《Windows游戏编程大师技巧》(第二版)第1章(下)

《Windows游戏编程大师技巧》(第二版)第1章(下) 实例: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 <windows.h> // include important windows stuff

#include <windowsx.h>

#include <mmsystem.h>

#include <iostream.h> // include important C/C++ stuff

#include <conio.h>

#include <stdlib.h>

#include <malloc.h>

#include <memory.h>

#include <string.h>

#include <stdarg.h>

#include <stdio.h>

#include <math.h>

#include <io.h>

#include <fcntl.h>

#include <ddraw.h> // 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(阅读那恼人的使用手册!)。我等着你们。

 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
 
《Windows游戏编程大师技巧》(第二版)第1章(下) 《Windows游戏编程大师技巧》(第二版)第1章(下) 实例: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 <windows.h> // include important windows stuff #include <windowsx.h> #include <mmsystem.h> #include <iostream.h> // include important C/C++ stuff #include <conio.h> #include <stdlib.h> #include <malloc.h> #include <memory.h> #include <string.h> #include <stdarg.h> #include <stdio.h> #include <math.h> #include <io.h> #include <fcntl.h> #include <ddraw.h> // 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(阅读那恼人的使用手册!)。我等着你们。
󰈣󰈤
 
 
 
>>返回首页<<
 
 热帖排行
 
 
静静地坐在废墟上,四周的荒凉一望无际,忽然觉得,凄凉也很美
©2005- 王朝网络 版权所有