Постоянная скорость игры не зависит от переменной FPS в OpenGL с GLUT?

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

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

Во-первых, благодаря моему поисковому опыту, есть пара человек, которые, вероятно, обладают знаниями, чтобы помочь с этим, но не знают, что такое GLUT, и я собираюсь попытаться объяснить (не стесняйтесь меня поправлять) соответствующие функции для Моя проблема этого набора инструментов OpenGL. Пропустите этот раздел, если вы знаете, что такое GLUT и как с ним играть.

GLUT Инструментарий:

  • GLUT - это инструментарий OpenGL, который помогает выполнять общие задачи в OpenGL.
  • glutDisplayFunc(renderScene) принимает указатель на renderScene() функция обратного вызова, которая будет отвечать за рендеринг всего. renderScene() Функция будет вызываться только один раз после регистрации обратного вызова.
  • glutTimerFunc(TIMER_MILLISECONDS, processAnimationTimer, 0) принимает количество миллисекунд, переданных перед обратным вызовом processAnimationTimer(), Последний аргумент - это просто значение для передачи в обратный вызов таймера. processAnimationTimer() не будет называться каждый TIMER_MILLISECONDS но только один раз.
  • glutPostRedisplay() Функция запрашивает GLUT для рендеринга нового кадра, поэтому мы должны вызывать его каждый раз, когда что-то меняем в сцене.
  • glutIdleFunc(renderScene) может быть использован для регистрации обратного вызова renderScene() (это не делает glutDisplayFunc() не имеет значения) но этой функции следует избегать, потому что обратный вызов в режиме ожидания постоянно вызывается, когда события не принимаются, увеличивая нагрузку на процессор.
  • glutGet(GLUT_ELAPSED_TIME) функция возвращает количество миллисекунд с glutInit был вызван (или первый звонок glutGet(GLUT_ELAPSED_TIME)). Это таймер у нас с GLUT. Я знаю, что есть лучшие альтернативы для таймеров с высоким разрешением, но давайте пока остановимся на этом.

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

Текущая реализация:

Теперь я не уверен, что правильно реализовал второе решение, предложенное Koen, " Скорость игры зависит от переменной FPS". Соответствующий код для этого выглядит так:

#define TICKS_PER_SECOND 30
#define MOVEMENT_SPEED 2.0f

const int TIMER_MILLISECONDS = 1000 / TICKS_PER_SECOND;

int previousTime;
int currentTime;
int elapsedTime;

void renderScene(void) {
    (...)

    // Setup the camera position and looking point
    SceneCamera.LookAt();

    // Do all drawing below...

    (...)
}

void processAnimationTimer(int value) {
    // setups the timer to be called again
    glutTimerFunc(TIMER_MILLISECONDS, processAnimationTimer, 0);

    // Get the time when the previous frame was rendered
    previousTime = currentTime;

    // Get the current time (in milliseconds) and calculate the elapsed time
    currentTime = glutGet(GLUT_ELAPSED_TIME);
    elapsedTime = currentTime - previousTime;

    /* Multiply the camera direction vector by constant speed then by the
       elapsed time (in seconds) and then move the camera */
    SceneCamera.Move(cameraDirection * MOVEMENT_SPEED * (elapsedTime / 1000.0f));

    // Requests to render a new frame (this will call my renderScene() once)
    glutPostRedisplay();
}

void main(int argc, char **argv) {
    glutInit(&argc, argv);

    (...)

    glutDisplayFunc(renderScene);

    (...)

    // Setup the timer to be called one first time
    glutTimerFunc(TIMER_MILLISECONDS, processAnimationTimer, 0);
    // Read the current time since glutInit was called
    currentTime = glutGet(GLUT_ELAPSED_TIME);

    glutMainLoop();
}

Эта реализация не удалась. Это работает в том смысле, что скорость игры постоянно зависит от FPS. Таким образом, перемещение из точки A в точку B занимает одно и то же время, независимо от высокой / низкой частоты кадров. Тем не менее, я считаю, что я ограничиваю частоту кадров при таком подходе. [РЕДАКТИРОВАТЬ: каждый кадр будет отображаться только при вызове обратного вызова времени, это означает, что частота кадров будет примерно TICKS_PER_SECOND кадров в секунду. Это не правильно, вы не должны ограничивать свое мощное оборудование, это неправильно. Я понимаю, что мне все еще нужно вычислить elapsedTime, Просто потому, что я говорю GLUT, чтобы каждый раз вызывал таймер TIMER_MILLISECONDS, это не значит, что он всегда будет делать это вовремя.]

Я не уверен, как я могу это исправить, и, если честно, я понятия не имею, что такое игровой цикл в GLUT, вы знаете, while( game_is_running ) цикл в статье Коэна. [РЕДАКТИРОВАТЬ: Я понимаю, что GLUT управляется событиями и что цикл игры начинается, когда я звоню glutMainLoop() (который никогда не возвращается), да?]

Я думал, что смогу зарегистрировать обратный вызов в режиме ожидания glutIdleFunc() и использовать это как замену glutTimerFunc(), только рендеринг, когда это необходимо (а не все время, как обычно), но когда я проверял это с пустым обратным вызовом (например, void gameLoop() {}) и он в основном ничего не делал, только черный экран, процессор ускорился до 25% и оставался там до тех пор, пока я не убил игру, и она не пришла в норму. Так что я не думаю, что это путь для подражания.

С помощью glutTimerFunc() определенно не очень хороший подход для выполнения всех движений / анимаций, основанных на этом, так как я ограничиваю свою игру постоянным FPS, а не крутой. Или, может быть, я использую это неправильно, и моя реализация не так?

Как именно я могу иметь постоянную скорость игры с переменным FPS? Точнее, как правильно реализовать решение Koen Constant Game Speed ​​с максимальным FPS (четвертое в его статье) с GLUT? Может быть, это невозможно вообще с GLUT? Если нет, каковы мои альтернативы? Каков наилучший подход к этой проблеме (постоянная скорость игры) с GLUT?

[РЕДАКТИРОВАТЬ] Другой подход:

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

Мой код теперь превратился в это:

int previousTime;
int currentTime;
int elapsedTime;

void renderScene(void) {
    (...)

    // Setup the camera position and looking point
    SceneCamera.LookAt();

    // Do all drawing below...

    (...)
}

void renderScene(void) {
    (...)

    // Get the time when the previous frame was rendered
    previousTime = currentTime;

    // Get the current time (in milliseconds) and calculate the elapsed time
    currentTime = glutGet(GLUT_ELAPSED_TIME);
    elapsedTime = currentTime - previousTime;

    /* Multiply the camera direction vector by constant speed then by the
       elapsed time (in seconds) and then move the camera */
    SceneCamera.Move(cameraDirection * MOVEMENT_SPEED * (elapsedTime / 1000.0f));

    // Setup the camera position and looking point
    SceneCamera.LookAt();

    // All drawing code goes inside this function
    drawCompleteScene();

    glutSwapBuffers();

    /* Redraw the frame ONLY if the user is moving the camera
       (similar code will be needed to redraw the frame for other events) */
    if(!IsTupleEmpty(cameraDirection)) {
        glutPostRedisplay();
    }
}

void main(int argc, char **argv) {
    glutInit(&argc, argv);

    (...)

    glutDisplayFunc(renderScene);

    (...)

    currentTime = glutGet(GLUT_ELAPSED_TIME);

    glutMainLoop();
}

Вывод, это работает, или так кажется. Если я не перемещаю камеру, загрузка процессора низкая, ничего не отображается (для целей тестирования у меня есть только сетка, растягивающаяся на 4000.0f, в то время как zFar установлен на 1000.0f). Когда я начинаю двигать камеру, сцена начинает перерисовываться. Если я продолжу нажимать клавиши перемещения, загрузка процессора увеличится; это нормальное поведение. Когда я перестаю двигаться, он падает обратно.

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

Обратите внимание, что я просто делаю это ради забавы, я не собираюсь создавать какую-то игру для распространения или что-то в этом роде, по крайней мере, в ближайшем будущем. Если бы я это сделал, я бы, вероятно, пошел с чем-то еще, кроме GLUT. Но поскольку я использую GLUT, а не проблему, описанную в iDevGames, вы считаете, что этой последней реализации достаточно для GLUT? Единственная реальная проблема, о которой я могу думать сейчас, это то, что мне нужно будет продолжать звонить glutPostRedisplay() каждый раз, когда сцена меняет что-то и продолжает называть это, пока нет ничего нового для перерисовки. Я думаю, что в код добавлена ​​небольшая сложность для лучшего дела.

Как вы думаете?

3 ответа

Решение

Переизбыток разработан, чтобы быть игровым циклом. Когда вы вызываете glutMainLoop(), он выполняет цикл for без условия завершения, кроме сигнала exit(). Вы можете реализовать свою программу так, как вы делаете сейчас, но вам нужно внести небольшие изменения. Во-первых, если вы хотите узнать, что такое FPS, вы должны поместить это отслеживание в функцию renderScene(), а не в функцию обновления. Естественно, ваша функция обновления вызывается так быстро, как указано таймером, и вы рассматриваете elapsedTime как меру времени между кадрами. В общем, это будет верно, потому что вы вызываете glutPostRedisplay довольно медленно, и glut не будет пытаться обновить экран, если в этом нет необходимости (нет необходимости перерисовывать, если сцена не изменилась). Тем не менее, существуют другие случаи, когда renderScene будет вызываться. Например, если вы перетащите что-нибудь через окно. Если вы это сделаете, вы увидите более высокий FPS (если вы правильно отслеживали FPS в функции рендеринга).

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

Отпустите кнопку мыши во время перетаскивания, и вы можете "выбросить" объект, преобразованный этой матрицей.

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

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

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

int xSt, ySt, xCr, yCr, msM = 0, msOld = 0;
bool dragging = false, spin = false, moving = false;
glm::mat4 mouseRot(1.0f), continRot(1.0f);
float twoOvHght; // Set in reshape()
glm::mat4 mouseRotate(bool slow) {
    glm::vec3 axis(twoOvHght * (yCr - ySt), twoOvHght * (xCr - xSt), 0); // Perpendicular to mouse motion
    float len = glm::length(axis);
    if (slow) { // Slow rotation; divide angle by mouse-delay in milliseconds; it is multiplied by frame delay to speed it up later
        int msP = msM - msOld;
        len /= (msP != 0 ? msP : 1);
    }
    if (len != 0) axis = glm::normalize(axis); else axis = glm::vec3(0.0f, 0.0f, 1.0f);
    return rotate(axis, cosf(len), sinf(len));
}
void mouseMotion(int x, int y) {
    moving = (xCr != x) | (yCr != y);
    if (dragging & moving) {
        xSt = xCr; xCr = x; ySt = yCr; yCr = y; msOld = msM; msM = glutGet(GLUT_ELAPSED_TIME);
        mouseRot = mouseRotate(false) * mouseRot;
    }
}
void mouseButton(int button, int state, int x, int y) {
    if (button == 0) {
        if (state == 0) {
            dragging = true; moving = false; spin = false;
            xCr = x; yCr = y; msM = glutGet(GLUT_ELAPSED_TIME);
            glutPostRedisplay();
        } else {
            dragging = false; spin = moving;
            if (spin) continRot = mouseRotate(true);
        }
    }
}

А потом позже...

bool fxFPS = false;
int T = 0, ms = 0;
const int fDel = 20;
void display() {
    ms = glutGet(GLUT_ELAPSED_TIME);
    if (T <= ms) { T = ms + fDel;
        for (int lp = 0; lp < fDel; lp++) {
            orient = rotY * orient; orientCu = rotX * rotY * orientCu; // Auto-rotate two orientation quaternions
            if (spin) mouseRot = continRot * mouseRot; // Track rotation from thowing action by mouse
        }
        orient1 = glm::mat4_cast(orient); orient2 = glm::mat4_cast(orientCu);
    }
    // Top secret animation code that will make me rich goes here
    glutSwapBuffers();
    if (spin | dragging) { if (fxFPS) while (glutGet(GLUT_ELAPSED_TIME) < T); glutPostRedisplay(); } // Fast, repeated updates of the screen
}

Наслаждайтесь метаниями вокруг оси; Я считаю, что большинство людей делают. Обратите внимание, что fps никак не влияет ни на интерфейс, ни на рендеринг. Я минимизировал использование делений, поэтому сравнения должны быть хорошими и точными, и любая неточность в часах не накапливается без необходимости.

Синхронизация многопользовательских игр - это еще 18 разговоров, я бы сказал.

Вы могли бы использовать glutIdleFunc, который вызывается непрерывно, когда это возможно - аналогично while(game_is_running) петля. То есть, какую бы логику вы бы не использовали в этом while цикл, вы можете поместить в обратный вызов для glutIdleFunc, Вы можете избежать использования glutTimerFunc отслеживая галочки самостоятельно, как в статье, которую вы связали (используя glutGet(GLUT_ELAPSED_TIME)).

Другие вопросы по тегам