2D-игра на холсте для Android: проблема джиттера FPS

Я основал свою игру на демо-версии лунного посадочного модуля, хотя и сильно модифицированной, и я могу получить около 40-50 кадров в секунду, но проблема в том, что она колеблется между 40-50 кадрами в секунду настолько сильно, что это вызывает дрожание движущейся графики! Это очень раздражает и заставляет мою игру выглядеть дерьмово, когда на самом деле она работает с хорошей частотой кадров.

Я попытался установить более высокий приоритет потока, но это только ухудшило ситуацию... теперь он будет колебаться между 40-60 кадрами в секунду...

Я думал ограничить 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;
}

person Cameron    schedule 22.07.2010    source источник


Ответы (5)


Не основывайте скорость обновления логики вашей игры (движение объектов и т. д.) на частоте кадров. Другими словами, поместите свой код обновления рисования и логики в два отдельных компонента/потока. Таким образом, ваша игровая логика полностью независима от частоты кадров.

Логика обновления должна основываться на том, сколько времени прошло с момента последнего обновления (назовем его delta). Следовательно, если у вас есть объект, движущийся со скоростью 1 пиксель в миллисекунду, то при каждом обновлении ваш объект должен делать что-то вроде этого:

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.
}

Надеюсь, это поможет! Пожалуйста, спросите, если у вас есть какие-либо другие вопросы; Я сам делаю игры для Android. :)


Образец кода:

Скопируйте эти два фрагмента (1 действие и 1 представление) и запустите код. В результате должна получиться белая точка, плавно падающая вниз по экрану, независимо от того, какой у вас FPS. Код выглядит довольно сложным и длинным, но на самом деле он довольно прост; комментарии должны все объяснить.

Этот класс активности не слишком важен. Вы можете игнорировать большую часть кода в нем.

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;
    }
}
person Andy Zhang    schedule 22.07.2010
comment
Привет, Энди, я обновил свой вопрос, включив в него цикл игры. Это то, как вы говорили о его реализации? У меня есть две отдельные функции, одна для физики и одна для рисования. Внутри физики у меня есть прошедшее время или дельта, как вы описали это в своем ответе. Почему-то у меня до сих пор дергается графика. Я думаю, что происходит то, что падающие объекты не падают с постоянной скоростью, потому что дельта продолжает колебаться. - person Cameron; 23.07.2010
comment
Вы уверены, что функции физики и рисования находятся в двух отдельных потоках? Наличие двух отдельных потоков сделает их выполнение независимым друг от друга во времени. Что касается дрожания, вы уверены, что правильно рассчитываете дельту (время между текущим моментом и последним обновлением)? Также убедитесь, что вся физика в вашей игре зависит от этой дельты, а не от того, сколько раз вызывалось обновление. Если вы не возражаете, опубликуйте часть кода, о котором идет речь, чтобы я мог дать более подробный совет. Я постараюсь найти часть своего кода для публикации. Спасибо! - person Andy Zhang; 23.07.2010
comment
Они не находятся в двух отдельных потоках, в одном и том же потоке разные функции. Можете ли вы добавить 2 потока к одному и тому же действию? - person Cameron; 23.07.2010
comment
О, тогда это проблема. Они должны работать в 2 отдельных потоках. Здесь я опубликую код, который только что написал выше, и объясню его более подробно (через пару минут). - person Andy Zhang; 23.07.2010
comment
Я только что посмотрел на ваш код. Я заметил, что ваша последовательность вызовов была обновлена, нарисована, повторена. Таким образом, ваш вызов обновления, конечно же, будет отложен, если отрисовка займет больше времени, чем обычно. Вот где удобно иметь их в 2 отдельных потоках. Рисование может занять сколько угодно времени, но обновление будет по-прежнему обновляться с той же скоростью, что обеспечивает плавную логику. Если он все еще отстает, единственной оставшейся причиной может быть то, что телефон буквально не способен так быстро обрабатывать вашу логику / рисование. Если это так, реквизит для вашей высококлассной игры с интенсивной графикой. ^^ - person Andy Zhang; 23.07.2010
comment
Большое спасибо! Мне потребуется некоторое время, чтобы реализовать это, но я думаю, что это будет начало! + Энди! - person Cameron; 23.07.2010
comment
Ничего себе, Энди, это было умно! Я тоже посмотрю на это! - person Curtain; 24.08.2010
comment
Хороший ответ. С 2010 года дела немного продвинулись; теперь вы можете использовать Choreographer для дальнейшего улучшения плавности. См. source.android.com/devices/graphics/architecture.html#loops - person fadden; 24.11.2014

Я думаю, что на самом деле что-то не так с некоторыми из приведенных выше кодов, а скорее неэффективность. Я говорю об этом коде...

   // 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();

Удачи, и любой отзыв от любого, кто использует это, наверняка поможет нам всем чему-то научиться...

рпбарбати

person Rodney Barbati    schedule 12.11.2011

Я думаю, что это о сборщике мусора

person Steven Shih    schedule 21.10.2010
comment
Да, обычно это проблема, но я предпринял шаги, чтобы ничего не было выделено или уничтожено во время выполнения. В любом случае, я исправил это, это было как-то связано с моим временем, я использовал неправильное системное время в неправильных местах, что вызывало несогласованное время. Также была проблема с преобразованием единиц измерения, превращая двойное -> int, я делал это слишком рано и делал ошибки округления, поэтому я ждал до тех пор, пока не использовал его в onCanvas.draw для преобразования. - person Cameron; 21.10.2010

Я бы использовал SurfaceView вместо View, если ваша игра насыщена действием. Если вам не нужно быстро обновлять графический интерфейс, View подойдет, но для 2D-игр всегда лучше использовать SurfaceView.

person broody    schedule 05.12.2010

У меня похожая проблема, из-за дрожания движения больших объектов выглядят неравномерно. Несмотря на то, что «скорость» одинакова, разная длина шагов делает движения дергаными. Broody. Вы говорите, что SurfaceView лучше, однако это неверно после Android 3.0, так как View ускоряется HW, а холст, возвращаемый .lockCanvas, — нет. Стивен – Да, это, вероятно, вызывает проблемы, но это легко обнаружить. /Джейкоб

person Jacob L    schedule 11.06.2013