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