游戏编程入门(15):开发 MeteorDefense(抵御流星)游戏

本文开发一个用鼠标控制导弹系统,阻止从天空中落下的流星破坏城市的游戏。当陨石将所有城市摧毁完毕,则失败。

本文内容包括:

  • MeteorDefense游戏的概念介绍
  • 如何设计MeteorDefense 游戏
  • 如何向游戏引擎添加几个新的子画面特性

接上文 游戏编程入门(14):创建子画面背景


游戏的玩法

MeteorDefense 游戏的玩法是,向飞来的流星发射导弹,阻止他们破坏下面的城市。

MeteorDefense 的目标是尽可能长时间地保护城市避开飞来的流星,增加积分。但是,玩家每发射一枚导弹都会失分。这意味着,在向流星开火时,提高效率非常重要。

设计游戏

这个游戏非常适合上一章创建和使用的布满星星的动画子背景,而且对于流星来说应该用动画子画面表示流星。因为必须能够检测到流星与城市之间的碰撞,所以即使城市是不会移动的,也需要将它们作为子画面。将城市表示为子画面也使我们能够在它们被流星摧毁的时候将其隐藏。

游戏中的导弹移动并且会与流星产生碰撞,因此也很适合表示为子画面。在确定哪些图形对象应该表示为子画面,而哪些图形对象只需要放在背景上时(作为普通的位图),有一个不错的判断标准:如果对象需要移动、使用动画或与其他对象发生碰撞,那么它应该是一个子画面。 例如:发射导弹的发射架,既不会移动也不使用动画,也不需要检测它们与其他任何物体的碰撞,因此只需要将发射架作为普通的位图显示在背景图像中即可。

Meteor Defense 游戏实际上使用了一个混合背景,它在布满星星的背景之上显示了一副地面图像,并且也显示发射导弹的发射架。MeteorDefense 游戏的布局如下图所示:

游戏编程入门(15):开发 MeteorDefense(抵御流星)游戏_第1张图片

这幅图揭示了发射架如何构成游戏屏幕底部边缘的背景的一部分。布满星星的背景仍然占据大部分屏幕,背景图像只是显示城市所在的地面。城市子画面位于地面边缘的顶部,这样它们就像与地面图像浑然一体。导弹和流星子画面在布满星星的背景之上移动,而分数显示在游戏屏幕顶部的中央。

下面是在MeteorDefense 游戏中使用的子画面列表:

  • 城市子画面
  • 流星子画面
  • 导弹子画面
  • 爆炸子画面
  • 靶心子画面

城市子画面出现在屏幕底部,流星子画面是随机创建的,从屏幕顶部向着底部的城市下落。导弹子画面是从屏幕上发射架的位置向上面的流星发出的。在每一次流星被击中或者城市被摧毁时都会出现爆炸子画面,它显示一个剧烈爆炸的动画。最后,靶心子画面显示为一个十字,玩家使用鼠标来引导它,用来定位通过鼠标左键发射的导弹。

要想制作子画面,MeteorDefense 游戏需要几个位图图像。下面是这个游戏所需的7个位图图像:

  • 背景地面图像
  • 城市图像(参见图17.2)
  • 动画流星图像(参见图17.3)
  • 导弹图像(参见图17.4)
  • 动画爆炸图像(参见图17.5)
  • 十字靶心图像(参见图17.6)
  • 游戏结束图像

游戏编程入门(15):开发 MeteorDefense(抵御流星)游戏_第2张图片

准备好图像对象之后,开始考虑游戏维护的其他数据。例如,在整个游戏中需要维护一个分数以及剩余的城市数。当4个城市都被流星摧毁时,游戏便结束了。对于这个游戏来说,随着游戏的进展提高其难度级别非常重要。因此,要保存游戏的难度级别,并随着玩家继续保护好城市而逐渐增加难度。还需要一个记录游戏是否结束的布尔变量。

MeteorDefense 游戏 需要管理一下几个信息:

  • 剩余的城市数
  • 分数
  • 难度级别
  • 布尔游戏结束变量

增强游戏引擎中的子画面

在这里我们需要考虑爆炸子画面该如何工作。因为游戏引擎中的Sprite 子画面类现在支持动画子画面,所以动画显示部分很简单。但是,爆炸子画面必须循环显示动画帧后,然后立刻显示。这听起来很简单,但是却显露了游戏引擎的一个问题。

这个问题就是,目前还没一种在不再 需要子画面时 自动 隐藏或者删除它的机制(边界动作隐藏不可以,因为不一定该子画面消失时就位于边界,可能在屏幕其他地方)。

向游戏引擎添加这个功能的关键是先在Sprite 子画面类中添加两个新变量

  BOOL          m_bDying;        //子画面删除标志
  BOOL          m_bOneCycle;     //是否在显示所有帧之后删除子画面

普通子画面的m_bDying 都设置为FALSE,而将要删除的子画面的m_bDying 变量设置为TRUE。m_bOneCycle只对于帧动画子画面才有意义,所以在调用SetNumFrames( )方法时设置它。修改SetNumFrames( )方法如下:

 //声明部分
 void    SetNumFrames(int iNumFrames, BOOL bOneCycle = FALSE);
 //定义部分
 // 设置总帧数。通过设置子画面的帧数来将普通的子画面转化为动画子画面。
inline void Sprite::SetNumFrames(int iNumFrames, BOOL bOneCycle)
{
  // 设置帧数和m_bOneCycle
  m_iNumFrames = iNumFrames;
  m_bOneCycle = bOneCycle;

  // 计算位置
  RECT rect = GetPosition();
  rect.bottom = rect.top + ((rect.bottom - rect.top) / iNumFrames);
  SetPosition(rect);
}

因为增加了两个新的Sprite 成员变量。所以要在Sprite( )构造函数中初始化它们。

  m_bDying = FALSE;
  m_bOneCycle = FALSE;

各个变量的默认值都是FALSE,这使得子画面能正常工作。

还需要添加一个Kill( )方法,将m_bDying 成员变量的值设置为TRUE。

 void     Kill()      //删除子画面
  { 
      m_bDying = TRUE; 
  };

虽然Kill( ) 方法提供了一种删除子画面的直接方法,但是更完美的一种方法是允许子画面在完成帧动画循环时自己删除。 因此对UpdateFrame( )方法做出改善,增加检测m_bOneCycle 标志的判断:

// 更新子画面的当前动画帧
inline void Sprite::UpdateFrame()
{
 if ((m_iFrameDelay >= 0) && (--m_iFrameTrigger <= 0))
  {
    // 重置帧触发器
    m_iFrameTrigger = m_iFrameDelay;

    // 增加当前帧并检查是否超过总帧数
    if (++m_iCurFrame >= m_iNumFrames)
    {
      // 显示完所有帧后,删除子画面
      if (m_bOneCycle)
        m_bDying = TRUE;
      else
        m_iCurFrame = 0;
    }
  }
}

真正实现删除子画面的位置是在Update( )方法中

//根据速度更改子画面的位置,并根据其移动做出相应,从而更新子画面
SPRITEACTION Sprite::Update()
{
  // 查看是否需要删除子画面
  if (m_bDying)
    return SA_KILL;

  // 更新帧
  UpdateFrame();
  ....
}

每次要删除子画面的时候要调用SpriteDying( )函数,这个函数放在GameEngine里。

void SpriteDying(Sprite* pSpriteDying);

对游戏引擎做出的最后一个修改在UpdateSprites( )方法里,它现在包括了对SpriteDying( )的调用,通知游戏正在破坏一个子画面并从子画面列表中删除它。这使得游戏能够响应这个子画面的删除。

//更新子画面的位置
void GameEngine::UpdateSprites()
{
 ...
    // 如果从Sprite::Update()返回的子画面动作是SA_KILL,则删除所更新的子画面
    if (saSpriteAction & SA_KILL)
    {
      // 通知游戏正破坏一个子画面并从子画面列表中删除它
      SpriteDying(*siSprite);
 ...    
}

开发游戏

注意:若出现编译错误,请在项目设置->连接->对象/库模块中 加入 msimg32.lib winmm.lib

MeteorDefense(抵御流星)目录结构和效果图

MeteorDefense(抵御流星)目录结构:

游戏编程入门(15):开发 MeteorDefense(抵御流星)游戏_第3张图片

MeteorDefense(抵御流星)效果图:

游戏编程入门(15):开发 MeteorDefense(抵御流星)游戏_第4张图片

游戏编程入门(15):开发 MeteorDefense(抵御流星)游戏_第5张图片

游戏编程入门(15):开发 MeteorDefense(抵御流星)游戏_第6张图片

编写游戏代码

MeteorDefense.h

#pragma once

//-----------------------------------------------------------------
// 包含的文件
//-----------------------------------------------------------------
#include 
#include "Resource.h"
#include "GameEngine.h"
#include "Bitmap.h"
#include "Sprite.h"
#include "Background.h"

//-----------------------------------------------------------------
// 全局变量
//-----------------------------------------------------------------
HINSTANCE         g_hInstance;          //程序实例句柄
GameEngine*       g_pGame;              //游戏引擎指针
HDC               g_hOffscreenDC;       //屏幕外设备环境
HBITMAP           g_hOffscreenBitmap;   //屏幕外位图
Bitmap*           g_pGroundBitmap;      //背景地面位图
Bitmap*           g_pTargetBitmap;      //靶心子画面
Bitmap*           g_pCityBitmap;        //城市子画面
Bitmap*           g_pMeteorBitmap;      //流星子画面
Bitmap*           g_pMissileBitmap;     //导弹子画面
Bitmap*           g_pExplosionBitmap;   //爆炸子画面
Bitmap*           g_pGameOverBitmap;    //游戏结束位图
StarryBackground* g_pBackground;        //星空动画背景
Sprite*           g_pTargetSprite;      //靶心子画面
int               g_iNumCities, g_iScore, g_iDifficulty;//剩余的城市数、得分、难度级别
BOOL              g_bGameOver;          //游戏结束布尔值

//-----------------------------------------------------------------
// Function Declarations
//-----------------------------------------------------------------
void NewGame();
void AddMeteor();

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_pGroundBitmap = new Bitmap(hDC, IDB_GROUND, g_hInstance);
  g_pTargetBitmap = new Bitmap(hDC, IDB_TARGET, g_hInstance);
  g_pCityBitmap = new Bitmap(hDC, IDB_CITY, g_hInstance);
  g_pMeteorBitmap = new Bitmap(hDC, IDB_METEOR, g_hInstance);
  g_pMissileBitmap = new Bitmap(hDC, IDB_MISSILE, g_hInstance);
  g_pExplosionBitmap = new Bitmap(hDC, IDB_EXPLOSION, g_hInstance);
  g_pGameOverBitmap = new Bitmap(hDC, IDB_GAMEOVER, g_hInstance);

  // 创建布满星星的背景
  g_pBackground = new StarryBackground(600, 450);

  // 播放背景音乐
  g_pGame->PlayMIDISong(TEXT("Music.mid"));

  // 开始游戏
  NewGame();
}

GameEnd( )

GameEnd( ) 函数在结束游戏后执行清理工作。

// 游戏结束
void GameEnd()
{
  // 关闭MIDI播放器
  g_pGame->CloseMIDIPlayer();

  // 清理屏幕外设备环境和位图
  DeleteObject(g_hOffscreenBitmap);
  DeleteDC(g_hOffscreenDC);  

  // 清理位图
  delete g_pGroundBitmap;
  delete g_pTargetBitmap;
  delete g_pCityBitmap;
  delete g_pMeteorBitmap;
  delete g_pMissileBitmap;
  delete g_pExplosionBitmap;
  delete g_pGameOverBitmap;

  // 清理背景
  delete g_pBackground;

  // 清理子画面
  g_pGame->CleanupSprites();

  // 清理游戏引擎
  delete g_pGame;
}

GamePaint( )

GamePaint( ) 函数绘制背景、地面位图、子画面、得分以及游戏结束消息。

// 绘制游戏
void GamePaint(HDC hDC)
{
  // 绘制背景
  g_pBackground->Draw(hDC);

  // 绘制地面位图
  g_pGroundBitmap->Draw(hDC, 0, 398, TRUE);

  // 绘制子画面
  g_pGame->DrawSprites(hDC);

  // 绘制得分
  TCHAR szText[64];
  RECT  rect = { 275, 0, 325, 50 };
  wsprintf(szText, "%d", g_iScore);
  SetBkMode(hDC, TRANSPARENT);
  SetTextColor(hDC, RGB(255, 255, 255));
  DrawText(hDC, szText, -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);

  // 绘制游戏结束图像
  if (g_bGameOver)
    g_pGameOverBitmap->Draw(hDC, 170, 150, TRUE);
}

GameCycle( )

GameCycle( ) 函数根据难度级别随机地向游戏添加流星。

// 游戏循环
void GameCycle()
{
  if (!g_bGameOver)
  {
    // 随机添加流星
    if ((rand() % g_iDifficulty) == 0)
      AddMeteor();

    // 更新背景
    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);
  }
}

MouseButtonDown( )

MouseButtonDown( ) 函数向鼠标指针的位置发射一枚导弹子画面。

// 按下鼠标
void MouseButtonDown(int x, int y, BOOL bLeft)
{
  if (!g_bGameOver && bLeft)
  {
    // 创建新的导弹子画面并设置其位置
    RECT    rcBounds = { 0, 0, 600, 450 };
    int     iXPos = (x < 300) ? 144 : 449;
    Sprite* pSprite = new Sprite(g_pMissileBitmap, rcBounds, BA_DIE);
    pSprite->SetPosition(iXPos, 365);

    // 计算导弹飞向靶子的速度,使之瞄准目标
    int iXVel, iYVel = -6;
    y = min(y, 300);
    iXVel = (iYVel * ((iXPos + 8) - x)) / (365 - y);
    pSprite->SetVelocity(iXVel, iYVel);

    // 添加导弹子画面
    g_pGame->AddSprite(pSprite);

    // 播放发射声音
    PlaySound((LPCSTR)IDW_FIRE, g_hInstance, SND_ASYNC |
      SND_RESOURCE | SND_NOSTOP);

    // 更新得分
    g_iScore = max(--g_iScore, 0); //导弹发出时,分数会减少
  }
  else if (g_bGameOver && !bLeft)
    // 开始一个新游戏
    NewGame();
}

玩家单击了左键,将根据鼠标指针的位置创建一枚导弹子画面。这个位置非常重要,它决定了使用哪一台发射架来发射导弹,左边的发射架向游戏屏幕左边的目标发射导弹,而右边的发射架负责屏幕右边。目标位置也很重要,它决定了导弹轨道,从而决定了导弹的xy速度。

MouseMove( )

MouseMove( ) 函数使靶心子画面跟踪鼠标指针。

// 鼠标移动
void MouseMove(int x, int y)
{
  // 使靶心子画面跟踪鼠标指针
  g_pTargetSprite->SetPosition(x - (g_pTargetSprite->GetWidth() / 2),
    y - (g_pTargetSprite->GetHeight() / 2));
}

SpriteCollison( )

SpriteCollison( ) 函数检测并响应导弹、流星及城市之间的碰撞。

// 碰撞检测
BOOL SpriteCollision(Sprite* pSpriteHitter, Sprite* pSpriteHittee)
{
  // 查看导弹是否与流星相撞
  if ((pSpriteHitter->GetBitmap() == g_pMissileBitmap &&
    pSpriteHittee->GetBitmap() == g_pMeteorBitmap) ||
    (pSpriteHitter->GetBitmap() == g_pMeteorBitmap &&
    pSpriteHittee->GetBitmap() == g_pMissileBitmap))
  {
    // 删除这两个子画面
    pSpriteHitter->Kill();
    pSpriteHittee->Kill();

    // 更新得分
    g_iScore += 6;
    g_iDifficulty = max(50 - (g_iScore / 10), 5);
  }

  // 查看流星是否与城市相撞
  if (pSpriteHitter->GetBitmap() == g_pMeteorBitmap &&
    pSpriteHittee->GetBitmap() == g_pCityBitmap)
  {
    // 播放爆炸声音
    PlaySound((LPCSTR)IDW_BIGEXPLODE, g_hInstance, SND_ASYNC |
      SND_RESOURCE);

    // 删除这两个子画面
    pSpriteHitter->Kill();
    pSpriteHittee->Kill();

    // 查看游戏是否结束
    if (--g_iNumCities == 0)
      g_bGameOver = TRUE;
  }

  return FALSE;
}

位图指针是识别和区分子画面的一种方便而有效的方法,因为子画面的位图指针已经存储在了全局变量中。

SpriteDying( )

每次破坏一个流星子画面时,SpriteDying( ) 函数都会创建一个爆炸子画面。

// 通知游戏正破坏一个子画面并从子画面列表中删除它
void SpriteDying(Sprite* pSpriteDying)
{
  // 查看是否正删除一个流星子画面
  if (pSpriteDying->GetBitmap() == g_pMeteorBitmap)
  {
    // 播放爆炸声音
    PlaySound((LPCSTR)IDW_EXPLODE, g_hInstance, SND_ASYNC |
      SND_RESOURCE | SND_NOSTOP);

    // 在流星的位置上创建一个爆炸子画面
    RECT rcBounds = { 0, 0, 600, 450 };
    RECT rcPos = pSpriteDying->GetPosition();
    Sprite* pSprite = new Sprite(g_pExplosionBitmap, rcBounds);
    pSprite->SetNumFrames(12, TRUE);
    pSprite->SetPosition(rcPos.left, rcPos.top);
    g_pGame->AddSprite(pSprite);
  }
}

MeteorDefense游戏中余下的两个函数都是支持函数,它们完全是这个游戏所特有的。

NewGame( )

NewGame( ) 函数负责在其他一切准备就绪之后实际开始一个新游戏。

// 为新游戏做好一切准备
void NewGame()
{
  // 清理子画面
  g_pGame->CleanupSprites();

  // 创建靶心子画面
  RECT rcBounds = { 0, 0, 600, 450 };
  g_pTargetSprite = new Sprite(g_pTargetBitmap, rcBounds, BA_STOP);
  g_pTargetSprite->SetZOrder(10);
  g_pGame->AddSprite(g_pTargetSprite);

  // 创建城市子画面
  Sprite* pSprite = new Sprite(g_pCityBitmap, rcBounds);
  pSprite->SetPosition(2, 370);
  g_pGame->AddSprite(pSprite);
  pSprite = new Sprite(g_pCityBitmap, rcBounds);
  pSprite->SetPosition(186, 370);
  g_pGame->AddSprite(pSprite);
  pSprite = new Sprite(g_pCityBitmap, rcBounds);
  pSprite->SetPosition(302, 370);
  g_pGame->AddSprite(pSprite);
  pSprite = new Sprite(g_pCityBitmap, rcBounds);
  pSprite->SetPosition(490, 370);
  g_pGame->AddSprite(pSprite);

  // 初始化游戏变量
  g_iScore = 0;
  g_iNumCities = 4;
  g_iDifficulty = 50;
  g_bGameOver = FALSE;

  // 播放背景音乐
  g_pGame->PlayMIDISong();
}

AddMeteor( )

为了防止在游戏中出现重复的代码,因此将创建新流星的代码放在它自己的函数里,即AddMeteor( )。

AddMeteor( ) 是一个支持函数,用来简化向游戏添加流星子画面的任务。

// 在随机位置添加一个新流星,并瞄准一个随机的城市
void AddMeteor()
{
  // 创建新的流星子画面并设置其位置
  RECT    rcBounds = { 0, 0, 600, 390 };
  int     iXPos = rand() % 600;
  Sprite* pSprite = new Sprite(g_pMeteorBitmap, rcBounds, BA_DIE);
  pSprite->SetNumFrames(14);
  pSprite->SetPosition(iXPos, 0);

  // 计算速度,使之瞄准一个城市
  int iXVel, iYVel = (rand() % 4) + 3;
  switch(rand() % 4)
  {
  case 0:
    iXVel = (iYVel * (56 - (iXPos + 50))) / 400;
    break;
  case 1:
    iXVel = (iYVel * (240 - (iXPos + 50))) / 400;
    break;
  case 2:
    iXVel = (iYVel * (360 - (iXPos + 50))) / 400;
    break;
  case 3:
    iXVel = (iYVel * (546 - (iXPos + 50))) / 400;
    break;
  }
  pSprite->SetVelocity(iXVel, iYVel);

  // 添加流星子画面
  g_pGame->AddSprite(pSprite);
}

源代码下载

http://pan.baidu.com/s/1ge2Vzr1

你可能感兴趣的:(?.游戏编程入门,游戏编程入门)