用C++和SFML写游戏-Game类的创建(2)
这一节我们将会学习到游戏的基本结构,其中的内容包括了:
- Game类的创建
- 什么是帧数
- Player类的创建
- 事件管理器
Game类
在上一节中,我们用尽可能少的代码创建了一个基本游戏,它包括了:
- 窗口的创建
- 图形的绘制
- 处理用户的输入
- 将游戏元素绘制到屏幕上
上一节中的实例代码全部写在了 main 函数了,并没有使用到 C++ 的面向对象特性。为了提高我们代码的可复用性,从本节开始,我们将会一步步的使用 OOP 设计思想封装我们的游戏基本元素,搭建出一个游戏框架的雏形。首先是Game类的实现,如下所示:
1 | class Game { |
Game类中的 =delete 是为了删除C++类默认的拷贝构造函数以及拷贝赋值运算符,有关这方面的介绍可以去了解一下,这里就不做赘述。
可以看到,我们的 main 函数里面不在包含任何循环,游戏的运行只需要调用 Game 类中的 run 方法。对于类中的其它方法 processEvents(),update(),render() ,下面一一介绍:
- processEvents(): 这里处理用户的输入
- update(): 更新游戏的状态,计算出下一步
- render(): 绘制游戏的画面(渲染)
下面,先做简单的实现,为了简单,我们的游戏角色先用一个圆形表示:
1、构造方法的实现
1 | Game::Game() : _window(sf::VideoMode(800, 600),"02_Game_Archi"), |
2、Game.run() 隐藏了 main 里面的循环体
1 | void Game::run() { |
3、processEvents() 用于处理用户的输入,这里它只是简单地通过轮询从上一帧到现在的事件。例如点击按钮或者按下键盘按键,我们这里就只检查用户按下窗口的关闭按钮以及 ESC 键,然后窗口就会关闭。
1 | void Game::processEvents() { |
4、update() 方法更新了我们的游戏逻辑。但是现在我们还没有具体的逻辑实现,后面将会加入。
1 | void Game::update() {} |
5、render() 负责将游戏画面渲染到屏幕上。首先默认用 sf::Color::Black 清除窗口,然后将我们的游戏对象渲染到窗口,最后在屏幕上显示出来。
1 | void Game::render() { |
运行效果跟前一节是一样的
FPS
FPS(frames per second): 每秒的帧数,一帧就是通常指一个画面。
由于电脑硬件的不同,同一个游戏在不同电脑的运行速度很可能是不一样的。如果开发者没有注意到这个问题,可能会出现角色穿墙的情况,如 图 1 所示。
为了解决这个问题,通常有三种方案,一是动态时间步长,二是固定时间步长,三是两者一起使用。
1、动态时间步长
由于每台计算机的性能可能不一样,因此处理一帧所花费的时间也是不同的,但是现实世界中时间的流逝是一样的。 因此这种方法更新的主要原理就是计算出上帧到现在所花费的时间,然后将这个时间传入到 update() 函数。
最后的绘制效果如 图 2 所示,可以看到在运行快的计算机上面用户的帧数更高,而运行较慢的计算机上帧数较低,但是完成这个过程所花费的时间是一样的。
这个过程的代码大概长这个样子:
1 | void Game::run() { |
这时候,我们的 update 方法也需要做些改变:
1 | void update(sf::Time deltaTime); |
deltaTime 参数代表的是上次调用 update 到现在经过了多少时间
2、固定时间步长
自上一次游戏循环过去了一定量的真实时间。 需要为游戏的“当前时间”模拟推进相同长度的时间,以追上玩家的时间。 我们使用一系列的固定时间步长。 代码大致如下:
1 | void Game::run(int frame_per_seconds) { |
在每帧的开始,根据过去了多少真实的时间,更新timeSinceLastUpdate
。 这个变量表明了游戏世界时钟比真实世界落后了多少,然后我们使用一个固定时间步长(fix time step)的内部循环进行追赶。 一旦我们追上真实时间,我们就渲染然后开始新一轮循环。
3、最小时间步长
这个方法把前面两个方法结合。通过确保传入 update() 方法的时间参数不那么高使得游戏需要运行的足够快,也就是我们通过这个方法设置了最小的帧数,但是没有最大的。
就是将传入 update() 方法的时间参数不大于一个值,具体过程如图所示:
具体代码实现:
1 | void Game::run(int minimum_frame_per_seconds)) { |
在每一帧中,update() 方法都被调用,但是我们确保了传入参数不会太大。
下一节将会学习如何移动我们的角色😃。