在上一篇博客里,我们介绍了SDL基本的函数用法,今天,我们就来研究一下Game类的代码。让我们再次打开模板项目。
注:在代码中,用到了一个头文件Math.h(不是C语言中的math.h),这是那本书的作者自己编写的一个头文件,非常有用,点击此处下载。一定要把解压后的Math.h和Math.cpp加入到项目里,才能编译通过。
Game类代码
先上代码:
Game.h
#pragma once
#include "SDL.h"
#include <unordered_map>
#include <string>
#include <vector>
class Game
{
public:
Game();
bool Initialize();
void RunLoop();
void Shutdown();
void AddActor(class Actor* actor);
void RemoveActor(class Actor* actor);
void AddDrawComponent(class DrawComponent* com);
void RemoveDrawComponent(class DrawComponent* com);
SDL_Texture* GetTexture(const std::string& fileName);
private:
void ProcessInput();
void UpdateGame();
void GenerateOutput();
void LoadData();
void UnloadData();
std::vector<class DrawComponent*> mDrawComponents;
// Map of textures loaded
std::unordered_map<std::string, SDL_Texture*> mTextures;
// All the actors in the game
std::vector<class Actor*> mActors;
// Any pending actors
std::vector<class Actor*> mPendingActors;
// All the sprite components drawn
SDL_Window* mWindow;
SDL_Renderer* mRenderer;
Uint32 mTicksCount;
bool mIsRunning;
// Track if we're updating actors right now
bool mUpdatingActors;
};
Game.cpp
#include "Game.h"
#include "SDL_image.h"
#include <algorithm>
#include "Actor.h"
#include"Plane.h"
#include"DrawPlaneComponent.h"
#pragma execution_character_set("utf-8")
Game::Game()
:mWindow(nullptr)
,mRenderer(nullptr)
,mIsRunning(true)
,mUpdatingActors(false)
{
}
bool Game::Initialize()
{
if (SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO) != 0)
{
SDL_Log("Unable to initialize SDL: %s", SDL_GetError());
return false;
}
mWindow = SDL_CreateWindow("飞机大战", 150, 40, 1024, 768, 0);
if (!mWindow)
{
SDL_Log("Failed to create window: %s", SDL_GetError());
return false;
}
mRenderer = SDL_CreateRenderer(mWindow, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (!mRenderer)
{
SDL_Log("Failed to create renderer: %s", SDL_GetError());
return false;
}
if (IMG_Init(IMG_INIT_PNG) == 0)
{
SDL_Log("Unable to initialize SDL_image: %s", SDL_GetError());
return false;
}
LoadData();
//添加额外初始化代码
mTicksCount = SDL_GetTicks();
return true;
}
void Game::RunLoop()
{
while (mIsRunning)
{
ProcessInput();
UpdateGame();
GenerateOutput();
}
}
void Game::ProcessInput()
{
SDL_Event event;
while (SDL_PollEvent(&event))
{
switch (event.type)
{
case SDL_QUIT:
mIsRunning = false;
break;
}
}
const Uint8* state = SDL_GetKeyboardState(NULL);
mUpdatingActors = true;
for (auto actor : mActors)
{
actor->ProcessInput(state);
}
mUpdatingActors = false;
//添加额外处理键盘输入代码
}
void Game::UpdateGame()
{
// Compute delta time
// Wait until 16ms has elapsed since last frame
while (!SDL_TICKS_PASSED(SDL_GetTicks(), mTicksCount + 16))
;
float deltaTime = (SDL_GetTicks() - mTicksCount) / 1000.0f;
if (deltaTime > 0.05f)
{
deltaTime = 0.05f;
}
mTicksCount = SDL_GetTicks();
// Update all actors
mUpdatingActors = true;
for (auto actor : mActors)
{
actor->Update(deltaTime);
}
mUpdatingActors = false;
// Move any pending actors to mActors
for (auto pending : mPendingActors)
{
mActors.emplace_back(pending);
}
mPendingActors.clear();
// Add any dead actors to a temp vector
std::vector<Actor*> deadActors;
for (auto actor : mActors)
{
if (actor->GetState() == Actor::EDead)
{
deadActors.emplace_back(actor);
}
}
// Delete dead actors (which removes them from mActors)
for (auto actor : deadActors)
{
delete actor;
}
//添加额外更新代码
}
void Game::GenerateOutput()
{
SDL_SetRenderDrawColor(mRenderer, 0, 0, 0, 255);//背景色
SDL_RenderClear(mRenderer);
for (auto sprite : mDrawComponents)
{
sprite->Draw(mRenderer);
}
//添加额外输出代码
SDL_RenderPresent(mRenderer);
}
void Game::LoadData()
{
//添加加载数据代码
}
void Game::UnloadData()
{
//删除所有角色
while (!mActors.empty())
{
delete mActors.back();
}
//删除纹理
for (auto i : mTextures)
{
SDL_DestroyTexture(i.second);
}
mTextures.clear();
//添加额外删除数据的代码
}
SDL_Texture* Game::GetTexture(const std::string& fileName)
{
SDL_Texture* tex = nullptr;
// Is the texture already in the map?
auto iter = mTextures.find(fileName);
if (iter != mTextures.end())
{
tex = iter->second;
}
else
{
// Load from file
SDL_Surface* surf = IMG_Load(fileName.c_str());
if (!surf)
{
SDL_Log("Failed to load texture file %s", fileName.c_str());
return nullptr;
}
// Create texture from surface
tex = SDL_CreateTextureFromSurface(mRenderer, surf);
SDL_FreeSurface(surf);
if (!tex)
{
SDL_Log("Failed to convert surface to texture for %s", fileName.c_str());
return nullptr;
}
mTextures.emplace(fileName.c_str(), tex);
}
return tex;
}
void Game::Shutdown()
{
UnloadData();
IMG_Quit();
SDL_DestroyRenderer(mRenderer);
SDL_DestroyWindow(mWindow);
SDL_Quit();
}
void Game::AddActor(Actor* actor)
{
// If we're updating actors, need to add to pending
if (mUpdatingActors)
{
mPendingActors.emplace_back(actor);
}
else
{
mActors.emplace_back(actor);
}
}
void Game::RemoveActor(Actor* actor)
{
// Is it in pending actors?
auto iter = std::find(mPendingActors.begin(), mPendingActors.end(), actor);
if (iter != mPendingActors.end())
{
// Swap to end of vector and pop off (avoid erase copies)
std::iter_swap(iter, mPendingActors.end() - 1);
mPendingActors.pop_back();
}
// Is it in actors?
iter = std::find(mActors.begin(), mActors.end(), actor);
if (iter != mActors.end())
{
// Swap to end of vector and pop off (avoid erase copies)
std::iter_swap(iter, mActors.end() - 1);
mActors.pop_back();
}
}
void Game::AddDrawComponent(DrawComponent* com)
{
int myDrawOrder = com->GetUpdateOrder();
auto iter = mDrawComponents.begin();
for (;
iter != mDrawComponents.end();
++iter)
{
if (myDrawOrder < (*iter)->GetUpdateOrder())
{
break;
}
}
mDrawComponents.insert(iter, com);
}
void Game::RemoveDrawComponent(DrawComponent* com)
{
mDrawComponents.erase(std::find(mDrawComponents.begin(), mDrawComponents.end(), com));
}
代码分析
现在,我们来分析一下代码。
成员变量
mDrawComponents:记录绘画组件的容器。
mTextures:记录纹理的映射。纹理也就是图片加载到内存里,用于在游戏界面中显示图片。这个功能暂时还用不到,这里不过多介绍。这个map以字符串作关键字,相当于为纹理起名字。可以用int代替,效率会更高。
mActors:记录角色的容器。
mPendingActors:记录正在等待的角色的容器。为什么会出现这么一个容器呢?可以思考一个飞机大战的游戏,玩家控制一架飞机在战斗,按下空格会发射子弹,也就是new一个子弹的角色,但此刻程序正在遍历所有的角色,进行游戏更新,如果遍历过程中插入一个角色,会造成许多bug,所以增加了这个容器,在遍历过程中new的角色会被插入这个容器暂存。
mWindow:窗口的指针。
mRenderer:渲染器的指针。
mTicksCount:记录时间的变量。
mIsRunning:记录游戏是否在运行。
mUpdatingActors:记录是否在遍历角色。
成员函数
Game类主要的3个函数是Initialize,RunLoop和Shutdown,此外还有许多辅助函数。
构造函数
构造函数很简单,就是初始化几个成员变量。
Initialize
初始化SDL,创建窗口和渲染器,并执行LoadData操作。
首先,SDL_Init需要初始化的是关于音频的几个库。代码如下:
if (SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO) != 0)
{
SDL_Log("Unable to initialize SDL: %s", SDL_GetError());
return false;
}
后面的很多API,如SDL_PollEvent,都与音频有关。另外,SDL_Log的功能是把消息输出到控制台和调试器。用法与printf类似。
然后就是创建窗口、渲染器,这里就不说了。后面的IMG_Init是用来初始化图形库的,用来加载纹理。参数取值如下(用按位或连接):
typedef enum
{
IMG_INIT_JPG = 0x00000001,
IMG_INIT_PNG = 0x00000002,
IMG_INIT_TIF = 0x00000004,
IMG_INIT_WEBP = 0x00000008
} IMG_InitFlags;
LoadData
该函数实现为空,可以根据需要往里面添加加载纹理、创建基本角色的代码,但由于这是模板程序,所以什么代码也不添加,加个TODO就行。
注:Game类的垃圾回收机制非常完善,所有的组件或角色,直接new出来就行了,不用手动delete,完全不用担心内存泄漏。
RunLoop
这个函数非常重要,它用来控制游戏循环。不过它的代码很简单,主要是循环调用了3个函数,也就是上篇博客里说的游戏运行框架:处理输入、更新游戏、生成输出。
![](/qrcode.jpg)
ProcessInput
这就是处理输入的函数。让我们回忆以下,输入不仅包括键盘,还包括各种消息和服务器发来的信息。不过我们现在还用不到网络编程,所以这部分可以先忽略掉。剩下的部分就是先处理消息,如果有退出消息,就把mRunning设为false,然后使用SDL_GetKeyboardState函数获取键盘状态,最后遍历所有角色,调用角色的ProcessInput函数(这里后面再说)。由于这是模板项目,无需添加额外代码,可以在后面添上个TODO,提示可以在函数的最后添加额外代码。
UpdateGame
这个函数的作用是更新游戏,它的代码比较复杂。首先,让我们想想,如果有一个角色需要移动,我们要是直接在UpdateGame里简单地pos.x+=10
这样,在运行速度不同的电脑上,循环一次的时间也不同,导致在有些电脑上移动速度很快,有些则移动很慢。解决这个问题的办法就是使用增量时间,即用一个变量记录自上一帧以来经过的时间,然后所有的运动代码都要乘上这个时间,即pos.x+=deltaTime*10
。最前面的while循环则是固定每帧运行的时间。后面的代码则是更新角色、把等待角色加入到角色容器中,并删除死亡角色。
GenerateOutput
这个函数是生成输出的函数。首先,它会设置背景色,然后清空屏幕,遍历所有的绘画组件进行绘图,最后显示。
Shutdown
这个函数是最后的清理函数,首先调用UnLoadData函数释放内存里的东西,然后依次退出纹理库、删除渲染器和窗口,最后退出SDL。
UnloadData
这是一个自定义函数,它删除所有角色和纹理。根据需要,可以添加代码,但模板项目标上个TODO就好。
GetTexture
这个函数作用是从磁盘加载纹理。首先,要查找纹理映射,如果已经有了,直接返回,否则先从磁盘加载一个表面(注意:不是纹理),然后调用见名知义的SDL_CreateTextureFromSurface函数,创建一个纹理,并加入到纹理映射中。
AddActor
这个函数非常简单,就是添加一个Actor,如果正在更新,就添加到等待容器中,否则直接添加到容器里。需要注意的是,在Game类里不需要调用这个函数,还是那句话,角色直接new出来就可以,Actor的构造函数里会调用这个函数。
RemoveActor
这个函数也很简单,就是删除角色,为了加快速度,找到角色后不能简单调用vector的erase方法,因为erase会把后面的都往前移动。要做的是先和最后一个交换,然后直接pop_back。用list实现也可以。
AddDrawComponent
这个函数和AddActor差不多,是插入一个绘画组件,不过mDrawComponent是有序的,要按照从大到小的顺序排列。为什么要有序呢?这跟画家算法有关。画家算法是2D游戏中常用的画图方式,即先画远景,后画近景。因为后画的会覆盖住先画的,所以要把前面的放到最后画。
RemoveDrawComponent
这个函数和RemoveActor也差不多,不过这个是直接erase。
总结
我们写了这么多代码,还有Actor类和Component类的代码没写,如果现在运行,只是出现一个黑窗口。有些人可能好奇:写这么多代码干什么?要是直接实现,几行代码就行了,加角色的时候稍微修改一下不就行了?对这个问题,我们可以用两个函数来回答。这也是编写大型游戏时面向对象与面向过程的PK。
为了更直观,我们把游戏的规模设为x,f(x)表示编写游戏的代价。
面向过程: f ( x ) = x 2 f(x)=x^2 f(x)=x2
当编写非常简单的小游戏时 ( x ≤ 5 ) (x≤5) (x≤5),代码明显比面向对象简单。但游戏规模一旦增加,代码量开始飙升,比面向对象复杂很多。
面向对象: f ( x ) = 30 + x f(x)=30+x f(x)=30+x
使用面向对象编程,代码复杂度几乎等于游戏规模加上一个常数。因为写好框架类后,直接派生几个子类就行了,代码具有极低的耦合性,主类的代码基本不用怎么改,而且结构清晰,分工明确,代码便于阅读。在编写大型游戏时,面向对象成了必然选择。