Stunt Jumper 是一个横向滚动的摩托车跳跃游戏,玩家必须仔细控制摩托车手的速度,以便帮助它越过一排汽车。
本章内容包括:
- Stunt Jumper 的基本思路
- 如何设计 Stunt Jumper 游戏
- 开发 Stunt Jumper 游戏的细节
- 如何驾驶摩托车腾空而没有受重伤的危险
游戏的玩法
Stunt Jumper 中,玩家是一个喜爱特技的摩托车手,喜欢腾空越过排成一行的汽车。任何跳跃者都面临的一个挑战是,确定适合的速度,以便在越过障碍物的同时不要飞过了着陆斜面。如果速度太低,那么会碰到障碍物,而速度太高又会飞过着陆斜面,装在硬着陆区上。
玩Stunt Jumper 游戏的难点在于要快速改变摩托车的速度,以便要与跳跃的距离相符,而在每一次成功的跳跃之后,跳跃距离都会发生变化。更具体地说,在每一次跳跃之后,汽车的数量将发生变化,因此玩家必须经常衡量所需要的速度并相应地做出调整。玩家通过按左(减速)和右(加速)方向键来改变游戏中摩托车的速度。
Stunt Jumper 游戏中的图形对象包括摩托车手、汽车以及摩托车上的跳跃和着陆斜面。除了这些直接影响游戏玩法的对象之后,一个视差滚动背景有助于增添在视觉上的趣味性。这个背景包含几个以不同速度滚动的图层,从而提供了一种移动和深度的错觉。
游戏中的摩托车手总是水平经过游戏屏幕。虽然可以让他减速和加速,但是无法使他完全停下来。因为这样,玩家在跳跃之间就没有多少时间思考,所以这使游戏更加富有挑战性。完成一次跳跃之后,玩家必须立即开始调整速度,准备进行下一次跳跃。当玩家对速度估计错误,提前着地或者飞过了着陆斜面时,游戏就结束了。
设计游戏
对 Stunt Jumper 游戏有了基本的了解之后,我们继续着手几个与游戏的设计有关的细节。首先,这个游戏中最棘手的地方是使摩托车的移动看起来很真实。使摩托车水平经过屏幕不是什么大问题,但是在现实世界中,摩托车的跳跃时却不是水平的。从跳跃斜面起跳应该会使摩托车倾斜,使其看上去像是在行驶,而不是滑上斜面。此外,当摩托车腾空时,其倾斜度应该不断变化,使其开始沿弧形向上,然后沿弧形向下行驶。
因为我们需要能够改变摩托车起跳斜面、着陆斜面以及汽车的位置并检查它们与摩托车手之间的碰撞,所以它们都适合被创建为子画面。除了摩托车之外,所有子画面都是静态的。视差滚动背景只是用来使游戏更加美观的。下图显示Stunt Jumper 游戏中的各种元素,说明了游戏屏幕中的图形对象。
就子画面而言,游戏中的交互包含摩托车子画面撞上起跳斜面,开始一次跳跃。只是汽车相撞,就会导致跳跃失败,跳过了着陆斜面也会导致失败,因此,成功的跳跃意味着跳过汽车但是不要飞过着陆斜面。
Stunt Jumper 游戏中使用的子画面如下所示:
- 摩托车手子画面
- 起跳斜面子画面
- 着陆斜面子画面
- 汽车子画面
Stunt Jumper 游戏 使用的位图图像如下所示:
- 背景天空图像(参见图24.2)
- 背景山脉图像(参见图24.3)
- 背景树图像(参见图24.4)
- 背景公路图像(参见图24.5)
- 动画摩托车手图像(参见图24.6)
- 起跳斜面图像(参见图24.7)
- 着陆斜面图像(参见图24.8)
- 汽车图像(参见图24.9)
- 闪屏图像
- 游戏结束图像
摩托车手图像的动画帧(参加图24.6),这是游戏中唯一的一个动画图像,每一帧都显示了旋转角度不同的摩托车,用来显示摩托车在空中跳跃时具有不同的倾斜度。摩托车的中间位置(完全水平的位置)出现在第7帧,然后是6个在各个角度上倾斜的帧。摩托车图像一共包含13个动画帧,我们不能在游戏中不加区分地循环显示这些动画帧,因此将为摩托车手创建一个自定义的子画面类,以便智能地管理动画帧。
Stunt Jumper 游戏不记录难度级别,也不会记录游戏得分。
开发Stunt Jumper 游戏
注意:若出现编译错误,请在项目设置->连接->对象/库模块中 加入 msimg32.lib winmm.lib
Stunt Jumper 目录结构和效果图
Stunt Jumper 目录结构:
Stunt Jumper 效果图:
编写游戏代码
因为整个 Stunt Jumper 游戏都以摩托车手及其腾空功能为中心,所以我们先处理摩托车手子画面的代码。
摩托车手使用一个包含13个动画帧的位图,摩托车手处于水平位置的图像就在这些帧的中间。摩托车手子画面的难点在于确定如何循环显示这些动画帧,使摩托车在跳跃过程中的倾斜看起来很真实。
MotocycleSprite.h
MotocycleSprite.h 头文件声明了MotocycleSprite 类,它是从 Sprite 类派生的。
#pragma once
//-----------------------------------------------------------------
// 包含文件
//-----------------------------------------------------------------
#include <windows.h>
#include "Resource.h"
#include "Sprite.h"
//-----------------------------------------------------------------
// MotorcycleSprite 摩托车子画面类
//-----------------------------------------------------------------
class MotorcycleSprite : public Sprite
{
protected:
// 成员变量
const int m_iMINSPEED, m_iMAXSPEED; //摩托车的最小速度,最大速度
const int m_iHANGTIME; //控制摩托车在每次跳跃过程中在空中停留多长时间
BOOL m_bJumping; //摩托车是否在跳跃
int m_iJumpCounter; //摩托车在每次跳跃过程中在空中停留了多长时间
BOOL m_bLandedSafely; //摩托车是否安全着陆
public:
// 构造函数/析构函数
MotorcycleSprite(Bitmap* pBitmap, RECT& rcBounds,
BOUNDSACTION baBoundsAction = BA_STOP);
virtual ~MotorcycleSprite();
// 帮助器方法
virtual void UpdateFrame(); //更新动画帧
// 常规方法
void IncreaseSpeed(); //增加速度
void DecreaseSpeed(); //减少速度
void StartJumping(); //开始跳跃
void LandedSafely(); //将摩托车标记为安全着陆
BOOL HasLandedSafely() { return m_bLandedSafely; };
};
m_iHANGTIME 常量用来控制摩托车在每次跳跃过程中在空中停留多长时间,它的值越大,摩托车就跳的越高,在反复试验之后,我将这个值设置为6。
m_bJumping 存储了摩托车的跳跃状态,而m_bLandedSafely 成员变量纪录了摩托车是否成功跳起并安全着陆。m_iJumpCounter 是摩托车在每次跳跃过程中在空中停留了多长时间,用来控制每次跳跃的持续时间的计数器。开始一次跳跃时,将根据摩托车的速度和 m_iHANGTIME 来设置 m_iJumpCounter 变量的值,在跳跃的持续过程中,这个值将倒计数,直到 0 为止。
MotocycleSprite::UpdateFrame( )
MotocycleSprite 类 重写了 Sprite 类中的 UpdateFrame( ),它控制了摩托车动画帧的变化方式,以便产生跳跃的错觉。它的实际工作是以跳跃计数器为基础,确定对摩托车使用哪一个动画帧,使摩托车的倾斜度在跳跃过程中不断变化。
// 更改动画帧,使摩托车的倾斜度在跳跃过程中不断变化
void MotorcycleSprite::UpdateFrame()
{
if (m_bJumping)
{
// 开始跳跃
if (m_iJumpCounter-- >= 0)
{
// 查看摩托车是否在上升
if (m_iJumpCounter > (m_ptVelocity.x * m_iHANGTIME / 2))
{
// 更改帧,将摩托车显示为向上倾斜
m_iCurFrame = min(m_iCurFrame + 1, 12);
// 更改垂直速度,使摩托车上升
if (m_iJumpCounter % (m_iHANGTIME / 2) == 0)
m_ptVelocity.y++;
}
// 查看摩托车是否在下降
else if (m_iJumpCounter <= (m_ptVelocity.x * m_iHANGTIME / 2))
{
// 更改帧,将摩托车显示为向下倾斜
m_iCurFrame = max(m_iCurFrame - 1, 0);
// 更改垂直速度,使摩托车下降
if (m_iJumpCounter % (m_iHANGTIME / 2) == 0)
m_ptVelocity.y++;
}
}
else
{
// 停止跳跃并使摩托车保持水平
m_bJumping = FALSE;
m_iCurFrame = 6;
m_ptVelocity.y = 0;
// 查看摩托车是否跳过了着陆斜面
if (!m_bLandedSafely)
{
// 播放摧毁声音
PlaySound((LPCSTR)IDW_CRASH, g_hInstance, SND_ASYNC |
SND_RESOURCE);
// 结束游戏
m_ptVelocity.x = 0;
g_bGameOver = TRUE;
}
}
}
}
在每一个游戏周期中都要在摩托车子画面上调用 UpdateFrame( )方法,以便更新子画面的外观。UpdateFrame( )方法首先检查摩托车是否正在跳跃。如果是,则减小跳跃计数器,进行检查,以确保它仍然大于0,这表示摩托车仍然在空中。然后检查摩托车是在上升还是在下降,不论哪一种情况,都必须改变摩托车速度的Y部分。如果摩托车是在上升,那么因为摩托车的倾斜度在向上增加(从中间帧到末帧),因此动画帧增加。与此正好相反,在跳跃的下降部分,摩托车的倾斜度是从中间帧到第一帧,因此减小动画帧。
在跳跃代码中还要注意的是如何使用 m_iHANGTIME 常量来确定是否需要调整摩托车速度的 y 部分。在跳跃结束之后,将 m_bJumping 标记重新设置为 FLASE,将动画帧设置为中间帧,从而显示保持水平的摩托车,并将摩托车的垂直速度设置为0,使它重新行驶在公路上并经过屏幕。然后检查 m_bLanderSafely 标志,查看摩托车是否确实已经安全着陆。如果没有安全着陆,则播放碰撞声音,游戏结束。
m_bLanderSafely 标志 在摩托车初次开始跳跃时,清空这个标志,表示它还没有安全着陆。如果摩托车安全着陆了(撞到了着陆斜面),则将这个标志设置为 TRUE,否则,m_bLanderSafely 标志会保持为 FALSE。
MotocycleSprite::StartJumping( )
MotocycleSprite::StartJumping( ) 方法,显示开始跳跃,帮助控制摩托车子画面中的跳跃逻辑。
// 开始跳跃
void MotorcycleSprite::StartJumping()
{
if (!m_bJumping)
{
// 开始摩托车跳跃
m_iJumpCounter = m_ptVelocity.x * m_iHANGTIME;
m_ptVelocity.y = -m_ptVelocity.x;
m_bJumping = TRUE;
m_bLandedSafely = FALSE;
}
}
MotocycleSprite::StartJumping( ) 方法首先确保摩托车还没有开始跳跃,根据 m_iHANGTIME 常量的值以及摩托车当前速度来计算跳跃计数器m_iJumpCounter 的初始值。为了使摩托车起跳,其垂直速度设置为其水平速度的相反值,因为Windows 中的图像坐标系是向屏幕下方增加,所以垂直速度必须为负数。在这里,摩托车向屏幕上方跳跃,它的y位置的值则在减小。
MotocycleSprite::LandedSafely( )
MotocycleSprite::LandedSafely( ) 方法,将m_bLanderSafely 标志 设置为 TRUE,表示摩托车已经安全着陆,帮助控制摩托车子画面中的跳跃逻辑。
void MotorcycleSprite::LandedSafely()
{
// 将摩托车标记为安全着陆(击中着陆斜面)
m_bLandedSafely = TRUE;
}
MotocycleSprite::IncreaseSpeed( )
MotocycleSprite::IncreaseSpeed( ) 方法增加摩托车子画面的速度。
void MotorcycleSprite::IncreaseSpeed()
{
if (!m_bJumping)
// 增加摩托车的水平速度
m_ptVelocity.x = min(m_ptVelocity.x + 1, m_iMAXSPEED);
}
MotocycleSprite::DecreaseSpeed( )
MotocycleSprite::DecreaseSpeed( )方法减小摩托车子画面的速度。
void MotorcycleSprite::DecreaseSpeed()
{
if (!m_bJumping)
// 减小摩托车的水平速度
m_ptVelocity.x = max(m_ptVelocity.x - 1, m_iMINSPEED);
}
StuntJumper.h
StuntJumper.h 头文件用来声明管理游戏的全局变量。
#pragma once
//-----------------------------------------------------------------
// Include Files
//-----------------------------------------------------------------
#include <windows.h>
#include "Resource.h"
#include "GameEngine.h"
#include "Bitmap.h"
#include "Sprite.h"
#include "ScrollingBackground.h"
#include "MotorcycleSprite.h"
//-----------------------------------------------------------------
// Global Variables
//-----------------------------------------------------------------
HINSTANCE g_hInstance; //程序实例句柄
GameEngine* g_pGame; //游戏引擎指针
HDC g_hOffscreenDC; //屏幕外设备环境
HBITMAP g_hOffscreenBitmap; //屏幕外位图
Bitmap* g_pSplashBitmap; //闪屏位图
BackgroundLayer* g_pBGRoadLayer; //背景公路
BackgroundLayer* g_pBGTreesLayer; //背景树
BackgroundLayer* g_pBGMountainsLayer; //背景山脉
BackgroundLayer* g_pBGSkyLayer; //背景天空
Bitmap* g_pJumperBitmap; //起跳斜面
Bitmap* g_pBusBitmap; //汽车
Bitmap* g_pRampBitmap[2]; //斜道
Bitmap* g_pGameOverBitmap; //游戏结束位图
ScrollingBackground* g_pBackground; //多图层滚动背景
MotorcycleSprite* g_pJumperSprite; //摩托车
Sprite* g_pLaunchRampSprite; //开始的斜道
Sprite* g_pLandingRampSprite; //着陆的斜道
Sprite* g_pBusSprite[7]; //汽车子画面
int g_iInputDelay; //输入延迟
BOOL g_bGameOver; //是否游戏结束
BOOL g_bSplash; //是否在闪屏
//-----------------------------------------------------------------
// 独有的方法
//-----------------------------------------------------------------
void NewGame(); //开始新游戏
void NewJump(int iNumBuses); //安排斜面和安置汽车子画面
GameStart( )
GameStart( ) 函数初始化游戏的位图和滚动背景。
// 开始游戏
void GameStart(HWND hWindow)
{
// 生成随机数生成器种子
srand(GetTickCount());
// 创建屏幕外设备环境和位图
g_hOffscreenDC = CreateCompatibleDC(GetDC(hWindow));
g_hOffscreenBitmap = CreateCompatibleBitmap(GetDC(hWindow),
g_pGame->GetWidth(), g_pGame->GetHeight());
SelectObject(g_hOffscreenDC, g_hOffscreenBitmap);
// 创建并加载位图
HDC hDC = GetDC(hWindow);
g_pSplashBitmap = new Bitmap(hDC, IDB_SPLASH, g_hInstance);
g_pJumperBitmap = new Bitmap(hDC, IDB_JUMPER, g_hInstance);
g_pBusBitmap = new Bitmap(hDC, IDB_BUS, g_hInstance);
g_pRampBitmap[0] = new Bitmap(hDC, IDB_RAMPLEFT, g_hInstance);
g_pRampBitmap[1] = new Bitmap(hDC, IDB_RAMPRIGHT, g_hInstance);
g_pGameOverBitmap = new Bitmap(hDC, IDB_GAMEOVER, g_hInstance);
// 创建滚动背景和图层
g_pBackground = new ScrollingBackground(750, 250);
g_pBGSkyLayer = new BackgroundLayer(hDC, IDB_BG_SKY, g_hInstance, 1, SD_LEFT);
g_pBackground->AddLayer(g_pBGSkyLayer);
g_pBGMountainsLayer = new BackgroundLayer(hDC, IDB_BG_MOUNTAINS, g_hInstance, 2, SD_LEFT);
g_pBackground->AddLayer(g_pBGMountainsLayer);
g_pBGTreesLayer = new BackgroundLayer(hDC, IDB_BG_TREES, g_hInstance, 3, SD_LEFT);
g_pBackground->AddLayer(g_pBGTreesLayer);
g_pBGRoadLayer = new BackgroundLayer(hDC, IDB_BG_ROAD, g_hInstance);
g_pBackground->AddLayer(g_pBGRoadLayer);
// 设置闪屏变量
g_bSplash = TRUE;
g_bGameOver = TRUE;
}
向滚动背景添加图层的顺序很重要,总是首先添加后面的图层,最后添加最近的图层。
GamePaint( )
GamePaint( ) 函数绘制闪屏图像、视差滚动背景、子画面以及游戏结束消息。
// 绘制游戏
void GamePaint(HDC hDC)
{
// 绘制滚动背景
g_pBackground->Draw(hDC, TRUE);
if (g_bSplash)
{
// 绘制闪屏图像
g_pSplashBitmap->Draw(hDC, 175, 15, TRUE);
}
else
{
// 绘制子画面
g_pGame->DrawSprites(hDC);
// 绘制游戏结束图像
if (g_bGameOver)
g_pGameOverBitmap->Draw(hDC, 175, 15, FALSE);
}
}
GameCylce( )
GameCylce( ) 函数更新背景和子画面,并检查摩托车是否驶过了游戏屏幕。
// 游戏循环
void GameCycle()
{
if (!g_bGameOver)
{
// 更新背景
g_pBackground->Update();
// 更新子画面
g_pGame->UpdateSprites();
// 获取设备环境以重新绘制游戏
HWND hWindow = g_pGame->GetWindow();
HDC hDC = GetDC(hWindow);
// 在屏幕外设备环境上绘制游戏
GamePaint(g_hOffscreenDC);
// 将屏幕外位图位块传送到游戏屏幕
BitBlt(hDC, 0, 0, g_pGame->GetWidth(), g_pGame->GetHeight(),
g_hOffscreenDC, 0, 0, SRCCOPY);
// 清理
ReleaseDC(hWindow, hDC);
// 查看摩托车是否驶过了屏幕
RECT& rc = g_pJumperSprite->GetPosition();
if (rc.right > g_pGame->GetWidth())
// 创建另一次跳跃(最多7期汽车)
NewJump(rand() % 7 + 1);
}
}
除了更新滚动背景和子画面之外,GameCylce( ) 函数还负责确定何时创建一个新跳跃。思路是在摩托车每一次驶过屏幕时,游戏都创建一个新跳跃,这以为着新跳跃的创建是由摩托车从屏幕右侧环绕到左侧所触发的。创建一个新跳跃只涉及更改要跳过的汽车数量以及重新安放汽车和斜面。
HandleKeys( )
HandleKeys( ) 函数允许用户使用键盘上的键来控制摩托车手的速度。
// 键盘监听事件
void HandleKeys()
{
if (!g_bGameOver)
{
// 按左/右方向键时移动摩托车手
POINT ptVelocity = g_pJumperSprite->GetVelocity();
if (g_iInputDelay++ > 1)
{
if (GetAsyncKeyState(VK_LEFT) < 0)
{
// 播放刹车声音
PlaySound((LPCSTR)IDW_BRAKES, g_hInstance, SND_ASYNC |
SND_RESOURCE);
// 降低速度(左键降低速度)
g_pJumperSprite->DecreaseSpeed();
// 重置输入延迟
g_iInputDelay = 0;
}
else if (GetAsyncKeyState(VK_RIGHT) < 0)
{
// 播放发动机声音
PlaySound((LPCSTR)IDW_ENGINE, g_hInstance, SND_ASYNC |
SND_RESOURCE);
// 右方向家,加速
g_pJumperSprite->IncreaseSpeed();
// 重置输入延迟
g_iInputDelay = 0;
}
}
}
// 按Enter键看是新游戏
if (GetAsyncKeyState(VK_RETURN) < 0)
if (g_bSplash)
{
// 开始没有闪屏的新游戏
g_bSplash = FALSE;
NewGame();
}
else if (g_bGameOver)
{
// 开始新游戏
NewGame();
}
}
SpriteCollison( )
SpriteCollison( ) 响应摩托车与汽车和斜面子画面之间的碰撞。
// 碰撞检测函数
BOOL SpriteCollision(Sprite* pSpriteHitter, Sprite* pSpriteHittee)
{
Bitmap* pHitter = pSpriteHitter->GetBitmap();
Bitmap* pHittee = pSpriteHittee->GetBitmap();
// 只检查没有隐藏的子画面之间的碰撞
if (!pSpriteHitter->IsHidden() && !pSpriteHittee->IsHidden())
{
// 查看摩托车是否击中了起跳斜面
if ((pHitter == g_pJumperBitmap) && (pHittee == g_pRampBitmap[0]))
{
// 开始跳跃
g_pJumperSprite->StartJumping();
}
// 查看摩托车是否击中了着陆斜面
else if ((pHitter == g_pJumperBitmap) && (pHittee == g_pRampBitmap[1]))
{
if (!g_pJumperSprite->HasLandedSafely())
{
// 播放庆祝声音
PlaySound((LPCSTR)IDW_CELEBRATION, g_hInstance, SND_ASYNC |
SND_RESOURCE);
// 指出摩托车安全着陆了
g_pJumperSprite->LandedSafely();
}
}
// 查看摩托车是否击中了汽车
else if ((pHitter == g_pJumperBitmap) && (pHittee == g_pBusBitmap))
{
// 播放撞毁声音
PlaySound((LPCSTR)IDW_CRASH, g_hInstance, SND_ASYNC |
SND_RESOURCE);
// 结束游戏
g_bGameOver = TRUE;
}
}
return FALSE;
}
NewGame( )
NewGame( ) 函数为开始一个新游戏做好准备。
// 开始一个新游戏
void NewGame()
{
// 清空子画面
g_pGame->CleanupSprites();
// 初始化游戏变量
g_iInputDelay = 0;
g_bGameOver = FALSE;
// 创建斜面和汽车子画面
RECT rcBounds = { 0, 0, 750, 250 };
g_pLaunchRampSprite = new Sprite(g_pRampBitmap[0], rcBounds);
g_pGame->AddSprite(g_pLaunchRampSprite);
g_pLandingRampSprite = new Sprite(g_pRampBitmap[1], rcBounds);
g_pGame->AddSprite(g_pLandingRampSprite);
for (int i = 0; i < 7; i++)
{
g_pBusSprite[i] = new Sprite(g_pBusBitmap, rcBounds);
g_pGame->AddSprite(g_pBusSprite[i]);
}
// 创建摩托车手子画面
g_pJumperSprite = new MotorcycleSprite(g_pJumperBitmap, rcBounds, BA_WRAP);
g_pJumperSprite->SetNumFrames(13);
g_pJumperSprite->SetVelocity(4, 0);
g_pJumperSprite->SetPosition(0, 200);
g_pGame->AddSprite(g_pJumperSprite);
// 设置第一次跳跃最多3辆汽车
NewJump(rand() % 3 + 1);
// 播放背景音乐
g_pGame->PlayMIDISong(TEXT("Music.mid"));
}
NewJump( )
NewJump( ) 函数排列汽车并重新安放斜面,准备好一个新跳跃。
// 排列汽车并重新安放斜面,准备好一个新跳跃
void NewJump(int iNumBuses)
{
// 设置起跳斜面的位置
int iXStart = (g_pGame->GetWidth() / 2) - (iNumBuses * 40);
g_pLaunchRampSprite->SetPosition(iXStart, 215);
// 设置汽车的位置和可见性
for (int i = 0; i < 7; i++)
{
if (i < iNumBuses)
{
// 排列并显示汽车
g_pBusSprite[i]->SetPosition(iXStart + g_pRampBitmap[0]->GetWidth() +
5 + i * g_pBusBitmap->GetWidth(), 200);
g_pBusSprite[i]->SetHidden(FALSE);
}
else
{
// 隐藏这些汽车
g_pBusSprite[i]->SetPosition(0, 0);
g_pBusSprite[i]->SetHidden(TRUE);
}
}
// 设置着陆斜面的位置
g_pLandingRampSprite->SetPosition(iXStart + g_pRampBitmap[0]->GetWidth() +
5 + iNumBuses * g_pBusBitmap->GetWidth() + 5, 215);
}