C++游戏编程教程(七)——改进飞机大战游戏

注:在本篇博客中,对上一篇博客的飞机大战游戏进行了完善,但有很多细微的修改,由于篇幅原因,没有把所有代码列出来,大家需要仔细阅读,否则可能漏掉一些地方,导致编译错误或产生bug。
PS:如果大家有什么好的想法,比如想出什么新功能,可以在评论区留言。

简介

之前的博客中,我们做过一个飞机大战游戏,但我后来重新看了一下代码,发现当时的代码质量太烂了。举一个例子:在设计敌人飞机时,我应该写一个AutoMoveComponent类,然后把这个组件和Plane一组合就够了,当时却傻傻乎乎重写了个Enemy类。痛定思痛,我今天打算把代码重构一下,同时增加一些功能,比如召唤友军等。加上召唤友军这个功能也是非常爽的,敌人毫无还手之力,不信看下游戏截图:
召唤友军
召唤友军

重构代码

重构后程序主要的类大概有这些:
程序框架

首先,我们删掉Enemy类,新建AutoMoveComponent,AutoFireBulletsComponent,InputComponent这三个类。然后,我们就要把以前写在Plane类的功能抽出来分别放到三个类里。方法很简单,这里不再赘述,我们只来看一下修改后四个类的代码。

Plane

Plane.h:

#pragma once
#include"Actor.h"
class Plane :
    public Actor
{
public:
    Plane(class Game* game, const Vector2& pos, bool IsEnemy = false);
    bool IsEnemy()const { return mIsEnemy; }
private:
    bool mIsEnemy;
};

可以看出,这里删去了大量实现细节的成员变量和函数,只增加了一个IsEnemy区分我方和敌方。
Plane.cpp的实现就更简单了,直接删去了ActorInput和UpdateActor这“两大巨头”,剩下的只有一个构造函数了。全部代码如下:

#include "Plane.h"
Plane::Plane(Game* game, const Vector2& pos,bool IsEnemy) :Actor(game),mIsEnemy(IsEnemy)
{
	SetPosition(pos);
}

AutoMoveComponent

这个类就是把以前Plane和Enemy类的代码相关部分移动到了这里。
AutoMoveComponent.h:

#pragma once
#include "Component.h"
class AutoMoveComponent :
    public Component
{
public:
    AutoMoveComponent(Actor* owner, int updateOrder = 100);
    virtual void Update(float deltaTime);
private:
    Uint32 mMoveTicks;
    short mMove;
};

AutoMoveComponent.cpp:

#include "AutoMoveComponent.h"

AutoMoveComponent::AutoMoveComponent(Actor* owner, int updateOrder) : Component(owner, updateOrder), mMoveTicks(SDL_GetTicks())
{
	mMove = 200 + rand() % 100;
	if (rand() % 2)
		mMove = -mMove;
}

void AutoMoveComponent::Update(float deltaTime)
{
	Vector2 pos = mOwner->GetPosition();
	if (SDL_TICKS_PASSED(SDL_GetTicks(), mMoveTicks + 1000))//随机移动位置
	{
		mMoveTicks = SDL_GetTicks();
		mMove = 100 + rand() % 100;
		if (rand() % 2)
			mMove = -mMove;
	}
	pos.x += deltaTime * mMove;
	if (pos.x > 1024 - 50)
		pos.x = 1024 - 50;
	if (pos.x < 0)
		pos.x = 0;
	mOwner->SetPosition(pos);
}

AutoFireBulletsComponent

同上。
AutoFireBulletsComponent.h:

#pragma once
#include "Component.h"
class AutoFireBulletsComponent :
    public Component
{
public:
    AutoFireBulletsComponent(Actor* owner, int updateOrder = 100);
    virtual void Update(float deltaTime);
private:
    Uint32 mTicks;
};

AutoFireBulletsComponent.cpp:

#include "AutoFireBulletsComponent.h"
#include"DrawRectangleComponent.h"
#include"Bullet.h"
#include"Plane.h"
AutoFireBulletsComponent::AutoFireBulletsComponent(Actor* owner, int updateOrder):Component(owner,updateOrder),mTicks(SDL_GetTicks())
{
}

void AutoFireBulletsComponent::Update(float deltaTime)
{
	if (SDL_TICKS_PASSED(SDL_GetTicks(), mTicks + 1000) && !(rand() % 25))//1秒发射子弹
	{
		Vector2 pos = mOwner->GetPosition();
		mTicks = SDL_GetTicks();
		pos.x += 20;
		pos.y += (((Plane*)mOwner)->IsEnemy() ? 40 : -40);
		new DrawRectangleComponent(new Bullet(mOwner->GetGame(), pos, ((Plane*)mOwner)->IsEnemy() ? 700 : -700) , Vector2(10, 20), 255, 0, 0, 0);
	}
}

InputComponent

同上。
InputComponent.h:

#pragma once
#include "Component.h"
class InputComponent :
    public Component
{
public:
    InputComponent(Actor* owner, int updateOrder = 100);
    virtual void ProcessInput(const uint8_t* keyState);
    virtual void Update(float deltaTime);
private:
    short mPlaneDir;
    Uint32 mTick;
};

InputComponent.cpp:

#include "InputComponent.h"
#include"DrawRectangleComponent.h"
#include"Bullet.h"
InputComponent::InputComponent(Actor* owner, int updateOrder):Component(owner,updateOrder), mPlaneDir(0),mTick(SDL_GetTicks())
{

}

void InputComponent::ProcessInput(const uint8_t* keyState)
{
	mPlaneDir = 0;
	if (keyState[SDL_SCANCODE_RIGHT])
		mPlaneDir += 1;
	if (keyState[SDL_SCANCODE_LEFT])
		mPlaneDir -= 1;
	if (keyState[SDL_SCANCODE_SPACE] && SDL_TICKS_PASSED(SDL_GetTicks(), mTick + 300))//0.3秒发射一颗子弹
	{
		mTick = SDL_GetTicks();
		Vector2 pos = mOwner->GetPosition();
		pos.x += 20;
		pos.y -= 40;
		new DrawRectangleComponent(new Bullet(mOwner->GetGame(), pos, -700), Vector2(10, 20), 255, 0, 0, 0);
	}
}

void InputComponent::Update(float deltaTime)
{
	Vector2 pos = mOwner->GetPosition();
	pos.x += mPlaneDir * 300 * deltaTime;
	if (pos.x < 0)
		pos.x = 0;
	if (pos.x > 1024 - 50)
		pos.x = 1024 - 50;
	mOwner->SetPosition(pos);
}

这样一来,代码质量就好多了。我们只需要把Game类中new出飞机的代码稍微一改就可以了。具体来说,需要添加多个组件,new出我方飞机的示例:

Plane* plane = new Plane(this, Vector2(492, 700));
new DrawPlaneComponent(plane);//绘制组件
new InputComponent(plane);//控制飞机组件

new出敌方飞机的示例:

Plane* enemy = new Plane(this, Vector2(rand() % 984, 10),true);
new DrawPlaneComponent(enemy);//绘制组件
new AutoFireBulletsComponent(enemy);//自动射击组件
new AutoMoveComponent(enemy);//自动移动组件

DrawPlaneComponent

由于博主还得上学,这篇博客断断续续写了三周左右,这个类不确定改没改,直接上代码,如果和以前的不一样,就用这个版本的。
DrawPlaneComponent.h:

#pragma once
#include"DrawComponent.h"
class DrawPlaneComponent :
    public DrawComponent
{
public:
    DrawPlaneComponent(class Plane* actor, int drawOrder = 100);
    virtual void Draw(SDL_Renderer* renderer);
private:
    bool mIsEnemy;
};

DrawPlaneComponent.cpp:

#include "DrawPlaneComponent.h"
#include"Plane.h"
DrawPlaneComponent::DrawPlaneComponent(Plane* actor, int drawOrder) :DrawComponent(actor, drawOrder),mIsEnemy(actor->IsEnemy())
{
}

void DrawPlaneComponent::Draw(SDL_Renderer* renderer)
{
	SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
	const Vector2& pos = mOwner->GetPosition();
	SDL_Rect rect = { pos.x,pos.y,50,30 };
	SDL_RenderFillRect(renderer, &rect);
	SDL_SetRenderDrawColor(renderer, 255, 255, 0, 255);
	rect = { (int)pos.x + 20,(int)pos.y - 20,10,20 };
	if (mIsEnemy)
		rect.y += 40;
	SDL_RenderFillRect(renderer, &rect);
}

修改代码

大框架的重构完成了,我们再来修改一下细节,优化效率。

使用list代替vector

首先,我们之前提到过,Game类中的一系列容器完全可以用list代替 ,效率更高,我们就直接把vector改成list,然后编译,把报错地方略微修改就可以。

飞机和石头单独存储

在上一个版本里,飞机是没有单独存储在一个地方,判断子弹击中飞机,我们用的是RTTI(运行时类型信息)。但这样有一个缺点,大家可以看一下第二个运行示意图,有没有发现什么不足之处?没错,如果后期加入召唤友军功能,子弹数量急剧增多,遍历整个mActors效率非常低下。这样,我们可以添加一个单独的mPlanes容器,只需要遍历mPlanes就行,不需要RTTI。当然,这是一种以空间换时间的方法,所有的飞机都会被存储两份。类似地,对Stone类也做相同的处理。具体来说,我们在Game类中增加两个容器,mPlanes和mStones,然后类比AddActor和RemoveActor,添加AddXXX和RemoveXXX函数,不用考虑是否在更新。然后,在Plane和Stone类的构造函数中分别调用game类的AddPlane/Stone函数,重写析构函数,调用RemovePlane/Stone函数。
这样一来,还需要修改一下,将Stone类中判断与子弹碰撞的代码放到Bullet类中,单独遍历mStones,其它遍历的地方也进行相应的修改。修改后的Bullet::UpdateActor:

void Bullet::UpdateActor(float deltaTime)
{
	Vector2 pos = GetPosition();
	pos.y += mSpeed * deltaTime;
	SetPosition(pos);
	if (pos.y > 768 || pos.y < 0)
		SetState(EDead);
	for (auto i : GetGame()->mPlanes)
	{
		if (i->IsEnemy())
		{
			Vector2 bPos = i->GetPosition();
			if (bPos.x - 10 < pos.x && bPos.x + 50 > pos.x && bPos.y + 50 > pos.y)
			{
				SetState(EDead);
				i->SetState(EDead);
			}
		}
		else
		{
			Vector2 bPos = i->GetPosition();
			if (bPos.x - 10 < pos.x && bPos.x + 50 > pos.x && bPos.y < pos.y + 20 && bPos.y + 30 > pos.y)
			{
				SetState(EDead);
				i->SetState(EDead);
			}
		}
	}
	for (auto i : GetGame()->mStones)
	{
		Vector2 bPos = i->GetPosition();
		if (pos.x + 20 > bPos.x && pos.x < bPos.x + 50 && pos.y < bPos.y + 50)
		{
			SetState(EDead);
			i->SetState(EDead);
			GetGame()->mStoneSpeed *= 1.02;
		}
	}
}

Stone::UpdateActor:

void Stone::UpdateActor(float deltaTime)
{
	Vector2 pos = GetPosition();
	pos.y += deltaTime * mSpeed;
	if (pos.y > 768)
		SetState(EDead);
	SetPosition(pos);
	for (auto i : GetGame()->mPlanes)
	{
		if (!i->IsEnemy())
		{
			Vector2 bPos = i->GetPosition();
			if (bPos.x + 50 > pos.x && bPos.x < pos.x + 50 && bPos.y < pos.y + 50 && bPos.y + 30>pos.y)
			{
				SetState(EDead);
				i->SetState(EDead);
			}
		}
	}
}

增加新功能

目前我就想出来一个召唤友军的功能,我们就先添加这一个功能。得益于以前代码的低耦合度,实现这个功能,根本不需要添加类,直接在想召唤的地方new出来,添加敌方飞机的那几个组件就行了。
我们可以在Game类中增加mFriendCount变量,记录友军数量。每new出一个友军就+1,每死亡一个友军-1。这部分代码可以在Game类的Add/RemovePlane函数中实现,此处略去。当然,这样一来,所有new飞机的地方都不再需要手动修改mFriendCount和mEnemyCount的数量了。
然后,在ProcessInput函数中加入以下代码,实现按下F键召唤友军:

if (state[SDL_SCANCODE_F])//召唤友军
{
	Plane* myfriend = new Plane(this, Vector2(rand() % 984, 700));
	new DrawPlaneComponent(myfriend);
	new AutoFireBulletsComponent(myfriend);
	new AutoMoveComponent(myfriend);
}

这样一来,就可以实现按下F键召唤友军了。如果想实现一些很好玩的画面,可以在游戏开始的时候new出大量友军和大量敌人,然后坐观虎斗🙂。
最后,博主创作不易,三连支持一下吧!

  • 14
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
用DDraw实现射击游戏说明文档 要点一:画图自动切割 IDirectDrawSurface7::BltFast()方法中没有自动切割功能,即当画图元素超出窗口以外时不会自动切割,DDraw选择自动忽略不画,造成一旦超出窗口,画图元素会突然消失。 解决这一问题的方法是手动切割,代码如下: //自动切割 RECT scRect; //存放当前窗口大小区域 ZeroMemory( &scRect, sizeof( scRect ) ); GetWindowRect( GetActiveWindow(), &scRect ); //防止图片左上角超过窗口左上角 if ( x < 0 ) { m_rect.left -= x; x = 0; } if ( y scRect.right ? scRect.right : x; y = y > scRect.bottom ? scRect.bottom : y; m_rect.right = x + m_rect.right - m_rect.left > scRect.right ? scRect.right - x + m_rect.left : m_rect.right; m_rect.bottom = y + m_rect.bottom - m_rect.top > scRect.bottom ? scRect.bottom - y + m_rect.top : m_rect.bottom; 只需将上述代码加在CGraphic::BltBBuffer() 中的m_bRect = m_rect; 前即可。 要点二:背景的滚轴实现 画背景可以分为以下三种情况: 情况一:背景图片与窗口等高 情况二:背景图片高度小于窗口高度 情况三:背景图片高度大于窗口高度 上述讲解图与代码相对应地看,有助于容易理解。 另外,要点一实现之后,由于已经可以自动切割,画背景可以用其它方法。 要点三:精灵图的实现 在游戏中,如RPG游戏中的人物图、射击类游戏飞机、爆炸等,叫做精灵图。 精灵图实际上是将所有帧的图片放在一个文件中,游戏时靠一个RECT来控制画图像文件中的哪一部分,进而控制游戏显示哪一帧图,只需控制好RECT的位置即可。如下图: 控制RECT的四个角的坐标的移动,有以下代码: if (m_timeEnd – m_timeStart > 100) //只有到了100ms之后才绘图 { m_ImageID++; if(m_ImageID - m_beginID >= num) { m_ImageID = m_beginID; //最后一帧的下一帧是第一帧 } m_timeStart = timeGetTime(); } int id = m_ImageID++; SetRect(&m_rect, 41 * id, 0, 41 * (id + 1), 41); //飞机精灵图大小是41×41 m_pGraph->BltBBuffer(m_pImageBuffer, true, m_Pos.x, m_Pos.y, m_rect); 这样就实现了精灵动画的效果。 要点四:拿STL进行子弹的实现 子弹的实现可以使用STL中的vector,当按下开火键时发出一颗子弹,就往vector中添加一个结点;当子弹飞出窗口或击中敌机时,再将结点从vector中删除。每帧游戏画面中子弹飞行时只需将vector中的所有子弹进行处理、绘画即可。 参考代码如下: 1.添加子弹 if (g_ctrlDown) //当ctrl键按下时开炮! { m_BulletEnd = m_Gtime->GetTime(); if ((m_BulletEnd - m_BulletStart) * 1000 > 120) //如果连续按着开火键不放,这里控制不会发出太多子弹 { m_BulletStart = m_BulletEnd; MBULLET tmpBullet; tmpBullet.pos.x = m_SPos.x - 1; //记录开火时的子弹位置 tmpBullet.pos.y = m_SPos.y - 26; tmpBullet.speed = 5; //该子弹的飞行速度 m_BulletList.push_back(tmpBullet); //将子弹添加到vector中

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值