Подтвердить что ты не робот

Раздражающие лагами/заиканиями в игре для Android

Я только начал с разработки игр в android, и я работаю над супер простой игрой.

Игра в основном похожа на птицу.

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

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

В основном есть 4 "препятствия", которые являются шириной экрана в целом друг от друга.
Когда игра начинается, препятствия начинают двигаться (к персонажу) с постоянной скоростью. Значение символа x игрока является последовательным на протяжении всей игры, а значение y изменяется.

Задержка происходит в основном, когда персонаж проходит через препятствие (а иногда и немного после этого препятствия). Случается, что на каждом рисунке состояния игры возникают неравномерные задержки, вызывающие заикания в движениях.

  • GC не запускается в соответствии с журналом.
  • Заикания НЕ вызваны слишком высокой скоростью (я знаю, потому что в начале игры, когда препятствия находятся вне поля зрения, персонаж движется плавно)
  • Я не думаю, что проблема связана с FPS, потому что даже когда для поля MAX_FPS установлено значение 100, все еще заикаются.

Моя мысль заключается в том, что существует строка или несколько строк кода, которые вызывают некоторую задержку (и, следовательно, пропускаются кадры). Я также думаю, что эти строки должны быть вокруг методов update() и draw() PlayerCharacter, Obstacle и MainGameBoard.

Проблема в том, что я по-прежнему новичок в разработке android development android game, поэтому я понятия не имею, что может вызвать такую ​​задержку.

Я попробовал поискать в Интернете ответы... К сожалению, все, что я нашел, указывало на то, что GC обвиняется. Однако, я не верю, что это так (поправьте меня, если я ошибаюсь), эти ответы не распространяются на меня. Я также прочитал страницу разработчика Performance Tips разработчика Android, но не смог найти ничего, что помогло.

Итак, пожалуйста, помогите мне найти ответ на решение этих досадных лаг!

Некоторый код

MainThread.java:

public class MainThread extends Thread {

public static final String TAG = MainThread.class.getSimpleName();
private final static int    MAX_FPS = 60;   // desired fps
private final static int    MAX_FRAME_SKIPS = 5;    // maximum number of frames to be skipped
private final static int    FRAME_PERIOD = 1000 / MAX_FPS;  // the frame period

private boolean running;
public void setRunning(boolean running) {
    this.running = running;
}

private SurfaceHolder mSurfaceHolder;
private MainGameBoard mMainGameBoard;

public MainThread(SurfaceHolder surfaceHolder, MainGameBoard gameBoard) {
    super();
    mSurfaceHolder = surfaceHolder;
    mMainGameBoard = gameBoard;
}

@Override
public void run() {
    Canvas mCanvas;
    Log.d(TAG, "Starting game loop");

    long beginTime;     // the time when the cycle begun
    long timeDiff;      // the time it took for the cycle to execute
    int sleepTime;      // ms to sleep (<0 if we're behind)
    int framesSkipped;  // number of frames being skipped 

    sleepTime = 0;

    while(running) {
        mCanvas = null;
        try {
            mCanvas = this.mSurfaceHolder.lockCanvas();
            synchronized (mSurfaceHolder) {
                beginTime = System.currentTimeMillis();
                framesSkipped = 0;


                this.mMainGameBoard.update();

                this.mMainGameBoard.render(mCanvas);

                timeDiff = System.currentTimeMillis() - beginTime;

                sleepTime = (int) (FRAME_PERIOD - timeDiff);

                if(sleepTime > 0) {
                    try {
                        Thread.sleep(sleepTime);
                    } catch (InterruptedException e) {}
                }

                while(sleepTime < 0 && framesSkipped < MAX_FRAME_SKIPS) {
                    // catch up - update w/o render
                    this.mMainGameBoard.update();
                    sleepTime += FRAME_PERIOD;
                    framesSkipped++;
                }
            }
        } finally {
            if(mCanvas != null)
                mSurfaceHolder.unlockCanvasAndPost(mCanvas);
        }
    }
}
}

MainGameBoard.java:

public class MainGameBoard extends SurfaceView implements
    SurfaceHolder.Callback {

private MainThread mThread;
private PlayerCharacter mPlayer;
private Obstacle[] mObstacleArray = new Obstacle[4];
public static final String TAG = MainGameBoard.class.getSimpleName();
private long width, height;
private boolean gameStartedFlag = false, gameOver = false, update = true;
private Paint textPaint = new Paint();
private int scoreCount = 0;
private Obstacle collidedObs;

public MainGameBoard(Context context) {
    super(context);
    getHolder().addCallback(this);

    DisplayMetrics displaymetrics = new DisplayMetrics();
    ((Activity) getContext()).getWindowManager().getDefaultDisplay().getMetrics(displaymetrics);
    height = displaymetrics.heightPixels;
    width = displaymetrics.widthPixels;

    mPlayer = new PlayerCharacter(BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher), width/2, height/2);

    for (int i = 1; i <= 4; i++) {
        mObstacleArray[i-1] = new Obstacle(width*(i+1) - 200, height, i);
    }

    mThread = new MainThread(getHolder(), this);

    setFocusable(true);
}

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
        int height) {
}

@Override
public void surfaceCreated(SurfaceHolder holder) {
    mThread.setRunning(true);
    mThread.start();
}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
    Log.d(TAG, "Surface is being destroyed");
    // tell the thread to shut down and wait for it to finish
    // this is a clean shutdown
    boolean retry = true;
    while (retry) {
        try {
            mThread.join();
            retry = false;
        } catch (InterruptedException e) {
            // try again shutting down the thread
        }
    }
    Log.d(TAG, "Thread was shut down cleanly");
}

@Override
public boolean onTouchEvent(MotionEvent event) {

    if(event.getAction() == MotionEvent.ACTION_DOWN) {
        if(update && !gameOver) {
            if(gameStartedFlag) {
                mPlayer.cancelJump();
                mPlayer.setJumping(true);
            }

            if(!gameStartedFlag)
                gameStartedFlag = true;
        }
    } 


    return true;
}

@SuppressLint("WrongCall")
public void render(Canvas canvas) {
    onDraw(canvas);
}

@Override
protected void onDraw(Canvas canvas) {
    canvas.drawColor(Color.GRAY);
    mPlayer.draw(canvas);

    for (Obstacle obs : mObstacleArray) {
        obs.draw(canvas);
    }

    if(gameStartedFlag) {
        textPaint.reset();
        textPaint.setColor(Color.WHITE);
        textPaint.setTextAlign(Paint.Align.CENTER);
        textPaint.setTextSize(100);
        canvas.drawText(String.valueOf(scoreCount), width/2, 400, textPaint);
    }

    if(!gameStartedFlag && !gameOver) {
        textPaint.reset();
        textPaint.setColor(Color.WHITE);
        textPaint.setTextAlign(Paint.Align.CENTER);
        textPaint.setTextSize(72);
        canvas.drawText("Tap to start", width/2, 200, textPaint);
    }

    if(gameOver) {      
        textPaint.reset();
        textPaint.setColor(Color.WHITE);
        textPaint.setTextAlign(Paint.Align.CENTER);
        textPaint.setTextSize(86);

        canvas.drawText("GAME OVER", width/2, 200, textPaint);
    }

}

public void update() {
    if(gameStartedFlag && !gameOver) {  
        for (Obstacle obs : mObstacleArray) {
            if(update) {
                if(obs.isColidingWith(mPlayer)) {
                    collidedObs = obs;
                    update = false;
                    gameOver = true;
                    return;
                } else {
                    obs.update(width);
                    if(obs.isScore(mPlayer))
                        scoreCount++;
                }
            }
        }

        if(!mPlayer.update() || !update)
            gameOver = true;
    }
}

}

PlayerCharacter.java:

public void draw(Canvas canvas) {
    canvas.drawBitmap(mBitmap, (float) x - (mBitmap.getWidth() / 2), (float) y - (mBitmap.getHeight() / 2), null);
}

public boolean update() {
    if(jumping) {
        y -= jumpSpeed;
        jumpSpeed -= startJumpSpd/20f;

        jumpTick--;
    } else if(!jumping) {
        if(getBottomY() >= startY*2)
            return false;

        y += speed;
        speed += startSpd/25f;
    }

    if(jumpTick == 0) {
        jumping = false;
        cancelJump(); //rename
    }

    return true;
}

public void cancelJump() { //also called when the user touches the screen in order to stop a jump and start a new jump
    jumpTick = 20;

    speed = Math.abs(jumpSpeed);
    jumpSpeed = 20f;
}

Obstacle.java:

public void draw(Canvas canvas) {
    Paint pnt = new Paint();
    pnt.setColor(Color.CYAN);
    canvas.drawRect(x, 0, x+200, ySpaceStart, pnt);
    canvas.drawRect(x, ySpaceStart+500, x+200, y, pnt);
    pnt.setColor(Color.RED);
    canvas.drawCircle(x, y, 20f, pnt);
}

public void update(long width) {
    x -= speed;

    if(x+200 <= 0) {
        x = ((startX+200)/(index+1))*4 - 200;
        ySpaceStart = r.nextInt((int) (y-750-250+1)) + 250;
        scoreGiven = false;
    }
}

public boolean isColidingWith(PlayerCharacter mPlayer) {
    if(mPlayer.getRightX() >= x && mPlayer.getLeftX() <= x+20)
        if(mPlayer.getTopY() <= ySpaceStart || mPlayer.getBottomY() >= ySpaceStart+500)
            return true;

    return false;
}

public boolean isScore(PlayerCharacter mPlayer) {
    if(mPlayer.getRightX() >= x+100 && !scoreGiven) {
        scoreGiven = true;
        return true;
    }

    return false;
}
4b9b3361

Ответ 1

Обновление: Насколько это было возможно, он едва поцарапал поверхность. Более подробное описание теперь доступно. Совет игрового цикла приведен в Приложении A. Если вы действительно хотите понять, что происходит, начните с этого.

Оригинальный пост следует...


Я собираюсь начать с сводки капсул о том, как работает графический конвейер в Android. Вы можете найти более тщательные процедуры (например, некоторые хорошо подробные переговоры по вводу/выводу Google), поэтому я просто нажимаю на высокие очки. Это оказалось намного дольше, чем я ожидал, но я хотел немного написать немного об этом.

SurfaceFlinger

Ваше приложение не использует шаблон Framebuffer. Некоторые устройства даже не имеют Framebuffer. Ваше приложение имеет сторону "производителя" объекта BufferQueue. Когда он завершил рендеринг кадра, он вызывает unlockCanvasAndPost() или eglSwapBuffers(), который ставит в очередь заполненный буфер для отображения. (Технически рендеринг может даже не начаться, пока вы не скажете ему об обмене и можете продолжить, пока буфер перемещается по конвейеру, но это история в другое время.)

Буфер отправляется на "потребительскую" сторону очереди, которая в этом случае является SurfaceFlinger, системным компоновщиком поверхности. Буферы передаются с помощью ручки; содержимое не копируется. Каждый раз, когда начинается обновление дисплея (пусть его называют "VSYNC" ), SurfaceFlinger просматривает все различные очереди, чтобы узнать, какие буферы доступны. Если он находит новый контент, он задерживает следующий буфер из этой очереди. Если это не так, оно использует все, что было ранее.

Коллекция окон (или "слоев" ), которые имеют видимый контент, затем объединяются вместе. Это может быть сделано SurfaceFlinger (используя OpenGL ES для визуализации слоев в новый буфер) или через аппаратный композитор HAL. Аппаратный композитор (доступный на самых последних устройствах) предоставляется OEM-производителем оборудования и может предоставить несколько "оверлейных" самолетов. Если SurfaceFlinger имеет три окна для отображения, и HWC имеет три плоскости наложения, он помещает каждое окно в один оверлей и отображает композицию в качестве кадра. Буфер никогда не хранит все данные. Это, как правило, более эффективно, чем одно и то же в GLES. (Кстати, именно поэтому вы не можете захватить снимок экрана на самых последних устройствах, просто открыв запись дефрагментатора и пиксели чтения.)

Итак, как выглядит потребительская сторона. Вы можете полюбоваться ею на adb shell dumpsys SurfaceFlinger. Вернитесь к производителю (т.е. К вашему приложению).

производитель

Вы используете SurfaceView, который состоит из двух частей: прозрачный вид, который живет с пользовательским интерфейсом системы, и отдельный Surface-слой. Поверхность SurfaceView переходит непосредственно в SurfaceFlinger, поэтому она имеет гораздо меньше накладных расходов, чем другие подходы (например, TextureView).

BufferQueue для поверхности SurfaceView имеет тройную буферизацию. Это означает, что вы можете отобразить один буфер для отображения, один буфер, который сидит в SurfaceFlinger, ожидающий следующего VSYNC, и один буфер для вашего приложения для рисования. Наличие большего количества буферов улучшает пропускную способность и сглаживает удары, но увеличивает задержку между тем, когда вы касаетесь экрана и когда видите обновление. Добавление дополнительной буферизации целых кадров поверх этого обычно не принесет вам большой пользы.

Если вы рисуете быстрее, чем дисплей может отображать кадры, вы, в конечном счете, пополните очередь, и ваш вызов смены буфера (unlockCanvasAndPost()) остановится. Это простой способ сделать так, чтобы скорость обновления вашей игры была такой же, как и скорость отображения - как можно быстрее, и пусть система замедлит вас. Каждый кадр, вы продвигаете состояние в соответствии с тем, сколько времени прошло. (Я использовал этот подход в Android Breakout.) Это не совсем правильно, но при 60 кадрах в секунду вы не заметите недостатков. Вы получите тот же эффект при вызовах sleep(), если вы не спите достаточно долго - вы проснетесь только, чтобы ждать в очереди. В этом случае нет преимущества для сна, потому что спать в очереди одинаково эффективно.

Если вы рисуете медленнее, чем дисплей может отображать кадры, очередь в конечном итоге будет работать сухими, а SurfaceFlinger отобразит один и тот же фрейм на двух последовательных обновлениях дисплея. Это будет происходить периодически, если вы пытаетесь выполнить темп своей игры с помощью вызовов sleep(), и вы слишком долго спите. невозможночтобы точно соответствовать частоте обновления дисплея по теоретическим причинам (трудно реализовать PLL без механизма обратной связи) и практические причины (частота обновления может меняться со временем, например, я видел, что она варьируется от 58 кадров в секунду до 62 кадров в секунду на данном устройстве).

Использование sleep() вызовов в игровом цикле для продвижения анимации - плохая идея.

идет без сна

У вас есть несколько вариантов. Вы можете использовать "рисовать как можно быстрее до тех пор, пока не будет вызван обратный вызов" буфер-своп ", что и делает множество приложений на основе GLSurfaceView#onDraw() (знают ли они это или нет). Или вы можете использовать Choreographer.

Хореограф позволяет установить обратный вызов, который запускается на следующем VSYNC. Важно отметить, что аргументом обратного вызова является фактическое время VSYNC. Поэтому, даже если ваше приложение не сразу проснется, вы все равно получите точную картину, когда началось обновление дисплея. Это оказывается очень полезным при обновлении состояния игры.

Код, обновляющий состояние игры, никогда не должен быть предназначен для продвижения "одного кадра". Учитывая разнообразие устройств и разнообразие частот обновления, которые может использовать одно устройство, вы не можете знать, что такое "кадр". Ваша игра будет играть медленно или немного быстро - или если вам повезет, а кто-то попытается воспроизвести ее на телевизоре с блокировкой до 48 Гц по HDMI, вы будете серьезно вялыми. Вам нужно определить разницу во времени между предыдущим кадром и текущим фреймом и соответствующим образом продвинуть игровое состояние.

Это может потребовать немного перестановки в мышлении, но это того стоит.

Вы можете увидеть это в действии в Breakout, который продвигает положение мяча в зависимости от прошедшего времени. Он сокращает большие прыжки во времени на более мелкие кусочки, чтобы упростить обнаружение столкновения. Проблема с Breakout заключается в том, что при использовании метода "полная очередь", временные метки подвержены изменениям времени, необходимого SurfaceFlinger для выполнения работы. Кроме того, когда очередь буфера изначально пуста, вы можете быстро отправлять кадры. (Это означает, что вы вычисляете два кадра с дельтами с почти нулевым временем, но они все равно отправляются на дисплей со скоростью 60 кадров в секунду. На практике вы этого не видите, потому что разница в меток времени настолько мала, что она выглядит просто как один и тот же кадр дважды, и это происходит только при переходе от неанимации к анимации, чтобы вы не видели ничего заикающегося.)

С хореографом вы получаете фактическое время VSYNC, поэтому вы получаете хорошие регулярные часы, чтобы основывать ваши интервалы времени. Поскольку вы используете время обновления дисплея в качестве источника синхронизации, вы никогда не выходите из синхронизации с дисплеем.

Конечно, вы все равно должны быть готовы отказаться от кадров.

рамка не оставлена ​​

A назад я добавил демонстрационную запись на экран Grafika ( "Запись приложения GL" ), которая делает очень простую анимацию - просто плоский затушеванный прямоугольник и вращающийся треугольник. Он продвигает состояние и рисует, когда хореограф сигнализирует. Я закодировал его, запустил... и начал замечать обратные вызовы хореографа.

После того, как он копал в нем systrace, я обнаружил, что пользовательский интерфейс инфраструктуры иногда выполнял некоторые макеты (возможно, с кнопками и текст в слое пользовательского интерфейса, который находится поверх поверхности SurfaceView). Обычно это занимало 6 мс, но если бы я не двигался пальцем по экрану, мой Nexus 5 замедлял различные часы, чтобы снизить потребление энергии и улучшить время работы от батареи. Вместо этого ре-макет занял 28 мс. Имейте в виду, что кадр 60 кадров в секунду составляет 16,7 мс.

GL-рендеринг был почти мгновенным, но обновление хореографа доставлялось в поток пользовательского интерфейса, который искажался в макете, поэтому мой поток рендеринга не получал сигнал до намного позже. (Вы могли бы заставить Хореографа передать сигнал непосредственно в поток рендеринга, но там есть ошибка в хореографе, которая вызовет утечку памяти, если вы это сделаете.) Исправлено падение кадров, когда текущее время составляет более 15 мс после времени VSYNC. Приложение по-прежнему обновляет состояние - обнаружение столкновения настолько рудиментарно, что странные вещи случаются, если вы позволяете увеличению временного промежутка слишком велико - но он не передает буфер SurfaceFlinger.

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

Ключевые уроки:

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

Рисование

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

Два важных бита чтения: узнайте о аппаратно-ускоренном рендеринге и с помощью аппаратного масштабирования, чтобы уменьшить количество пикселей, которое нужно коснуться вашего приложения. "Аппаратный сканер-тренажер" в Grafika даст вам представление о том, что происходит, когда вы уменьшаете размер вашей поверхности чертежа - вы можете стать довольно маленькими до того, как эффекты заметны. (Мне показалось странно забавным наблюдать, как GL делает вращающийся треугольник на поверхности 100x64.)

Вы также можете взять некоторые тайны из рендеринга, используя OpenGL ES напрямую. Там немного изумление, как все работает, но Breakout (и, для более сложного примера, Replica Island) показать все, что вам нужно для простая игра.

Ответ 2

Без создания игры в Android я сделал 2D-игры в Java/AWT, используя Canvas и bufferStrategy...

Если вы испытываете мерцание, вы всегда можете перейти к ручному двойному буферизу (избавиться от мерцания) путем рендеринга на внеэкранное изображение, а затем просто перетащить страницы/рисовать изображение с помощью нового предварительно обработанного содержимого.

Но у меня возникает ощущение, что вас больше волнует "плавность" в вашей анимации, и в этом случае я бы рекомендовал вам расширить свой код с помощью интерполяции между различными тиками анимации;

В настоящее время ваше логическое состояние обновления цикла обновления (логически перемещается) в том же темпе, что и рендер, и измеряет с некоторым опорным временем и пытается отслеживать пройденное время.

Вместо этого вы должны обновляться в зависимости от того, какая частота вам кажется желательной для работы "логики" в вашем коде - обычно 10 или 25 Гц - это нормально (я называю это "тиканием обновления", который полностью отличается от фактического FPS), в то время как рендеринг выполняется путем хранения дорожки времени с высоким разрешением для измерения "сколько времени" занимает ваш фактический рендеринг (я использовал nanoTime, и этого было достаточно, а currentTimeInMillis - бесполезно...),

Таким образом, вы можете интерполировать между тиками и отображать как можно больше кадров до следующего тика, вычисляя мелкозернистые позиции в зависимости от того, сколько времени прошло с момента последнего тика, по сравнению с тем, сколько времени "нужно", быть между двумя тиками (так как вы всегда знаете, где вы находитесь - позиция и где вы находитесь - скорость)

Таким образом, вы получите ту же "скорость анимации", независимо от CPU/платформы, но более или менее гладкость, поскольку более быстрые процессоры будут выполнять больше операций между различными тиками.

ИЗМЕНИТЬ

Некоторая копия-вставка/концептуальный код - но обратите внимание, что это были AWT и J2SE, без Android. Однако, как концепция и некоторые Androidization, я уверен, что этот подход должен быть плавным, если исчисление, сделанное в вашей логике/обновлении, слишком тяжелое (например, алгоритмы N ^ 2 для обнаружения столкновений и N растут большими с системами частиц и т.п.).

Активный цикл рендеринга

Вместо того, чтобы полагаться на перерисовку, чтобы сделать картину для вас (что может занять различное время, в зависимости от того, что делает ОС), первым шагом является активное управление циклом рендеринга и использование BufferStrategy, где вы выполняете визуализацию, а затем активно "показывать" содержимое, когда вы закончите, прежде чем снова вернуться к нему.

Стратегия буфера

Возможно, вам понадобится специальный Android-материал, но он довольно прямолинейный. Я использую 2-страницы для bufferStrategy для создания механизма "перетаскивания страницы".

try
{
     EventQueue.invokeAndWait(new Runnable() {
        public void run()
        {
            canvas.createBufferStrategy(2);
        }
    });
}    
catch(Exception x)
{
    //BufferStrategy creation interrupted!        
}

Основной цикл анимации

Затем в вашем основном цикле получите стратегию и возьмите активный элемент управления (не используйте перерисовку)!

long previousTime = 0L;
long passedTime = 0L;

BufferStrategy strategy = canvas.getBufferStrategy();

while(...)
{
    Graphics2D bufferGraphics = (Graphics2D)strategy.getDrawGraphics();

    //Ensure that the bufferStrategy is there..., else abort loop!
    if(strategy.contentsLost())
        break;

    //Calc interpolation value as a double value in the range [0.0 ... 1.0] 
    double interpolation = (double)passedTime / (double)desiredInterval;

    //1:st -- interpolate all objects and let them calc new positions
    interpolateObjects(interpolation);

    //2:nd -- render all objects
    renderObjects(bufferGraphics);

    //Update knowledge of elapsed time
    long time = System.nanoTime();
    passedTime += time - previousTime;
    previousTime = time;

    //Let others work for a while...
    Thread.yield();

    strategy.show();
    bufferGraphics.dispose();

    //Is it time for an animation update?
    if(passedTime > desiredInterval)
    {
        //Update all objects with new "real" positions, collision detection, etc... 
        animateObjects();

        //Consume slack...
        for(; passedTime > desiredInterval; passedTime -= desiredInterval);
    }
}

Управляемый объект должен быть указанным выше основным циклом, тогда он будет выглядеть примерно так:

public abstract class GfxObject
{
    //Where you were
    private GfxPoint oldCurrentPosition;

    //Current position (where you are right now, logically)
    protected GfxPoint currentPosition;

    //Last known interpolated postion (
    private GfxPoint interpolatedPosition;

    //You're heading somewhere?
    protected GfxPoint velocity;

    //Gravity might affect as well...?
    protected GfxPoint gravity;

    public GfxObject(...)
    {
        ...
    }

    public GfxPoint getInterpolatedPosition()
    {
        return this.interpolatedPosition;
    }

    //Time to move the object, taking velocity and gravity into consideration
    public void moveObject()
    {
        velocity.add(gravity);
        oldCurrentPosition.set(currentPosition);
        currentPosition.add(velocity);
    }

    //Abstract method forcing subclasses to define their own actual appearance, using "getInterpolatedPosition" to get the object current position for rendering smoothly...
    public abstract void renderObject(Graphics2D graphics, ...);

    public void animateObject()
    {
        //Well, move as default -- subclasses can then extend this behavior and add collision detection etc depending on need
        moveObject();
    }

    public void interpolatePosition(double interpolation)
    {
        interpolatedPosition.set(
                                 (currentPosition.x - oldCurrentPosition.x) * interpolation + oldCurrentPosition.x,
                                 (currentPosition.y - oldCurrentPosition.y) * interpolation + oldCurrentPosition.y);
    }
}

Все 2D-позиции управляются с использованием служебного класса GfxPoint с двойной точностью (поскольку интерполированные движения могут быть очень точными, и округление обычно не требуется до отображения фактической графики). Чтобы упростить необходимый математический материал и сделать код более читаемым, я также добавил различные методы.

public class GfxPoint
{
    public double x;
    public double y;

    public GfxPoint()
    {
        x = 0.0;
        y = 0.0;
    }

    public GfxPoint(double init_x, double init_y)
    {
        x = init_x;
        y = init_y;
    }

    public void add(GfxPoint p)
    {
        x += p.x;
        y += p.y;
    }

    public void add(double x_inc, double y_inc)
    {
        x += x_inc;
        y += y_inc;
    }

    public void sub(GfxPoint p)
    {
        x -= p.x;
        y -= p.y;
    }

    public void sub(double x_dec, double y_dec)
    {
        x -= x_dec;
        y -= y_dec;
    }

    public void set(GfxPoint p)
    {
        x = p.x;
        y = p.y;
    }

    public void set(double x_new, double y_new)
    {
        x = x_new;
        y = y_new;
    }

    public void mult(GfxPoint p)
    {
        x *= p.x;
        y *= p.y;
    }



    public void mult(double x_mult, double y_mult)
    {
        x *= x_mult;
        y *= y_mult;
    }

    public void mult(double factor)
    {
        x *= factor;
        y *= factor;
    }

    public void reset()
    {
        x = 0.0D;
        y = 0.0D;
    }

    public double length()
    {
        double quadDistance = x * x + y * y;

        if(quadDistance != 0.0D)
            return Math.sqrt(quadDistance);
        else
            return 0.0D;
    }

    public double scalarProduct(GfxPoint p)
    {
        return scalarProduct(p.x, p.y);
    }

    public double scalarProduct(double x_comp, double y_comp)
    {
        return x * x_comp + y * y_comp;
    }

    public static double crossProduct(GfxPoint p1, GfxPoint p2, GfxPoint p3)
    {
        return (p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y);
    }

    public double getAngle()
    {
        double angle = 0.0D;

        if(x > 0.0D)
            angle = Math.atan(y / x);
        else if(x < 0.0D)
            angle = Math.PI + Math.atan(y / x);
        else if(y > 0.0D)
            angle = Math.PI / 2;
        else
            angle = - Math.PI / 2;

        if(angle < 0.0D)
            angle += 2 * Math.PI;
        if(angle > 2 * Math.PI)
            angle -= 2 * Math.PI;

        return angle;
    }
}

Ответ 3

Попробуйте этот вариант для размера. Вы заметите, что вы только синхронизируете и блокируете холст в течение самого короткого периода времени. В противном случае ОС будет либо A) Бросьте буфер, потому что вы слишком медленны, либо B) не обновляетесь вообще до тех пор, пока не закончится ваш спящий режим.

public class MainThread extends Thread
{
    public static final String TAG = MainThread.class.getSimpleName();
    private final static int    MAX_FPS = 60;   // desired fps
    private final static int    MAX_FRAME_SKIPS = 5;    // maximum number of frames to be skipped
    private final static int    FRAME_PERIOD = 1000 / MAX_FPS;  // the frame period

    private boolean running;


    public void setRunning(boolean running) {
        this.running = running;
    }

    private SurfaceHolder mSurfaceHolder;
    private MainGameBoard mMainGameBoard;

    public MainThread(SurfaceHolder surfaceHolder, MainGameBoard gameBoard) {
        super();
        mSurfaceHolder = surfaceHolder;
        mMainGameBoard = gameBoard;
    }

    @Override
    public void run()
    {
        Log.d(TAG, "Starting game loop");
        long beginTime;     // the time when the cycle begun
        long timeDiff;      // the time it took for the cycle to execute
        int sleepTime;      // ms to sleep (<0 if we're behind)
        int framesSkipped;  // number of frames being skipped 
        sleepTime = 0;

        while(running)
        {
            beginTime = System.currentTimeMillis();
            framesSkipped = 0;
            synchronized(mSurfaceHolder){
                Canvas canvas = null;
                try{
                    canvas = mSurfaceHolder.lockCanvas();
                    mMainGameBoard.update();
                    mMainGameBoard.render(canvas);
                }
                finally{
                    if(canvas != null){
                        mSurfaceHolder.unlockCanvasAndPost(canvas);
                    }
                }
            }
            timeDiff = System.currentTimeMillis() - beginTime;
            sleepTime = (int)(FRAME_PERIOD - timeDiff);
            if(sleepTime > 0){
                try{
                    Thread.sleep(sleepTime);
                }
                catch(InterruptedException e){
                    //
                }
            }
            while(sleepTime < 0 && framesSkipped < MAX_FRAME_SKIPS) {
                // catch up - update w/o render
                mMainGameBoard.update();
                sleepTime += FRAME_PERIOD;
                framesSkipped++;
            }
        }
    }
}

Ответ 4

Прежде всего, Canvas может работать плохо, поэтому не ожидайте слишком многого. Вы можете попробовать пример lunarlander из SDK и посмотреть, какую производительность вы получаете на своем оборудовании.

Попробуйте снизить макс fps до примерно 30, цель должна быть гладкой не быстро.

 private final static int    MAX_FPS = 30;   // desired fps

Также избавиться от вызовов сна, рендеринг на холст, вероятно, будет достаточно поспать. Попробуйте что-то еще:

        synchronized (mSurfaceHolder) {
            beginTime = System.currentTimeMillis();
            framesSkipped = 0;

            timeDiff = System.currentTimeMillis() - beginTime;

            sleepTime = (int) (FRAME_PERIOD - timeDiff);

            if(sleepTime <= 0) {

                this.mMainGameBoard.update();

                this.mMainGameBoard.render(mCanvas);

            }
        }

Если вы хотите, вы можете сделать свой this.mMainGameBoard.update() чаще, чем ваш рендер.

Edit: Также, поскольку вы говорите, что все становится медленным, когда появляются препятствия. Попробуйте использовать их на экране Canvas/Bitmap. Я слышал, что некоторые из методов drawSHAPE оптимизированы на основе ЦП, и вы получите более высокую производительность, вытягивая их в автономное canvas/bitmap, поскольку они не ускоряются с аппаратным /gpu.

Edit2: Что возвращает Canvas.isHardwareAccelerated() для вас?

Ответ 5

Одной из наиболее распространенных причин замедления и заикания в игре является графический конвейер. Логика игры намного быстрее обрабатывается, чем рисовать (в общем), поэтому вы хотите удостовериться, что вы делаете все наиболее эффективным способом. Ниже вы можете найти несколько советов о том, как достичь этого.

несколько советов, чтобы сделать его лучше

https://www.yoyogames.com/tech_blog/30