【问题标题】:Variable Timesteps and Gravity/Friction可变时间步长和重力/摩擦
【发布时间】:2012-01-23 05:41:45
【问题描述】:

我正在尝试复制 Sonic physics engine 的逻辑,它是为固定时间步长系统 (60 FPS) 在可变时间步长时代(准确地说是 Slick2D)中编写的。

在原版中按下跳跃按钮时,玩家的velocity.y 设置为-6.5,每个刻度 0.21875 添加到velocity.y 以模拟重力。

每次调用我的逻辑更新时,都会传递一个时间增量参数,指定已传递多少毫秒。如果经过的毫秒数比我预期的要多,那么我重复更新逻辑,传递一个最多为 1 的“内部增量”,或者如果我们正在处理目标帧的“剩余”,则传递更少。

例如如果我们预计一帧需要 16 毫秒,而它确实需要 16 毫秒,则循环将迭代一次并将 thisMiniTick 作为 1 传递。如果增量不是 16 毫秒而是 40 毫秒,则循环将执行三个次,经过 1、1,最后是 0.5。

我错误地认为在每个内部更新循环中我都可以执行velocity.y += (gravity * thisMiniTickRelative),但这不起作用。在较快的帧速率下,没有施加足够的重力导致更高的跳跃,而在较慢的帧速率上,跳跃较低(尽管没有那么明显)。

有没有一种方法可以适用于几乎所有帧速率,还是我必须求助于为delta 设置上限和下限?

“内部更新”循环:

    float timeRemaining = delta/1000f; 
    while(timeRemaining > 0)
    {
        float thisMiniTick = Math.min(timeRemaining, 1f / FRAMES_PER_SECOND);
        float thisMiniTickRelative = thisMiniTick / (1f / FRAMES_PER_SECOND);

        updateInput(container, game, thisMiniTickRelative);
        if (playerAirState)
        {
            playerVelocity.y += (GRAVITY * thisMiniTickRelative);
        }
        clampPlayerVelocity();
        playerPosition.add(playerVelocity);
        doCollisions();
        timeRemaining -= thisMiniTick;
    }

【问题讨论】:

    标签: game-physics lwjgl slick2d 2d-games


    【解决方案1】:

    不要认为它是“为delta 设置上限和下限”。您的应用程序和线程受制于您的应用程序的操作系统调度时间,以及对系统的所有其他需求,以及您只需要注意的一些事情。这一挑战在 PC 游戏中与我们从单任务操作系统转向多任务操作系统的那一天一样古老。

    使用 Slick,您可以(并且应该)断开逻辑更新与渲染更新的连接,这就是 delta 值在您的应用程序中传递的原因。使用.setMinimumLogicUpdateInterval and .setMaximumUpdateInterval 方法来做到这一点。

    在我参与过的项目(包括 Slick 中的一个)中,我发现每秒 30-60 次逻辑更新(更新之间的间隔为 30.3 毫秒到 16.6 毫秒)中的任何内容都非常有效,并且可以为您提供所需的平滑度您的运动、物理和碰撞计算。

    字面意思是,对于每秒 30-60 次逻辑更新的范围,您需要执行以下操作:

    container.setMinimumLogicUpdateInterval(16);  // max 60 logic updates per second
    container.setMaximumLogicUpdateInterval(31);  // min 30 logic updates per second
    

    另外,尝试计算timeRemaing 值是一个常见错误,但您不想这样做。你只是想将你的移动量乘以已经过去了多少时间。如果 30 毫秒过去了,那大约是 1/33 秒,因此您应该将游戏对象移动 1 秒内移动量的 1/33。

    float timeElapsed = delta/1000f;
    
    playerVelocity.y += (GRAVITY * timeElapsed);
    

    根据上面指定的上限/下限设置,您可以确定timeElapsed 始终是0.030.06 之间的值。如果您的游戏陷入困境并且您的帧速率变慢,您的逻辑更新仍然不会超出这些界限。取而代之的是,整个游戏似乎会变慢(应该如此,就像在过去的 Sega 时代,屏幕上太多了),但碰撞和物理计算仍将按预期工作。

    【讨论】:

    • 感谢您的详细回答,令人欣慰的是,没有一些我想不出的神奇解决方案。为什么你会认为 timeRemaining 方法是一个错误?我最初将我的速度乘以经过的时间,但这意味着碰撞代码需要大量重写,因为玩家可以在一个刻度内移动超过一整块瓷砖,从而“跳过”瓷砖。
    • 是的,我可能夸大了它,但我想我看到 timeRemaining 出错的地方是人们试图使用该值来计算他们还有多少时间做其他事情诸如 AI 计算之类的东西。不是你不能用它,只是用各种方法来解决这个问题,把所有东西都挂在 delta 值中会给你最可靠的结果。
    • 对于玩家移动太快而无法碰撞,有几种方法可以解决这个问题。一种是对玩家穿过的整个空间进行碰撞计算(如果他们移动了 3 个瓷砖,那么你需要找出其中任何一个瓷砖发生的碰撞,并将它们反弹回来。另一种更简单方法是增加逻辑更新的粒度(调整最小/最大逻辑更新间隔),或者让玩家移动得更慢。
    • 我可能会采用的最后一种方法是将碰撞边界向前(而不是向后)投射,如果他们的玩家可能与附近的某物发生碰撞,那么你在接下来的几帧中,您需要在玩家和仅靠近的那些项目之间进行更细粒度的碰撞检测。显然,如果你在做一款类似于 Sonic the Hedgehog 的游戏,那么你正在处理非常快速的动作,而这种动作是游戏玩法的核心,所以你不能真正作弊。
    • 我敢打赌,索尼克(和他所有的朋友)在碰撞圈中导航世界的部分原因是计算圈和其他物体之间的碰撞(尤其是其他圆)在数学上比多边多边形之间的复杂碰撞更简单(因此更快)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-10-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-16
    • 1970-01-01
    相关资源
    最近更新 更多