Android 2d canvas игра: проблема FPS джиттера - PullRequest
5 голосов
/ 22 июля 2010

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

Я пытался установить приоритет потока выше, но это только ухудшило его ... теперь оно будет колебаться между 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;
}

Ответы [ 5 ]

21 голосов
/ 23 июля 2010

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

Обновление логики должно основываться на том, сколько времени прошло с момента последнего обновления (назовем это 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.
}

Надеюсь, это поможет! Пожалуйста, спросите, есть ли у вас другие вопросы; Я сам делал игры для 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;
    }
}
3 голосов
/ 12 ноября 2011

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

   // 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 миллис (или любой другой выбранной вами частотой кадров). Таким образом, обновление будет происходить во время рендеринга текущего кадра, и будет завершено, поэтому при следующем обновлении кадра будут использоваться обновленные значения.

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

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

Это, конечно, потребует одного потока для запуска обновлений - если вы используете 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();

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

rpbarbati

0 голосов
/ 11 июня 2013

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

0 голосов
/ 05 декабря 2010

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

0 голосов
/ 21 октября 2010

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

...