【问题标题】:Fixed time step stutter even with interpolation?即使使用插值,也修复了时间步的卡顿?
【发布时间】:2017-07-27 05:52:29
【问题描述】:

首先让我描述一下我所说的口吃是什么意思。当玩家移动时,它看起来好像向前移动了一点,然后又回到了它应该在的位置并继续这样做。我正在制作一个用于 lwjgl3 学习目的的小游戏,并且我使用 JOML 作为我的数学库。我实现了一个固定的时间步长循环(FPS = 60 和 UPS = 30),我使用插值来尝试平滑我的玩家运动。有时它工作得很好(虽然没有我想要的那么流畅),但有时它就像没有它一样结结巴巴。有想法该怎么解决这个吗?我是否正确地进行了插值?

游戏循环:

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

实体播放器:

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

计时器:

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

【问题讨论】:

    标签: java opengl lwjgl


    【解决方案1】:

    您的“固定时间步长”并不像您想象的那么顺利。
    这段代码:

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

    可能以 10000000Hz 或 0.1Hz 运行,具体取决于 gameApp.update() 执行多长时间。

    编辑:您无法确定每次调用timer.getDelta() 时的值大致相同。 accumulator 也是如此,这也取决于最后一次 -=interval 调用后的剩余值,但每次都以不同的 delta 开头。
    操作系统可能会花费更多时间来处理自己的进程,从而延迟您的进程。有时您的基于度量的时间步长可能运行良好,但下一秒它会停止几毫秒,足以弄乱这些度量。
    另外,请注意,向 GPU 发送命令并不能保证它们会立即得到处理;也许它们会累积起来,然后连续运行。

    如果您希望某些代码每 M 毫秒执行一次(例如 60 FPS 为 16.6 毫秒),请使用 TimerscheduleAtFixedRate()。见this

    您必须处理的下一个问题是渲染必须在比固定步骤更短的时间内完成,否则会出现一些延迟。为了实现这一目标,只需将大部分数据(顶点、纹理等)发送到 GPU 一次。并且对于每一帧渲染只发送更新的数据(相机位置,或者只是几个对象)。

    【讨论】:

    • 第一部分我使用了我自己的计时器类。 (我编辑了我的原始帖子以包含它)。我不确定我明白你在说什么。我目前正在那个 while 循环中以 30 UPS 运行更新方法。这与 scheduleAtFixedRate() 的作用不同吗?还是 scheduleAtFixedRate() 只是使用 ms 来测量而不是测量称为秒的次数?如果是这样,为什么我使用的结果会不一样?对于你的第二部分,我目前正在努力实施。你有什么资源可以给我吗?
    • 另外,我认为这与渲染速度没有任何关系,因为我只处理渲染 1 个东西,即播放器。
    • 感谢您解释为什么我应该使用 scheduleAtFixedRate()。不过,我仍然想使用自己的计时器,这样我就可以计算 fps、ups 并计算 alpha 值。您所说的是用java计时器替换我的计时器是否正确?并用 scheduleAtFixedRate() 替换我的 while 循环。我尝试用这种方法替换我的 while 循环,我最终以大约 6270 UPS 运行。
    猜你喜欢
    • 1970-01-01
    • 2013-02-26
    • 2014-09-07
    • 1970-01-01
    • 1970-01-01
    • 2018-01-15
    • 2020-12-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多