3

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

1 Answers1

0

Your "fixed time step" is not as smooth as you think.
This code:

while (accumulator >= interval) {
    gameApp.update();
    timer.updateUPS();
    accumulator -= interval;
}

may run at 10000000Hz or at 0.1Hz depending on how long gameApp.update()takes to execute.

Edit: You can't take for sure that timer.getDelta() is aproximately the same value each time is called. Same goes for accumulator, which also depends on the remaining value after last -=interval call but starts with a different delta each time.
The OS can take more time for its own proccesses, delaying yours. Sometimes your time-step based on measures may run fine, and the next second it halts for a few milliseconds, enough to mess those measures.
Also, be aware that sending commands to GPU doesn't guarantee they get processed immediately; perhaps they accumulate and later run all in a row.

If you wish some code to be executed every M milliseconds (e.g. 16.6ms for 60 FPS) then use a Timer and scheduleAtFixedRate(). See this

The next issue you must deal with is that rendering must be done in a shorter time than the fixed step, or else some delay appears. To achieve this goal send to the GPU most of data (vertices, textures, etc) just once. And for each frame render send only the updated data (the camera position, or just a few objects).

Ripi2
  • 7,031
  • 1
  • 17
  • 33
  • For the first part I am using my own timer class. (I edited my original post to include it). Im not sure I understand what you are saying. I am currently running the update method at 30 UPS in that while loop. Is this not the same as what the scheduleAtFixedRate() would do? Or is scheduleAtFixedRate() just using ms to measure instead of measuring the amount of times called a second? If thats the case why would it matter which I use wouldn't the result be the same? For you second part I am currently working on implementing that. Is there any resources you can give me on that? – Robert Harbison Jul 27 '17 at 22:34
  • Also I don't think it has anything to do with the speed of the rendering as I am only dealing with rendering 1 thing which is the player. – Robert Harbison Jul 27 '17 at 23:41
  • Thanks for explaining why I should use scheduleAtFixedRate(). I would still like to use my own Timer though so I can count fps, ups and calculate a alpha value. Is it correct that what you are saying is to replace my timer with the java timer? And to replace my while loop with scheduleAtFixedRate(). I tried just replacing my while loop with that method and I end up running at about 6270 UPS. – Robert Harbison Jul 28 '17 at 03:48