android游戏循环与渲染线程中的更新 [英] android game loop vs updating in the rendering thread

查看:41
本文介绍了android游戏循环与渲染线程中的更新的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在制作一款安卓游戏,但目前无法获得我想要的性能.我在自己的线程中有一个游戏循环,可以更新对象的位置.渲染线程将遍历这些对象并绘制它们.当前的行为似乎是波动/不均匀的运动.我无法解释的是,在我将更新逻辑放在它自己的线程中之前,我将它放在 onDrawFrame 方法中,就在 gl 调用之前.在这种情况下,动画非常流畅,只有当我尝试通过 Thread.sleep 限制更新循环时,它才会变得不稳定/不均匀.即使我让更新线程发疯(不休眠),动画也很流畅,只有在涉及到 Thread.sleep 时才会影响动画质量.

I'm making an android game and am currently not getting the performance I'd like. I have a game loop in its own thread which updates an object's position. The rendering thread will traverse these objects and draw them. The current behavior is what seems like choppy/uneven movement. What I cannot explain is that before I put the update logic in its own thread, I had it in the onDrawFrame method, right before the gl calls. In that case, the animation was perfectly smooth, it only becomes choppy/uneven specifically when I try to throttle my update loop via Thread.sleep. Even when I allow the update thread to go berserk (no sleep), the animation is smooth, only when Thread.sleep is involved does it affect the quality of the animation.

我已经创建了一个框架项目来查看是否可以重新创建问题,下面是渲染器中的更新循环和 onDrawFrame 方法:更新循环

I've created a skeleton project to see if I could recreate the issue, below are the update loop and the onDrawFrame method in the renderer: Update Loop

    @Override
public void run() 
{
    while(gameOn) 
    {
        long currentRun = SystemClock.uptimeMillis();
        if(lastRun == 0)
        {
            lastRun = currentRun - 16;
        }
        long delta = currentRun - lastRun;
        lastRun = currentRun;

        posY += moveY*delta/20.0;

        GlobalObjects.ypos = posY;

        long rightNow = SystemClock.uptimeMillis();
        if(rightNow - currentRun < 16)
        {
            try {
                Thread.sleep(16 - (rightNow - currentRun));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

这是我的 onDrawFrame 方法:

        @Override
public void onDrawFrame(GL10 gl) {
    gl.glClearColor(1f, 1f, 0, 0);
    gl.glClear(GL10.GL_COLOR_BUFFER_BIT |
            GL10.GL_DEPTH_BUFFER_BIT);

    gl.glLoadIdentity();

    gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
    gl.glTranslatef(transX, GlobalObjects.ypos, transZ);
    //gl.glRotatef(45, 0, 0, 1);
    //gl.glColor4f(0, 1, 0, 0);

    gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
    gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

    gl.glVertexPointer(3,  GL10.GL_FLOAT, 0, vertexBuffer);
    gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, uvBuffer);

    gl.glDrawElements(GL10.GL_TRIANGLES, drawOrder.length,
              GL10.GL_UNSIGNED_SHORT, indiceBuffer);

    gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
    gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
}

我查看了副本岛的源代码,他在单独的线程中执行更新逻辑,并使用 Thread.sleep 对其进行限制,但他的游戏看起来非常流畅.有没有人有任何想法或有没有人经历过我所描述的?

I've looked through replica island's source and he's doing his update logic in a separate thread, as well as throttling it with Thread.sleep, but his game looks very smooth. Does anyone have any ideas or has anyone experienced what I'm describing?

---2013 年 1 月 25 日---
我有一些时间思考并大大平滑了这个游戏引擎.我的处理方式可能是对实际游戏程序员的亵渎或侮辱,因此请随时纠正这些想法.

--- 1/25/13---
I've had some time to think and have smoothed out this game engine considerably. How I managed this might be blasphemous or insulting to actual game programmers, so please feel free to correct any of these ideas.

基本思想是保持更新、绘制...更新、绘制...的模式,同时保持时间增量相对相同(通常不受您的控制).我的第一个行动方案是同步我的渲染器,使其仅在被通知允许这样做后才绘制.这看起来像这样:

The basic idea is to keep a pattern of update, draw... update, draw... while keeping the time delta relatively the same (often out of your control). My first course of action was to synchronize my renderer in such a way that it only drew after being notified it was allowed to do so. This looks something like this:

public void onDrawFrame(GL10 gl10) {
        synchronized(drawLock)
    {
        while(!GlobalGameObjects.getInstance().isUpdateHappened())
        {
            try
            {
                Log.d("test1", "draw locking");
                drawLock.wait();
            } 
            catch (InterruptedException e) 
            {
                e.printStackTrace();
            }
        }
    }

当我完成更新逻辑时,我调用 drawLock.notify(),释放渲染线程来绘制我刚刚更新的内容.这样做的目的是帮助建立更新、绘制...更新、绘制...等模式.

When I finish my update logic, I call drawLock.notify(), releasing the rendering thread to draw what I just updated. The purpose of this is to help establish the pattern of update, draw... update, draw... etc.

一旦我实现了这一点,它就会变得相当流畅,尽管我仍然偶尔会遇到运动跳跃.经过一些测试,我发现在 ondrawFrame 调用之间发生了多次更新.这导致一帧显示两次(或更多)更新的结果,比正常情况跳跃更大.

Once I implemented that, it was considerably smoother, although I was still experiencing occasional jumps in movement. After some testing, I saw that I had multiple updates occurring between calls of ondrawFrame. This was causing one frame to show the result of two (or more) updates, a larger jump than normal.

我为解决这个问题所做的是将两次 onDrawFrame 调用之间的时间增量限制为某个值,例如 18 毫秒,并将额外时间存储在剩余时间中.如果他们可以处理,这个剩余部分将在接下来的几次更新中分配给后续的时间增量.这个想法防止了所有突然的长跳,基本上平滑了多帧的时间尖峰.这样做给了我很好的结果.

What I did to resolve this was to cap the time delta to some value, say 18ms, between two onDrawFrame calls and store the extra time in a remainder. This remainder would be distributed to subsequent time deltas over the next few updates if they could handle it. This idea prevents all sudden long jumps, essentially smoothing a time spike out over multiple frames. Doing this gave me great results.

这种方法的缺点是,在一段时间内,物体的位置会随着时间的推移而变得不准确,实际上会加速以弥补这种差异.但是比较流畅,速度变化不是很明显.

The downside to this approach is that for a little time, the position of objects will not be accurate with time, and will actually speed up to make up for that difference. But it's smoother and change in speed is not very noticeable.

最后,我决定根据以上两个想法重写我的引擎,而不是修补我最初制作的引擎.我对线程同步做了一些优化,也许有人可以评论一下.

Finally, I decided to rewrite my engine with the two above ideas in mind, rather than patching up the engine I had originally made. I made some optimizations for the thread synchronization that perhaps someone could comment on.

我当前的线程交互如下:
-更新线程更新当前缓冲区(双缓冲区系统,以便同时更新和绘制),然后如果前一帧已绘制,则将此缓冲区提供给渲染器.
- 如果前一帧还没有绘制,或者正在绘制,更新线程将等待,直到渲染线程通知它已经绘制.
-渲染线程一直等待,直到更新线程通知发生了更新.
-当渲染线程绘制时,它设置一个最后绘制的变量",指示它最后绘制的两个缓冲区中的哪一个,并且如果它正在等待前一个缓冲区被绘制,也会通知更新线程.

My current threads interact like this:
-Update thread updates the current buffer (double buffer system in order to update and draw simultaneously) and will then give this buffer to the renderer if the previous frame has been drawn.
-If the previous frame has not yet draw, or is drawing, the update thread will wait until the render thread notifies it that it has drawn.
-Render thread waits until notified by update thread that an update has occurred.
-When the render thread draws, it sets a "last drawn variable" indicating which of the two buffers it last drew and also notifies the update thread if it was waiting on the previous buffer to be drawn.

这可能有点令人费解,但这样做是为了发挥多线程的优势,因为它可以在帧 n-1 正在绘制时执行帧 n 的更新,同时还可以防止渲染器每帧多次更新迭代需要很长时间.为了进一步解释,如果检测到 lastDrawn 缓冲区等于刚刚更新的缓冲区,则此多次更新场景由更新线程锁定处理.如果它们相等,这向更新线程表明之前的帧尚未绘制.

That may be a little convoluted, but what that's doing is allowing for the advantages of multithreading, in that it can perform the update for frame n while frame n-1 is drawing while also preventing multiple update iterations per frame if the renderer is taking a long time. To further explain, this multiple-update scenario is handled by the update thread locking if it detects that the lastDrawn buffer is equal to the one which was just updated. If they are equal, this indicates to the update thread that the frame before has not yet been drawn.

到目前为止,我取得了不错的成绩.如果有人有任何意见,请告诉我,很高兴听到您对我所做的任何事情的看法,无论是对是错.

So far I'm getting good results. Let me know if anyone has any comments, would be happy to hear your thoughts on anything I'm doing, right or wrong.

谢谢

推荐答案

(Blackhex 的回答提出了一些有趣的观点,但我无法将所有这些都塞进评论中.)

(The answer from Blackhex raised some interesting points, but I can't cram all this into a comment.)

让两个线程异步操作必然会导致这样的问题.这样看:驱动动画的事件是硬件vsync"信号,即Android表面合成器向显示硬件提供一个充满数据的新屏幕的点.每当 vsync 到达时,您都希望拥有一个新的数据帧.如果您没有新数据,则游戏看起来很不稳定.如果您在此期间生成 3 帧数据,则会忽略其中的两帧,而您只是在浪费电池寿命.

Having two threads operating asynchronously is bound to lead to issues like this. Look at it this way: the event that drives animation is the hardware "vsync" signal, i.e. the point at which the Android surface compositor provides a new screen full of data to the display hardware. You want to have a new frame of data whenever vsync arrives. If you don't have new data, the game looks choppy. If you generated 3 frames of data in that period, two will be ignored, and you're just wasting battery life.

(全速运行 CPU 也可能导致设备变热,从而导致热节流,从而减慢系统中的所有内容...并且可能使您的动画断断续续.)

(Running a CPU full out may also cause the device to heat up, which can lead to thermal throttling, which slows everything in the system down... and can make your animation choppy.)

与显示保持同步的最简单方法是在 onDrawFrame() 中执行所有状态更新.如果有时执行状态更新和渲染帧所需的时间超过一帧,那么你会看起来很糟糕,需要修改你的方法.简单地将所有游戏状态更新转移到第二个核心并不会像您希望的那样有很大帮助——如果核心#1 是渲染器线程,核心#2 是游戏状态更新线程,那么核心#1 将会运行在核心#2 更新状态时处于空闲状态,之后核心#1 将在核心#2 处于空闲状态时恢复进行实际渲染,并且需要同样长的时间.要实际增加每帧可以执行的计算量,您需要有两个(或更多)内核同时工作,这会引发一些有趣的同步问题,具体取决于您如何定义分工(请参阅 http://developer.android.com/training/articles/smp.html 如果你想沿着那条路走).

The easiest way to stay in sync with the display is to perform all of your state updates in onDrawFrame(). If it sometimes takes longer than one frame to perform your state updates and render the frame, then you're going to look bad, and need to modify your approach. Simply shifting all game state updates to a second core isn't going to help as much as you might like -- if core #1 is the renderer thread, and core #2 is the game state update thread, then core #1 is going to sit idle while core #2 updates the state, after which core #1 will resume to do the actual rendering while core #2 sits idle, and it's going to take just as long. To actually increase the amount of computation you can do per frame, you'd need to have two (or more) cores working simultaneously, which raises some interesting synchronization issues depending on how you define your division of labor (see http://developer.android.com/training/articles/smp.html if you want to go down that road).

尝试使用 Thread.sleep() 来管理帧速率通常会以失败告终.您无法知道 vsync 之间的时间间隔是多长时间,或者下一次到达还有多长时间.每个设备都不同,在某些设备上它可能是可变的.你最终会得到两个时钟——vsync 和 sleep——互相跳动,结果就是动画不连贯.最重要的是,Thread.sleep() 不对准确性或最短睡眠时间做出任何具体保证.

Attempting to use Thread.sleep() to manage the frame rate generally ends badly. You can't know how long the period between vsync is, or how long until the next one arrives. It's different for every device, and on some devices it may be variable. You essentially end up with two clocks -- vsync and sleep -- beating against each other, and the result is choppy animation. On top of that, Thread.sleep() doesn't make any specific guarantees about accuracy or minimum sleep duration.

我还没有真正浏览过 Replica Island 的源代码,但是在 GameRenderer.onDrawFrame() 中,您可以看到他们的游戏状态线程(创建要绘制的对象列表)和GL 渲染器线程(仅绘制列表).在他们的模型中,游戏状态只根据需要更新,如果没有任何改变,它只是重新绘制之前的抽签列表.此模型适用于事件驱动的游戏,即当发生某些事情时屏幕上的内容会更新(您按下键、计时器触发等).当事件发生时,他们可以进行最小的状态更新并适当调整抽奖列表.

I haven't really gone through the Replica Island sources, but in GameRenderer.onDrawFrame() you can see the interaction between their game state thread (which creates a list of objects to draw) and the GL renderer thread (which just draws the list). In their model, the game state only updates as needed, and if nothing has changed it just re-draws the previous draw list. This model works well for an event-driven game, i.e. where the contents on screen update when something happens (you hit a key, a timer fires, etc). When an event occurs, they can do a minimal state update and adjust the draw list as appropriate.

从另一个角度来看,渲染线程和游戏状态是并行工作的,因为它们没有严格地联系在一起.游戏状态只是根据需要更新东西,渲染线程将它锁定在每个 vsync 并绘制它找到的任何东西.只要双方都没有将任何东西锁定太久,他们就不会明显干涉.唯一有趣的共享状态是绘制列表,由互斥锁保护,因此它们的多核问题被最小化.

Viewed another way, the render thread and the game state work in parallel because they're not rigidly tied together. The game state just runs around updating things as needed, and the render thread locks it down every vsync and draws whatever it finds. So long as neither side keeps anything locked up for too long, they don't visibly interfere. The only interesting shared state is the draw list, guarded with a mutex, so their multi-core issues are minimized.

对于 Android 突破(http://code.google.com/p/android-breakout/ ),游戏中有一个连续弹跳的球.在那里,我们希望尽可能频繁地更新我们的状态,因此我们驱动 vsync 的状态变化,使用前一帧的时间增量来确定事情进展了多远.每帧的计算量很小,对于现代 GL 设备来说,渲染非常简单,所以它很容易在 1/60 秒内完成.如果显示器更新得更快(240Hz),我们可能偶尔会丢帧(同样,不太可能被注意到),并且我们会在帧更新时消耗 4 倍的 CPU(这是不幸的).

For Android Breakout ( http://code.google.com/p/android-breakout/ ), the game has a ball bouncing around, in continuous motion. There we want to update our state as frequently as the display allows us to, so we drive the state change off of vsync, using a time delta from the previous frame to determine how far things have advanced. The per-frame computation is small, and the rendering is pretty trivial for a modern GL device, so it all fits easily in 1/60th of a second. If the display updated much faster (240Hz) we might occasionally drop frames (again, unlikely to be noticed) and we'd be burning 4x as much CPU on frame updates (which is unfortunate).

如果由于某种原因其中一款游戏错过了垂直同步,玩家可能会也可能不会注意到.状态按经过的时间推进,而不是固定持续时间帧"的预设概念,例如球要么在连续两帧中移动 1 个单位,要么在一帧中移动 2 个单位.根据帧速率和显示器的响应能力,这可能是不可见的.(这是一个关键的设计问题,如果您将游戏状态设想为滴答声",可能会让您头疼.)

If for some reason one of these games missed a vsync, the player may or may not notice. The state advances by elapsed time, not a pre-set notion of a fixed-duration "frame", so e.g. the ball will either move 1 unit on each of two consecutive frames, or 2 units on one frame. Depending on the frame rate and the responsiveness of the display, this may not be visible. (This is a key design issue, and one that can mess with your head if you envisioned your game state in terms of "ticks".)

这两种方法都是有效的.关键是在调用 onDrawFrame 时绘制当前状态,并尽可能不频繁地更新状态.

Both of these are valid approaches. The key is to draw the current state whenever onDrawFrame is called, and to update state as infrequently as possible.

其他碰巧读到此内容的人请注意:不要使用 System.currentTimeMillis().问题中的示例使用了 SystemClock.uptimeMillis(),它基于单调时钟而不是挂钟时间.那,或 System.nanoTime(),是更好的选择.(我正在对 currentTimeMillis 进行小规模的讨伐,它在移动设备上可能会突然向前或向后跳跃.)

Note for anyone else who happens to read this: don't use System.currentTimeMillis(). The example in the question used SystemClock.uptimeMillis(), which is based on the monotonic clock rather than wall-clock time. That, or System.nanoTime(), are better choices. (I'm on a minor crusade against currentTimeMillis, which on a mobile device could suddenly jump forward or backward.)

更新:我写了一个 更长的答案类似的问题.

Update: I wrote an even longer answer to a similar question.

更新 2: 我写了一个 更长的答案 关于一般问题(见附录 A).

Update 2: I wrote an even longer longer answer about the general problem (see Appendix A).

这篇关于android游戏循环与渲染线程中的更新的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆