Раздражающие лаги / заикания в игре на андроид
Я только начал с разработки игр для Android, и я работаю над супер простой игрой.
Игра в основном похожа на летучую птичку.
Мне удалось заставить все работать, но я получаю много заиканий и лагов.
Телефон, который я использую для тестирования, - LG G2, поэтому он должен запускать игры, намного тяжелее и сложнее, чем этот.
В основном есть 4 "препятствия", которые имеют полную ширину экрана друг от друга.
Когда игра начинается, препятствия начинают двигаться (навстречу персонажу) с постоянной скоростью. Значение x игрового персонажа является неизменным на протяжении всей игры, в то время как его значение y изменяется.
Отставание происходит в основном, когда персонаж проходит через препятствие (а иногда и немного позже этого препятствия). Что происходит, так это то, что на каждом рисунке состояния игры наблюдаются неравномерные задержки, вызывающие заикания в движениях.
- GC не работает по логу.
- Заикание НЕ вызвано слишком высокой скоростью (я знаю это, потому что в начале игры, когда препятствия не видны, персонаж движется плавно)
- Я не думаю, что проблема также связана с FPS, потому что, даже когда поле MAX_FPS установлено в 100, все еще есть заикания.
Я думаю, что есть строка или несколько строк кода, которые вызывают некоторую задержку (и, таким образом, пропускаются кадры). Я также думаю, что эти строки должны быть вокруг update()
а также draw()
методы PlayerCharacter
, Obstacle
, а также MainGameBoard
,
Проблема в том, что я все еще плохо знаком с разработкой Android и разработкой игр для Android, поэтому я понятия не имею, что может вызвать такую задержку.
Я пытался искать ответы в Интернете... К сожалению, все, что я нашел, указывало на то, что виноват GC. Однако, я не верю, что это так (поправьте меня, если я ошибаюсь), эти ответы не относятся ко мне. Я также читаю Android разработчика Performance Tips
страницы, но не смог найти ничего, что помогло.
Поэтому, пожалуйста, помогите мне найти ответ на решение этих раздражающих лагов!
Некоторый код
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;
}
5 ответов
Обновление: как бы подробно это ни было, оно едва поцарапало поверхность. Более подробное объяснение теперь доступно. Совет по игровому циклу приведен в Приложении А. Если вы действительно хотите понять, что происходит, начните с этого.
Оригинальный пост следует...
Я собираюсь начать с краткой сводки о том, как работает графический конвейер в Android. Вы можете найти более подробное описание (например, некоторые подробные доклады о вводе / выводе в Google), так что я просто поражаюсь самыми высокими моментами. Это оказалось намного дольше, чем я ожидал, но я давно хотел написать кое-что из этого.
SurfaceFlinger
Ваше приложение не использует Framebuffer. Некоторые устройства даже не имеют Framebuffer. Ваше приложение содержит сторону "производителя" BufferQueue
объект. Когда он завершил рендеринг кадра, он вызывает unlockCanvasAndPost()
или же eglSwapBuffers()
, который ставит в очередь заполненный буфер для отображения. (Технически рендеринг может даже не начаться, пока вы не скажете ему поменяться местами, и может продолжаться, пока буфер движется по конвейеру, но это история для другого времени.)
Буфер отправляется на "потребительскую" сторону очереди, которой в данном случае является SurfaceFlinger, композитор поверхности системы. Буферы передаются ручкой; содержимое не копируется. Каждый раз, когда начинается обновление дисплея (назовем его "VSYNC"), SurfaceFlinger просматривает все различные очереди, чтобы увидеть, какие буферы доступны. Если он находит новое содержимое, он фиксирует следующий буфер из этой очереди. Если это не так, он использует все, что получил ранее.
Коллекция окон (или "слоев"), которые имеют видимый контент, затем объединяются вместе. Это может быть сделано SurfaceFlinger (используя OpenGL ES для рендеринга слоев в новый буфер) или через HAL Hardware Composer. Аппаратный компоновщик (доступный на большинстве последних устройств) предоставляется OEM-производителем оборудования и может предоставлять несколько "накладываемых" плоскостей. Если SurfaceFlinger имеет три окна для отображения, а HWC имеет три доступные плоскости наложения, он помещает каждое окно в одно наложение и создает композицию в виде отображаемого кадра. Там никогда не будет буфера, который содержит все данные. Как правило, это более эффективно, чем делать то же самое в GLES. (Между прочим, именно поэтому вы не можете сделать снимок экрана на большинстве последних устройств, просто открыв запись dev кадрового буфера и считывая пиксели.)
Вот так выглядит потребительская сторона. Вы можете полюбоваться этим для себя с adb shell dumpsys SurfaceFlinger
, Давайте вернемся к производителю (то есть к вашему приложению).
производитель
Вы используете SurfaceView
, которая состоит из двух частей: прозрачного представления, которое взаимодействует с пользовательским интерфейсом системы, и отдельного отдельного поверхностного слоя. SurfaceView
Поверхность идет непосредственно к SurfaceFlinger, поэтому она имеет гораздо меньше накладных расходов, чем другие подходы (например, TextureView
).
BufferQueue для SurfaceView
Поверхность имеет тройную буферизацию. Это означает, что у вас может быть один буфер, сканируемый для отображения, один буфер, который находится на SurfaceFlinger в ожидании следующего VSYNC, и один буфер для вашего приложения для рисования. Наличие большего количества буферов улучшает пропускную способность и сглаживает неровности, но увеличивает задержку между прикосновением к экрану и обновлением. Добавление дополнительной буферизации целых кадров поверх этого, как правило, не принесет вам большой пользы.
Если вы рисуете быстрее, чем дисплей может рендерить кадры, вы в конечном итоге будете заполнять очередь, и ваш вызов замены буфера (unlockCanvasAndPost()
) остановится. Это простой способ сделать частоту обновления вашей игры такой же, как и скорость отображения - рисуйте так быстро, как только можете, и пусть система замедлит вас. Каждый кадр, вы продвигаете состояние в зависимости от того, сколько времени прошло. (Я использовал этот подход в Android Breakout.) Это не совсем правильно, но при 60 кадрах в секунду вы не заметите недостатков. Вы получите тот же эффект с sleep()
звонки, если вы не спите достаточно долго - вы проснетесь только для ожидания в очереди. В этом случае нет никакого преимущества для сна, потому что сон в очереди одинаково эффективен.
Если вы рисуете медленнее, чем дисплей может визуализировать кадры, очередь в конечном итоге станет сухой, и SurfaceFlinger отобразит один и тот же кадр при двух последовательных обновлениях дисплея. Это будет периодически происходить, если вы пытаетесь sleep()
звонит и спишь слишком долго. Невозможно точно подобрать частоту обновления дисплея по теоретическим причинам (трудно реализовать PLL без механизма обратной связи) и по практическим причинам (частота обновления может изменяться со временем, например, я видел, что она изменяется от 58 кадр / с до 62 кадр / с на данное устройство).
С помощью sleep()
вызовы в игровом цикле для ускорения анимации - плохая идея.
идти без сна
У вас есть пара вариантов. Вы можете использовать подход "рисовать так быстро, как только можете, пока не произойдет замена буфера при резервном копировании", на этом основано множество приложений. GLSurfaceView#onDraw()
делать (знают ли они это или нет). Или вы можете использовать хореограф.
Хореограф позволяет установить обратный вызов, который срабатывает при следующем VSYNC. Важно, что аргументом для обратного вызова является фактическое время VSYNC. Таким образом, даже если ваше приложение не сразу просыпается, у вас все равно есть точное представление о том, когда началось обновление дисплея. Это оказывается очень полезным при обновлении состояния игры.
Код, который обновляет игровое состояние, никогда не должен быть разработан так, чтобы продвигать "один кадр". Учитывая разнообразие устройств и разную частоту обновления, которую может использовать одно устройство, вы не можете знать, что такое "кадр". Ваша игра будет играть немного медленнее или немного быстрее - или если вам повезет, и кто-то попытается сыграть в нее на телевизоре, настроенном на частоту 48 Гц через HDMI, вы будете серьезно вялым. Вам необходимо определить разницу во времени между предыдущим и текущим кадрами и соответствующим образом улучшить состояние игры.
Это может потребовать небольшого умственного перестановки, но оно того стоит.
Вы можете увидеть это в действии в Breakout, который продвигает положение мяча на основе прошедшего времени. Он разрезает большие прыжки во времени на более мелкие части, чтобы упростить обнаружение столкновений. Проблема с Breakout заключается в том, что он использует подход "заполнение очереди", временные метки могут изменяться во времени, необходимом для работы SurfaceFlinger. Кроме того, когда очередь буфера изначально пуста, вы можете отправлять кадры очень быстро. (Это означает, что вы вычисляете два кадра с почти нулевой разницей во времени, но они все равно отправляются на дисплей со скоростью 60 кадров в секунду. На практике вы этого не видите, потому что разница в отметках времени настолько мала, что выглядит просто как один и тот же кадр рисуется дважды, и это происходит только при переходе от неанимации к анимации, поэтому вы не видите ничего заикающегося.)
С помощью Choreographer вы получаете фактическое время VSYNC, поэтому у вас есть хорошие регулярные часы, чтобы основывать свои временные интервалы. Поскольку вы используете время обновления дисплея в качестве источника часов, вы никогда не будете синхронизированы с дисплеем.
Конечно, вы все равно должны быть готовы к тому, чтобы сбросить кадры.
кадр не остался позади
Некоторое время назад я добавил демо-запись записи экрана в Grafika ("Приложение Record GL"), которая выполняет очень простую анимацию - просто прыгающий прямоугольник с плоскими тенями и вращающийся треугольник. Это продвигает состояние и рисует, когда хореограф сигнализирует. Я закодировал это, запустил это... и начал замечать обратные вызовы Хореографа, поддерживающие.
Покопавшись в этом с помощью systrace, я обнаружил, что интерфейс фреймворка иногда выполняет некоторую работу с макетом (возможно, для кнопок и текста в слое пользовательского интерфейса, который расположен поверх SurfaceView
поверхность). Обычно это занимало 6 мс, но если я не активно двигал пальцем по экрану, мой Nexus 5 замедлял различные часы, чтобы снизить энергопотребление и увеличить время автономной работы. Перепланировка заняла 28 мс. Имейте в виду, что кадр 60fps составляет 16,7 мс.
Рендеринг GL был почти мгновенным, но обновление Choreographer доставлялось в поток пользовательского интерфейса, который занимался компоновкой, поэтому мой поток визуализации не получил сигнал намного позже. (Вы можете сделать так, чтобы Choreographer доставлял сигнал непосредственно в поток рендерера, но в Choreographer есть ошибка, которая может вызвать утечку памяти, если вы это сделаете.) Исправление было в том, чтобы отбрасывать кадры, когда текущее время превышает 15 мс после времени VSYNC. Приложение по-прежнему выполняет обновление состояния - обнаружение столкновений настолько элементарно, что странные вещи случаются, если вы позволяете увеличить промежуток времени, - но оно не передает буфер в SurfaceFlinger.
Во время работы приложения вы можете сказать, когда кадры сбрасываются, потому что Графика мигает красной рамкой и обновляет счетчик на экране. Вы не можете сказать, наблюдая за анимацией. Поскольку обновления состояния основаны на временных интервалах, а не на количестве кадров, все движется так же быстро, как если бы кадр был сброшен или нет, и на скорости 60 кадров в секунду вы не заметите ни одного пропущенного кадра. (В некоторой степени зависит от ваших глаз, игры и характеристик оборудования дисплея.)
Ключевые уроки:
- Падение кадров может быть вызвано внешними факторами - зависимостью от другого потока, тактовой частотой процессора, фоновой синхронизацией Gmail и т. Д.
- Вы не можете избежать всех падений кадра.
- Если вы правильно настроите цикл рисования, никто не заметит.
Рисование
Рендеринг на Canvas может быть очень эффективным, если он аппаратно ускорен. Если это не так, и вы рисуете в программном обеспечении, это может занять некоторое время - особенно если вы касаетесь большого количества пикселей.
Два важных момента чтения: узнайте об аппаратно-ускоренном рендеринге и используйте аппаратный масштабер, чтобы уменьшить количество пикселей, к которым должно приложиться ваше приложение. "Тренажер аппаратного масштабирования" в Grafika даст вам представление о том, что происходит, когда вы уменьшаете размер поверхности рисования - вы можете стать довольно маленькими, прежде чем эффекты станут заметными. (Мне странно забавно наблюдать, как GL визуализирует вращающийся треугольник на поверхности размером 100x64.)
Вы также можете убрать некоторые тайны из рендеринга, используя OpenGL ES напрямую. Есть некоторое затруднение, изучающее, как все работает, но Breakout (и, для более сложного примера, Replica Island) показывают все, что вам нужно для простой игры.
Даже не создав игры для Android, я создал 2D-игры на Java/AWT, используя Canvas и bufferStrategy...
Если вы испытываете мерцание, вы всегда можете воспользоваться двойным буфером вручную (избавиться от мерцания), выполнив рендеринг в закадровое изображение, а затем просто перелистать страницу / drawImage с новым предварительно визуализированным содержимым напрямую.
Но у меня возникает ощущение, что вас больше беспокоит "плавность" в вашей анимации, и в этом случае я бы порекомендовал вам расширить свой код с интерполяцией между различными тактами анимации;
В настоящее время ваше обновление цикла рендеринга логическое состояние (переместить вещи логически) в том же темпе, как вы оказываете, и измерить с некоторым эталонным временем и пытаются следить за прошедшее время.
Вместо этого вы должны обновляться с той частотой, которая, по вашему мнению, желательна для работы "логики" в вашем коде - обычно 10 или 25 Гц - это нормально (я называю это "обновлениями", что полностью отличается от фактического FPS), в то время как рендеринг выполняется путем отслеживания времени с высоким разрешением для измерения того, "сколько времени" занимает фактический рендеринг (я использовал nanoTime, и этого было вполне достаточно, тогда как currentTimeInMillis довольно бесполезен...),
Таким образом, вы можете интерполировать тики и визуализировать как можно больше кадров до следующего тика, рассчитывая детализированные позиции на основе того, сколько времени прошло с момента последнего тика, по сравнению с тем, сколько времени "должно" быть между двумя тики (так как вы всегда знаете, где вы находитесь - положение и куда вы движетесь - скорость)
Таким образом, вы получите одинаковую "скорость анимации" независимо от ЦП / платформы, но более или менее плавную, поскольку более быстрые ЦП будут выполнять больше рендеров между различными тактами.
РЕДАКТИРОВАТЬ
Некоторая копия-вставка / концептуальный код - но учтите, что это были AWT и J2SE, а не Android. Тем не менее, как концепция и с некоторой Androidification, я уверен, что этот подход должен воспроизводиться гладко, если исчисление, выполненное в вашей логике / обновлении, не является слишком тяжелым (например, 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's 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;
}
}
Попробуйте это для размера. Вы заметите, что синхронизируете и блокируете только холст на кратчайший период времени. В противном случае операционная система будет либо 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++;
}
}
}
}
Во-первых, Canvas может работать плохо, поэтому не ожидайте слишком многого. Возможно, вы захотите попробовать пример lunarlander из SDK и посмотреть, какую производительность вы получаете на своем оборудовании.
Попробуйте понизить максимальную частоту кадров до 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()
чаще, чем ваш рендер.
Изменить: Кроме того, так как вы говорите, что дела замедляются, когда появляются препятствия. Попробуйте нарисовать их на внеэкранном Canvas / Bitmap. Я слышал, что некоторые методы drawSHAPE оптимизированы для ЦП, и вы получите лучшую производительность, отрисовывая их в автономном холст / растровое изображение, потому что они не ускорены аппаратно / GPU.
Edit2: Что Canvas.isHardwareAccelerated() возвращает для вас?
Одной из наиболее распространенных причин замедления и заикания в игре является графический конвейер. Логика игры обрабатывается намного быстрее, чем рисование (в общем), поэтому вы хотите убедиться, что вы рисуете все максимально эффективным способом. Ниже вы можете найти несколько советов о том, как этого добиться.
какое-то предложение, чтобы сделать это лучше