Вопрос по thread-sleep, game-loop, opengl-es, android – Android-игра цикл против обновления в потоке рендеринга

19

я делаю игру для Android и в настоящее время не получаю производительность, которую ямне нравится У меня есть игровой цикл в своем собственном потоке, который обновляет объектс позиции. Поток рендеринга будет проходить через эти объекты и рисовать их. Текущее поведение - это то, что кажется прерывистым / неравномерным движением. Что я не могу объяснить, так это то, что до того, как я поместил логику обновления в ее собственный поток, я поместил ее в метод onDrawFrame, прямо перед вызовом gl. В этом случае анимация была совершенно плавной, она становится прерывистой / неровной, особенно когда я пытаюсь регулировать цикл обновления через Thread.sleep. Даже когда я позволяю потоку обновлений сходить с ума (без сна), анимация плавная, только когда задействован Thread.sleep, это влияет на качество анимации.

Мы создали скелетный проект, чтобы посмотреть, смогу ли я воссоздать проблему, ниже представлены цикл обновления и метод onDrawFrame в рендерере:Обновить цикл

    @Override
public void run() 
{
    while(gameOn) 
    {
        long currentRun = SystemClock.uptimeMillis();
        if(lastRun == 0)
        {
            lastRun = currentRun - 16;
        }
        long delta = currentRun - lastRun;
        lastRun = currentRun;

        posY += moveY*delta/20.0;

        GlobalObjects.ypos = posY;

        long rightNow = SystemClock.uptimeMillis();
        if(rightNow - currentRun < 16)
        {
            try {
                Thread.sleep(16 - (rightNow - currentRun));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

А вот мойonDrawFrame Метод: Я

        @Override
public void onDrawFrame(GL10 gl) {
    gl.glClearColor(1f, 1f, 0, 0);
    gl.glClear(GL10.GL_COLOR_BUFFER_BIT |
            GL10.GL_DEPTH_BUFFER_BIT);

    gl.glLoadIdentity();

    gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
    gl.glTranslatef(transX, GlobalObjects.ypos, transZ);
    //gl.glRotatef(45, 0, 0, 1);
    //gl.glColor4f(0, 1, 0, 0);

    gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
    gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

    gl.glVertexPointer(3,  GL10.GL_FLOAT, 0, vertexBuffer);
    gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, uvBuffer);

    gl.glDrawElements(GL10.GL_TRIANGLES, drawOrder.length,
              GL10.GL_UNSIGNED_SHORT, indiceBuffer);

    gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
    gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
}

просмотрел реплику островаисточник и онвыполняет свою логику обновления в отдельном потоке, а также регулирует ее с помощью Thread.sleep, но его игра выглядит очень гладкой. Есть ли у кого-нибудь идеи или кто-то испытал то, что яя описываю?

--- РЕДАКТИРОВАТЬ: 1/25/13 --- I '

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

Основная идея заключается в том, чтобы сохранять шаблон обновления, рисовать ... обновлять, рисовать ..., сохраняя при этом разницу времени относительно одинаковой (часто вне вашего контроля). Моим первым шагом было синхронизировать мой рендерер таким образом, чтобы он рисовал только после того, как получил уведомление о том, что ему разрешено это делать. Это выглядит примерно так:

public void onDrawFrame(GL10 gl10) {
        synchronized(drawLock)
    {
        while(!GlobalGameObjects.getInstance().isUpdateHappened())
        {
            try
            {
                Log.d("test1", "draw locking");
                drawLock.wait();
            } 
            catch (InterruptedException e) 
            {
                e.printStackTrace();
            }
        }
    }

Когда я заканчиваю свою логику обновления, я вызываю drawLock.notify (), освобождая поток рендеринга, чтобы нарисовать то, что я только что обновил. Цель этого - помочь установить шаблон обновления, нарисовать ... обновить, нарисовать ... и т. Д.

Как только я это реализовал, все стало намного плавнее, хотя я все еще испытывал случайные скачки в движении. После некоторого тестирования я увидел, что между вызовами ondrawFrame происходит несколько обновлений. Это приводило к тому, что один кадр показывал результат двух (или более) обновлений, больший скачок, чем обычно.

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

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

Наконец, я решил переписать свой двигатель с учетом двух вышеупомянутых идей, а не исправлять двигатель, который я изначально сделал. Я сделал некоторые оптимизации для синхронизации потоков, которые, возможно, кто-то может прокомментировать.

Мои текущие темы взаимодействуют так:

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

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

Рендеринг темы ждет, пока не получит уведомление отобновить ветку что произошло обновление.

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

Это может быть немного запутанным, но что этоЭто позволяет использовать преимущества многопоточности, заключающиеся в том, что он может выполнять обновление для кадра n, в то время как кадр n-1 рисует, и в то же время предотвращать множественные итерации обновления на кадр, если визуализация занимает много времени. Для дальнейшего объяснения этот сценарий множественного обновления обрабатывается блокировкой потока обновления, если он обнаруживает, что буфер lastDrawn равен только что обновленному. Если они равны, это указывает потоку обновления, что предыдущий кадр еще не нарисован.

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

Спасибо

Ваш Ответ

2   ответа
1

что Thread.sleep () не является точным. Попробуйте выяснить, каково реальное время сна.

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

РЕДАКТИРОВАТЬ: В качестве примера, вот как PlayN делает это:

@Override
public void run() {
  // The thread can be stopped between runs.
  if (!running.get())
    return;

  int now = time();
  float delta = now - lastTime;
  if (delta > MAX_DELTA)
    delta = MAX_DELTA;
  lastTime = now;

  if (updateRate == 0) {
    platform.update(delta);
    accum = 0;
  } else {
    accum += delta;
    while (accum >= updateRate) {
      platform.update(updateRate);
      accum -= updateRate;
    }
  }

  platform.graphics().paint(platform.game, (updateRate == 0) ? 0 : accum / updateRate);

  if (LOG_FPS) {
    totalTime += delta / 1000;
    framesPainted++;
    if (totalTime > 1) {
      log().info("FPS: " + framesPainted / totalTime);
      totalTime = framesPainted = 0;
    }
  }
}
Я предполагаю, что первый достаточно хороший и самый простой, поэтому он будет наиболее используемым. Смотрите отредактированный пример, поскольку PlayN использует его. Blackhex
Кроме того, вы можете даже сохранить все несколько предыдущих состояний обновления и их время отключения (не менее 3) и задержать цикл рендеринга по сравнению с циклом обновления на два кадра, чтобы получить полностью плавную анимацию, но это может быть сложно реализовать и потреблять память. Blackhex
этот подход кажется логичным. как бы вы получили время между кадрами? используя System.currentTimeMillis () и сохраняя последний раз, когда он обновлялся для каждой анимации? android developer
Теперь, насколько я понимаю, проблема в том, что при логике обновления в другом потоке состояние объекта при запуске средства визуализации не гарантируется как обновленное до этой миллисекунды. Обновление могло произойти за 1 мс до розыгрыша или около 15 мс. В следующем кадре, учитывая любое отклонение от Thread.sleep или просто скачок в любом потоке, время между обновлением и отрисовкой может отличаться. Есть еще идеи по этому поводу? user1578101
17

(Ответ от Blackhex поднял некоторые интересные моменты, но я могувпихнуть все это в комментарий.

Наличие двух потоков, работающих асинхронно, обязательно приведет к таким проблемам. Посмотрите на это так: событие, которое управляет анимацией, является аппаратнымVSync» сигнал, то есть точка, в которой поверхностный композитор Android предоставляет новый экран, полный данных для оборудования дисплея. Вы хотите иметь новый фрейм данных всякий раз, когда приходит vsync. Если вы неНовые данные отсутствуют, игра выглядит нестабильной. Если вы сгенерировали 3 кадра данных за этот период, два будут проигнорированы, и вы 'просто тратить время автономной работы.

(Заполнение процессора может также привести к нагреву устройства, что может привести к тепловому дросселированию, что замедляет работу всей системы ... и может привести к прерыванию анимации.

Самый простой способ синхронизироваться с дисплеем - выполнить все обновления состояния вonDrawFrame(, Если иногда для обновления вашего состояния и его рендеринга требуется больше одного кадра, то вы 'будет выглядеть плохо, и нужно изменить ваш подход. Просто перенести все обновления состояния игры на второе ядроне поможет так много, как хотелось бы - если ядро № 1 - это поток рендерера, а ядро № 2 - поток обновления состояния игры, то ядро № 1 будет бездействовать, а ядро № 2 обновит состояние после того, как какое ядро # 1 возобновит выполнение фактического рендеринга, в то время как ядро # 2 бездействует, и оно 'Это займет столько же времени. Чтобы реально увеличить объем вычислений, которые вы можете выполнить за кадр,Мне нужно, чтобы два (или более ядра работали одновременно, что поднимает некоторые интересные проблемы синхронизации в зависимости от того, как вы определяете свое разделение труда (см.http://developer.android.com/training/articles/smp.html если вы хотите пойти по этому пути.

Попытка использоватьThread.sleep( управлять частотой кадров вообще плохо кончается. Вы можете'Не знаю, как долго длится период между vsync или как долго наступает следующий. Это's отличается для каждого устройства, и на некоторых устройствах оно может быть переменным. По сути, вы получаете два такта - vsync и sleep - которые бьют друг друга, и в результате получается прерывистая анимация. Более того,Thread.sleep( Безразлично»не дает никаких конкретных гарантий относительно точности или минимальной продолжительности сна.

У меня нетна самом деле прошел через источники на острове Реплика, но вGameRenderer.onDrawFrame( вы можете видеть взаимодействие между их потоком состояния игры (который создает список объектов для рисования и потоком визуализации GL (который просто рисует список. В их модели состояние игры обновляется только по мере необходимости, и, если ничего не изменилось, оно просто перерисовывает предыдущий список розыгрышей. Эта модель хорошо работает для событийно-ориентированной игры, то есть когда содержимое на экране обновляется, когда что-то происходит (вы нажимаете клавишу, срабатывает таймер и т. Д.. Когда происходит событие, они могут выполнить минимальное обновление состояния и соответствующим образом настроить список отрисовки.

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

Для Android Breakout (http://code.google.com/p/android-breakout/ , в игре есть мяч, подпрыгивающий в непрерывном движении. Там мы хотим обновлять наше состояние так часто, как нам позволяет отображение, поэтому мы отключаем изменение состояния с помощью vsync, используя временную дельту из предыдущего кадра, чтобы определить, насколько далеко продвинулись события. Вычисление за кадр мало, и рендеринг довольно прост для современного устройства GL, так что все это легко вписывается в 1/60 секунды. Если дисплей обновляется намного быстрее (240 Гц, мы можем иногда пропустить кадры (опять же, вряд ли это заметят, и мыЯ буду сжигать в 4 раза больше ресурсов процессора при обновлении фреймов (что очень печально.

Если по какой-либо причине в одной из этих игр пропущена синхронизация, игрок может заметить или не заметить. Состояние продвигается к истекшему времени, а не к заранее установленному понятию фиксированной продолжительности »Рамка"так, например, мяч будет перемещаться на 1 единицу на каждом из двух последовательных кадров или на 2 единицы на один кадр. В зависимости от частоты кадров и чувствительности дисплея это может быть невидимым. (Это ключевой вопрос дизайна, который может запутаться в вашей голове, если вы представляете состояние игры с точки зрения «тики».

Оба из них являются действительными подходами. Ключ должен нарисовать текущее состояние всякий раз, когдаonDrawFrame и обновлять состояние как можно реже.

Примечание для всех, кто случайно прочитал это:т использоватьSystem.currentTimeMillis(, Пример в вопросе используетсяSystemClock.uptimeMillis(, который основан на монотонных часах, а не на настенных часах. Это илиSystem.nanoTime(Есть лучший выбор. (Я'м на небольшой крестовый поход противcurrentTimeMillis, который на мобильном устройстве может внезапно прыгнуть вперед или назад.

Обновить: Я написалеще дольше отвечу на похожий вопрос.

Обновление 2: Я написалеще дольше отвечу Об общей проблеме (см. Приложение А.

После того, как я это реализовал, я все еще заметил скачки в своем движении. То, что происходило здесь, было двумя обновлениями между розыгрышами. Это по сути сдвинуло мяч вдвое дальше, чем в других кадрах. Я воспользовался подходом для ограничения разницы во времени между двумя рендерами и поместил дополнительное время в остаток, который затем распределялся на последующие итерации обновления. Это значительно сгладило ситуацию. Недостатком является то, что мяч технически ускоряется, чтобы наверстать упущенное. Но это'вряд ли заметно и неперевешивает гладкость. Это типичный подход? user1578101
Я думаю, что это было бы так, да, но из того, что яЯ вижу, обновление только на основе дельты времени приводит к прерывистым результатам. Ограничение большой дельты времени, а затем ускорение объекта в течение следующих нескольких кадров, чтобы компенсировать потерянное время, выглядит намного более плавным. Если бы был какой-то способ лучше регулировать вызовы onDrawFrame, мне кажется, достаточно просто основать его на дельте времени, но я неЯ еще не понял, как это сделать. user1578101
Спасибо за вдумчивый ответ. Я'У меня было несколько недель с тех пор, как я написал оригинальный пост, чтобы поиграть и глубже вникнуть в проблему. Я'мы обнаружили, что синхронизация рендерера с рисованием только после обновления означает, что он будет ждать (как это происходит в реплике), пока поток обновления не сообщит, что это имело огромное значение. user1578101
Re: выше + обновления к вопросу. Помните, что вы можетезаранее не знаю, какова задержка между перерисовками экрана - на некоторых устройствах она может даже измениться в середине игры - поэтому попытка думать с точки зрения временных рамок фиксированной длины может привести к проблемам (пропущенные кадры или двойные -шаг по рамкам). Тем не менее, частота кадров должна быть достаточно постоянной в краткосрочной перспективе, поэтому вы можете основывать сумму, которую вы продвигаете для следующего кадра, по истекшему времени между вызовами onDrawFrame; со временем все должно выглядеть гладко. (Это то, что делает Breakout.) Я fadden

Похожие вопросы