8

我最近尝试进入游戏编程。我对 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);
    }
}

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

在网站上,说明了以下插值示例:

如果在第 10 个 gametick 中位置是 500,速度是 100,那么在第 11 个 gametick 中,位置将是 600。那么当你渲染它的时候你会把你的车放在哪里呢?您可以只取最后一个gametick 的位置(在本例中为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);
    }
}

如您所见,有一些变化:

  • delta = UPDATE_INTERVAL? 是的。到目前为止,这是实验性的,但我认为它会起作用。问题是,一旦您从两个时间戳实际计算出一个增量,您就会引入浮点计算错误。这些很小,但考虑到您的更新被调用了数百万次,它们加起来。由于第二个 while 循环确保我们赶上错过的更新(例如,如果渲染需要很长时间),我们可以很确定我们每秒获得 25 次更新。最坏的情况:我们错过的不仅仅是MAX_FRAMESKIP更新——在这种情况下,更新会丢失并且游戏会滞后。不过,就像我说的,实验性的。我可能会再次将其更改为实际的增量。
  • 预测下一场比赛状态?是的。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
}

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

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

1 回答 1

4

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

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

car.position += car.velocity * delta;

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

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

于 2013-02-26T22:30:25.733 回答