即使采用插值,固定时间步长仍然会出现卡顿问题?

3

首先,让我描述一下我所说的卡顿现象。当玩家移动时,它看起来好像向前移动了一点,然后又回到了原来的位置,并且一直这样循环。我正在使用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();
    }
}

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

计时器:

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

我知道这篇帖子很旧了,但我正在使用Delphi作为编程语言遇到了完全相同的卡顿问题。同样是“修复你的时间步长”的循环。在过去的一个月里,我已经花了接近40个小时来解决这个问题。我将主循环定时到微秒级别,一切都按照预期触发,动画非常流畅,但在极其随机的时刻会出现一些卡顿。我已经将动画和循环简化到最低限度,一个方块从左到右移动,一遍又一遍地重复。无论是开启VSync还是关闭,限制帧率还是不限制,你解决了你的问题吗? - undefined
1
@EricFortier 没有看到MCVE,我只是猜测,你是实际测量时间还是只使用定时器间隔作为常数?你知道,在Windows上,定时器不是非常准确的时间源(可能会受到操作系统的影响)。对于C++ Builder(类似于Delphi)中的时间关键性任务,我更喜欢直接测量经过的时间,可以使用PerformanceCounters(Windows)或RDTSC(x86 x64)。引起卡顿的另一个原因可能是你的渲染与显示器同步信号不同步,以及双/三重缓冲或缓冲序列中的错误(偶尔交换它们)。 - undefined
1
@EricFortier 举个例子,我做了一个完美模拟和定时的Z80模拟器,就像这样(定时以[us]为单位)。我通过连接其他集成电路并通过ZEXALL来传递信号,这比仅仅渲染要困难得多。请参考这个链接:通过DeltaTime控制精灵序列 以及其中的所有子链接,这应该是解决你的问题的良好起点... 如果问题与时间无关,那可能是你使用的图形API是什么(GDI封装在VCL、FireMonkey或GL、DX、X11等)? - undefined
1
@EricFortier 如果你在渲染和物理方面有单独的线程,那可能是你的问题的核心源头,同步它们的锁定机制(至少在Windows上)是一场噩梦,通常行为会随着每个Windows版本和SP的变化而改变...这使得无锁方案在更新中变得不可靠...我最近的经验(从win10+开始)是,有时甚至操作系统的锁定也无法按预期或根本无法工作。 - undefined
1个回答

0
你的“固定时间步”并没有你想象中的那么平滑。
这段代码:
while (accumulator >= interval) {
    gameApp.update();
    timer.updateUPS();
    accumulator -= interval;
}

根据gameApp.update()的执行时间,可能以10000000Hz或0.1Hz的速度运行。

编辑:不能确定每次调用timer.getDelta()时其值都大致相同。同样,accumulator也取决于上一次-=interval调用后剩余的值,但每次都以不同的delta开始。
操作系统可能会为自己的进程花费更多时间,从而延迟您的进程。有时,基于时间步长的测量可能正常运行,下一秒钟它就会停顿几毫秒,足以破坏这些测量。
此外,请注意,向GPU发送命令并不能保证它们会立即处理;也许它们会累积起来,然后一起运行。

如果您希望每M毫秒(例如60 FPS的16.6ms)执行一些代码,则使用计时器scheduleAtFixedRate()。请参见this

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


对于第一部分,我正在使用自己的计时器类(我编辑了原帖以包括它)。我不确定你在说什么。我目前正在以30 UPS的速度运行更新方法,在那个while循环中。这不是与scheduleAtFixedRate()所做的相同吗?还是scheduleAtFixedRate()只是使用毫秒来测量,而不是测量每秒调用的次数?如果是这样,为什么会有所区别,我使用哪个不都会得到相同的结果吗?对于你的第二部分,我目前正在努力实现它。你能给我一些相关资源吗? - Robert Harbison
我认为这与渲染速度无关,因为我只处理一个东西,即玩家。 - Robert Harbison
感谢您解释为什么我应该使用scheduleAtFixedRate()。但我仍然想使用自己的计时器,以便我可以计算fps、ups并计算alpha值。您的意思是用Java计时器替换我的计时器,并用scheduleAtFixedRate()替换我的while循环,对吗?我尝试只用这种方法替换我的while循环,结果运行速度约为6270 UPS。 - Robert Harbison

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接