Problem z gameloopem i renderowaniem animacji

Problem z gameloopem i renderowaniem animacji
Kokoniłaj
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 191
0

Cześć. Próbuję zaimplementować game loop z artykułu "Fix Your Timestep!"

Nie jestem zadowolony z efektu. Podczas niektórych uruchomień animacja postaci zaczyna się jakoś dziwnie animować. Czy widzicie tu jakiś błąd? Jak podchodzicie do debugowania takich problemów? Na windowsie można próbować powtórzyć ten problem poprzez trzymanie paska aplikacji wtedy rl.GetFrameTime() zwróci dość sporą wartość, ale czasem trafiam w ten stan bez ingerencji w frame time.

Kopiuj
//main.odin
for !rl.WindowShouldClose() {
    frameTime: f32 = rl.GetFrameTime()
    
    if frameTime > maxFrameTime {
        frameTime = maxFrameTime
    }
    
    accumulator += frameTime
                                                                                        
    updateInputs(&inputs)
                                                                                        
    for accumulator >= FIXED_DT {
        updatePlayerInput(&gameState, &inputs)
        updateEntities(&gameState, FIXED_DT)
        accumulator -= FIXED_DT
    }
                                                                                        
    blendFactor: f32 = accumulator / FIXED_DT
                                                                                        
    clearRenderFrame(&renderFrame)
    drawEntitiesToFrame(&renderFrame, &gameState, blendFactor)
    
    // interpCameraPos := interpolateVector2(
    //     gameState.oldCameraPos, 
    //     gameState.cameraPos, 
    //     blendFactor
    // )
    
    camera.target.x = math.round_f32(gameState.cameraPos.x)
    camera.target.y = math.round_f32(gameState.cameraPos.y)
                                                                                        
    rl.BeginDrawing()
    rl.ClearBackground(rl.GRAY)
                                                                                        
    rl.BeginMode2D(camera)
    drawTilemap(&tilemap, camera)
    flushRenderFrame(&renderFrame)
    rl.EndMode2D()
                                                                                        
    str := fmt.tprintf("%v\nvelocity: %v", gameState.hero.pos, gameState.hero.velocity)
    text_cstr := strings.clone_to_cstring(str)
    defer delete(text_cstr)
    rl.DrawText(text_cstr, 0, 0, 18, rl.BLACK)
                                                                                        
    rl.EndDrawing()
}

//game_state.odin
drawEntitiesToFrame :: proc(rf: ^RenderFrame, state: ^GameState, blendFactor: f32) {
    if state.hero != nil {
        drawEntityToFrame(rf, state.hero.entity, blendFactor)
    }
    
    for i in 0..<state.entityCount {
        drawEntityToFrame(rf, state.enemies[i], blendFactor)
    }
}

//entity.odin
drawEntityToFrame :: proc(rf: ^RenderFrame, e: ^Entity, blendFactor: f32) {
    if !e.active do return
    
    // Interpolate position for smooth rendering
    interpPos := rl.Vector2{
        e.oldPos.x + (e.pos.x - e.oldPos.x) * blendFactor,
        e.oldPos.y + (e.pos.y - e.oldPos.y) * blendFactor,
    }


    renderPos := interpPos

    // renderPos := rl.Vector2{
    //     math.round(interpPos.x),
    //     math.round(interpPos.y),
    // }
    
    drawSpritePlayerToFrame(rf, &e.spritePlayer, renderPos, e.z)

    collisionRect := rl.Rectangle{
        x = e.pos.x,
        y = e.pos.y,
        width = e.size.x,
        height = e.size.y,
    }

    pushDebugRectangle(rf, collisionRect)
}

Projekt można łatwo uruchomić, potrzebujemy kompilator odina.
Budowa projektu: odin build .
Budowa i uruchomienie odin run .

https://github.com/Krztk/shooter-a

several
  • Rejestracja: dni
  • Ostatnio: dni
2

No ja bym spróbował najpierw zrobić to z palca zamiast polegać na frameTime: f32 = rl.GetFrameTime() który z jakiegoś powodu zwraca float? Sprawdzałeś pod debuggerem czy wszystkie zmienne i stałe czasowe mają tą samą rozdzielczość? Nie wiem też co robi updatePlayerInput ale w tej pętli "nadganiającej" wykonujesz tylko symulację, wejście aplikujesz w głównej na początku lub na końcu.

Coś w ten deseń:

Kopiuj
const int tickRate = 120;
int ptime = clock(); // time since last frame
int offset = 0;      // time since last physics tick

MyState state;

while (state.active) {
   
    int delta = deltaTime(ptime, offset) + offset;
    ptime = clock();

    for (int i = 0; i < delta / (1000 / tickRate); i++) {
        simulationTick(state);
    }

    offset = delta % tickRate;

    render(state);

    auto input = getInput();
    applyInput(input, state);
}

(edit)
Albo prościej wg. https://www.gameprogrammingpatterns.com/game-loop.html

Kopiuj
double previous = getCurrentTime();
double lag = 0.0;
while (true)
{
  double current = getCurrentTime();
  double elapsed = current - previous;
  previous = current;
  lag += elapsed;

  processInput();

  while (lag >= MS_PER_UPDATE)
  {
    update();
    lag -= MS_PER_UPDATE;
  }

  render();
}
flowCRANE
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: Tuchów
  • Postów: 12271
2

Trzeba też zauważyć, że w przypadku lagu i konieczności zaktualizowania logiki kilka razy w ramach jednej iteracji głównej pętli, input nie powinien być przetwarzany tylko raz. Można powiedzieć, że robienie snapshotów inputu jest obecnie standardem w branży. Oznacza to, że dane inputu dostarczane w zdarzeniach wpisuje się do obiektów reprezentujących urządzenia wejścia, natomiast w trakcie aktualizacji logiki, dane inputu czyta się z tych obiektów, a nie bezpośrednio ze zdarzeń.

W danej iteracji głównej pętli, obliczasz ile razy trzeba zaktualizować logikę gry (na podstawie tego ile czasu minęło od poprzedniej aktualizacji), a następnie dzielisz zdarzenia na tyle pakietów, ile kroków trzeba wykonać. W ten sposób, gdy kroków trzeba wykonać kilka, gra tworzy kilka snapshotów inputu, po jednym dla każdego kroku. Przy czym ostatni krok zawsze przetwarza wszystkie zdarzenia z kolejki, nawet jeśli wykraczają poza okno czasowe tego kroku — w ten sposób likwiduje się dodatkowy input lag.

Ogólnie tematyka głównej pętli jest cholernie skomplikowana i zależna od wielu rzeczy, dlatego warto jest skupić się na implementacji tylko tego, czego faktycznie potrzebujemy, dostosowując rozwiązanie do konkretnej gry, zamiast silić się na uniwersalizm (którego i tak nie da się osiągnąć).

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.