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