在射击游戏中的子弹的发射,每一帧都要检测上一帧时子弹的位置a然后确定当前帧子弹的位置b然后将该位置传给重画游戏单元的部分,在当前帧b的位置贴上子弹的图象。
在即使战略游戏中两军对战时,程序在每一帧都要根据上一帧每个战斗单位的位置和该战斗单位移动的目的、到该目的之间的障碍物的位置以及一定的路径算法确定在当前帧该战斗单位的新位置;还有要取得在上一帧时该战斗单位的生命值和所受的打击次数及强度,以确定该战斗单位的生命值。
通过阅读ProcessInput()函数的代码,我想您一定已理解了刷新游戏单元的概念。而从上面的两个例子中,您也一定发现用例程的方法很难实现这两类游戏的要求。我们不可能对每一颗子弹,每一个战斗单位进行操作,而且我们并不知道游戏者会一次发射多少颗子弹也不知道游戏者会造多少个战斗单位。我们应该怎么办呢?
考虑到每一个战斗单位(或每一颗子弹)都有相似(或相同)的属性,那么我们可以采用结构数组来储存每一个战斗单位的位置和状态。这个办法好象可行!但是仔细想想,我们又遇到了上面谈到的问题我们并不知道游戏者会一次发射多少颗子弹也不知道游戏者会造多少个战斗单位。当然我们可以采用Age of Empire的方式----限制单位的数量(我并不是说Age of Empire采用的是这种办法)。但是这意味什么呢!意味着,如果我们限定数量为50的话,在游戏者只有一个士兵时,计算机却需要为这个士兵分配50倍的内存!而且游戏者还不一定造出50个士兵。显然这并不是一个好办法!
我们应该怎么办呢?链表!链表能满足我们的要求。
class Node
{
file://双向链表的指针。
Node* Next;
Node* Pre;
file://节点数据。
NODE_DATA data;
...
};
链表是一种结构体的集合。在链表中的每一个结构体都包含了一个元素或指针,它指向链表中的另一个结构体。这个指针用作两个结构体之间的联系。这个概念与数组有些相似,但它允许链表的动态增长。现在的游戏中凡是遇到这种问题的一般都是采用链表的。关于链表的更多的信息请阅读有关的资料。
第六节 画游戏单元
画游戏单位的作用是在每一桢往屏幕上画游戏单位的图象。
这就是本例程中画游戏单元的主函数:
/*
* NewGameFrame
*/
int NewGameFrame( void )
{
file://这里是设置游戏单元的位置:
SetSpriteX( hFox, 0, P_AUTOMATIC );
SetSpriteY( hFox, 0, P_AUTOMATIC );
SetPlaneVelX( hBackground, GetSpriteVelX(hFox), P_ABSOLUTE );
SetPlaneVelX( hMidground, GetSpriteVelX(hFox), P_ABSOLUTE );
SetPlaneVelX( hForeground, GetSpriteVelX(hFox), P_ABSOLUTE );
SetPlaneX( hBackground, 0, P_AUTOMATIC );
SetPlaneX( hMidground, 0, P_AUTOMATIC );
SetPlaneX( hForeground, 0, P_AUTOMATIC );
SetSpriteX( hBear, 0, P_AUTOMATIC );
SetSpriteX( hApple, 0, P_AUTOMATIC );
SetSpriteY( hApple, 0, P_AUTOMATIC );
file://将游戏单元的图形贴到BackBuffer上:
if( bTransDest )
{
gfxFillBack( dwColorKey );
DisplayFrameRate();
DisplaySprite( hBuffer, hApple, GetPlaneX(hForeground) );
DisplaySprite( hBuffer, hBear, GetPlaneX(hForeground) );
DisplaySprite( hBuffer, hFox, GetPlaneX(hForeground) );
DisplayPlane( hBuffer, hForeground );
DisplayPlane( hBuffer, hMidground );
DisplayPlane( hBuffer, hBackground );
}
else
{
DisplayPlane( hBuffer, hBackground );
DisplayPlane( hBuffer, hMidground );
DisplayPlane( hBuffer, hForeground );
DisplaySprite( hBuffer, hFox, GetPlaneX(hForeground) );
DisplaySprite( hBuffer, hBear, GetPlaneX(hForeground) );
DisplaySprite( hBuffer, hApple, GetPlaneX(hForeground) );
DisplayFrameRate();
}
file://更新前景:
gfxSwapBuffers();
return 0;
} /* NewGameFrame */
画游戏单元的顺序为:
1。清BackBuffer;
这是清BackBuffer的函数:
/*
* gfxFillBack
*/
void gfxFillBack( DWORD dwColor )
{
DDBLTFX ddbltfx;
ddbltfx.dwSize = sizeof( ddbltfx );
ddbltfx.dwFillColor = dwColor;
IDirectDrawSurface_Blt(
lpBackBuffer, // dest surface
NULL, // dest rect
NULL, // src surface
NULL, // src rect
DDBLT_COLORFILL | DDBLT_WAIT,
&ddbltfx);
} /* gfxFillBack */
2。检查游戏单元图形的Surface是否丢失;
这是检查游戏单元图形的Surface是否丢失的函数:
/*
* gfxRestoreAll
*
* restore the art when one or more surfaces are lost
*/
BOOL gfxRestoreAll()
{
GFX_BITMAP *curr;
HWND hwndF = GetForegroundWindow();
Splash();
for( curr = lpVRAM; curr != NULL; curr = curr->link)
{
if (curr->lpSurface &&
(fForceRestore || IDirectDrawSurface_IsLost(curr->lpSurface) == DDERR_SURFACELOST))
{
if( !gfxRestore(curr) )
{
Msg( "gfxRestoreAll: ************ Restore FAILED!" );
return FALSE;
}
}
}
DDClear();
fForceRestore = FALSE;
return TRUE;
} /* gfxRestoreAll */
3。将游戏单元的图形画到BackBuffer中;
这是画游戏单元图形的函数之一:
/*
* DisplayPlane
*/
BOOL DisplayPlane ( GFX_HBM hBuffer, HPLANE *hPlane )
{
USHORT n;
USHORT i;
USHORT j;
USHORT x1;
USHORT y1;
USHORT x2;
USHORT y2;
USHORT xmod;
USHORT ymod;
POINT src;
RECT dst;
x1 = (hPlane->x >> 16) / C_TILE_W;
y1 = (hPlane->y >> 16) / C_TILE_H;
x2 = x1 + C_SCREEN_W / C_TILE_W;
y2 = y1 + C_SCREEN_H / C_TILE_H;
xmod = (hPlane->x >> 16) % C_TILE_W;
ymod = (hPlane->y >> 16) % C_TILE_H;
for( j = y1; j < y2; ++j )
{
for( i = x1; i <= x2; ++i )
{
n = (i % hPlane->width) + j * hPlane->width;
if( hPlane->hBM[n] != NULL )
{
if( i == x1 )
{
dst.left = 0;
dst.right = dst.left + C_TILE_W - xmod;
src.x = xmod;
}
else if( i == x2 )
{
dst.left = (i - x1) * C_TILE_W - xmod;
dst.right = dst.left + xmod;
src.x = 0;
} else {
dst.left = (i - x1) * C_TILE_W - xmod;
dst.right = dst.left + C_TILE_W;
src.x = 0;
}
if( j == y1 )
{
dst.top = 0;
dst.bottom = dst.top + C_TILE_H - ymod;
src.y = ymod;
}
else if( j == y2 )
{
dst.top = (j - y1) * C_TILE_H - ymod;
dst.bottom = dst.top + ymod;
src.y = 0;
} else {
dst.top = (j - y1) * C_TILE_H - ymod;
dst.bottom = dst.top + C_TILE_H;
src.y = 0;
}
gfxBlt(&dst,hPlane->hBM[n],&src);
}
}
}
return TRUE;
} /* DisplayPlane */
4。将BackBuffer和FrontBuffer进行翻转;
这是全屏幕模式下的页面翻转函数:
/*
* gfxFlip
*/
BOOL gfxFlip( void )
{
HRESULT ddrval;
ddrval = IDirectDrawSurface_Flip( lpFrontBuffer, NULL, DDFLIP_WAIT );
if( ddrval != DD_OK )
{
Msg( "Flip FAILED, rc=%08lx", ddrval );
return FALSE;
}
return TRUE;
} /* gfxFlip */
这是窗口模式下的页面翻转函数:
/*
* gfxUpdateWindow
*/
BOOL gfxUpdateWindow()
{
HRESULT ddrval;
ddrval = IDirectDrawSurface_Blt(
lpFrontBuffer, // dest surface
&rcWindow, // dest rect
lpBackBuffer, // src surface
NULL, // src rect (all of it)
DDBLT_WAIT,
NULL);
return ddrval == DD_OK;
} /* gfxUpdateWindow */
第七节 计算机人工智能
计算机人工智能
记得吗?在第五节刷新游戏单元中我们谈到在刷新游戏单元时,说到在取得游戏单位的的位置后要经过一些算法的判断再确定游戏单位的新位置。包含这些算法的部
分就是游戏中实现人工智能的部分。
对于游戏中的人工智能,我比较赞同下面这个定义:一个非游戏者控制的对象在基于各种复杂因素时的决策行为就象时由真正的人作出的,这是通过使用 一个决策算法来完成的,这个决策算法根据设计者确定的规则和提供给程序的信息进行处理。
现在在大部分的游戏中采用的的方式主要有以下几种:
检索
许多人工智能的算法中都涉及到对所有可能性的检索。这个算法的实现方式是这样的,首先您应让您的程序列一个选项表,例如一个士兵到目的之间所有可能的路径。然后再使用其他的人工智能技术如排除法等来找一个最优的选择。
排序
排序与检索都是基本的人工智能技术,您可以用排序来确定最佳的决策次序。比 如,在战略游戏中计算机对手不断地根据当前的环境修改行动的优先级。
专家系统
专家系统是指运用“if then”语句的逻辑表达式来表示所有的基本规则,然后计算机根据这些规则作出智能决策。比如,在制作一个足球游戏时,就可以请一个足球专家,记下他的的足球经验,他会说明在各种情况下,他采取的踢球方式。根据这些信息,建立一套规则库,在游戏中计算机就可以按照这些规则作出决策。
其他的方式还有:机器学习和和神经网络系统,这两种方式的效果相当不错。但是却很不容易掌握,这里我们就不再详述了。
第八节 游戏内存管理
游戏内存管理
这一部分对于优质高效的游戏软件是十分重要的,内存管理不当会导致游戏性能的降低,甚至引起死机。现在的很多游戏都使用了大量的图象和复杂的规则,需要大量的内存。这就需要我们对游戏者所用机器的内存进行精心的分配和组织了。首先,我们应当调查一下现在的主流机型的内存是多少,再与达到游戏的设计目标所需的内存量之间权衡一下,然后确定一个粗略的分配方案。
这个方案一般可以这样指定:
1.这个游戏从情节上可以分为几个部分,在开始时将每个部分所共有的资料调入,然后根据情节的发展将该部分不用的资料所占用的内存释放再调入该部分所特有的资料。比如说可以分为几关或者RPG游戏中的“世界”的地图可以分成几个场景。然后在游戏者到达某一关或进入某一场景时再调入相应的图象或相应的资料。
2.在每个部分中有很多并不常用而且调用时没有严格的速度限制同时调用时并不需要太多时间(通常1秒左右即可完成)的资料,也可以在使用时调用。比如角色从大地图上走入一个城市,这个城市的图象和游戏规则等资料就可以在走入这个城市时调入。
在完成这个方案后,我们就完成了内存和硬盘之间数据交换的规划了,接下来就应考虑运行时内存内部的管理了。
在这一步中主要应注意两个问题:
1.迅速释放存放无效资料的内存;
例如:
描述GAMEWORLD的指针temp在初始化时分配了内存空间。
GAMEWORLD *temp=new GAMEWORLD(Init value);
。。。
在程序结束时要释放内存空间。
delete temp;
2.严禁使用空指针(这样会导致系统错误,甚至死机)。这里没有什么技巧,只有靠您自己的认真和仔细了。
例如:
当在程序中已经释放了temp;
下面的调用就可能导致死机:
temp->Function();
这两个问题的解决方法为:
GAMEWORLD *temp=new GAMEWORLD(Init value);
...
if(temp)
delete temp;
temp=NULL;
...
if(temp)
{
temp->Function();
...
}
else
{
提示temp为空指针。
}
第九节 游戏交互设计
游戏交互设计
交互设计,实际上就是您想让游戏者怎么去操纵游戏的发展。说简单了交互设计就是游戏者怎样去控制游戏角色的行动,在例程中对键盘的设置——用小建盘上的“456237”控制狐狸的行为就是一种简单的交互设计。说复杂了呢!就是您提供了一种什么样的方式让游戏者进入游戏之中成为游戏中的一员——他就是游戏中英勇无敌、侠肝义胆的剑客,他就是游戏中足智多谋、威震天下的将军……这就是好游戏的一个重要的要素——好的交互性。
交互性就是设计者创造的一个诱使人去玩的游戏所拥有的提供故事线索、情绪感染、真实的声音和其他创造性的媒介所给予的特性。交互设计的目的就是让游戏者进入“幻觉状态”,幻觉状态是游戏界的一个术语,它的意思是指游戏者的意识融入到游戏世界中,这样,他或她就不是在玩游戏,而是在体验另一个世界。
怎样作到这一点呢?作为编程人员应考虑的是:
第一步考虑输入设备问题,设备即是游戏者控制游戏的手段,也就是输入设备的选择和设置的问题。在这一步中应该考虑是选择键盘、鼠标、游戏杆还是几种结合的方式,或是其他类型的输入设备。然后是设置各种操作所代表的含义(就象例程中
小键盘的“4”代表左行,“5”代表停止等等,或是鼠标单击、双击某个区域及拖动时代表的含义)这些设置主要是考虑一个操作的方便性的问题。
typedef enum enum_ACTION {
NONE,
STILL,
WALK,
RUN,
JUMP,
THROW,
CROUCH,
STOP,
STUNNED,
JUMPTHROW,
CROUCHWALK,
BLURR,
STRIKE,
MISS,
CHEW,
} ACTION;
WinMainProc中:
case WM_KEYDOWN:
switch( wParam )
{
case VK_NUMPAD5:
lastInput=KEY_STOP;
break;
case VK_DOWN:
case VK_NUMPAD2:
lastInput=KEY_DOWN;
break;
case VK_LEFT:
case VK_NUMPAD4:
lastInput=KEY_LEFT;
break;
case VK_RIGHT:
case VK_NUMPAD6:
lastInput=KEY_RIGHT;
break;
case VK_UP:
case VK_NUMPAD8:
lastInput=KEY_UP;
break;
case VK_HOME:
case VK_NUMPAD7:
lastInput=KEY_JUMP;
break;
case VK_NUMPAD3:
lastInput=KEY_THROW;
break;
case VK_F5:
bShowFrameCount = !bShowFrameCount;
if( bShowFrameCount )
{
dwFrameCount = 0;
dwFrameTime = timeGetTime();
}
break;
第二步考虑信息返回的问题,这里主要是一个界面的设计的问题。这个问题我们在
第一章第六节游戏的界面设计中已经讨论过了,这里就不详述了。
第十节 游戏图形底层设计
在游戏中,计算机主要花时间在处理图象和画图象上,所以我们应尽力使这些操作适合主流机型的硬件水平或尽量少占用系统资源,这就是游戏图形底层设计的目的。在前面讲的DirectDraw和DirectX5 SDK中的Direct3D都是图形底层,还有ID在 QUAKE发行后提供的QUAKE C也是一种不错的图形底层。建立一套游戏图形底层需要大量的关于图形编程的知识和很多的时间精力,而且效果不一定好,同时在市场上也有很多图形底层可供选择。所以对于一般的游戏开发者来说,只要作的游戏使用的图象并没有使计算机不负重荷或并没有使用现有的底层所不支持的特性,我建议还是使用现有的底层。
本例程的图形底层十分简单,采用DirectDraw提供的IDirectDrawSurface_BltFast和IDirectDrawSurface_Blt函数:
if (pbm->lpSurface)
{
if( pbm->bTrans )
bltflags = bTransDest ? DDBLTFAST_DESTCOLORKEY : DDBLTFAST_SRCCOLORKEY;
else
bltflags = bTransDest ? DDBLTFAST_DESTCOLORKEY : DDBLTFAST_NOCOLORKEY;
ddrval = IDirectDrawSurface_BltFast(
lpBackBuffer, x, y,
pbm->lpSurface, &rc, bltflags | DDBLTFAST_WAIT);
if (ddrval != DD_OK)
{
Msg("BltFast failed err=%d", ddrval);
}
}
else
{
DDBLTFX ddbltfx;
rc.left = x;
rc.top = y;
rc.right = rc.left + dx;
rc.bottom = rc.top + dy;
ddbltfx.dwSize = sizeof( ddbltfx );
ddbltfx.dwFillColor = pbm->dwColor;
ddrval = IDirectDrawSurface_Blt(
lpBackBuffer, // dest surface
&rc, // dest rect
NULL, // src surface
NULL, // src rect
DDBLT_COLORFILL | DDBLT_WAIT,
&ddbltfx);
}
由于DirectDraw的通用性要求,所以虽然它提供的函数的速度很快,但是对特效的支持比较少。深入的图形底层应包括大量的高效的特效处理功能,所以我们应该能够直接对显存操作。
DirectDraw的DirectSurface提供了这个入口,它的DDSURFACEDESC结构中的变量
lpSurface就是显存映射的入口指针。
typedef struct _DDSURFACEDESC {
DWORD dwSize;
DWORD dwFlags;
DWORD dwHeight;
DWORD dwWidth;
union
{
LONG lPitch;
DWORD dwLinearSize;
};
DWORD dwBackBufferCount;
union
{
DWORD dwMipMapCount;
DWORD dwZBufferBitDepth;
DWORD dwRefreshRate;
};
DWORD dwAlphaBitDepth;
DWORD dwReserved;
LPVOID lpSurface;
DDCOLORKEY ddckCKDestOverlay;
DDCOLORKEY ddckCKDestBlt;
DDCOLORKEY ddckCKSrcOverlay;
DDCOLORKEY ddckCKSrcBlt;
DDPIXELFORMAT ddpfPixelFormat;
DDSCAPS ddsCaps;
} DDSURFACEDESC;
但是使用它之前,必须调用DirectDrawSurface3::Lock函数将此图形内存锁住,
在处理后,调用DirectDrawSurface3::Unlock函数将此内存交还给系统。