用C++和SFML写游戏-Game类的创建(2)

本文详细介绍游戏开发中的基本结构,包括Game类的创建、帧数概念、Player类的创建及事件管理。通过C++面向对象特性,封装游戏元素,构建游戏框架。讲解动态时间步长、固定时间步长和最小时间步长三种解决角色移动问题的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这一节我们将会学习到游戏的基本结构,其中的内容包括了:

  • Game类的创建
  • 什么是帧数
  • Player类的创建
  • 事件管理器

Game类

在上一节中,我们用尽可能少的代码创建了一个基本游戏,它包括了:

  • 窗口的创建
  • 图形的绘制
  • 处理用户的输入
  • 将游戏元素绘制到屏幕上

上一节中的实例代码全部写在了 main 函数了,并没有使用到 C++ 的面向对象特性。为了提高我们代码的可复用性,从本节开始,我们将会一步步的使用 OOP 设计思想封装我们的游戏基本元素,搭建出一个游戏框架的雏形。首先是Game类的实现,如下所示:

class Game {
       public:
           Game(const Game&) = delete;
           Game& operator=(const Game&) = delete;
           Game();
           void run();
       private:
           void processEvents();
           void update();
           void render();
           sf::RenderWindow _window;
           sf::CircleShape  _player;
};
int main(int argc,char* argv[]) {
    Game game;
    game.run();
		return 0; 
}

Game类中的 =delete 是为了删除C++类默认的拷贝构造函数以及拷贝赋值运算符,有关这方面的介绍可以去了解一下,这里就不做赘述。

可以看到,我们的 main 函数里面不在包含任何循环,游戏的运行只需要调用 Game 类中的 run 方法。对于类中的其它方法 processEvents(),update(),render() ,下面一一介绍:

  • processEvents(): 这里处理用户的输入
  • update(): 更新游戏的状态,计算出下一步
  • render(): 绘制游戏的画面(渲染)

下面,先做简单的实现,为了简单,我们的游戏角色先用一个圆形表示:

1、构造方法的实现

Game::Game() : _window(sf::VideoMode(800, 600),"02_Game_Archi"),
_player(150) {
    _player.setFillColor(sf::Color::Blue);
    _player.setPosition(10, 20);
}

2、Game.run() 隐藏了 main 里面的循环体

void Game::run() {
    while (_window.isOpen()) {
        processEvents();
        update();
        render();
    }
}

3、processEvents() 用于处理用户的输入,这里它只是简单地通过轮询从上一帧到现在的事件。例如点击按钮或者按下键盘按键,我们这里就只检查用户按下窗口的关闭按钮以及 ESC 键,然后窗口就会关闭。

void Game::processEvents() {
    sf::Event event;
    while (_window.pollEvent(event)) {
        if ((event.type == sf::Event::Closed)
          || ((event.type == sf::Event::KeyPressed) && (event.key.code == sf::Keyboard::Escape))) {
            _window.close();
        }
    }
}

4、update() 方法更新了我们的游戏逻辑。但是现在我们还没有具体的逻辑实现,后面将会加入。

void Game::update() {}

5、render() 负责将游戏画面渲染到屏幕上。首先默认用 sf::Color::Black 清除窗口,然后将我们的游戏对象渲染到窗口,最后在屏幕上显示出来。

void Game::render() {
    _window.clear();
    _window.draw(_player);
    _window.display();
}

运行效果跟前一节是一样的

02_Game_Archi

FPS

FPS(frames per second): 每秒的帧数,一帧就是通常指一个画面。

由于电脑硬件的不同,同一个游戏在不同电脑的运行速度很可能是不一样的。如果开发者没有注意到这个问题,可能会出现角色穿墙的情况,如 图 1 所示。

图 1

为了解决这个问题,通常有三种方案,一是动态时间步长,二是固定时间步长,三是两者一起使用。

1、动态时间步长

由于每台计算机的性能可能不一样,因此处理一帧所花费的时间也是不同的,但是现实世界中时间的流逝是一样的。 因此这种方法更新的主要原理就是计算出上帧到现在所花费的时间,然后将这个时间传入到 update() 函数。

最后的绘制效果如 图 2 所示,可以看到在运行快的计算机上面用户的帧数更高,而运行较慢的计算机上帧数较低,但是完成这个过程所花费的时间是一样的。

图2

这个过程的代码大概长这个样子:

void Game::run() {
    sf::Clock clock;
    while (_window.isOpen()) {
        processEvents();
        update(clock.restart());
        render();
    }
}

这时候,我们的 update 方法也需要做些改变:

void update(sf::Time deltaTime);

deltaTime 参数代表的是上次调用 update 到现在经过了多少时间


2、固定时间步长

自上一次游戏循环过去了一定量的真实时间。 需要为游戏的“当前时间”模拟推进相同长度的时间,以追上玩家的时间。 我们使用一系列的固定时间步长。 代码大致如下:

void Game::run(int frame_per_seconds) {
    sf::Clock clock;
    sf::Time timeSinceLastUpdate = sf::Time::Zero;
    sf::Time TimePerFrame = sf::seconds(1.f/frame_per_seconds);
    while (_window.isOpen()) {
        processEvents();
        bool repaint = false;
        timeSinceLastUpdate += clock.restart();
        while (timeSinceLastUpdate > TimePerFrame) {
            timeSinceLastUpdate -= TimePerFrame;
            repaint = true;
            update(TimePerFrame);
        }
        if(repaint)
            render();
    }
}

在每帧开始的时候,timeSinceLastUpdate 表示的是 deltaTime,也就是上次两次 while 循环之间的时间差。 由于采用的是 固定时间步长(fix time step) 方法,如果机器得很慢,timeSinceLastUpdate 大于 fix time step,我们需要多做几次 update,这样才能保证游戏中的对象移动速度不会受机器性能的影响,它总是按照我们设置的速度前进。

3、最小时间步长

这个方法把前面两个方法结合。通过确保传入 update() 方法的时间参数不那么高使得游戏需要运行的足够快,也就是我们通过这个方法设置了最小的帧数,但是没有最大的。

就是将传入 update() 方法的时间参数不大于一个值,具体过程如图所示:

图 3

具体代码实现:

void Game::run(int minimum_frame_per_seconds)) {
    sf::Clock clock;
    sf::Time timeSinceLastUpdate;
    sf::Time TimePerFrame = sf::seconds(1.f/minimum_frame_per_seconds);
    while (_window.isOpen()) {
        processEvents();
        timeSinceLastUpdate = clock.restart();
        while (timeSinceLastUpdate > TimePerFrame) {
            timeSinceLastUpdate -= TimePerFrame;
            update(TimePerFrame);
        }
        update(timeSinceLastUpdate);
        render();
    }
}

在每一帧中,update() 方法都被调用,但是我们确保了传入参数不会太大。


下一节将会学习如何移动我们的角色?。

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值