C++游戏编程教程

Posted Visual Studio 2022

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++游戏编程教程相关的知识,希望对你有一定的参考价值。

上一篇博客里,我们介绍了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个函数,也就是上篇博客里说的游戏运行框架:处理输入、更新游戏、生成输出。

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) (x5),代码明显比面向对象简单。但游戏规模一旦增加,代码量开始飙升,比面向对象复杂很多。

面向对象: f ( x ) = 30 + x f(x)=30+x f(x)=30+x


使用面向对象编程,代码复杂度几乎等于游戏规模加上一个常数。因为写好框架类后,直接派生几个子类就行了,代码具有极低的耦合性,主类的代码基本不用怎么改,而且结构清晰,分工明确,代码便于阅读。在编写大型游戏时,面向对象成了必然选择。

以上是关于C++游戏编程教程的主要内容,如果未能解决你的问题,请参考以下文章

C++游戏编程教程——项目实战

编程入门教程

C++编程基础: 14. 文件的读写

C++零基础教程

学习游戏开发应该从哪些方面入手

「游戏引擎 浅入浅出」4.3 片段着色器