游戏开发基础(8)
第一节窗口的移动和改变大小时
case WM_SIZE:
case WM_MOVE:
if (IsIconic(hWnd))
{
Msg("FoxBear is minimized, pausing");
PauseGame();
}
if (bFullscreen)
{
SetRect(&rcWindow, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN));
}
else
{
GetClientRect(hWnd, &rcWindow);
ClientToScreen(hWnd, (LPPOINT)&rcWindow);
ClientToScreen(hWnd, (LPPOINT)&rcWindow+1);
}
Msg("WINDOW RECT: [%d,%d,%d,%d]", rcWindow.left, rcWindow.top, rcWindow.right, rcWindow.bottom);
break;
case WM_SIZE:是在窗口的大小被改变时发送的。
case WM_MOVE:是在窗口被移动时发送的。
当收到这两个消息后,首先检测窗口是否最小化了,如果是则暂停游戏。否则再检测游戏是否转为全屏模式,若是则改变用户区的大小为全屏。如果只是改变了窗口的大小但并没有使显示模式变成全屏或窗口最小化,则先取得窗口的用户区坐标(左上角的x、y及右下角的x、y的值)将之存入结构rcWindows,再将rcWindows中的坐标转换为屏幕坐标。请注意rcWindows是RECT型的结构,但ClientToScreen()要求的是POINT型的结构,在结构RECT中存放的是一个矩形的左上角的x、y及右下角的x、y的值而结构POINT中存放的就只是x和y的值。故此在
ClientToScreen(hWnd, (LPPOINT)&rcWindow);
ClientToScreen(hWnd, (LPPOINT)&rcWindow+1);
中先对rcWindow进行强制类型转换,而且两次调用该函数,同时在第二次掉用时使rcWindow的地址自加1。
第二节窗口被激活时
case WM_ACTIVATEAPP:
bIsActive = (BOOL)wParam && GetForegroundWindow() == hWnd;
if (bIsActive)
Msg("FoxBear is active");
else
Msg("FoxBear is not active");
if (bPaused && bIsActive)
{
if (RestoreGame())
{
UnPauseGame();
}
else
{
if (GetForegroundWindow() == hWnd)
{
if (InitGame())
{
UnPauseGame();
}
}
}
}
break;
case WM_ACTIVATEAPP:当不同于当前窗口的应用程序的窗口被激活时发送本消息。
当接收到这个消息时,首先令bIsActive 等于 (BOOL)wParam && GetForegroundWindow() == hWnd,然后检测bIsActive是否为TURE。这里的意思就是检测游戏的窗口是否被激活且处于当前系统中最高优先级。接下来先检测游戏窗口是否是从暂停状态到激活的状态,若是则运行RestoreGame()从新开始,若RestoreGame()成功则运行UnPauseGame(),如果不是从暂停状态到激活的状态,则检测本程序的窗口是否拥有最高的优先级,若有则从新初始化游戏(运行InitGame()),初始化成功后则运行UnPauseGame()。
第三节 实现逻辑调色板时
case WM_QUERYNEWPALETTE:
if (!bFullscreen && lpPalette && lpFrontBuffer)
{
HRESULT ddrval;
ddrval = IDirectDrawSurface_SetPalette(lpFrontBuffer,lpPalette);
if( ddrval == DDERR_SURFACELOST )
{
IDirectDrawSurface_Restore( lpFrontBuffer );
ddrval= IDirectDrawSurface_SetPalette(lpFrontBuffer,lpPalette);
if( ddrval == DDERR_SURFACELOST )
{
Msg(" Failed to restore palette after second try");
}
}
if( ddrval == DD_OK )
{
SetWindowText( hWnd, OUR_APP_NAME );
}
}
break;
WM_QUERYNEWPALETTE:在窗口收到输入焦点前发出,当窗口收到输入焦点后将返回显示它是否能实现逻辑调色板。
在这个消息获得后,程序先检测是否运行于窗口模式且调色板和前缓冲区已设定,然后设定一次调色板,若失败则恢复前缓冲区然后再试一次,如果仍然失败则输出错误信息。两次设定若成功一次则将标题改为“Win Fox Application”。
第四节 改变系统调色板时
case WM_PALETTECHANGED:
if ((HWND)wParam != hWnd)
{
if( !bFullscreen )
{
if( !bStress )
{
Msg("***** PALETTE CHANGED, PAUSING GAME");
PauseGame();
}
else
{
Msg("Lost palette but continuing");
SetWindowText( hWnd, OUR_APP_NAME " - palette changed COLORS PROBABLY WRONG" )
}
}
}
break;
WM_PALETTECHANGED:本消息在拥有输入焦点的当前窗口实现其逻辑调色板时送往所有的窗口。这时,系统调色板被改变,本消息允许不带输入焦点的窗口使用调色板去实现自己的逻辑调色板和更新其用户区域。
在得到这个消息后,程序首先是否是当前窗口改变了系统调色板,如果是则直接跳出窗口过程,若不是则再检测是否是全屏模式,若是则直接跳出窗口过程,若非则先检测bStress是否为FLASE,若为FLASE则暂停游戏,若为TURE则将标题条改为“ - palette changed COLORS PROBABLY WRONG”。
第五节当操作键按下时
WM_KEYDOWN消息是在一个非系统键按下时产生的,非系统键是指没有按下ALT键时按下的键,或是当某窗口已有输入焦点时按下的键。
在该消息的wParam参数中包含了识别所按下的键的虚键码,由不同的虚键码就可以完成键盘对游戏的操作。我们知道键盘对游戏的操作中所按的键可以分为:操作键和功能键两类。下面让我们先看看例程中是如何定义操作键的吧。
在上一章我们就介绍过在本例程中操作键是小建盘上的“2345678”,但一直没有谈到如何实现的,现在就让我们来看一看。
对操作键的功能的定义是在
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;
您可以看到在得到每个虚键码之后都对lastInput进行赋值,这时就完成了对操作键的定义了,至于操作键是如何其作用的,在本章的第八节中您可以看到。
第六节 当功能键按下时
在本游戏中的功能键是F3、F4、F5、F6、F7、F8、F9。这些键的作用时什么,是如何实现的呢?下面就让我们一个一个的看看吧!
1.F3的作用是暂停游戏和解除暂停。在程序中这个作用是这样实现的:
case VK_F3:
bPaused = !bPaused;
break;
2.F4的作用是实现ALT+ENTER的作用。在程序中是使用
case VK_F4:
PostMessage(hWnd, WM_SYSKEYUP, VK_RETURN, 0);
break;
4.F6的作用是逐个使用在显示模式列表中的显示模式。
case VK_F6:
{
static i;
if(bFullscreen)
{
for (i=0; i<NumModes; i++)
{
if (ModeList[i].bpp == (int)GameBPP && ModeList[i].w == GameSize.cx && ModeList[i].h == GameSize.cy)
{
break;
}
}
}else
{
for (i=0; i<NumModes; i++)
{
if (ModeList[i].w == GameSize.cx &&ModeList[i].h == GameSize.cy)
{
break;
}
}
}
if (++i >= NumModes)
{
i = 0;
}
Msg("ModeList %d %d",i,NumModes);
GameMode.cx = ModeList[i].w;
GameMode.cy = ModeList[i].h;
GameBPP = ModeList[i].bpp;
bStretch = FALSE;
InitGame();
}
break;
在收到VK_F6的消息后,程序设立一个初值为0的变量,并以该变量为存放显示模式列表的数组的下标,然后令其自加直至找到一个与当前模式相同的。然后令其自加1,取以该值为下标的元素为显示模式,最后重新初始化游戏。
5.F7的作用是改变颜色数。
case VK_F7:
GameBPP = GameBPP == 8 ? 16 : 8;
InitGame();
break;
6.F8的作用是决定是否使用拉伸算法。
case VK_F8:
if (bFullscreen)
{
bStretch = !bStretch;
InitGame();
}
else
{
RECT rc;
GetClientRect(hWnd, &rc);
bStretch = (rc.right != GameSize.cx) || (rc.bottom != GameSize.cy);
if (bStretch = !bStretch)
SetRect(&rc, 0, 0, GameMode.cx*2, GameMode.cy*2);
else
SetRect(&rc, 0, 0, GameMode.cx, GameMode.cy);
AdjustWindowRectEx(&rc,GetWindowStyle(hWnd),GetMenu(hWnd) != NULL,GetWindowExStyle(hWnd));
SetWindowPos(hWnd, NULL, 0, 0, rc.right-rc.left, rc.bottom-rc.top,SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
}
break;
这部分的运行过程为:若为全屏模式则对 bStretch取非,然后初始化游戏。若为窗口模式,则取得当时窗口的特征,以确定bStretch的值,然后从新显示窗口。
7.F9的作用是取消使用软件模拟并逐个使用已有的驱动程序
case VK_F9:
DevIndex ++;
bUseEmulation = FALSE;
if (DevIndex >= MaxDevIndex)
DevIndex = 0;
ExitGame();
DDDisable(TRUE); // destroy DirectDraw object
InitGame();
break;
第七节 其他消息
case WM_DISPLAYCHANGE:
break;
case WM_CREATE:
break;
这两个消息收到后,将不做任何反应。
case WM_SETCURSOR:
if (bFullscreen && bIsActive)
{
SetCursor(NULL);
return TRUE;
}
break;
该信息在光标随鼠标的移动而输入未被捕获时发出。
由于本游戏不需鼠标,故在随后的检测是否是全屏模式或是被激活的语句,获得肯定的答案后,将光标从屏幕上删除。
case WM_PAINT:
hdc = BeginPaint( hWnd, &ps );
if (bPaused)
{
char *sz = "Game is paused, this is not a bug.";
TextOut(ps.hdc, 0, 0, sz, lstrlen(sz));
}
EndPaint( hWnd, &ps );
return 1;
该消息在请求重新绘制应用程序窗口是发出 。
程序在接收到这个消息后就调用BeginPaint()为hWnd所指的窗口作好绘画准备。然后判断游戏是否暂停,若是则向屏幕输出Game is paused, this is not a bug.最后调用EndPaint()
case WM_DESTROY:
hWndMain = NULL;
lastInput=0;
DestroyGame(); // end of game
DDDisable(TRUE); // destroy DirectDraw object
PostQuitMessage( 0 );
break;
该消息在要撤消某窗口时向该窗口发送。
在收到这个消息后程序令指向窗口的句柄为NULL,对游戏的刷新单元的下一次输入设为0,然后清除游戏及游戏工所占的内存。
第八节 刷新游戏单元
在讨论完窗口过程后,我们应开始介绍这个游戏的消息循环部分了。在这个游戏的消息循环部分中大部分在第二章 windows编程基础中已经谈到过了,所以在这里我们将只介绍刷新游戏单元和重画游戏单元部分。在消息循环中程序是调用函数ProcessFox(SHORT sInput)来进行这两部分的工作的。在ProcessFox(SHORT sInput)函数中,先对游戏是否正在运行或恢复游戏运行是否成功作了一个检测,然后就先调用ProcessInput(sInput)函数进行刷新游戏单元部分,再调用NewGameFrame()进行重画游戏单元的工作。下面就让我们先看看刷新游戏单元部分吧。
这一部分的运行过程为:
1.狐狸的行为刷新
首先取得当前狐狸的的速度、行为和方向。然后检测是否获得狐狸的位置或输入是否是4209,若检测的表达式为TURE则输入为0,即没有输入,若为FLASE则开始对各种输入进行响应。在响应的过程中程序先对狐狸的当前状态进行判断,然后根据狐狸的当前状态决定下一帧狐狸的基本状态。比如“↓”键或小键盘的“2”被按下时的响应过程为
case KEY_DOWN:
if( foxAction == STOP )
{
break;
}
else if( foxAction == STILL )
{
SetSpriteAction( hFox, CROUCH, SAME );
}
else if( foxAction == WALK )
{
SetSpriteAction( hFox, CROUCHWALK, SAME );
}
break;
在“↓”键被按下时,如果狐狸的动作是在急停的,则跳出;如果狐狸是静止的,则狐狸蹲下不动;若狐狸是在移动,则狐狸的行动改为爬行。
在对输入的初步处理后,程序便结合狐狸行为的其他属性,确定下一帧狐狸的状态。在这里同样是用switch——case语句和大量的if——else语句完成的。
2.熊和苹果的行为刷新
这两部分的运行过程实际上是一致的,都是首先进行碰撞检测,然后再根据检测结果决定角色新的行为属性。
第九节 重画游戏单元
这部分的工作是由NewGameFrame()完成的。这个函数首先调用SetSpriteX() 、SetSpriteY()两函数设定角色的新位置,再调用SetPlaneVelX()和SetPlaneX()函数设定三层背景的移动速度和下一帧的位置。然后通过检测bTransDest的值决定是先将角色的位图粘贴到Backbuffer还是先将背景的位图粘贴到BackiBuffer,
最后调用函数gfxSwapBuffers()进行页面翻转将后备缓冲区中的图显示到屏幕上。该函数是这样实现的:
BOOL gfxSwapBuffers( void )
{
if( bFullscreen )
{
if( bStretch )
{
gfxStretchBackbuffer();
}
if (nBufferCount > 1)
return gfxFlip();
else
return TRUE;
}
else
{
return gfxUpdateWindow();
}
它的运行过程是这样的:在全屏模式下且使用拉伸算法时用gfxStretchBackbuffer()对后备缓冲区和拉伸缓冲区之间进行blt操作,然后在有一个以上(不含一)的后备缓冲区时用gfxFlip()调用IDirectDrawSurface_Flip( lpFrontBuffer, NULL, DDFLIP_WAIT )进行一次页面翻转操作将后备缓冲区中的图显示到屏幕上。如果是窗口模式下,则通过gfxUpdateWindow()调用IDirectDrawSurface_Blt( lpFrontBuffer,&rcWindow,lpBackBuffer,NULL,DDBLT_WAIT,NULL)直接将后备缓冲区中的内容blt到主缓冲区中。这样就完成了重画游戏单元的任务了。