Game loop / physics step question

Background:
So I’m building a physics game with Codea where two players take turns and there is a turn-tracking server in the cloud. When one player moves something I want to be able to replay the same move on the other player’s app. This means I need to have perfectly replay-able physics.

Question:
So, thinking this through, I just need to make sure the framerate (calls to the draw() function) doesn’t make a difference when doing things like adding bodies or applying forces to the Box2D running in the background (since Box2D itself is supposed to be perfectly predictable, right?). So my question is… when is Box2D’s physics step occurring? Can a variable number of steps occur between draw() calls… or is it always:

  1. draw
  2. physics step
  3. draw
  4. physics step … ?

Any help is appreciated!

  • Mike

I dont know the details of box2d, but you cannot rely on 2 ipads having the same frame rate. And from time to time there is a small delay that will vary from ipad to ipad. So relying on box2d to have identical scenes is probably expecting too much.

Hmm, yeah the framerate would definitely vary… but I believe box2d is supposed to be pretty exact… like if I put a ball in a room, apply an exact force once, and replay that scene over and over… the ball should land in the exact same spot everytime, no matter what the framerate is, right?

If that’s true, it’s only a question of when is the framerate making some difference… like it’s common advice to not change a scene in a collision callback - you should save some marker to do the change in your next game loop… my concern is the physics scene could progress in a varying number of steps before the next game loop. So if I knew there was exactly one step between game loop calls, I’d be okay.

Does this make sense?

  • Mike

Neztec, I tried to accomplish this with my game splinerider but it just seems now that box2d wants to give different results every time, I managed to get it to being deterministic before but not now, I’ll have another play with it and get back to you.

How about one main iPad, and another secondary iPad? The main could run all the calculations and handle Box2D, and frequently sync all the positions and force and rotations etc. to the cloud, and the secondary could run its own Box2D calculations so it’s smooth movement, but also retrieves the data from the cloud and updates positions and force and rotations etc. frequently. Might require two projects though, one with the code to upload to the cloud, and one with the code to download from the cloud…

I think what you’re going to have to do is store enough telemetry about the move in order to perfectly reproduce it at the other end.

For example, take a game like Peggle. You’d have to store the starting condition of the board (which pegs are still alive) and the angle and force with which the ball is fired. Save the ball’s velocity and position periodically and store each collision with another object.

The key is to store enough data to tween the moving object reliably without having to store every single frame. 5-10 updates per second should be enough for a turn-based game.

@Luatee, weird it’s working fine for me. Try this code atleast for three loops, I get the same results even if the FPS drops from 60 to 12.


supportedOrientations(PORTRAIT_ANY)

function setup()
    --clearLocalData()
    font("Courier")
    loop=0
    tab1={}
    tab2={}
    restart=true
    fps = 60
    parameter.watch("fps")
    NumberOfCollisions = 5
    print("Number Of Collisions Before Check  = 5")
    print("change this value in setup if you wish too.")
    print("clear LocalData if you change the value")
end

function setup2()
    tab2={}
    count=0
    loop=loop+1
    print("loop: "..loop)
    if b1 then
        b1:destroy()
        b2:destroy()
        e1:destroy()
        e2:destroy()
        e3:destroy()
        e4:destroy()
    end
    WIDTH = 768
    HEIGHT = 1024
    e1=physics.body(EDGE,vec2(0,0),vec2(0,HEIGHT))
    e2=physics.body(EDGE,vec2(0,HEIGHT),vec2(WIDTH,HEIGHT))
    e3=physics.body(EDGE,vec2(WIDTH,HEIGHT),vec2(WIDTH,0))
    e4=physics.body(EDGE,vec2(WIDTH,0),vec2(0,0))
    
    b1=physics.body(CIRCLE,60)
    b1.gravityScale=0
    b1.restitution=1
    b1.linearDamping=0
    b1.linearVelocity=vec2(380,700)
    b1.x=200
    b1.y=600
    b1.friction=0

    b2=physics.body(CIRCLE,60)
    b2.gravityScale=0
    b2.restitution=1
    b2.linearDamping=0
    b2.linearVelocity=vec2(620,450)
    b2.x=400
    b2.y=200
    b2.friction=0

end

function draw()
    fps = .9*fps + .1/DeltaTime
    background(40,40,50)
    textMode(CORNER)
    if restart then
        setup2()  -- setup for the physics objects
        restart=false
    end
    if loop == 3 then
        for i = 1,5 do
            print("Drop FPS")
        end
    end
    noFill()
    strokeWidth(4)
    stroke(255)
    ellipse(b1.x,b1.y,120)
    ellipse(b2.x,b2.y,120)
    fill(255)
    text("x,y loop 1          x,y loop 2,3,4 etc.",100,1000)
    text("loop "..loop,600,1000)
    text("Shows the x,y position every 5 collisions for 100 collisions.",100,300)
    text("Retains the values for loop 1 to compare to loops 2,3,4, etc.",100,260)
    text("White fill represents that it mathches the previous value.",100,220)
    text("Red fill represent the value doesnt match.",100,180)
    text("Local data is saved to check values of first loop.",100,140)
    for a,b in pairs(tab1) do
        pushStyle()
        if readLocalData("tab1x"..a) == b.x and readLocalData("tab1y"..a) == b.y then
            fill(255, 255, 255, 255)
        else
            fill(255, 0, 0, 255)
        end
            text(b.x.." "..b.y,100,HEIGHT-50-a*20)
        popStyle()
    end
    for a,b in pairs(tab2) do
        pushStyle()
        if b.x == tab1[a].x and b.y == tab1[a].y then
            fill(255, 255, 255, 255)
        else
            fill(255, 0, 0, 255)
        end
        text(b.x.." "..b.y,300,HEIGHT-50-a*20)
        popStyle()
    end
    text(fps, WIDTH - 100, HEIGHT - 50)
end

function collide(c)
    if c.state==BEGAN then
        count = count + 1
        if count%NumberOfCollisions == 0 then
            if loop==1 then
                table.insert(tab1,vec2(b1.x,b1.y))
                if readLocalData("tab1x"..#tab1) == nil then
                    print("Saved Values")
                    saveLocalData("tab1x"..#tab1, b1.x)
                    saveLocalData("tab1y"..#tab1, b1.y)
                else
                    print("Checked Values")
                end
            end
            if loop>1 then
                table.insert(tab2,vec2(b1.x,b1.y))
            end
        end
        if count==20*NumberOfCollisions then
            restart=true
        end
    end  
end

@Saurabh Change b1 and b2 physics.body(CIRCLE,60) to (CIRCLE,85). Change ellipse(b1.x,b1.y,120) to ellipse(b1.x,b1.y,170) and change ellipse(b2.x,b2.y,120) to ellipse(b2.x,b2.y,170) and run it again. It might work sometimes for some values, but that doesn’t mean it works for all values.

Oh!! I never tried that out. But how does it make any sense sometimes it’s dead accurate in all loops and sometimes it isn’t? It can’t be coincidence that it gives the same value every loop.

Lots of interesting thoughts… @Luatee - I found and read your other post about your game and the wacky physics two-path issue…

I’m wondering… is there anywhere we can look at Codea’s code? I’m really curious to see how the step is being called. Re-reading the box2d docs - they guarantee deterministic behavior on the same device (but not across two devices with different OSes due to floating point number calculation differences).

Oh, here’s the link, btw: https://code.google.com/p/box2d/wiki/FAQ#Determinism

@Saurabh I never figured out what was happening when I originally wrote code to show the difference. I think that some variable isn’t being reset properly when a restart is done so it kind of alternates.