【问题标题】:Android 2d canvas game: FPS Jitter problemAndroid 2d 画布游戏:FPS 抖动问题
【发布时间】:2023-03-07 12:21:01
【问题描述】:

我的游戏基于月球着陆器演示,虽然经过大量修改,我可以达到 40-50fps 左右,但问题是它在 40-50fps 之间波动太大,导致运动图形抖动!这很烦人,让我的游戏看起来很糟糕,而实际上它以良好的帧速率运行。

我尝试将线程优先级设置得更高,但这只会让情况变得更糟……现在它会在 40-60fps 之间波动……

我正在考虑将 FPS 限制在 30 左右,以使其保持不变。这是个好主意吗?其他人有经验或不同的解决方案吗?

谢谢!

这是我的运行循环

@Override
    public void run() {
        while (mRun) {
            Canvas c = null;
            try {
                c = mSurfaceHolder.lockCanvas(null);
                synchronized (mSurfaceHolder) {
                    if(mMode == STATE_RUNNING){

                        updatePhysics();
                    }
                    doDraw(c);
                }
            } finally {
                // do this in a finally so that if an exception is thrown
                // during the above, we don't leave the Surface in an
                // inconsistent state
                if (c != null) {
                    mSurfaceHolder.unlockCanvasAndPost(c);
                }
            }
        }
        }

private void updatePhysics() {

        now = android.os.SystemClock.uptimeMillis();

        elapsed = (now - mLastTime) / 1000.0;

        posistionY += elapsed * speed;
        mLastTime = now;
}

【问题讨论】:

    标签: android frame-rate


    【解决方案1】:

    不要将游戏的逻辑(对象移动等)更新速率建立在帧速率上。换句话说,将您的绘图和逻辑更新代码放在两个单独的组件/线程中。这样你的游戏逻辑就完全独立于你的帧率了。

    逻辑更新应该基于自上次更新以来已经过去了多少时间(我们称之为delta)。因此,如果您有一个对象以 1px/毫秒的速度移动,那么在每次更新期间,您的对象应该执行以下操作:

    public void update(int delta) {
        this.x += this.speed * delta;
    }
    

    所以现在即使你的 FPS 滞后,它也不会影响你的对象的移动速度,因为增量会更大,使对象移动得更远以进行补偿(在某些情况下会出现并发症,但这就是它的要点)。

    这是在逻辑更新对象中计算增量的一种方法(在某个线程循环中运行):

    private long lastUpdateTime;
    private long currentTime;
    
    public void update() {
        currentTime = System.currentTimeMillis();
        int delta = (int) (currentTime - lastUpdateTime);
        lastUpdateTime = currentTime;
        myGameObject.update(delta); // This would call something like the update method above.
    }
    

    希望对您有所帮助!请询问您是否有任何其他问题;我一直在自己制作安卓游戏。 :)


    示例代码:

    复制这两个 sn-ps(1 个活动和 1 个视图)并运行代码。无论您的 FPS 是什么,结果都应该是一个白点从屏幕上顺利落下。代码看起来有点复杂和冗长,但实际上非常简单; cmets应该解释一切。

    这个活动课并不太重要。你可以忽略其中的大部分代码。

    public class TestActivity extends Activity {
    
        private TestView view;
    
        public void onCreate(Bundle savedInstanceState) {
            // These lines just add the view we're using.
            super.onCreate(savedInstanceState);
            setContentView(R.layout.randomimage);
            RelativeLayout rl = (RelativeLayout) findViewById(R.id.relative_layout);
            view = new TestView(this);
            RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
                    10000, 10000);
            rl.addView(view, params);
    
            // This starts our view's logic thread
            view.startMyLogicThread();
        }
    
        public void onPause() {
            super.onPause();
            // When our activity pauses, we want our view to stop updating its logic.
            // This prevents your application from running in the background, which eats up the battery.
            view.setActive(false);
        }
    }
    

    这门课是令人兴奋的地方!

    public class TestView extends View {
    
        // Of course, this stuff should be in its own object, but just for this example..
        private float position; // Where our dot is
        private float velocity; // How fast the dot's moving
    
        private Paint p; // Used during onDraw()
        private boolean active; // If our logic is still active
    
        public TestView(Context context) {
            super(context);
            // Set some initial arbitrary values
            position = 10f;
            velocity = .05f;
            p = new Paint();
            p.setColor(Color.WHITE);
            active = true;
        }
    
        // We draw everything here. This is by default in its own thread (the UI thread).
        // Let's just call this thread THREAD_A.
        public void onDraw(Canvas c) {
            c.drawCircle(150, position, 1, p);
        }
    
        // This just updates our position based on a delta that's given.
        public void update(int delta) {
            position += delta * velocity;
            postInvalidate(); // Tells our view to redraw itself, since our position changed.
        }
    
        // The important part!
        // This starts another thread (let's call this THREAD_B). THREAD_B will run completely
        // independent from THREAD_A (above); therefore, FPS changes will not affect how
        // our velocity increases our position.
        public void startMyLogicThread() {
            new Thread() {
                public void run() {
                    // Store the current time values.
                    long time1 = System.currentTimeMillis();
                    long time2;
    
                    // Once active is false, this loop (and thread) terminates.
                    while (active) {
                        try {
                            // This is your target delta. 25ms = 40fps
                            Thread.sleep(25);
                        } catch (InterruptedException e1) {
                            e1.printStackTrace();
                        }
    
                        time2 = System.currentTimeMillis(); // Get current time
                        int delta = (int) (time2 - time1); // Calculate how long it's been since last update
                        update(delta); // Call update with our delta
                        time1 = time2; // Update our time variables.
                    }
                }
            }.start(); // Start THREAD_B
        }
    
        // Method that's called by the activity
        public void setActive(boolean active) {
            this.active = active;
        }
    }
    

    【讨论】:

    • 嗨,安迪,我更新了我的问题以包含游戏循环。这是您所说的实施方式吗?我有两个独立的功能,一个用于物理,一个用于绘图。在物理学中,我有经过时间或“增量”,就像您在答案中描述的那样。出于某种原因,我仍然会抖动图形。我认为正在发生的是坠落的物体并没有以恒定的速度下降,因为 delta 一直在波动。
    • 你确定物理和绘图函数在两个单独的线程中吗?拥有两个单独的线程将使它们彼此运行时间独立。至于抖动,您确定您正在正确计算增量(从现在到上次更新之间的时间)吗?还要确保游戏中的所有物理都依赖于该增量,而不是调用了多少次更新。如果您不介意,请发布一些有问题的代码,以便我提供更详细的建议。我会尝试找到一些我的代码来发布。谢谢!
    • 它们不在 2 个单独的线程中,同一线程不同的功能。你能在同一个活动中添加 2 个线程吗?
    • 哦,这就是问题所在。他们需要在 2 个单独的线程中运行。在这里,我将发布一些我刚刚在上面编写的代码,并在此处进行更详细的解释(几分钟后)。
    • 我刚刚看了你的代码。我注意到您的调用顺序是“更新、绘制、重复”。因此,如果绘图时间比平时更长,您的更新调用当然会延迟。这就是将它们放在 2 个单独的线程中派上用场的地方。绘图可以花费任何时间,但更新仍将以相同的速率更新,从而产生平滑的逻辑。如果它仍然滞后,唯一剩下的原因是手机是否无法快速处理您的逻辑/绘图。如果是这样,请支持您的高端图形密集型游戏。 ^^
    【解决方案2】:

    我认为上面的某些代码可能不是真的有问题,而是效率低下。我说的是这段代码……

       // The important part!
    // This starts another thread (let's call this THREAD_B). THREAD_B will run completely
    // independent from THREAD_A (above); therefore, FPS changes will not affect how
    // our velocity increases our position.
    public void startMyLogicThread() {
        new Thread() {
            public void run() {
                // Store the current time values.
                long time1 = System.currentTimeMillis();
                long time2;
    
                // Once active is false, this loop (and thread) terminates.
                while (active) {
                    try {
                        // This is your target delta. 25ms = 40fps
                        Thread.sleep(25);
                    } catch (InterruptedException e1) {
                        e1.printStackTrace();
                    }
    
                    time2 = System.currentTimeMillis(); // Get current time
                    int delta = (int) (time2 - time1); // Calculate how long it's been since last update
                    update(delta); // Call update with our delta
                    time1 = time2; // Update our time variables.
                }
            }
        }.start(); // Start THREAD_B
    }
    

    具体来说,我正在考虑以下几行......

    // This is your target delta. 25ms = 40fps
    Thread.sleep(25);
    

    在我看来,只是让线程闲置无所事事是浪费宝贵的处理时间,而实际上你想要做的是执行更新,那么,如果更新所用的时间少于 25毫秒,然后休眠线程以获取更新期间使用的内容和 25 毫秒(或任何您选择的帧速率)的差异。这样,更新将在当前帧被渲染时发生,并将完成,以便下一帧更新使用更新后的值。

    我能想到的唯一问题是需要进行某种同步,以便当前帧渲染不使用部分更新的值。或许更新为您的一组值的新实例,然后在渲染之前使新实例成为当前实例。

    我记得我记得在一本图形书中读到过,目标是在保持所需帧速率的同时执行尽可能多的更新,然后并且只有他们执行屏幕更新。

    这当然需要一个线程来驱动更新 - 如果您使用 SurfaceView,当您锁定画布时,渲染由该线程控制(理论上,无论如何根据我的理解)。

    所以,在代码中,它更像是......

    // Calculate next render time
    nextRender = System.currentTimeInMillis() + 25;
    
    while (System.currentTimeInMillis() < nextRender)
    {
        // All objects must be updated here
        update();
    
        // I could see maintaining a pointer to the next object to be updated,
        // such that you update as many objects as you can before the next render, and 
        // then continue the update from where you left off in the next render...
    }
    
    // Perform a render (if using a surface view)
    c = lockCanvas() blah, blah...
    // Paint and unlock
    
    // If using a standard view
    postInvalidate();
    

    祝你好运,任何使用它的人的任何反馈肯定会帮助我们大家学到一些东西......

    巴巴蒂

    【讨论】:

      【解决方案3】:

      我认为是关于垃圾收集器

      【讨论】:

      • 是的,这通常是问题所在,但我已采取措施确保在运行时没有分配或销毁任何内容。无论如何我修复了它,这与我的时间有关,我在错误的地方使用了错误的系统时间,导致时间不一致。还有一个单位转换的问题,转double -> int,我做得太早了,出现舍入错误,所以我一直等到在onCanvas.draw中使用它进行转换。
      【解决方案4】:

      如果您的游戏动作繁重,我会使用 SurfaceView 而不是 View。如果您不需要快速更新 GUI,那么 View 很好,但对于 2D 游戏,使用 SurfaceView 总是更好。

      【讨论】:

        【解决方案5】:

        我有一个类似的问题,抖动使大物体的移动看起来不均匀。即使“速度”相同,但不同长度的步数使动作看起来很跳跃。 Broody - 你说 SurfaceView 更好,但是,在 Android 3.0 之后情况并非如此,因为 View 是硬件加速的,但 .lockCanvas 返回的画布不是。 史蒂文 - 是的,这可能会导致漏洞,但很容易检测到。 /雅各布

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2013-02-04
          • 1970-01-01
          • 2018-10-04
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-02-04
          • 2011-04-12
          相关资源
          最近更新 更多