"deWiTTERS游戏循环"是否假设一个恒定的UPS?

Bla*_*olf 6 java frame-rate game-engine game-loop

我最近尝试进入游戏编程.我对Java很有经验,但对游戏编程却没有.我阅读http://www.koonsolo.com/news/dewitters-gameloop/并使用以下代码实现了那里提出的游戏循环:

private static int UPDATES_PER_SECOND = 25;
private static int UPDATE_INTERVAL = 1000 / UPDATES_PER_SECOND * 1000000;
private static int MAX_FRAMESKIP = 5;

public void run() {
    while (true) {
        int skippedFrames = 0;
        while (System.nanoTime() > this.nextUpdate && skippedFrames < MAX_FRAMESKIP) {
            this.updateGame();
            this.nextUpdate += UPDATE_INTERVAL;
            skippedFrames++;
        }

        long currentNanoTime = System.nanoTime();
        double interpolation = (currentNanoTime + UPDATE_INTERVAL - this.nextUpdate) / UPDATE_INTERVAL;
        this.repaintGame(interpolation);
    }
}
Run Code Online (Sandbox Code Playgroud)

循环看起来很有前途和容易,但现在我实际上正在尝试用它做点什么我不再那么肯定了.如果我没有完全错误地updateGame()处理诸如计算位置,移动敌人,计算碰撞等事情,......?由于插值没有传递给updateGame(),这是否意味着我们假设每秒updateGame()精确稳定地调用UPDATES_PER_SECOND?这是否意味着我们所有的计算都基于这个假设?如果 - 无论出于何种原因 - 对updateGame()的调用被延迟,这不会给我们带来很多麻烦吗?
例如,如果我的角色精灵应该向右走,我们根据它的速度移动它updateGame()- 如果方法被延迟,这将意味着我们的计算只是关闭而角色会滞后?

在网站上,列出了以下插值示例:

如果在10Th的比赛中,位置是500,速度是100,那么在11Th的比赛中,位置将是600.所以当你渲染它时你将把你的车放在哪里?你可以采取最后一个游戏技巧的位置(在这种情况下为500).但更好的方法是预测,其中轿车将在精确的10.3,而这种情况是这样的:
view_position = position + (speed * interpolation)
这款车将随后在530位置呈现.

我知道这仅仅是一个例子,但这不会使车速依赖于UPDATES_PER_SECOND?那么更多UPS意味着更快的车?这不可能是正确的......?

任何帮助,教程,任何赞赏.

更新/解决方案

在所有的帮助之后(谢谢!),这是我目前使用的 - 到目前为止工作得很好(用于移动角色精灵),但让我们等待真正复杂的游戏内容来决定这一点.不过,我想分享一下.
我的游戏循环现在看起来像这样:

private static int UPDATES_PER_SECOND = 25;
private static int UPDATE_INTERVAL = 1000 / UPDATES_PER_SECOND * 1000000;
private static int MAX_FRAMESKIP = 5;

private long nextUpdate = System.nanoTime();

public void run() {
    while (true) {
        int skippedFrames = 0;
        while (System.nanoTime() > this.nextUpdate && skippedFrames < MAX_FRAMESKIP) {
            long delta = UPDATE_INTERVAL;
            this.currentState = this.createGameState(delta);
            this.newPredictedNextState = this.createGameState(delta + UPDATE_INTERVAL, true);

            this.nextUpdate += UPDATE_INTERVAL;
            skippedFrames++;
        }

        double interpolation = (System.nanoTime() + UPDATE_INTERVAL - this.nextUpdate) / (double) UPDATE_INTERVAL;
        this.repaintGame(interpolation);
    }
}
Run Code Online (Sandbox Code Playgroud)

如您所见,有一些变化:

  • delta = UPDATE_INTERVAL?是.到目前为止这是实验性的,但我认为它会起作用.问题是,只要您实际从两个时间戳计算一个增量,就会引入浮点计算错误.这些都很小,但考虑到你的更新被称为数百万次,他们加起来.而且由于第二个while循环确保我们能够了解错过的更新(例如,渲染需要很长时间),我们可以非常肯定我们每秒可以获得25次更新.最糟糕的情况:我们错过的不仅仅是MAX_FRAMESKIP更新 - 在这种情况下,更新将会丢失,游戏将会延迟.就像我说的那样,实验性的.我可能会再次将其更改为实际的delta.
  • 谓词下一个游戏状态?是.GameState是保存所有相关游戏信息的对象,渲染器传递此对象以将游戏渲染到屏幕.在我的情况下,我决定给渲染器两个状态:我们通常会通过他的状态,当前的游戏状态,以及未来预测的未来状态UPDATE_INTERVAL.这样,渲染器可以使用插值来在两者之间轻松插值.计算未来的游戏状态实际上非常简单 - 因为你的更新方法(createGameState())无论如何都要采用delta值,只需增加delta值UPDATE_INTERVAL- 这样就可以预测未来的状态.当然,未来状态假设用户输入等保持不变.如果没有,则下一个游戏状态更新将处理更改.
  • 其余的几乎保持不变,取自deWiTTERS游戏循环.MAX_FRAMESKIP在硬件真的很慢的情况下,它几乎是一个故障保护,以确保我们不时渲染一些东西.但是,如果这个开始,我猜我们将会有极端的滞后.插值与之前相同 - 但现在渲染器可以简单地在两个游戏状态之间进行插值,除了插值数字之外,它不必具有任何逻辑.真好!

也许是为了澄清一个例子.这是我如何计算角色位置(简化一点):

public GameState createGameState(long delta, boolean ignoreNewInput) {
    //Handle User Input and stuff if ignoreNewInput=false

    GameState newState = this.currentState.copy();
    Sprite charSprite = newState.getCharacterSprite();
    charSprite.moveByX(charSprite.getMaxSpeed() * delta * charSprite.getMoveDirection().getX()); 
    //getMoveDirection().getX() is 1.0 when the right arrow key is pressed, otherwise 0.0
}
Run Code Online (Sandbox Code Playgroud)

......然后,在窗口的绘画方法中......

public void paint(Graphics g) {
    super.paint(g);

    Graphics2D g2d = (Graphics2D) g;

    Sprite currentCharSprite = currentGameState.getCharacterSprite();
    Sprite nextCharSprite = predictedNextState.getCharacterSprite(); 
    Position currentPos = currentCharSprite.getPosition();
    Position nextPos = nextCharSprite.getPosition();
    //Interpolate position
    double x = currentPos.getX() + (nextPos.getX() - currentPos.getX()) * this.currentInterpolation;
    double y = currentPos.getY() + (nextPos.getY() - currentPos.getY()) * this.currentInterpolation;
    Position interpolatedCharPos = new Position(x, y);
    g2d.drawImage(currentCharSprite.getImage(), (int) interpolatedCharPos.getX(), (int) interpolatedCharPos.getY(), null);
}
Run Code Online (Sandbox Code Playgroud)

Luc*_*ius 4

不要将您的游戏逻辑建立在更新间隔恒定的假设之上。包括一个精确测量两次更新之间经过的时间的游戏时钟。然后,您可以根据延迟进行所有计算,而不必担心实际的更新速率。

在这种情况下,汽车速度将以单位/秒为单位给出,增量将是自上次更新以来的总秒数:

car.position += car.velocity * delta;
Run Code Online (Sandbox Code Playgroud)

将更新游戏逻辑与绘制框架分开是一种常见的做法。正如您所说,这使得可以通过时不时地跳过渲染帧来保持稳定的更新率。

但这并不是要保持更新间隔不变。每个时间单位的更新次数最少非常重要。想象一下一辆车辆快速驶向障碍物。如果更新频率太低,两次更新之间的行进距离可能大于障碍物的总大小。车辆将直接穿过它。

  • 嗯,是的,我想我会这样实现它,因为这对我来说也更有意义。作为旁注,我仍然认为将增量传递给渲染仍然有用 - 它使得可以在两个游戏更新之间插入运动。否则如果每秒有25次游戏更新,但每秒有200次渲染更新,那么175次渲染基本上就被浪费了。正如您所说,这_可能_意味着汽车“穿过”障碍物,但很可能更新方法被足够频繁地调用,因此这种情况不会发生/未被注意到。 (2认同)