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

本文介绍了对之前飞机大战游戏的代码重构,通过拆分Plane类为多个组件类,提高了代码质量。新增了召唤友军功能,利用list替代vector提升效率,并将飞机和石头单独存储以优化碰撞检测。同时,详细展示了重构后的各个关键类及其职责,以及如何实现友军召唤。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

注:在本篇博客中,对上一篇博客的飞机大战游戏进行了完善,但有很多细微的修改,由于篇幅原因,没有把所有代码列出来,大家需要仔细阅读,否则可能漏掉一些地方,导致编译错误或产生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出大量友军和大量敌人,然后坐观虎斗🙂。
最后,博主创作不易,三连支持一下吧!

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值