Tutorial de SDL – Parte 44 – Movimento independente do frame

Continuando nossa série de artigos traduzidos do site lazyfoo, veremos agora como lidar com frame rates instáveis ou suportar múltiplos frame rates. Seja quando você quiser lidar com frame rates instáveis ou dar suporte para múltiplos frame rates, você pode configurar o seu movimento no tempo independente do fame rate.

//The dot that will move around on the screen
class Dot
{
    public:
        //The dimensions of the dot
        static const int DOT_WIDTH = 20;
        static const int DOT_HEIGHT = 20;
        //Maximum axis velocity of the dot
        static const int DOT_VEL = 640;
        //Initializes the variables
        Dot();
        //Takes key presses and adjusts the dot's velocity
        void handleEvent( SDL_Event& e );
        //Moves the dot
        void move( float timeStep );
        //Shows the dot on the screen
        void render();
    private:
        float mPosX, mPosY;
        float mVelX, mVelY;
};

A classe dot retorna adaptada para movimento independente do frame. Observe como a velocidade agora é 640. Do jeito que fizemos a velocidade por frame anteriormente faria com que esse objeto voasse pela tela em um único frame. Nesse exemplo, iremos nos basear no tempo e a unidade padrão de tempo será 1 segundo. 640 pixels por segundo se traduz em um pouco mais do que 10 pixels por frame em uma aplicação que roda à 60 frame por segundo. Para que possamos move o objeto baseado no tempo do frame, a função que gera o movimento precisa saber quanto tempo se passa por cada frame. Por isso essa função agora recebe como parametro um passo de tempo, que é quanto tempo se passou desde a ultima atualização. Também oberve como a posição e a velocidade são do tipo float ao invés de inteiros. Se usarmos inteiros, o movimento seria sempre truncado para o inteiro mais próximo, o que causaria grandes discrepâncias de valores.

void Dot::move( float timeStep )
{
    //Move the dot left or right
    mPosX += mVelX * timeStep;
    //If the dot went too far to the left or right
    if( mPosX < 0 ) { mPosX = 0; } else if( mPosX > SCREEN_WIDTH - DOT_WIDTH )
    {
        mPosX = SCREEN_WIDTH - DOT_WIDTH;
    }
    //Move the dot up or down
    mPosY += mVelY * timeStep;
    //If the dot went too far up or down
    if( mPosY < 0 ) { mPosY = 0; } else if( mPosY > SCREEN_HEIGHT - DOT_HEIGHT )
    {
        mPosY = SCREEN_HEIGHT - DOT_HEIGHT;
    }
}

Aqui temos a função que gera o movimento alterada para movimento baseado no tempo ao invés de movimento baseado no frame. Atualizamos a posição movendo o obejto por velocidade * tempo. Digamos que temos (para simplificar) uma velocidade de 600 * 1/60 pixels ou 10 pixels. Por causa do movimento não uniforme não podemos mover o objeto para trás quando ele vai para fora da tela, temos que corrigir o valor para um valor que esteja dentro da tela.

void Dot::render()
{
    //Show the dot
    gDotTexture.render( (int)mPosX, (int)mPosY );
}

Para evitar que o compilador dispare alguma mensagem de erro, convertemos as posições para inteiros quando renderizamos o ponto.

            //Main loop flag
            bool quit = false;
            //Event handler
            SDL_Event e;
            //The dot that will be moving around on the screen
            Dot dot;
            //Keeps track of time between steps
            LTimer stepTimer;

Para esse exemplo, desativamos vsync para mostrar que ele pode ser executado não importando o frame rate. Para podermos saber quando tempo se passou entre as renderizações, precisamos de um timer para manter um registro dos passos do tempo.

            //While application is running
            while( !quit )
            {
                //Handle events on queue
                while( SDL_PollEvent( &e ) != 0 )
                {
                    //User requests quit
                    if( e.type == SDL_QUIT )
                    {
                        quit = true;
                    }
                    //Handle input for the dot
                    dot.handleEvent( e );
                }
                //Calculate time step
                float timeStep = stepTimer.getTicks() / 1000.f;
                //Move for time step
                dot.move( timeStep );
                //Restart step timer
                stepTimer.start();
                //Clear screen
                SDL_SetRenderDrawColor( gRenderer, 0xFF, 0xFF, 0xFF, 0xFF );
                SDL_RenderClear( gRenderer );
                //Render dot
                dot.render();
                //Update screen
                SDL_RenderPresent( gRenderer );
            }

Quando movemos o ponto. primeiro obtemos o tempo do passo do tempo de forma que possamos saber quando tempo se passou desde a ultima vez que movemos o objeto. Depois que paramos de mover o objeto, reiniciamos o timer para que possamos saber quanto tempo se passou quando voltarmos a mover o objeto. Finalmente, renderizamos o objeto como fazemos normalmente. Na maioria dos artigos dessa série, as coisas são simplificadas para tornar tudo mais fácil de ser entendido. Para a maioria, se não todas, das aplicações usamos movimento baseado no tempo ao invés de baseado no frame. Mesmo quanto temos um frame rate fixo. só usamos um passo de tempo fixo. O negócio é que quando usamos movimento baseado no tempo. encontramos problemas com erros de ponto flutuante que requerem matemática vetorial para serem corrigidos, e esse assunto vai além do escopo desse artigo, e é por isso que movimento baseado no frame é usado na maioria dos outros artigos. Baixe os arquivo de mídia e de código fonte desse artigo aqui.