QGraphicsScene 上的抽搐运动 QGraphicsItem

Jerking movements QGraphicsItem on a QGraphicsScene

我正在尝试编写我的小游戏。为此,我用最简单的动作编写了一些代码:向左和向右。但是我有一个问题:如果我长时间按住移动键,动作会变得生涩,虽然这在代码中没有任何指示(记录在案,我一直按向右箭头,仅此而已) .

总的来说,方块移动的很流畅,但有时也会有这些急速的跳跃。

关于代码。

game_scene.cpp:

game_scene::game_scene()
{
    QTimer *update_timer = new QTimer();
    update_timer->setInterval(1000 / 30);
    connect(update_timer, &QTimer::timeout, this, &game_scene::update_rect);
    update_timer->start();
}

void game_scene::keyPressEvent(QKeyEvent *event)
{
    if (main_character ==  nullptr)
    {
        return;
    }

    std::thread *thd = nullptr;
    if (event->key() == Qt::Key_Right && !moving_right)
    {
        // move right
        moving_right = true;
        thd = new std::thread(&game_scene::move_right, this);
    }

    if (thd != nullptr)
    {
        thd->detach();
    }
}

void game_scene::keyReleaseEvent(QKeyEvent *event)
{
    if (event->key() == Qt::Key_Right)
    {
        // move right
        moving_right = false;
    }
}

void game_scene::move_right()
{
    while (moving_right)
    {
        x += main_character->move_right();
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
    }
}

character.cpp

int character::move_right()
{
    x += speed;
    return speed;
}

void character::paint(QPainter *painter, const QStyleOptionGraphicsItem */*option*/, QWidget */*widget*/)
{
    QPolygon polygon;
    polygon << QPoint(x, y) << QPoint(x, y + height) << QPoint(x + width, y + height) << QPoint(x + width, y);
    painter->setBrush(Qt::red);
    painter->drawPolygon(polygon);
}

分配一个单独的流来处理“行走”,这是必要的,以便可以从多个地方(行走、跳跃、任何其他)影响“角色”。

我哪里错了吗?也许线程太多了?但是有什么更好的方法呢?

在游戏中,当你想要实现100%流畅的运动错觉时,你需要考虑实际物理显示器(以及显卡构建的视频信号)的行为。

显示器是"drawing"从上到下,从左到右的屏幕,视频信号由每个像素的数据组成,数据量很大,所以通常会占用几乎所有可用时间在两帧之间(60Hz = 16.66..ms,100Hz=10ms,...)并且特定像素的信号基于建立信号时视频 ram 中的 "current" 值。

这些东西通常与 "vertical retrace period" 同步,这是旧 CRT 显示器重新配置磁铁以将 "beam" 从显示器右下角移回时的视频信号时间左上角..类似地,经典视频信号在每条扫描线的末尾包含较短的空闲期,以便让经典 CRT 管有时间将 "beam" 从右侧移动到左侧......我实际上不确定现代是否HDMI 信号仍然包含这些空闲回扫周期,因为 LCD 显示器不需要它们,但它肯定包含同步标记,让显示器知道整个帧和行的位置 start/end ... 信号的构建是时间上仍然连续。

在旧的 8 位计算机上,人们甚至设法以完美的代码计时与 "beam" 赛跑,并修改内存 ahead/after 视频 ram 内容被读取,以产生 "impossible" 输出,比如在单个 8x8 字符中有更多颜色,而视频模式规范建议每个字符只能有两种颜色,等等...

因为您根本不关心视频信号的时序...这很可能全部在 Qt、window 合成器和显卡驱动程序的管理中。这些很可能会缓冲您的绘画动作并在适当的时间与显示同步翻转新的屏幕缓冲区(新图像),以防止扩展 flickering/tearing(可能是 "VSYNC ON" 类机制)。所以这意味着如果你幸运地弄乱了两个视频帧之间的坐标,并且你将对象移动了恒定数量的像素,那么生成的图像将是 100% 流畅的动画。

如果您的线程卡住某些东西,或者它的周期与显示不同步,并且它确实每帧将对象移动不同数量的像素,移动将看起来 "jerky"。

(但如果它 "jumps" 的像素数量很大,那么它更有可能是代码中的一些多线程错误使对象在位置方面真正跳跃,上面描述的抖动是由于不是与视频帧同步应该更像是来自两帧之间的线程的 +-1 额​​外移动调用。

总体而言,旨在实现流畅的 60 FPS(在 60Hz 显示下)输出的(经典 2D 更简单)游戏通常根本不使用线程,而是让主循环随着显示的节拍而滴答作响,从在垂直回扫开始时准备缓冲区(因此显示器将在下一帧显示它),然后他们清除其他缓冲区,读取输入,处理 "physics",计算所有内容的新位置并将最终的新图像绘制到第二个缓冲区,这将在下一个循环中显示,如果还剩一些时间,他们会等待显示完成当前帧。当然,所有这些都必须适合那些 ~16ms,以实现流畅的 60FPS。

所以如果你真的很喜欢流畅的动画,你应该检查 Qt 是否有一些 API 来通知每个视频帧。我不知道 Qt API 但是这些高级别 APIs 通常要么让你编写自己的 "paint" 例程,你可以每次都使 window 内容无效,以强制框架不断调用你的 "paint"(这框架通常与视频信号本身同步,特别是如果它们有一些方法来设置 "vsync on" 或者 window 合成器处于这种模式)......或者它们可能有某种 API被通知回溯事件(不太可能像 Qt 这样的高级API)。并创建更严格的非线程版本,使用无限主循环,为每个显示帧准备新图像。

现代游戏中经常使用多线程来减轻主 CPU 线程的负担,但负责视频输出的部分仍然在很大程度上了解显示属性(刷新率和帧开始时间) ,并围绕它同步他们的行动。

使用像样的现代机器在屏幕上移动一个简单的 2D 正方形,您应该有很多空余 CPU 时间以简单的单线程主循环方式执行此操作,而不必求助于诸如返回之类的复杂技巧在 8/16 位计算机时代,由于内存限制,屏幕双缓冲通常甚至不是一个选项,并且主循环代码必须完美定时以在不破坏图像的情况下更改显存在屏幕上制作并保持完美的幻觉。

编辑:关于

的额外评论

"if I hold down the move key for a long time"...

上面的文章为什么你的方法不走运,"doomed" 从架构的角度来看是生涩的,但我没有在你的 gif 中具体解释。

问题是,您有不同的线程移动 "main character"(正方形)并滚动您的 "scene"(轴),试图将正方形保持在中间。因为所有这些都是在没有任何同步的独立线程中发生的,有时场景会在它们更新之前读取方块坐标,所以它将场景移动到旧位置,然后你重绘场景,但会读取新的方块位置,所以它显示再往前,然后在下一帧中,您终于将它拉回到中间。

即使你会在这些线程之间同步(比如场景首先将正方形位置复制到本地副本,然后解析场景的其他特征(基于所有 "live" 值的这些副本,忽略它们的进一步变化)并从这个本地副本绘制场景),在某些时候,你的线程 tick 很可能会与视频 tick 发生冲突,在帧的边缘附近滴答,有时在这里,有时那里,造成另一个抖动的来源。由于您的滴答声似乎遍布整个棋盘(33.33 毫秒、50 毫秒),它们可能会意外地开始大部分与彼此和视频信号同步,但有时会以特别不和谐的顺序发生动作,从而产生更多的抖动比平常多。

另外我猜测(因为你没有post定义你的moving_right你在调试模式下做这一切,没有正确使用mutex/atomic 类型在不同线程之间进行通信,所以一旦你尝试 "release" 构建这个,它可能会变得更糟(如果 moving_right 是简单的 bool,那么 while (moving_right) 在线程处理程序中将创建无限循环,因为优化器不知道该值可能会在当前线程之外更改。