Smooth character movement
-
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.
-
@8Observer8 Are you compiling from git or running the zip file ? If compiling the source do you have any errors and what version of Qt are you using ? (you will need Qt6.2 iirc) What's your gpu ?
-
@johngod I downloaded a zip with EXE. I have:
Asus K53SV; 8 GB RAM, i3 2.2 GHertz (2 cores); Intel HD Graphics 3000; Nvidia GeForce GT 540M (1 GB); Windows 10
It is because my laptop run your app with integrated video card (Intel HD Graphics 3000 that uses OpenGL 3.1). I can run it with the second one:
I click on the space ship (on Earth, and on ship again) in the top left corner:
and see a black screen only with interrupted music:
-
@8Observer8 I have no clue sorry, but that is a very old demo. I just made a new build https://drive.google.com/file/d/1pdyL4lq2XpOcpv0ePxB57SPE30rBvsgw/view?usp=sharing maybe this will work. It it still does not work, maybe you can compile from source and see if there are any errors. Thank you for the interest in my game.
-
@johngod this build works without problems above. I didn't select the discrete video card manually to run. But when I move the ship with the AD keys from side to side, the animation of the movement is very choppy, like in a slideshow.
-
@8Observer8 I had problems whith choppy movement all over the place, because of using a Timer with a fixed valued has Crhis Kawa exaplained in https://forum.qt.io/post/278010 . But it got partially solved when changing Timer to FrameAnimation, however the AD keys are using a camera drift animation which is still buggy. Also using FrameAnimation breaks the AD keys drift animation, I still have to fix this bugs.
If you click on the button "?" you will see some options where you can change back from FrameAnimation to Timer, and a lot more "chopiness" will appear. -
I added the next comment to the bug report: https://bugreports.qt.io/browse/QTBUG-123862
Try to run my example on macOS and Linux. Maybe it is a Windows issue. But requestAnimationFrame works perfectly smoothly on Windows: https://plnkr.co/edit/DebSXYMQ6TlgC7KT?preview It has a fixed 60 FPS, which means that the load on the processor is minimal.
The worst result was with swapInterval=1, which I had at the beginning (I had not tried other values before). swapInterval with 0 and 10 is better, but not as good as requestAnimationFrame in JavaScript: https://plnkr.co/edit/B5t34XdK3MVi1WNb?preview (it is just a translation animation). I attached a source code "simple-translation-animation" (without rotation and scale animations). I attached EXE files for swapInterval = 0, swapInterval = 1, and swapInterval = 10:
- swapInterval = 0 - simple-translation-animation-opengles2-qt6-cpp-win10x64-exe-swap-interval-0.zip
- swapInterval = 1 - simple-translation-animation-opengles2-qt6-cpp-win10x64-exe-swap-interval-1.zip
- swapInterval = 10 - simple-translation-animation-opengles2-qt6-cpp-win10x64-exe-swap-interval-10.zip
-