I want to fly higher
programming Explorer
posts - 112,comments - 263,trackbacks - 0

Intent

  • Decouple the progression of game time from user input and processor speed.
  • batch mode programs — once the work was done, the program stopped.

Motivation

Interview with a CPU

  • drop off a batch of code
    • a few hours later for the results was a terribly slow way
    • wanted immediate feedback.
  • Interactive programs were born.
    while (true)
    {
      char* command = readCommand();
      handleCommand(command);
    }
    

    • It waited for your input, then it would respond to you.
    • This loops forever, so there’s no way to quit the game. A real game would do something like while (!done) and set done to exit.

Event loops

  • Modern graphic UI applications
    while (true)
    {
      Event* event = waitForEvent();
      dispatchEvent(event);
    }
    

    • Most event loops do have “idle” events so you can intermittently do stuff without user input. That’s good enough for a blinking cursor or a progress bar, but too rudimentary for games
  • key part of a real game loop
    while (true)
    {
      processInput();
      update();
      render();
    }
    

    • it processes user input, but doesn’t wait for it. The loop always keeps spinning
    • processInput() handles any user input that has happened since the last call.
    • update() advances the game simulation one step. It runs AI and physics (usually in that order).
    • render() draws the game so the player can see what happened.

A world out of time

  • The common terms for one crank of the game loop are “tick” and “frame”.
  • this loop isn’t blocking on input.how fast does it spin?
  • we measure how quickly the game loop cycles in terms of real time, we get the game’s “frames per second”
  • two factors determine the frame rate
    • how much work it has to do each frame. Complex physics, a bunch of game objects, and lots of graphic detail
    • the speed of the underlying platform

Seconds per second

  • In early video games, that second factor was fixed.
  • Older games were carefully coded to do just enough work each frame so that the game ran at the speed the developers wanted. But if you tried to play that same game on a faster or slower machine, then the game itself would speed up or slow down
  • this is why old PCs used to have “turbo” buttons. New PCs were faster and couldn’t play old games because the games would run too fast. Turning the turbo button off would slow the machine down and make old games playable.
  • our games must intelligently adapt to a variety of devices
  • it runs the game at a consistent speed despite differences in the underlying hardware.

The Pattern

  • A game loop runs continuously during gameplay
  • Each turn of the loop, it processes user input without blocking, updates the game state, and renders the game
  • It tracks the passage of time to control the rate of gameplay

When to Use It

  • If you’re using a game engine, you won’t write it yourself, but it’s still there
  • With libraries, you own the main game loop and call into the library. An engine owns the loop and calls into your code.
  • Animation and music keep running even when the game is “waiting” for you to take your turn.
  • Take care with this code, and be mindful of its efficiency

Keep in Mind

  • a program spends 90% of its time in 10% of the code
  • Your game loop will be firmly in that 10%

You may need to coordinate with the platform’s event loop

  • If you’re building your game on top of an OS or platform that has a graphic UI and an event loop built in, then you have two application loops in play. They’ll need to play nice together.
  • if you’re writing a game against the venerable Windows API, your main() can just have a game loop. Inside, you can call PeekMessage() to handle and dispatch events from the OS. Unlike GetMessage(), PeekMessage() doesn’t block waiting for user input, so your game loop will keep cranking.
  • If you’re targeting a web browser, the event loop is deeply built into browser’s execution model. You’ll call something like requestAnimationFrame() and it will call back into your code to keep the game running.

Sample Code

  • the code for a game loop is actually pretty straightforward.
  • The game loop drives AI, rendering, and other game systems, but those aren’t the point of the pattern itself

Run, run as fast as you can

  • the simplest possible game loop
    while (true)
    {
      processInput();
      update();
      render();
    }
    

  • The problem with it is you have no control over how fast the game runs
  • On a fast machine, that loop will spin so fast users won’t be able to see what’s going on.
  • On a slow machine, the game will crawl.
  • 备注
    • 这是本文提到的第一个sample,很简单,完全根据硬件的速度走,让你完全无法控制
    • 快的机器快到用户都看不清楚;慢的机器慢到像爬一样

Take a little nap

  • you want your game to run at 60 FPS. That gives you about 16 milliseconds per frame.
  • As long as you can reliably do all of your game processing and rendering in less than that time, you can run at a steady frame rate.
  • All you do is process the frame and then wait until it’s time for the next one
  • 1000 ms / FPS = ms per frame.


    while (true)
    {
      double start = getCurrentTime();
      processInput();
      update();
      render();
      sleep(start + MS_PER_FRAME - getCurrentTime());
    }
    

  • The sleep() here makes sure the game doesn’t run too fast if it processes a frame quickly. It doesn’t help if your game runs too slowly.
  • If it takes longer than 16ms to update and render the frame, your sleep time goes negative
  • You can work around this by doing less work each frame — cut down on the graphics and razzle dazzle or dumb down the AI. But that impacts the quality of gameplay for all users, even ones on fast machines
  • 备注
    • 这个sample是比较经典
    • 如果逻辑处理的时间可以保证在16ms以内,那么就可以运行在稳定的帧率
    • 但是如果处理时间超过16ms,那么则游戏变慢;通常在游戏服务器处理的时候,出现这种情况表示服务器繁忙,则可以不sleep
    • 如果对于客户端来说,每帧可以减少工作量来避免处理时间超过16ms;但是这样对一些快速的机器是打折的(本可以获得更流畅的体验)

One small step, one giant step

  • Each update advances game time by a certain amount
  • It takes a certain amount of real time to process that
  • If step two takes longer than step one, the game slows down
  • if we can advance the game by more than 16ms of game time in a single step, then we can update the game less frequently and still keep up.
  • The idea then is to choose a time step to advance based on how much real time passed since the last frame.
    • The longer the frame takes, the bigger steps the game takes.
    • a variable or fluid time step

    double lastTime = getCurrentTime();
    while (true)
    {
      double current = getCurrentTime();
      double elapsed = current - lastTime;
      processInput();
      update(elapsed);
      render();
      lastTime = current;
    }
    

    • Each frame, we determine how much real time passed since the last game update (elapsed). When we update the game state, we pass that in. The engine is then responsible for advancing the game world forward by that amount of time.
  • Say you’ve got a bullet shooting across the screen
    • With a fixed time step, in each frame, you’ll move it according to its velocity.
    • With a variable time step, you scale that velocity by the elapsed time. As the time step gets bigger, the bullet moves farther in each frame.
    • That bullet will get across the screen in the same amount of real time whether it’s twenty small fast steps or four big slow ones
  • This looks like a winner
    • The game plays at a consistent rate on different hardware
    • Players with faster machines are rewarded with smoother gameplay
  • there’s a serious problem
    • we’ve made the game non-deterministic and unstable
    • Say we’ve got a two-player networked game and Fred has some beast of a gaming machine while George is using his grandmother’s antique PC
      • On Fred’s machine, the game is running super fast, so each time step is tiny. We cram, like, 50 frames in the second it takes the bullet to cross the screen.
      • Poor George’s machine can only fit in about five frames.
      • This means that on Fred’s machine, the physics engine updates the bullet’s position 50 times, but George’s only does it five times.
      • Most games use floating point numbers, and those are subject to rounding error.
      • Each time you add two floating point numbers, the answer you get back can be a bit off. Fred’s machine is doing ten times as many operations, so he’ll accumulate a bigger error than George. The same bullet will end up in different places on their machines.
    • This is just one nasty problem a variable time step can cause, but there are more.
      • the physics gets unstable.
  • 备注
    • 这个示例相对复杂一些,即变化的时间步长
    • 基于最后一帧以来经过了多少实际时间来选择一个时间步长(elapsed)
    • 如果一帧的时间越长,那么步长就越大
    • 根据计算的步长来将游戏世界向前推进
    • 优点:
      • 游戏在不同的硬件上速度一致
        • 子弹都是在相同的真实时间穿过屏幕无论是二十个小的快步还是四个大的慢步
        • 即快的机器速度频率更快,但是步长小;慢的机器速度频率更慢,但是步长大;但最终到达的效果是一样的
      • 更快的机器可以获得更流畅的玩法
    • 缺点:
      • 会引起游戏的不确定性和不稳定性
      • 如上述的子弹例子,快速的游戏机运行速度快,每一步都很小,1秒50帧,而慢的游戏机可能只是1秒5帧.这意味着,快速的机器的物理引擎更新子弹的位置50次,但慢的机器只有5次.大多数游戏都使用浮点数,并且有舍入误差.快的机器做了10倍与慢的机器的操作,因为累计更大的误差。而最终造成的结果是两个相同的子弹在不同的地方结束
      • 另外游戏中的物理引擎是力学的真实定律的模拟(damping_阻尼),变化会让物理变的不稳定

Play catch up

  • One part of the engine that usually isn’t affected by a variable time step is rendering. Since the rendering engine captures an instant in time, it doesn’t care how much time advanced since the last one.
  • This is more or less true. Things like motion blur can be affected by time step, but if they’re a bit off, the player doesn’t usually notice.
  • We’ll update the game using a fixed time step because that makes everything simpler and more stable for physics and AI.
    • But we’ll allow flexibility in when we render in order to free up some processor time.
  • A certain amount of real time has elapsed since the last turn of the game loop.

    • This is how much game time we need to simulate for the game’s “now” to catch up with the player’s.
    • We do that using a series of fixed time steps.

    double previous = getCurrentTime();
    double lag = 0.0;
    while (true)
    {
      double current = getCurrentTime();
      double elapsed = current - previous;
      previous = current;
      lag += elapsed;
      processInput();
      while (lag >= MS_PER_UPDATE)
      {
        update();
        lag -= MS_PER_UPDATE;
      }
      render();
    }
    

    • At the beginning of each frame, we update lag based on how much real time passed.
    • This measures how far the game’s clock is behind compared to the real world.
    • We then have an inner loop to update the game, one fixed step at a time, until it’s caught up.
    • MS_PER_UPDATE is just the granularity we use to update the game.
      • The shorter this step is, the more processing time it takes to catch up to real time.
      • The longer it is, the choppier the gameplay is.
      • But be careful not to make it too short. You need to make sure the time step is greater than the time it takes to process an update(), even on the slowest hardware. Otherwise, your game simply can’t catch up
  • we’ve yanked rendering out of the update loop.
    • That frees up a bunch of CPU time
    • The end result is the game simulates at a constant rate using safe fixed time steps across a range of hardware.
    • It’s just that the player’s visible window into the game gets choppier on a slower machine.
      • you can safeguard this by having the inner update loop bail after a maximum number of iterations. The game will slow down then, but that’s better than locking up completely.
  • 备注
    • 渲染通常不受变长时间步长的影响,因为渲染并不关心自从上一个时间到现在驱动了多长时间。所以我们可以使用固定的时间步长来更新游戏,因为这样使得物理和AI更简单稳定。而我们渲染时,可以更灵活用来释放一些处理器时间(从后面章节的更新和渲染的时间轴来看,渲染的频率比更新的频率要低,则间接释放了一些处理器时间;It updates with a fixed time step, but it can drop rendering frames(可丢弃渲染帧) if it needs to to catch up to the player's clock.)
    • 在每帧开始,我们根据真实时间过去了多少来更新lag,它描述了游戏的时钟与现实世界落后多少。然后在内循环,更新游戏,固定步长,直到赶上
    • MS_PER_\UPDATE只是我们用来更新游戏的粒度(步长).这一步长约小,则赶上需要的处理时间越多(while循环次数越多).这一步越长,则越起伏(长时间不更新).这个值要确保大于处理update的时间

Stuck in the middle

  • We update the game at a fixed time step, but we render at arbitrary points in time.
  • from the user’s perspective, the game will often display at a point in time between two updates.
  • imagine a bullet is flying across the screen. On the first update, it’s on the left side. The second update moves it to the right side. The game is rendered at a point in time between those two updates, so the user expects to see that bullet in the center of the screen. With our current implementation, it will still be on the left side.
  • When we go to render, we’ll pass that in:
    • render(lag / MS_PER_UPDATE);
    • We divide by MS_\PER_UPDATE here to normalize the value. The value passed to render() will vary from 0 (right at the previous frame) to just under 1.0 (right at the next frame), regardless of the update time step. This way, the renderer doesn’t have to worry about the frame rate. It just deals in values from 0 to 1.
    • The renderer knows each game object and its current velocity. Say that bullet is 20 pixels from the left side of the screen and is moving right 400 pixels per frame. If we are halfway between frames, then we’ll end up passing 0.5 to render(). So it draws the bullet half a frame ahead, at 220 pixels.
    • Of course, it may turn out that that extrapolation is wrong. When we calculate the next frame, we may discover the bullet hit an obstacle or slowed down or something.
    • We rendered its position interpolated between where it was on the last frame and where we think it will be on the next frame. But we don’t know that until we’ve actually done the full update with physics and AI.
  • So the extrapolation is a bit of a guess and sometimes ends up wrong. Fortunately, though, those kinds of corrections usually aren’t noticeable. At least, they’re less noticeable than the stuttering you get if you don’t extrapolate at all.
  • 备注
    • 我们用固定的时间步长更新游戏,但在任意点渲染,从用户的角度看,游戏通常会在两次更新之间的时间点显示
    • 想象一个子弹飞过屏幕。在第一次更新时,它在左侧。第二个更新将其移动到右侧。游戏在这两个更新之间的时间点呈现,因此用户期望在屏幕的中心看到子弹。在我们当前的实现中,它仍然在左侧
    • 但实际上从实现中可以看到,当lag小于更新步长的时候,会跳出更新循环,我们可以直接传递:render(lag / MSPERUPDATE);这个值是在0-1之间。如在之前的例子中,子弹在屏幕左侧20像素处,然后正每帧400像素向有移动,然后传入0.5(两次更新中间),则绘20 + 400 * 0.5 = 220像素,平稳运动
    • 但有些时候,这个推论是错误的,因为计算下一帧时,子弹可能碰到了障碍物或者减速等,所以需要直到完成了物理和AI的完成更新后才知道。但是幸运的时候,这些修正通常不明显,至少比不修正前的运动锯齿好

Design Decisions

Do you own the game loop, or does the platform?

  • 使用平台的事件循环
    • It’s simple
    • It plays nice with the platform
    • You lose control over timing
  • 使用引擎的循环
    • You don’t have to write it
    • You don’t get to write it
  • 自己写
    • Total control.
    • You have to interface with the platform.

如何管理功耗

  • 一个游戏运行的再漂亮,但是30分钟后手机发烫,体验很糟糕
  • 尽可能少用cpu,sleep
  • 运行速度尽可能快
    • 游戏体验好,但是如果玩家在笔记本上,他们的膝盖会很温暖
  • 限制帧率
    • 将设置帧速率的上限(通常为30或60 FPS)。如果游戏循环在该时间段之前完成处理,则其将仅休眠

如何控制游戏速度?

  • 游戏循环的两个关键部分
    • non-blocking user input
    • adapting to the passage of time
      • 需要适应各种各样的平台
  • Fixed time step with no synchronization(即第一个样例)
    • 简单
    • 游戏速度直接受硬件和游戏复杂性的影响。
  • Fixed time step with synchronization(sleep样例)
    • 还是很简单
    • 对电量友好,尤其是对于移动游戏.通过简单地睡眠几毫秒,而不是试图在每个tick中做更多的处理
    • 游戏不会太快
    • 游戏可能会很慢,因为如果一帧中更新和渲染需要时间过长,则播放速度会下降.(因为没有将更新与渲染分离)
  • Variable time step(可变时间步长)
    • 大多数游戏开发人员反对,因为这不是一个好主意
    • It adapts to playing both too slowly and too fast
    • It makes gameplay non-deterministic and unstable. 这是真正的问题,尤其是物理和网络会更加困难在这种可变步长的情况下
  • Fixed update time step, variable rendering(最后一个样例)
    • It adapts to playing both too slowly and too fast
    • It’s more complex

引用

posted on 2017-02-27 22:07 landon 阅读(1305) 评论(0)  编辑  收藏 所属分类: GameServer

只有注册用户登录后才能发表评论。


网站导航: