本文将介绍如何开发一个存储在磁盘上并且可以在游戏之间保留的高分列表。
本文内容包括:
- 如何在游戏中表示高分数据
- 如何使用文件存储和检索高分数据
- 如何向 Space Out 游戏中添加高分列表
确定高分数据模型
本文只记录游戏前5名的高分列表,因为不必考虑存储各个得分的玩家姓名,所以只需存储5个数字即可。
对于Space Out 游戏 来说,得分不大可能超过4位数字,意味着完全可以用一个5位数来存储最高得分。高分中的最大位数很重要,因为即使一个得分不需要那么多位,我们也要存储这么多位,这使得将得分写入磁盘之后更容易读取它们。
一个简单的整数数组就足以表示这个高分列表。因此,读取和写入高分列表的过程涉及初始化和存储一个整数数组。
存储和检索高分数据
整数数组中的每一个数字都转换成了5位数字。这些数字其实不是数字,而是文本字符。换句话说,在写入文件之前,将数字1250转换成了01250。因为这个过程,将数值数据转换为了字符流。
Windows 中的所有文件输入和输出都涉及一组Win32 API 函数,其中最重要的函数是 CreateFile( ),如下所示:
HANDLE WINAPI CreateFile(
LPCTSTR szFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile
);
CreateFile( ) 函数既用来创建文件,又用来打开文件。这里我们只介绍与读取和写入高分有关的参数。
第一个参数是 szFileName,它提供要创建或者打开的文件的名称。第二个参数是 dwDesiredAccess,它决定了我们想要这个文件做什么,例如从其中读取数据或者写入数据。第5个参数是 dwCreationDisposition,它决定了想要打开一个现有的文件还是创建一个新文件。最后,第6个参数是dwFlagsAndAttributes,它允许控制打开或者创建文件的其他方面,例如是否希望将访问权限限制为只读。
下面这个例子使用CreateFile( ) 函数创建一个准备用于写入的新文件。
HANDLE hFile=CreateFile(TEXT("HiScores.dat"),GENERIC_WRITE,0,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);
在这个例子中,新文件的名称是 HiScores.dat 。这里创建了这个文件,并准备好使用CreateFile( ) 函数返回的文件句柄进行写入。以后使用一个写入函数来将数据实际写入这个文件时,就使用hFile 句柄。下面这个例子显示了如何打开一个文件并进行读取。
HANDLE hFile=CreateFile(TEXT("HiScores.dat"),GENERIC_READ,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_READONLY,NULL);
第2个参数指出这是一个读取操作,第5个参数指出应该打开一个现有的文件而不是创建一个新文件,第6个参数文件时只读的,这可以防止使用所提供的文件句柄写入它(即使你想这么做也写不进去,因为设置了这个属性)。
现在已经有了可以用来读取和写入(取决于调用 CreateFile( )函数的方式)的文件句柄,接下来了解如何将数据写入文件,然后将其读取出来了。
用来将数据写入文件的 Win32 API 函数名为 WriteFile( ),如下所示。
BOOL WriteFile(
HANDLE hFile, //文件句柄
LPCVOID lpBuffer, //数据缓存区指针
DWORD nNumberOfBytesToWrite, //你要写的字节数
LPDWORD lpNumberOfBytesWritten, //用于保存实际写入字节数的存储区域的指针
LPOVERLAPPED lpOverlapped //OVERLAPPED结构体指针
);
与 CreateFile( ) 函数一样,对我们而言,并非 WriteFile( ) 的所有参数都重要。寿险我们需要在第1个参数中提供文件句柄,还要提供写入数据的一个数据缓冲区,指定要写入的字节数以及一个 DWORD 指针也很重要,这个 DWORD 指针接受实际写入的字节数。
下面列出了将一个5位数的高分写入文件的代码。
DWORD dwBytesWritten;
WriteFile(hFile,&cData,5,&dwBytesWritten,NULL);
这段代码假定已经将高分从一个整数转换为一个包含5个字符的字符串,并且存储在了 cData 变量中。
从文件中读取是 ReadFile( ) 函数。下面是Win32 API 中 ReadFile( ) 函数的定义:
BOOL ReadFile(
HANDLE hFile, //文件的句柄
LPVOID pBuffer, //用于保存读入数据的一个缓冲区
DWORD dwNumberOfBytesToRead, //要读入的字节数
LPDWORD pNumberOfBytesRead, //指向实际读取字节数的指针
LPOVERLAPPED pOverlapped
//如文件打开时指定了FILE_FLAG_OVERLAPPED,那么必须,用这个参数引用一个特殊的结构。
//该结构定义了一次异步读取操作。否则,应将这个参数设为NULL
);
可以看出, ReadFile( ) 函数的参数实际上与 WriteFile( ) 函数的参数相同。不过,在参数的用法上略有不同,因为是将数据读取到缓冲区中而不是从缓冲区中写入。下面这例子使用 ReadFile( ) 函数从文件中读取一个单独的高分。
char cData[6];
DWORD dwBytesRead;
ReadFile(hFile,&cData,5,&dwByteRead,NULL);
让存储各个得分的字符数组以NULL结束很重要,因此将cData 变量声明为6个字符而不是5个字符。在这段代码中,ReadFile( ) 函数从文件中读取5个字符并将其存储在cData 变量中。
完成了读取或写入文件后,还需要关闭文件。可以通过调用Win32 API 的 CloseHandle( )函数来完成,其调用方法如下。
CloseHandle();
开发 Space Out 4 游戏
对比与之前的 Space Out 3,我们将拥有高分列表的新版本命名为 Space Out 4 。
注意:若出现编译错误,请在项目设置->连接->对象/库模块中 加入 msimg32.lib winmm.lib
Space Out 4 目录结构和效果图
Space Out 4 目录结构:
Space Out 4 效果图:
编写游戏代码
Space Out 4 游戏只需要一个新的全局变量,它就是存储高分列表的整数数组。下面是SpaceOut.h 头文件中出现的 g_iHiScores 变量。
int g_iHiScores[5]; //存储最高分的数组
这个变量存储了游戏的前5个高分,得分从高到低排列。注意,这些得分都是整数数值,以为着它们还没有被格式化为要写入文件的字符。
还需要添加更新、读取和写入高分的几个新函数,如下所示。
void UpdateHiScores(); //更新最高分
BOOL ReadHiScores(); //读取最高分
BOOL WriteHiScores(); //写入最高分
GameStart( ) 函数负责初始化游戏,这个函数唯一的变化是新调用了ReadHiScores( ) 函数。
// 读取最高分
ReadHiScores();
GameEnd( )函数清理游戏使用的资源,这里很适合将高分列表写入进文件里,只需在这个函数中一行代码即可写入高分列表。
// 将最高分写入文件中
WriteHiScores();
显示高分列表的工作发生在GamePaint( ) 函数中。
GamePaint( )
如果游戏处于演示模式,那么 GamePaint( ) 函数将绘制高分列表。
// 绘制游戏
void GamePaint(HDC hDC)
{
// 绘制背景
g_pBackground->Draw(hDC);
// 绘制沙漠位图
g_pDesertBitmap->Draw(hDC, 0, 371);
// 绘制子画面
g_pGame->DrawSprites(hDC);
if (g_bDemo)
{
// 绘制闪屏位图
g_pSplashBitmap->Draw(hDC, 142, 100, TRUE);
// 绘制高分
TCHAR szText[64];
RECT rect = { 275, 250, 325, 270};
SetBkMode(hDC, TRANSPARENT);
SetTextColor(hDC, RGB(255, 255, 255));
for (int i = 0; i < 5; i++)
{
wsprintf(szText, "%d", g_iHiScores[i]);
DrawText(hDC, szText, -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
rect.top += 20;
rect.bottom += 20;
}
}
else
{
// 绘制得分
TCHAR szText[64];
RECT rect = { 460, 0, 510, 30 };
wsprintf(szText, "%d", g_iScore);
SetBkMode(hDC, TRANSPARENT);
SetTextColor(hDC, RGB(255, 255, 255));
DrawText(hDC, szText, -1, &rect, DT_SINGLELINE | DT_RIGHT | DT_VCENTER);
// 绘制剩余生命(小汽车)数量
for (int i = 0; i < g_iNumLives; i++)
g_pSmCarBitmap->Draw(hDC, 520 + (g_pSmCarBitmap->GetWidth() * i),
10, TRUE);
// 绘制游戏结束
if (g_bGameOver)
g_pGameOverBitmap->Draw(hDC, 190, 149, TRUE);
}
}
受高分列表影响的最后一个游戏函数是 SpriteCollison( )函数,这个函数检查游戏何时结束(因为是碰撞函数,每碰撞一次都要检测是否还有生命,没生命则游戏结束)。
SpriteCollison( )
SpriteCollison( ) 函数在游戏结束时更新高分列表。
// 碰撞检测
BOOL SpriteCollision(Sprite* pSpriteHitter, Sprite* pSpriteHittee)
{
// 查看玩家子画面是否与外星人子画面相撞
Bitmap* pHitter = pSpriteHitter->GetBitmap();
Bitmap* pHittee = pSpriteHittee->GetBitmap();
if ((pHitter == g_pMissileBitmap && (pHittee == g_pBlobboBitmap ||
pHittee == g_pJellyBitmap || pHittee == g_pTimmyBitmap)) ||
(pHittee == g_pMissileBitmap && (pHitter == g_pBlobboBitmap ||
pHitter == g_pJellyBitmap || pHitter == g_pTimmyBitmap)))
{
// 播放较小的爆炸声音
PlaySound((LPCSTR)IDW_LGEXPLODE, g_hInstance, SND_ASYNC |
SND_RESOURCE);
// 删除这两个子画面
pSpriteHitter->Kill();
pSpriteHittee->Kill();
// 在外星人的位置创建一个较大的爆炸子画面
RECT rcBounds = { 0, 0, 600, 450 };
RECT rcPos;
if (pHitter == g_pMissileBitmap)
rcPos = pSpriteHittee->GetPosition();
else
rcPos = pSpriteHitter->GetPosition();
Sprite* pSprite = new Sprite(g_pLgExplosionBitmap, rcBounds);
pSprite->SetNumFrames(8, TRUE);
pSprite->SetPosition(rcPos.left, rcPos.top);
g_pGame->AddSprite(pSprite);
// 更新得分
g_iScore += 25;
g_iDifficulty = max(80 - (g_iScore / 20), 20);
}
// 查看外星人子画面是否与汽车相撞
if ((pHitter == g_pCarBitmap && (pHittee == g_pBMissileBitmap ||
pHittee == g_pJMissileBitmap || pHittee == g_pTMissileBitmap)) ||
(pHittee == g_pCarBitmap && (pHitter == g_pBMissileBitmap ||
pHitter == g_pJMissileBitmap || pHitter == g_pTMissileBitmap)))
{
// 播放较大的爆炸声音
PlaySound((LPCSTR)IDW_LGEXPLODE, g_hInstance, SND_ASYNC |
SND_RESOURCE);
// 删除导弹子画面
if (pHitter == g_pCarBitmap)
pSpriteHittee->Kill();
else
pSpriteHitter->Kill();
// 在汽车的位置创建一个较大的爆炸子画面
RECT rcBounds = { 0, 0, 600, 480 };
RECT rcPos;
if (pHitter == g_pCarBitmap)
rcPos = pSpriteHitter->GetPosition();
else
rcPos = pSpriteHittee->GetPosition();
Sprite* pSprite = new Sprite(g_pLgExplosionBitmap, rcBounds);
pSprite->SetNumFrames(8, TRUE);
pSprite->SetPosition(rcPos.left, rcPos.top);
g_pGame->AddSprite(pSprite);
// 将汽车移回开始位置,以模拟创建一辆新汽车
g_pCarSprite->SetPosition(300, 405);
// 查看游戏是否结束
if (--g_iNumLives == 0)
{
// 播放游戏结束声音
PlaySound((LPCSTR)IDW_GAMEOVER, g_hInstance, SND_ASYNC |
SND_RESOURCE);
g_bGameOver = TRUE;
g_iGameOverDelay = 150;
// 更新高分列表
UpdateHiScores();
}
}
return FALSE;
}
UpdateHiScores( )
UpdateHiScores( ) 函数查看是否应该向高分列表添加一个高分并更新高分列表。
// 更新高分列表
void UpdateHiScores()
{
// 查看当前得分是否足以进入高分列表
int i;
for (i = 0; i < 5; i++)
{
if (g_iScore > g_iHiScores[i])
break;
}
// 将当前得分插入高分列表
if (i < 5)
{
for (int j = 4; j > i; j--)
{
g_iHiScores[j] = g_iHiScores[j - 1];
}
g_iHiScores[i] = g_iScore;
}
}
WriteHiScores( )
WriteHiScores( ) 函数将高分列表写入 HiScores.dat 文件中。
// 将高分列表写入到 HiScores.dat 文件中
BOOL WriteHiScores()
{
// 创建要写入的高分文件(HiScores.dat)
HANDLE hFile = CreateFile(TEXT("HiScores.dat"), GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
// 创建失败
return FALSE;
// 写入得分
for (int i = 0; i < 5; i++)
{
// 格式化各个得分,将数值转化成字符流
CHAR cData[6];
wsprintf(cData, "%05d", g_iHiScores[i]);
// 写入得分
DWORD dwBytesWritten;
if (!WriteFile(hFile, &cData, 5, &dwBytesWritten, NULL))
{
// 出现错误,关闭文件句柄
CloseHandle(hFile);
return FALSE;
}
}
// 关闭文件
return CloseHandle(hFile);
}
ReadHiScores( )
ReadHiScores( ) 函数从 HiScores.dat 文件中读取高分列表。
// 从文件中读取高分列表
BOOL ReadHiScores()
{
// 打开高分文件 (HiScores.dat)
HANDLE hFile = CreateFile(TEXT("HiScores.dat"), GENERIC_READ, 0, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
// 高分文件不存在,初始化高分列表
for (int i = 0; i < 5; i++)
g_iHiScores[i] = 0;
return FALSE;
}
// 读取得分
for (int i = 0; i < 5; i++)
{
char cData[6];
DWORD dwBytesRead;
if (!ReadFile(hFile, &cData, 5, &dwBytesRead, NULL))
{
// 出现错误,关闭文件句柄
CloseHandle(hFile);
return FALSE;
}
// 从得分数据中提取各个得分 atoi() 将字符转化为整数
g_iHiScores[i] = atoi(cData);
}
// 关闭文件
return CloseHandle(hFile);
}