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