Search code examples
c++qtqgraphicssceneqgraphicsitem

Jerking movements QGraphicsItem on a QGraphicsScene


I'm trying to write my little game. To do this, I wrote some code with the simplest movements: left and right. But I have a problem: if I hold down the move key for a long time, the movements become jerky, although this does not indicate anything in the code (on the record, I just press the right arrow all the time, nothing more).

In general, the square moves smoothly, but sometimes there are these sharp jumps.

enter image description here

About the code.

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);
}

Allocation of a separate stream for processing "walk", it is necessary in order that it was possible to influence "character" from several places (walking, jumps, anything else).

Was I wrong somewhere? Maybe the threads is too much? But what better way to do it?


Solution

  • In games, when you want to achieve 100% fluent illusion of movement, you need to consider the behaviour of the actual physical display (and video signal built by graphics card).

    The display is "drawing" screen from top to bottom, left to right, the video signal consisting of data for every pixel, which amounts to lot of data, so usually it takes pretty much all the available time between two frames (60Hz = 16.66..ms, 100Hz=10ms, ...) and the signal for particular pixel is based on the "current" value in video ram at the point the signal is built.

    These things are often synced to "vertical retrace period", which is the time of video signal when old CRT displays were given to reconfigure their magnets to move the "beam" from bottom right corner of display back to top left .. similarly the classic video signal contains shorter idle periods at end of each scanline, to give classic CRT tube time to move the "beam" from right side to left side ... I'm actually not sure if modern HDMI signal still contains these idle retrace periods, as LCD display doesn't need them, but it for sure contains sync-marks to let display know where whole frame and lines start/end ... and the build of the signal is still continuous in time.

    On old 8bit computers people even managed to race the "beam" with perfect timing of code and modify the memory just ahead/after the video ram content was read, to produce "impossible" output, like having more colours in single 8x8 character while the video mode spec suggested only two colour per characters are possible, etc...

    Because you don't bother with timing to video signal at all ... it's very likely all in the management of Qt, window compositor and graphics card driver. These are very likely buffering your painting actions and flipping new screen buffer (new image) at appropriate time in sync with display to prevent extensive flickering/tearing (probably "VSYNC ON"-like mechanism). So that means if you are lucky to mess with the coordinates just in between two video frames, and you move the object by constant amount of pixels between, the resulting image will be 100% fluent animation.

    If your thread chokes on something, or it's period is not in sync with display and it does move the object by different amount of pixels each frame, the movement will look "jerky".

    (but if it "jumps" by serious amount of pixels, then it's more likely some multi-thread bug in your code making the object truly jump in terms of position, the jerkiness described above due to being not in sync with video frames should be more like +-1 extra movement call from thread between two frames).

    Overall the (classic 2D simpler) games aiming for fluent 60 FPS (at 60Hz display) output usually don't use threads at all, but instead have main loop ticking in the beat of the display, flipping new image from prepared buffer at beginning of vertical retrace (so the display will display it in next frame), and then they clear other buffer, read inputs, process "physics", calculate new positions of everything and draw the final new image into second buffer, which will be displayed in next loop and if some time is left, they wait for the display to finish current frame.. all of this must fit into those ~16ms of course, to achieve fluent 60FPS.

    So if you are really into fluent animation, you should check if the Qt has some API to be notified of every video frame.. I don't know Qt API but these high level APIs usually either let you write your own "paint" routine, and you can invalidate the window content every time, to force the framework keep calling your "paint" continuously (which the frameworks usually sync with video signal themselves, especially if they have some method to set "vsync on" or the window compositor is in such mode) ... or they may have some kind of API to be notified of the retrace event (less likely with such high-level API as Qt). And create more rigid non-thread version working with infinite main loop, preparing new image for every display frame.

    The multithreading is often used in modern games to offload the main CPU thread a bit, but still the video output responsible parts are largely aware of the display properties (refresh rate, and when the frame starts), and sync their actions around that.

    With a simple 2D square moving on screen with a decent modern machine you should have lot of spare CPU time to do this in trivial single thread main loop way, without having to resort to complicated tricks like back in the age of 8/16bit computers, when double-buffering of screen was often not even an option due to memory limits, and the main loop code had to be timed perfectly to make changes to video memory in a way to not ruin the image produced on screen and keep the illusion perfect.

    edit: extra comments about

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

    The essay above why your approach is unlucky and "doomed" to be jerky from architectural point of view, but I didn't explain specifically the thing in your gif.

    The issue is, that you have different threads moving the "main character" (square) and scrolling your "scene" (axis), trying to keep the square in the middle. Because all of this is happening in independent threads without any syncing, sometimes the scene reads square coordinates just before they get updated, so it moves the scene to old position, then you redraw the scene, but reads the new square position, so it shows further ahead, then in next frame you finally snap it back into the middle.

    Even if you would sync between these threads (like scene copying the square position into local copy first, then resolving other features of scene (based on these copies of all "live" values, ignoring their further changes) and drawing the scene from this local copy), at some point your thread tick will get highly likely into clash with the video tick, ticking just around the edge of frames, sometimes here, sometimes there, creating another source of jerkiness. As your ticks seems all over the board (33,33ms, 50ms), they may accidentally start mostly in sync with each other and video signal, but there will be periods of time when the actions will happen in particularly disharmonious order, producing more jerkiness than usually.

    Also I guess (as you didn't post definition of your moving_right you are doing this everything in Debug mode, and not using correctly mutex/atomic types to communicate between different threads, so once you will try "release" build of this, it may get much worse (if the moving_right is simple bool, then the while (moving_right) in the thread handler will create infinite loop, because the optimizer is not aware the value may change outside of the current thread.