Timestep e loop de jogo de renderização corrigidos

Oct 07 2020

Então, eu escrevi este cronograma fixo e loop de renderização do jogo. Estou postando aqui para ver se há maneiras de otimizá-lo / torná-lo mais preciso.


    const float updatesRate = 1 / 60.0f;
    const float framesRate = 1 / 308.0f;

    double currentTime = hireTimeInSeconds( );
    double accumulator = 0.0;
    double accumulator2 = 0.0;
    uint32_t frames = 0, updates = 0;
    double timer = hireTimeInSeconds( );

    while ( true ) {

        double newTime = hireTimeInSeconds( );

        if ( newTime - timer >= 1 ) {

            //Printing fps and ups here

            updates = 0;
            frames = 0;
            timer = newTime;
        }

        double frameTime = newTime - currentTime;
        if ( frameTime > 0.25 ) frameTime = 0.25;
        currentTime = newTime;
        accumulator += frameTime;
        accumulator2 += frameTime;

        //Polling input here

        while ( accumulator >= updatesRate ) {

            //Updating here
            accumulator -= updatesRate;
            updates++;
        }

        const double alpha = accumulator / updatesRate;

        while ( accumulator2 >= framesRate ) {
            //Rendering
            frames++;
            accumulator2 -= framesRate;
        }
    }

``` 

Respostas

4 user673679 Oct 07 2020 at 16:21
const float updatesRate = 1 / 60.0f;
const float framesRate = 1 / 308.0f;

Os outros valores de tempo são todos doubles, é um pouco estranho usar floats aqui.


    if ( newTime - timer >= 1 ) {
    ...
    if ( frameTime > 0.25 ) frameTime = 0.25;

Esses números mágicos devem ser nomeados constantes.


C ++ agora fornece uma variedade de utilitários relacionados ao tempo por meio do <chrono>cabeçalho. Devemos usar os tipos de relógio, tipos de duração de tipo seguro e conversões que ele fornece.


double accumulator = 0.0;
double accumulator2 = 0.0;
uint32_t frames = 0, updates = 0;
double timer = hireTimeInSeconds( );

A nomenclatura dessas variáveis ​​deixa sua finalidade muito obscura. Talvez pudéssemos definir uma Accumulatorclasse para agrupar as variáveis ​​para cada acumulador e remover a duplicação (veja o código de exemplo abaixo).


hireTimeInSecondsProvavelmente deveria ser highResTimeInSeconds.


    while ( accumulator2 >= framesRate ) {
        //Rendering
        frames++;
        accumulator2 -= framesRate;
    }

Eu não acho que isso faça sentido. Queremos apenas renderizar um quadro, mesmo que muito tempo tenha passado. (Não estamos fazendo nenhuma atualização entre os quadros de renderização neste loop, então apenas renderizaríamos exatamente a mesma coisa repetidamente).


Aplicando as sugestões acima, podemos obter algo como o código abaixo. São algumas linhas de código a mais, mas também são muito mais reutilizáveis ​​(especialmente as classes accumulatore frame_timer) e, com sorte, são mais claras em sua intenção:

#include <chrono>
#include <iostream>

template<class c_t>
class accumulator
{
public:

    using clock_t = c_t;
    using duration_t = typename clock_t::duration;

    template<class d_t>
    explicit accumulator(d_t tick_length):
        m_tick_length(std::chrono::duration_cast<duration_t>(tick_length)) { }

    std::size_t accumulate(duration_t delta_time) // note: returns the number of ticks of m_tick_length triggered by this call
    {
        m_accumulated_time += delta_time;

        auto ticks = std::size_t{ 0 };

        while (m_accumulated_time >= m_tick_length)
        {
            ++ticks;
            m_accumulated_time -= m_tick_length;
        }

        return ticks;
    }

    duration_t get_tick_length() const
    {
        return m_tick_length;
    }

private:

    duration_t m_tick_length;
    duration_t m_accumulated_time;
};

template<class c_t>
class frame_timer
{
public:

    using clock_t = c_t;
    using duration_t = typename clock_t::duration;
    using time_point_t = typename clock_t::time_point;

    template<class d_t>
    explicit frame_timer(d_t initial_last_frame_time):
        m_last_frame_time(std::chrono::duration_cast<duration_t>(initial_last_frame_time)),
        m_last_tick(clock_t::now()) { }
    
    void tick()
    {
        auto now = clock_t::now();
        m_last_frame_time = now - m_last_tick;
        m_last_tick = now;
    }

    duration_t get_last_frame_time() const
    {
        return m_last_frame_time;
    }

private:

    duration_t m_last_frame_time;
    time_point_t m_last_tick;
};

int main()
{
    using clock_t = std::chrono::high_resolution_clock;
    using duration_t = clock_t::duration;
    using time_point_t = clock_t::time_point;
    using seconds_t = std::chrono::duration<float>;

    auto const update_time = seconds_t(1.f / 60.f);
    auto update_accumulator = accumulator<clock_t>(update_time);

    auto const render_time = seconds_t(1.f / 308.f);
    auto render_accumulator = accumulator<clock_t>(render_time);

    auto const print_time = seconds_t(1.f);
    auto print_accumulator = accumulator<clock_t>(print_time);

    auto const initial_frame_time = update_time;
    auto timer = frame_timer<clock_t>(initial_frame_time);

    auto const max_frame_time = std::chrono::duration_cast<duration_t>(seconds_t(0.25f));

    while (true)
    {
        auto const last_frame_time = (timer.get_last_frame_time() > max_frame_time) ? max_frame_time : timer.get_last_frame_time();

        if (print_accumulator.accumulate(last_frame_time) != 0) // note: will skip update periods if frames are super long (> print_time)
        {
            // ... print frames
            std::cout << "ping!" << std::endl;
        }

        auto const update_ticks = update_accumulator.accumulate(last_frame_time); // note: don't put this directly in the loop condition, we only want to call it once!

        for (auto i = std::size_t{ 0 }; i != update_ticks; ++i)
        {
            // ... do update
            std::cout << "update" << std::endl;
        }

        if (render_accumulator.accumulate(last_frame_time) != 0) // note: only ever render 1 frame, even if we should have rendered more
        {
            // ... render
            std::cout << "render" << std::endl;
        }

        timer.tick();
    }
}