Smooth character movement
-
Hi, welcome to devnet.
You should not move your character in a key press event. Different keybords, OS settings, custom drivers etc. can all alter the frequency in which these happen. Even if you managed to fiddle with the values to look good on your computer it doesn't mean it would on others. For example your character might move at super speeds on a pro-gaming keyboard tuned for high-frequency updates.
The correct way to do that is to "mark" your character as moving on key press and "unmark" it on key release. Then, in some timer or however you advance your game, you would move the characters that are marked. If the update rate is well synced with the refresh rate of the display you will get a smooth movement, independent of jittery key presses.
-
Ok. So here's what I've come up with.
void PlayerCharacter::keyPressEvent(QKeyEvent *event) { const int FPS = 60; // move player left if (event->key() == Qt::Key_A) { if (direction == directions::right) { // face character left setTransform(QTransform::fromScale(-1, 1)); direction = directions::left; } if (!moveTimer->isActive()) { moveTimer->start(1000/FPS); } } // move player right else if (event->key() == Qt::Key_D) { if (direction == directions::left) { // face character right setTransform(QTransform::fromScale(1, 1)); direction = directions::right; } if (!moveTimer->isActive()) { moveTimer->start(1000/FPS); } } } void PlayerCharacter::keyReleaseEvent(QKeyEvent *event) { // stop player moving left or right if (event->key() == Qt::Key_A || event->key() == Qt::Key_D) { if (moveTimer->isActive()) { moveTimer->stop(); } } } void Character::move() { const double DISTANCE_MOVED_PER_FRAME = 1.0; if (direction == directions::right) { // player wants to move right setPos(x() + DISTANCE_MOVED_PER_FRAME,y()); } else { // player wants to move left setPos(x() - DISTANCE_MOVED_PER_FRAME,y()); } }
I'm not sure if I'm understanding the timeout with the FPS and the DISTANCE_MOVED_PER_FRAME correctly as I'm still having trouble moving the sprite with speed in a smooth fashion.
-
You're starting the timer in a key press event so that skips a bit. That's one potential problem.
The other is the timer - it runs at 1000/FPS interval. Is that synchronized to the monitor refresh somehow? Timers are not very good method for smooth animation. See my older response in another thread for some details about smooth animation. -
Ok.
So on the KeyPressEvent I mark the character as moving.
On KeyReleaseEvent I mark moving as false for that character.
During the game I check for the vertical sync and redraw during that event with the marked character at its new position.
Is that right?
I've had a look for a VSync event and I'm having trouble finding something in Qt directly. It seems that I need to use some OpenGL methods.
Maybe something like OpenGL Window Example.
My Game is placed in a QGraphicsView which contains the QGraphicsScene. The sprite is a QGraphicsItem. Would I be redrawing the entire View, Scene or just the Item? Or if I use the method from the OpenGL example do I need to do all of the drawing using OpenGL directly?
-
Did you ever solve this? Or is the conclusion that qGraphicsView can not be used to get smooth character movement?
I've spent weeks googeling and experimenting but I am still unable to get movement that is not horribly choppy/stuttering or flawed in some other way. One of many examples:
#include "mainwindow.h" #include "ui_mainwindow.h" #include <QGraphicsPixmapItem> #include <QKeyEvent> MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); installEventFilter(this); /* //These settings result in smooth movement, but now the movement is affected by the window size //A small window will move much much faster than a big window moveSpeed = 1; frameRate = 0.5; */ //This give same movement speed regardless of window size, but is instead very choppy moveSpeed = 8; frameRate = 1000/60; keyWActive = keyAActive = keySActive = keyDActive = false; timerMove = new QTimer(this); timerMove->setTimerType(Qt::PreciseTimer); connect(timerMove, SIGNAL(timeout()), this, SLOT(move())); hScrollBar = ui->graphicsView->horizontalScrollBar(); vScrollBar = ui->graphicsView->verticalScrollBar(); graphicsScene = new QGraphicsScene(this); ui->graphicsView->setScene(graphicsScene); QPixmap pixmap(":/new/prefix1/trees_forest_4000x2656.jpg"); QGraphicsPixmapItem *item = new QGraphicsPixmapItem(pixmap); graphicsScene->addItem(item); } MainWindow::~MainWindow() { delete ui; } void MainWindow::move(){ if (keyAActive){ //using the ScrollBars is choppy, tried .translate() instead, same result //int x = hScrollBar->value(); //hScrollBar->setValue(x-moveSpeed); ui->graphicsView->translate(moveSpeed,0); } if (keyWActive){ //int y = vScrollBar->value(); //vScrollBar->setValue(y-moveSpeed); ui->graphicsView->translate(0,moveSpeed); } if (keySActive){ //int y = vScrollBar->value(); //vScrollBar->setValue(y+moveSpeed); ui->graphicsView->translate(0,-moveSpeed); } if (keyDActive){ //int x = hScrollBar->value(); //hScrollBar->setValue(x+moveSpeed); ui->graphicsView->translate(-moveSpeed,0); } } void MainWindow::stopMove(){ if (keyAActive==false && keyWActive==false && keySActive==false && keyDActive==false){ timerMove->stop(); } } bool MainWindow::eventFilter(QObject *obj, QEvent *event) { // This function repeatedly call for those QObjects // which have installed eventFilter (Step 2) if (obj == (QObject*)this){ if (event->type() == QEvent::KeyPress){ QKeyEvent *KeyEvent = (QKeyEvent*)event; switch(KeyEvent->key()) { case Qt::Key_A: if (keyAActive == false){ timerMove->start(frameRate); keyAActive=true; } return true; case Qt::Key_W: if (keyWActive == false){ timerMove->start(frameRate); keyWActive=true; } return true; case Qt::Key_S: if (keySActive == false){ timerMove->start(frameRate); keySActive=true; } return true; case Qt::Key_D: if (keyDActive == false){ timerMove->start(frameRate); keyDActive=true; } return true; default: break; } }else if (event->type() == QEvent::KeyRelease){ QKeyEvent *KeyEvent = (QKeyEvent*)event; switch(KeyEvent->key()) { case Qt::Key_W: if (KeyEvent->isAutoRepeat()==false){ stopMove(); keyWActive=false; } return true; case Qt::Key_A: if (KeyEvent->isAutoRepeat()==false){ stopMove(); keyAActive=false; } return true; case Qt::Key_S: if (KeyEvent->isAutoRepeat()==false){ stopMove(); keySActive=false; } return true; case Qt::Key_D: if (KeyEvent->isAutoRepeat()==false){ stopMove(); keyDActive=false; } return true; default: break; } } return QWidget::eventFilter(obj, event); }else { // pass the event on to the parent class return QWidget::eventFilter(obj, event); } }
-
Hi,
I don't know if there's a solution for this yet, but for creating games with Qt, you can also have a look at V-Play Engine for qt-based mobile games and apps.
The QML-based gaming components make it easy to add animations, movement, physics or collision detection.
In addition, the SDK comes with many full-featured open-source demos and tutorials, for example to create a Flappy Bird game.
Best,
GT -
@Tengger said in Smooth character movement:
I am still unable to get movement that is not horribly choppy/stuttering or flawed in some other way.
Is it possible to solve this problem with Qt?
Please, compare the same demos:
- WebGL demo: https://8observer8.github.io/webgl10-ts/jumps/
- Qt demo for Windows 10 64-bit: Jumps_Box2DOpenGL2_Qt6Cpp.zip (7.58 MB)
-
I am rewriting my sample game with SDL2 + OpenGL 2.1. I made the same example as above. As you can see, this is better than the Qt demo. But WebGL is the best. I expected SDL2 to be as good as WebGL. You can download EXE for Windows 10 64-bit. Use wad-keys or arrow-keys (or spacebar to jump)
- SDL2 + OpenGL 2.1 + GLM. Demo for Windows 10 64 bit: Jumps_Box2DOpenGL2_SDL2Cpp.zip (3.21 MB)
I use Free Texture Packer and Tiled Map Editor to load textures and level/colliders from the JSON file. You can see Qt6Core.dll in SDL2 project because I use QResource, QJson and QFile:
-
I should note that all three demos above use 60 fps. I use QTimer and QElapsedTimer for game loop. What are the options for improving the game loop? Maybe because of the message queue in Qt it is not possible to improve it?
#include <QtCore/QElapsedTimer> #include <QtCore/QTimer> /* ... */ QElapsedTimer m_elapsedTimer; QTimer m_timer; float m_deltaTime; /* ... */ /* ... */ connect(&m_timer, &QTimer::timeout, this, &Widget::animationLoop); /* ... */ /* ... */ m_elapsedTimer.start(); m_timer.start(1000.f / 60.f); /* ... */ /* ... */ void Widget::paintGL() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); /* ... */ void Widget::animationLoop() { m_deltaTime = m_elapsedTimer.elapsed() / 1000.f; m_elapsedTimer.restart(); if (m_isLeftPressed) { b2Vec2 vel = m_pPlayerBody->GetLinearVelocity(); vel.x = -5.f; m_pPlayerBody->SetLinearVelocity(vel); } if (m_isRightPressed) { b2Vec2 vel = m_pPlayerBody->GetLinearVelocity(); vel.x = 5.f; m_pPlayerBody->SetLinearVelocity(vel); } if (!m_isLeftPressed && !m_isRightPressed) { b2Vec2 vel = m_pPlayerBody->GetLinearVelocity(); vel.x = 0.f; m_pPlayerBody->SetLinearVelocity(vel); } m_pWorld->Step(m_deltaTime, 6, 2); update(); }
-
@8Observer8 You are not rendering at 60 FPS. You are scheduling an update and it's only at around 60 times a second. Timers are not precise enough to get a steady clock like that and they are not synchronized in any way whatsoever with the monitor's refresh rate. Because of that every few frames you're missing the vertical sync by just a little and that is a skipped frame that results in the choppy animation.
The only way to get a smooth animation like that is to not use timer at all and tie yourself to the vertical refresh instead. Qt has multiple ways to draw graphics on screen and sadly not all of them expose this moment to you, but if you're using a QOpenGLWindow there's a frameSwapped() signal that is emitted after a vertical sync. Connect
update()
to it and inside the paint event handler calculate the amount of time passed from last update (QElapsedTimer is fine for that).Keep in mind that this signal will be fired at the native refresh rate of the monitor, so for example on a 165Hz screen it will be emitted 165 times a second. If your game can keep up with that that's great, but if not you'll have to calculate a reasonable rate at which you update. For example if the monitor refreshes at 120Hz and you want to update at 60Hz you'll have to skip every other frame. For smooth animation the frequency at which you update needs to be an integer divisor of the monitor's refresh rate e.g. for a 60Hz monitor you can update at 60, 30, 20, 15 etc. Everything between will either skip or repeat frames and will look choppy.
Also note that refreshing at a fixed rate like 60 is not a good idea. For example on a 165Hz monitor a 60 updates per second would result in a frame every 2.75 vertical syncs and that obviously won't work smoothly. You'll again either lag behind or skip ahead and get a choppy animation. You need to figure out monitor's refresh rate and choose your update rate accordingly.
-
@Chris-Kawa thank you very much! I found your reply here that has another very useful links on your replies. I will rewrite my example with QOpenGLWindow instead of QOpenGLWidget. I will explore your idea with the frameSwapped() signal. Thanks again!
-
@8Observer8 No problem. You might find this old post useful too, at least for visualization of what happens. The context is a bit different. It was about drawing with QPainter, so on the CPU, but the idea is the same. Your case falls into the 17ms example category there - being occasionally just a little too late for the refresh.
-
@Chris-Kawa said in Smooth character movement:
but if you're using a QOpenGLWindow there's a frameSwapped() signal that is emitted after a vertical sync. Connect update() to it and inside the paint event handler calculate the amount of time passed from last update (QElapsedTimer is fine for that)
I made this part in this simple example, source code: QOpenGLWindow_frameSwapped.zip with movement a square with WASD or arrow keys. The first problem is that movement does not start at once (you should to try run it to understand) and the second problem is the same - the choppy movement.
I connected the
frameSwapped
signal with theupdate
slot:connect(this, SIGNAL(frameSwapped()), this, SLOT(update()));
@Chris-Kawa said in Smooth character movement:
Keep in mind that this signal will be fired at the native refresh rate of the monitor, so for example on a 165Hz screen it will be emitted 165 times a second. If your game can keep up with that that's great, but if not you'll have to calculate a reasonable rate at which you update. For example if the monitor refreshes at 120Hz and you want to update at 60Hz you'll have to skip every other frame. For smooth animation the frequency at which you update needs to be an integer divisor of the monitor's refresh rate e.g. for a 60Hz monitor you can update at 60, 30, 20, 15 etc. Everything between will either skip or repeat frames and will look choppy.
I printed the delta time, but it is 5-6 ms. It's 166-200 frames per second. I do not understand what to do now, but I will study your messages very carefully.
QOpenGLWindow_frameSwapped.pro
QT += core gui opengl QT += widgets CONFIG += c++11 SOURCES += \ OpenGLWindow.cpp \ main.cpp HEADERS += \ OpenGLWindow.h RESOURCES += \ Shaders.qrc
OpenGLWindow.h
#ifndef OPENGLWINDOW_H #define OPENGLWINDOW_H #include <QtCore/QElapsedTimer> #include <QtGui/QOpenGLFunctions> #include <QtOpenGL/QOpenGLWindow> #include <QtOpenGL/QOpenGLShaderProgram> #include <QtOpenGL/QOpenGLBuffer> #include <QtGui/QMatrix4x4> #include <QtGui/QKeyEvent> class OpenGLWindow : public QOpenGLWindow, private QOpenGLFunctions { Q_OBJECT public: OpenGLWindow(); ~OpenGLWindow(); private: void initializeGL() override; void resizeGL(int w, int h) override; void paintGL() override; void keyPressEvent(QKeyEvent *event) override; private: QOpenGLShaderProgram m_program; QOpenGLBuffer m_vertBuffer; int m_uMvpMatrixLocation; QMatrix4x4 m_projMatrix; QMatrix4x4 m_viewMatrix; QMatrix4x4 m_projViewMatrix; QMatrix4x4 m_modelMatrix; QMatrix4x4 m_mvpMatrix; float m_x = 50.f; float m_y = 50.f; float m_speed = 200.f; QElapsedTimer m_elapsedTimer; float m_dt; }; #endif // OPENGLWINDOW_H
OpenGLWindow.cpp
#include "OpenGLWindow.h" OpenGLWindow::OpenGLWindow() { resize(500, 500); setTitle("QOpenGLWindow, OpenGL 2.1, C++"); } OpenGLWindow::~OpenGLWindow() { } void OpenGLWindow::initializeGL() { initializeOpenGLFunctions(); glClearColor(0.2f, 0.2f, 0.2f, 1.f); connect(this, SIGNAL(frameSwapped()), this, SLOT(update())); m_program.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/assets/shaders/default.vert"); m_program.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/assets/shaders/default.frag"); m_program.link(); float vertPositions[] = { -0.5f, -0.5f, 0.f, -0.5f, 0.5f, 0.f, 0.5f, -0.5f, 0.f, 0.5f, -0.5f, 0.f, -0.5f, 0.5f, 0.f, 0.5f, 0.5f, 0.f }; m_vertBuffer.create(); m_vertBuffer.bind(); m_vertBuffer.allocate(vertPositions, sizeof(vertPositions)); m_program.bind(); int aPositionLocation = m_program.attributeLocation("aPosition"); m_program.setAttributeBuffer(aPositionLocation, GL_FLOAT, 0, 3); m_program.enableAttributeArray(aPositionLocation); m_uMvpMatrixLocation = m_program.uniformLocation("uMvpMatrix"); m_projMatrix.ortho(0.f, 100.f, 100.f, 0.f, 50.f, 0.f); m_viewMatrix.lookAt(QVector3D(0.f, 0.f, 40.f), QVector3D(0.f, 0.f, 0.f), QVector3D(0.f, 1.f, 0.f)); m_projViewMatrix = m_projMatrix * m_viewMatrix; m_elapsedTimer.start(); } void OpenGLWindow::resizeGL(int w, int h) { glViewport(0, 0, w, h); } void OpenGLWindow::paintGL() { glClear(GL_COLOR_BUFFER_BIT); m_dt = m_elapsedTimer.elapsed() / 1000.f; m_elapsedTimer.restart(); m_modelMatrix.setToIdentity(); m_modelMatrix.translate(m_x, m_y, 0.f); m_modelMatrix.scale(5.f, 5.f, 1.f); m_mvpMatrix = m_projViewMatrix * m_modelMatrix; m_program.bind(); m_program.setUniformValue(m_uMvpMatrixLocation, m_mvpMatrix); glDrawArrays(GL_TRIANGLES, 0, 6); } void OpenGLWindow::keyPressEvent(QKeyEvent *event) { switch (event->key()) { case Qt::Key::Key_W: case Qt::Key::Key_Up: { m_y -= m_speed * m_dt; break; } case Qt::Key::Key_A: case Qt::Key::Key_Left: { m_x -= m_speed * m_dt; break; } case Qt::Key::Key_S: case Qt::Key::Key_Down: { m_y += m_speed * m_dt; break; } case Qt::Key::Key_D: case Qt::Key::Key_Right: { m_x += m_speed * m_dt; break; } } }
main.cpp
#include "OpenGLWindow.h" #include <QtWidgets/QApplication> int main(int argc, char *argv[]) { QApplication a(argc, argv); OpenGLWindow w; w.show(); return a.exec(); }
default.vert
attribute vec3 aPosition; uniform mat4 uMvpMatrix; void main() { gl_Position = uMvpMatrix * vec4(aPosition, 1.0); }
default.frag
void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); }
-
First of all you're doing your input handling wrong. Different keyboards have a different signal frequency. Second, if you press and hold a key you will get an immediate key press event, then a pause and then consecutive key presses in a steady interval, which also varies from keyboard to keyboard and can be changed in Windows settings. That's because keyboards are made for typing. Game input is not their primary role.
Apart from all that keyboard events are, again, not at all synced with your monitor refresh rate. The way you have it now your character moves at your keyboard update rate and will vary with different keyboards, which is not good at all.To handle input correctly you would make an array or a map of key states (bools). At key press you set a given key to true, on release you set it to false. In your game update code, which is synced to your monitor's refresh rate, you query that state. This way you're updating the game independently of the input frequency. You're just checking if the key is pressed at the time of the update.
See this other old thread for an example code how to handle keyboard for gaming input correctly.
The other thing is - make sure you actually have vertical synchronization turned on. In vanilla OpenGL this is done through platform specific extensions like WGL_EXT_swap_control, but Qt has it abstracted as QSurfaceFormat::setSwapInterval(). In your class constructor set a format that has swap interval set to 1, which means you will get the frameSwapped signal at monitor's refresh rate. If you set it to 0 you will get that signal as fast as is possible i.e. a lot faster than your monitor actually refreshes. With a value of 2 you will get a signal every other refresh, with 3 every third and so on.
-
@Chris-Kawa said in Big Issue with Qt key inputs for gaming:
Sorry if I made it sound scarry. It's actually as easy as this:
class MyWidget : public QWidget { Q_OBJECT public: MyWidget() { setFocusPolicy(Qt::StrongFocus); startTimer(1000/60); } void keyPressEvent(QKeyEvent *e) { keys[e->key()] = true; QWidget::keyPressEvent(e); } void keyReleaseEvent(QKeyEvent *e) { keys[e->key()] = false; QWidget::keyReleaseEvent(e); } void timerEvent(QTimerEvent *) { if(keys[Qt::Key_Up]) /* some game logic */; } private: QMap<int, bool> keys; };
As you can see input can be put into the map from wherever, key events, mouse events, gamepad or whatever.
Of course there are considerations, like map performance or accessing the map in multi-threaded environment but that's the basic idea.EDIT: I don't know Python that well but you should be able to translate it easily
So far I've only tried this method on my sample. It's already much better than it used to be:
- EXE for Window 10, 64 bit: KeysQMap_OpenGL2_Qt6_Cpp_EXE.rar (6 MB)
- Source code: KeysQMap_OpenGL2_Qt6_Cpp_Source.rar (2.84 KB)
KeysQMap_OpenGL2_Qt6_Cpp.pro
QT += core gui opengl QT += widgets CONFIG += c++11 SOURCES += \ OpenGLWindow.cpp \ main.cpp HEADERS += \ OpenGLWindow.h RESOURCES += \ Shaders.qrc
OpenGLWindow.h
#ifndef OPENGLWINDOW_H #define OPENGLWINDOW_H #include <QtCore/QElapsedTimer> #include <QtCore/QMap> #include <QtGui/QOpenGLFunctions> #include <QtOpenGL/QOpenGLWindow> #include <QtOpenGL/QOpenGLShaderProgram> #include <QtOpenGL/QOpenGLBuffer> #include <QtGui/QMatrix4x4> #include <QtGui/QKeyEvent> class OpenGLWindow : public QOpenGLWindow, private QOpenGLFunctions { Q_OBJECT public: OpenGLWindow(QWindow *parent = 0); ~OpenGLWindow(); private: void initializeGL() override; void resizeGL(int w, int h) override; void paintGL() override; void keyPressEvent(QKeyEvent *event) override; void keyReleaseEvent(QKeyEvent *event) override; private: QOpenGLShaderProgram m_program; QOpenGLBuffer m_vertBuffer; int m_uMvpMatrixLocation; QMatrix4x4 m_projMatrix; QMatrix4x4 m_viewMatrix; QMatrix4x4 m_projViewMatrix; QMatrix4x4 m_modelMatrix; QMatrix4x4 m_mvpMatrix; float m_x = 50.f; float m_y = 50.f; float m_speed = 50.f; QElapsedTimer m_elapsedTimer; float m_dt; QMap<int, bool> m_keys; }; #endif // OPENGLWINDOW_H
OpenGLWindow.cpp
#include "OpenGLWindow.h" OpenGLWindow::OpenGLWindow(QWindow *parent) : QOpenGLWindow(NoPartialUpdate, parent) { resize(500, 500); setTitle("QOpenGLWindow, OpenGL 2.1, C++"); } OpenGLWindow::~OpenGLWindow() { } void OpenGLWindow::initializeGL() { initializeOpenGLFunctions(); glClearColor(0.2f, 0.2f, 0.2f, 1.f); connect(this, SIGNAL(frameSwapped()), this, SLOT(update())); m_program.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/assets/shaders/default.vert"); m_program.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/assets/shaders/default.frag"); m_program.link(); float vertPositions[] = { -0.5f, -0.5f, 0.f, -0.5f, 0.5f, 0.f, 0.5f, -0.5f, 0.f, 0.5f, -0.5f, 0.f, -0.5f, 0.5f, 0.f, 0.5f, 0.5f, 0.f }; m_vertBuffer.create(); m_vertBuffer.bind(); m_vertBuffer.allocate(vertPositions, sizeof(vertPositions)); m_program.bind(); int aPositionLocation = m_program.attributeLocation("aPosition"); m_program.setAttributeBuffer(aPositionLocation, GL_FLOAT, 0, 3); m_program.enableAttributeArray(aPositionLocation); m_uMvpMatrixLocation = m_program.uniformLocation("uMvpMatrix"); m_projMatrix.ortho(0.f, 100.f, 100.f, 0.f, 50.f, 0.f); m_viewMatrix.lookAt(QVector3D(0.f, 0.f, 40.f), QVector3D(0.f, 0.f, 0.f), QVector3D(0.f, 1.f, 0.f)); m_projViewMatrix = m_projMatrix * m_viewMatrix; m_elapsedTimer.start(); } void OpenGLWindow::resizeGL(int w, int h) { glViewport(0, 0, w, h); } void OpenGLWindow::paintGL() { glClear(GL_COLOR_BUFFER_BIT); m_dt = m_elapsedTimer.elapsed() / 1000.f; m_elapsedTimer.restart(); if (m_keys[Qt::Key::Key_W] || m_keys[Qt::Key::Key_Up]) { m_y -= m_speed * m_dt; } if (m_keys[Qt::Key::Key_A] || m_keys[Qt::Key::Key_Left]) { m_x -= m_speed * m_dt; } if (m_keys[Qt::Key::Key_S] || m_keys[Qt::Key::Key_Down]) { m_y += m_speed * m_dt; } if (m_keys[Qt::Key::Key_D] || m_keys[Qt::Key::Key_Right]) { m_x += m_speed * m_dt; } m_modelMatrix.setToIdentity(); m_modelMatrix.translate(m_x, m_y, 0.f); m_modelMatrix.scale(5.f, 5.f, 1.f); m_mvpMatrix = m_projViewMatrix * m_modelMatrix; m_program.bind(); m_program.setUniformValue(m_uMvpMatrixLocation, m_mvpMatrix); glDrawArrays(GL_TRIANGLES, 0, 6); } void OpenGLWindow::keyPressEvent(QKeyEvent *event) { switch (event->key()) { case Qt::Key::Key_W: case Qt::Key::Key_Up: { m_keys[Qt::Key::Key_W] = true; m_keys[Qt::Key::Key_Up] = true; break; } case Qt::Key::Key_A: case Qt::Key::Key_Left: { m_keys[Qt::Key::Key_A] = true; m_keys[Qt::Key::Key_Left] = true; break; } case Qt::Key::Key_S: case Qt::Key::Key_Down: { m_keys[Qt::Key::Key_S] = true; m_keys[Qt::Key::Key_Down] = true; break; } case Qt::Key::Key_D: case Qt::Key::Key_Right: { m_keys[Qt::Key::Key_D] = true; m_keys[Qt::Key::Key_Right] = true; break; } } } void OpenGLWindow::keyReleaseEvent(QKeyEvent *event) { switch (event->key()) { case Qt::Key::Key_W: case Qt::Key::Key_Up: { m_keys[Qt::Key::Key_W] = false; m_keys[Qt::Key::Key_Up] = false; break; } case Qt::Key::Key_A: case Qt::Key::Key_Left: { m_keys[Qt::Key::Key_A] = false; m_keys[Qt::Key::Key_Left] = false; break; } case Qt::Key::Key_S: case Qt::Key::Key_Down: { m_keys[Qt::Key::Key_S] = false; m_keys[Qt::Key::Key_Down] = false; break; } case Qt::Key::Key_D: case Qt::Key::Key_Right: { m_keys[Qt::Key::Key_D] = false; m_keys[Qt::Key::Key_Right] = false; break; } } }
main.cpp
#include "OpenGLWindow.h" #include <QtWidgets/QApplication> int main(int argc, char *argv[]) { QApplication a(argc, argv); OpenGLWindow w; w.show(); return a.exec(); }
default.vert
attribute vec3 aPosition; uniform mat4 uMvpMatrix; void main() { gl_Position = uMvpMatrix * vec4(aPosition, 1.0); }
default.frag
void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); }
-
@Chris-Kawa said in Smooth character movement:
The other thing is - make sure you actually have vertical synchronization turned on. In vanilla OpenGL this is done through platform specific extensions like WGL_EXT_swap_control, but Qt has it abstracted as QSurfaceFormat::setSwapInterval(). In your class constructor set a format that has swap interval set to 1, which means you will get the frameSwapped signal at monitor's refresh rate. If you set it to 0 you will get that signal as fast as is possible i.e. a lot faster than your monitor actually refreshes. With a value of 2 you will get a signal every other refresh, with 3 every third and so on.
Seems to be a little better.
- EXE for Window 10, 64 bit: setSwapInterval_OpenGL2_Qt6_Cpp_EXE.rar (6 MB)
- Source code: setSwapInterval_OpenGL2_Qt6_Cpp_Source.rar (3 KB)
setSwapInterval_OpenGL2_Qt6_Cpp.pro
QT += core gui opengl QT += widgets CONFIG += c++11 SOURCES += \ OpenGLWindow.cpp \ main.cpp HEADERS += \ OpenGLWindow.h RESOURCES += \ Shaders.qrc
OpenGLWindow.h
#ifndef OPENGLWINDOW_H #define OPENGLWINDOW_H #include <QtCore/QElapsedTimer> #include <QtCore/QMap> #include <QtGui/QOpenGLFunctions> #include <QtOpenGL/QOpenGLWindow> #include <QtOpenGL/QOpenGLShaderProgram> #include <QtOpenGL/QOpenGLBuffer> #include <QtGui/QMatrix4x4> #include <QtGui/QKeyEvent> class OpenGLWindow : public QOpenGLWindow, private QOpenGLFunctions { Q_OBJECT public: OpenGLWindow(QWindow *parent = 0); ~OpenGLWindow(); private: void initializeGL() override; void resizeGL(int w, int h) override; void paintGL() override; void keyPressEvent(QKeyEvent *event) override; void keyReleaseEvent(QKeyEvent *event) override; private: QOpenGLShaderProgram m_program; QOpenGLBuffer m_vertBuffer; int m_uMvpMatrixLocation; QMatrix4x4 m_projMatrix; QMatrix4x4 m_viewMatrix; QMatrix4x4 m_projViewMatrix; QMatrix4x4 m_modelMatrix; QMatrix4x4 m_mvpMatrix; float m_x = 50.f; float m_y = 50.f; float m_speed = 50.f; QElapsedTimer m_elapsedTimer; float m_dt; QMap<int, bool> m_keys; }; #endif // OPENGLWINDOW_H
OpenGLWindow.cpp
#include "OpenGLWindow.h" OpenGLWindow::OpenGLWindow(QWindow *parent) : QOpenGLWindow(NoPartialUpdate, parent) { resize(500, 500); setTitle("QOpenGLWindow, OpenGL 2.1, C++"); QSurfaceFormat format; format.setSwapInterval(1); setFormat(format); } OpenGLWindow::~OpenGLWindow() { } void OpenGLWindow::initializeGL() { initializeOpenGLFunctions(); glClearColor(0.2f, 0.2f, 0.2f, 1.f); connect(this, SIGNAL(frameSwapped()), this, SLOT(update())); m_program.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/assets/shaders/default.vert"); m_program.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/assets/shaders/default.frag"); m_program.link(); float vertPositions[] = { -0.5f, -0.5f, 0.f, -0.5f, 0.5f, 0.f, 0.5f, -0.5f, 0.f, 0.5f, -0.5f, 0.f, -0.5f, 0.5f, 0.f, 0.5f, 0.5f, 0.f }; m_vertBuffer.create(); m_vertBuffer.bind(); m_vertBuffer.allocate(vertPositions, sizeof(vertPositions)); m_program.bind(); int aPositionLocation = m_program.attributeLocation("aPosition"); m_program.setAttributeBuffer(aPositionLocation, GL_FLOAT, 0, 3); m_program.enableAttributeArray(aPositionLocation); m_uMvpMatrixLocation = m_program.uniformLocation("uMvpMatrix"); m_projMatrix.ortho(0.f, 100.f, 100.f, 0.f, 50.f, 0.f); m_viewMatrix.lookAt(QVector3D(0.f, 0.f, 40.f), QVector3D(0.f, 0.f, 0.f), QVector3D(0.f, 1.f, 0.f)); m_projViewMatrix = m_projMatrix * m_viewMatrix; m_elapsedTimer.start(); } void OpenGLWindow::resizeGL(int w, int h) { glViewport(0, 0, w, h); } void OpenGLWindow::paintGL() { glClear(GL_COLOR_BUFFER_BIT); m_dt = m_elapsedTimer.elapsed() / 1000.f; m_elapsedTimer.restart(); if (m_keys[Qt::Key::Key_W] || m_keys[Qt::Key::Key_Up]) { m_y -= m_speed * m_dt; } if (m_keys[Qt::Key::Key_A] || m_keys[Qt::Key::Key_Left]) { m_x -= m_speed * m_dt; } if (m_keys[Qt::Key::Key_S] || m_keys[Qt::Key::Key_Down]) { m_y += m_speed * m_dt; } if (m_keys[Qt::Key::Key_D] || m_keys[Qt::Key::Key_Right]) { m_x += m_speed * m_dt; } m_modelMatrix.setToIdentity(); m_modelMatrix.translate(m_x, m_y, 0.f); m_modelMatrix.scale(5.f, 5.f, 1.f); m_mvpMatrix = m_projViewMatrix * m_modelMatrix; m_program.bind(); m_program.setUniformValue(m_uMvpMatrixLocation, m_mvpMatrix); glDrawArrays(GL_TRIANGLES, 0, 6); } void OpenGLWindow::keyPressEvent(QKeyEvent *event) { switch (event->key()) { case Qt::Key::Key_W: case Qt::Key::Key_Up: { m_keys[Qt::Key::Key_W] = true; m_keys[Qt::Key::Key_Up] = true; break; } case Qt::Key::Key_A: case Qt::Key::Key_Left: { m_keys[Qt::Key::Key_A] = true; m_keys[Qt::Key::Key_Left] = true; break; } case Qt::Key::Key_S: case Qt::Key::Key_Down: { m_keys[Qt::Key::Key_S] = true; m_keys[Qt::Key::Key_Down] = true; break; } case Qt::Key::Key_D: case Qt::Key::Key_Right: { m_keys[Qt::Key::Key_D] = true; m_keys[Qt::Key::Key_Right] = true; break; } } } void OpenGLWindow::keyReleaseEvent(QKeyEvent *event) { switch (event->key()) { case Qt::Key::Key_W: case Qt::Key::Key_Up: { m_keys[Qt::Key::Key_W] = false; m_keys[Qt::Key::Key_Up] = false; break; } case Qt::Key::Key_A: case Qt::Key::Key_Left: { m_keys[Qt::Key::Key_A] = false; m_keys[Qt::Key::Key_Left] = false; break; } case Qt::Key::Key_S: case Qt::Key::Key_Down: { m_keys[Qt::Key::Key_S] = false; m_keys[Qt::Key::Key_Down] = false; break; } case Qt::Key::Key_D: case Qt::Key::Key_Right: { m_keys[Qt::Key::Key_D] = false; m_keys[Qt::Key::Key_Right] = false; break; } } }
main.cpp
#include "OpenGLWindow.h" #include <QtWidgets/QApplication> //#include <QtGui/QSurfaceFormat> int main(int argc, char *argv[]) { QApplication a(argc, argv); OpenGLWindow w; // QSurfaceFormat format; // format.setSwapInterval(1); // w.setFormat(format); w.show(); return a.exec(); }
default.vert
attribute vec3 aPosition; uniform mat4 uMvpMatrix; void main() { gl_Position = uMvpMatrix * vec4(aPosition, 1.0); }
default.frag
void main() { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); }
-
Thank you all for this usefull post. I have a qml game, I'm using a Timer that is triggered with a bool variable that is binded to keys pressed and release, at a fixed speed of 1000/60.
The animation looks smooth in some cases in but in other cases it does not. So the correct aproach should be not to use a Timer, but trigger the animations with vsync, right? Is it possible to do this in qml? Also it seems qml does not 'respect' the timer interval, sometimes I get higher frame rates, maybe I have something else mess up. Besides Timers, I am using NumberAnimations, I think that will mess the Timer interval ? I have done some testings using Timers, and qml render.stats, in my laptop with a Nvidia GTX1050 TI, I get:- windows with native screen, animations are ok, smooth, renderstats shows aprox. 60fps
- windows with gaming monitor of 165Hz, animations are not smooth, renderstats shows aprox. 77fps
- linux mint, native screen, animations are ok, smooth, renderstats shows aprox. 60fps
- linux mint, gaming monitor of 165Hz, animations are ok, smooth, renderstats shows aprox. 120fps
- macbook air, native screen, animations are ok, smooth, renderstats shows aprox. 60fps
- macbook air, gaming monitor, animations not smooth, renderstats shows aprox. 45fps, maybe because of the hdmi cable with a display port adapter, that mess the performance
- iPhone 7, animations are ok, smooth, renderstats shows aprox. 60fps
- android phone, animations are ok, smooth, renderstats shows aprox. 60fps
Is there any cross plataform way of checking the vsinc and triger it to render the game loop ?
By the way, I have builds of my game for Windows, Mac, Linux (only binary) here https://drive.google.com/drive/folders/1JWrkcLyNfIZSmleJOJRkunM8mZUiIxDG?usp=sharing
also the code, if anybody wants to check it https://bitbucket.org/joaodeusmorgado/davidgalacticadventures/src/master/
Thanks
-
I created the next bug report and attached archives with examples for testing:
The frameSwapped() signal produces a terrible choppy animation on desktop, but a nice smooth animation on Android and WASM: https://bugreports.qt.io/browse/QTBUG-123862
I attached a source code (see the simple-keyframe-animation-opengles2-qt6-cpp.zip (5 kB) file) of my simple example with keyframe animation on QOpenGLWindow.
Try to run the WASM example: https://6606cb229a3fbbbdbb1d4ab5--resonant-brioche-d1b569.netlify.app/
Or Android example: see the attached simple-keyframe-animation-opengles2-qt6-cpp-android-apk.zip (8.92 MB) file)
And compare it with desktop example: see the attached simple-keyframe-animation-opengles2-qt6-cpp-win10x64-exe.zip 10.8 MB - zipped file, 26.1 MB - unzipped.
You will see that the frameSwapped() signal produces a terrible choppy animation on desktop, but a nice smooth animation on Android and WASM.