First let me describe what I mean by stutter. When the player moves it looks as if it moves forward a little then back to where it should be and keeps doing it. I am making a small game for learning purposes in lwjgl3 and I am using JOML as my math library. I implemented a fixed time step loop (FPS = 60 and UPS = 30) and I use interpolation to try and smooth my player movement. It works nicely sometimes (not as smooth as I want it though) but other times its just as stuttery as without it. Any ideas on how to fix this? Am I doing the interpolation correctly?
Game Loop:
@Override
public void run() {
window.init("Game", 1280, 720);
GL.createCapabilities();
gameApp.init();
timer.init();
float delta;
float accumulator = 0f;
float interval = 1f / Settings.TARGET_UPS;
float alpha;
while (running) {
delta = timer.getDelta();
accumulator += delta;
gameApp.input();
while (accumulator >= interval) {
gameApp.update();
timer.updateUPS();
accumulator -= interval;
}
alpha = accumulator / interval;
gameApp.render(alpha);
timer.updateFPS();
timer.update();
window.update();
if (Settings.SHOW_PERFORMANCE) {
System.out.println("FPS: " + timer.getFPS() + " UPS: " + timer.getUPS());
}
if (window.windowShouldClose()) {
running = false;
}
}
gameApp.cleanUp();
window.cleanUp();
}
SpriteRenderer:
public class SpriteRenderer {
public StaticShader staticShader;
public SpriteRenderer(StaticShader staticShader, Matrix4f projectionMatrix) {
this.staticShader = staticShader;
staticShader.start();
staticShader.loadProjectionMatrix(projectionMatrix);
staticShader.stop();
}
public void render(Map<TexturedMesh, List<Entity>> entities, float alpha) {
for (TexturedMesh mesh : entities.keySet()) {
prepareTexturedMesh(mesh);
List<Entity> batch = entities.get(mesh);
for (Entity entity : batch) {
Vector2f spritePos = entity.getSprite().getTransform().getPosition();
Vector2f playerPos = entity.getTransform().getPosition();
spritePos.x = playerPos.x * alpha + spritePos.x * (1.0f - alpha);
spritePos.y = playerPos.y * alpha + spritePos.y * (1.0f - alpha);
prepareInstance(entity.getSprite());
GL11.glDrawArrays(GL11.GL_TRIANGLES, 0, entity.getSprite().getTexturedMesh().getMesh().getVertexCount());
}
unbindTexturedMesh();
}
}
private void unbindTexturedMesh() {
GL20.glDisableVertexAttribArray(0);
GL20.glDisableVertexAttribArray(1);
GL30.glBindVertexArray(0);
}
private void prepareInstance(Sprite sprite) {
Transform spriteTransform = sprite.getTransform();
Matrix4f modelMatrix = Maths.createModelMatrix(spriteTransform.getPosition(), spriteTransform.getScale(), spriteTransform.getRotation());
staticShader.loadModelMatrix(modelMatrix);
}
private void prepareTexturedMesh(TexturedMesh texturedMesh) {
Mesh mesh = texturedMesh.getMesh();
mesh.getVao().bind();
GL20.glEnableVertexAttribArray(0);
GL20.glEnableVertexAttribArray(1);
GL13.glActiveTexture(GL13.GL_TEXTURE0);
texturedMesh.getTexture().bind();
}
}
EntityPlayer:
public class EntityPlayer extends Entity {
private float xspeed = 0;
private float yspeed = 0;
private final float SPEED = 0.04f;
public EntityPlayer(Sprite sprite, Vector2f position, Vector2f scale, float rotation) {
super(sprite, position, scale, rotation);
this.getSprite().getTransform().setPosition(position);
this.getSprite().getTransform().setScale(scale);
this.getSprite().getTransform().setRotation(rotation);
}
@Override
public void update() {
this.getTransform().setPosition(new Vector2f(this.getTransform().getPosition().x += xspeed, this.getTransform().getPosition().y += yspeed));
}
public void input() {
if (KeyboardHandler.isKeyDown(GLFW.GLFW_KEY_RIGHT)) {
xspeed = SPEED;
} else if (KeyboardHandler.isKeyDown(GLFW.GLFW_KEY_LEFT)) {
xspeed = -SPEED;
} else {
xspeed = 0;
}
if (KeyboardHandler.isKeyDown(GLFW.GLFW_KEY_UP)) {
yspeed = SPEED;
} else if (KeyboardHandler.isKeyDown(GLFW.GLFW_KEY_DOWN)) {
yspeed = -SPEED;
} else {
yspeed = 0;
}
}
}
Timer:
public class Timer {
private double lastLoopTime;
private float timeCount;
private int fps;
private int fpsCount;
private int ups;
private int upsCount;
public void init() {
lastLoopTime = getTime();
}
public double getTime() {
return GLFW.glfwGetTime();
}
public float getDelta() {
double time = getTime();
float delta = (float) (time - lastLoopTime);
lastLoopTime = time;
timeCount += delta;
return delta;
}
public void updateFPS() {
fpsCount++;
}
public void updateUPS() {
upsCount++;
}
// Update the FPS and UPS if a whole second has passed
public void update() {
if (timeCount > 1f) {
fps = fpsCount;
fpsCount = 0;
ups = upsCount;
upsCount = 0;
timeCount -= 1f;
}
}
public int getFPS() {
return fps > 0 ? fps : fpsCount;
}
public int getUPS() {
return ups > 0 ? ups : upsCount;
}
public double getLastLoopTime() {
return lastLoopTime;
}
}