Fixed timestep

There are times when calculations can be very complex when dealing with animations. One clear example can be in physics simulations and in games in general, but some other times, our calculations, even for a simple-ish custom view, can get a bit tricky when using time-based animation. Having a fixed timestep will allow us to abstract our animation logic from time variables, but still keep our animation tied to time.

The logic behind having a fixed timestep is to assume our animation logic will be always executed a fixed rate. For instance, we can assume it will be executed at 60 fps regardless of which is the actual rendering frames per second. To show how it could be done, we'll create a new custom view that will spawn particles at the position we're pressing or dragging on the screen and applying some very basic and simple physics.

First, let's create the basic custom view like our previous example:

private static final int BACKGROUND_COLOR = 0xff404060; 
private static final int FOREGROUND_COLOR = 0xffffffff; 
private static final int N_PARTICLES = 800; 
 
private Paint paint; 
private Particle[] particles; 
private long timeStart; 
private long accTime; 
private int previousVisibility; 
private long invisibleTimeStart; 
 
 
public FixedTimestepExample(Context context, AttributeSet attributeSet) { 
    super(context, attributeSet); 
 
    paint = new Paint(); 
    paint.setStyle(Paint.Style.FILL); 
    paint.setAntiAlias(true); 
    paint.setColor(FOREGROUND_COLOR); 
 
    particles = new Particle[N_PARTICLES]; 
    for (int i = 0; i < N_PARTICLES; i++) { 
        particles[i] = new Particle(); 
    } 
 
    particleIndex = 0; 
    timeStart = -1; 
    accTime = 0; 
    previousVisibility = View.GONE; 
} 

We're initializing the basic variables and we're also creating an array of particles. Also, as we've implemented the onVisibilityChange callback on our previous example, let's take advantage of it:

@Override 
protected void onVisibilityChanged(@NonNull View changedView, int visibility) { 
    super.onVisibilityChanged(changedView, visibility); 
    if (timeStartElapsed != -1) { 
        // avoid doing this check before View is even visible 
        if ((visibility == View.INVISIBLE ||  visibility == View.GONE)
&& previousVisibility == View.VISIBLE) { invisibleTimeStart = SystemClock.elapsedRealtime(); } if ((previousVisibility == View.INVISIBLE || previousVisibility
== View.GONE) && visibility == View.VISIBLE) { timeStart += SystemClock.elapsedRealtime() -
invisibleTimeStart; } } else { timeStart = SystemClock.elapsedRealtime(); } previousVisibility = visibility; }

Let's now define the Particle class, let's keep it as simple as possible:

class Particle { 
    float x; 
    float y; 
    float vx; 
    float vy; 
    float ttl; 
 
    Particle() { 
        ttl = 0.f; 
    } 
} 

We've only defined the x, y coordinates, the x and y velocity as vx and vy respectively, and the time to live of the particle. When the time to live of the particle reaches 0, we'll not update or draw it anymore.

Now, let's implement the onDraw() method:

@Override 
protected void onDraw(Canvas canvas) { 
    animateParticles(getWidth(), getHeight()); 
 
    canvas.drawColor(BACKGROUND_COLOR); 
 
    for(int i = 0; i < N_PARTICLES; i++) { 
        float px = particles[i].x; 
        float py = particles[i].y; 
        float ttl = particles[i].ttl; 
 
        if (ttl > 0) { 
            canvas.drawRect( 
                px - PARTICLE_SIZE, 
                py - PARTICLE_SIZE, 
                px + PARTICLE_SIZE, 
                py + PARTICLE_SIZE, paint); 
        } 
    } 
    postInvalidateDelayed(10); 
} 

We've delegated all the animation to the animateParticles() method and here we're just iterating through all the particles, checking if their time to live is positive and, in that case, drawing them.

Let's see now how we can implement the animateParticles() method with a fixed time step:

private static final int TIME_THRESHOLD = 16; 
private void animateParticles(int width, int height) { 
    long currentTime = SystemClock.elapsedRealtime(); 
    accTime += currentTime - timeStart; 
    timeStart = currentTime; 
 
    while(accTime > TIME_THRESHOLD) { 
        for (int i = 0; i < N_PARTICLES; i++) { 
            particles[i].logicTick(width, height); 
        } 
 
        accTime -= TIME_THRESHOLD; 
    } 
} 

We calculate the time difference from the last time, or delta of time, and we accumulate it in the accTime variable. Then, as long as accTime is higher than the threshold we've defined, we execute one logic step. It might happen that more than one logic steps are executed between renders or, in some other cases, it might not get executed during two different frames.

Finally, we subtract the time threshold we defined to the accTime for each logic step we've executed and we set the new timeStart to the time we used for calculating the difference of time from the previous call to animateParticles().

In this example, we've defined the time threshold to be 16, so every 16 milliseconds we'll execute one logic step, independently if we're rendering 10 or 60 frames per second.

The logicTick() method on the Particle class completely ignores the current value of the timer, as it assumes it'll be executed on a fixed time step:

void logicTick(int width, int height) { 
    ttl--; 
 
    if (ttl > 0) { 
        vx = vx * 0.95f; 
        vy = vy + 0.2f; 
 
        x += vx; 
        y += vy; 
 
        if (y < 0) { 
            y = 0; 
            vy = -vy * 0.8f; 
        } 
 
        if (x < 0) { 
            x = 0; 
            vx = -vx * 0.8f; 
        } 
 
        if (x >= width) { 
            x = width - 1; 
            vx = -vx * 0.8f; 
        } 
    } 
} 

It's an extreme over-simplification of a particle physic simulation. It basically applies friction and adds vertical acceleration to the particles, calculates if they have to bounce from the screen limits, and calculates the new x and y positions.

We're just missing the code to spawn new particles when we've a pressed or dragged a TouchEvent:

@Override 
public boolean onTouchEvent(MotionEvent event) { 
    switch (event.getAction()) { 
        case MotionEvent.ACTION_DOWN: 
        case MotionEvent.ACTION_MOVE: 
            spawnParticle(event.getX(), event.getY()); 
            return true; 
    } 
    return super.onTouchEvent(event); 
} 

Here, we're calling spawnParticle() as long as we've got a touch event that is a press or a move. The implementation of spawnParticle() is also very simple:

private static final int SPAWN_RATE = 8; 
private int particleIndex; 
 
private void spawnParticle(float x, float y) { 
    for (int i = 0; i < SPAWN_RATE; i++) { 
        particles[particleIndex].x = x; 
        particles[particleIndex].y = y; 
        particles[particleIndex].vx = (float) (Math.random() * 40.f) -
20.f; particles[particleIndex].vy = (float) (Math.random() * 20.f) -
10.f; particles[particleIndex].ttl = (float) (Math.random() * 100.f)
+ 150.f; particleIndex++; if (particleIndex == N_PARTICLES) particleIndex = 0; } }

We are using the particleIndex variable as a circular index of the particles array. Whenever it arrives at the end of the array it'll start again at the very beginning. This method sets the x and y coordinates of the touch event and it randomizes the velocity and time to live of each spawned particle. We've created a SPAWN_RATE constant to spawn multiple particles on the same touch event and improve the visual effect.

If we run the application, we can see it in action, and it'll be very similar to the following screenshot, but in this case, it's very hard to capture the idea of the animation in a screenshot:

But we're missing something. As we've mentioned before, sometimes we'll execute two, or maybe more, logic steps between two rendered frames, but on some other times, we'll not execute any logic steps between two consecutive frames. If we don't execute any logic steps between those two frames, the result will be the same and a waste of CPU and battery life.

Even if we're between logic steps, that doesn't mean it hasn't passed any time between frames. Actually, we're somewhere between the previous calculated logic step and the next one. The good news is that we can actually calculate that, improving the smoothness of the animation and solving this issue at the same time.

Let's include this modification to the animateParticles() method:

private void animateParticles(int width, int height) {
long currentTime = SystemClock.elapsedRealtime();
accTime += currentTime - timeStart;
timeStart = currentTime;

while(accTime > TIME_THRESHOLD) {
for (int i = 0; i < N_PARTICLES; i++) {
particles[i].logicTick(width, height);
}

accTime -= TIME_THRESHOLD;
}

float factor = ((float) accTime) / TIME_THRESHOLD;
for (int i = 0; i < N_PARTICLES; i++) {
particles[i].adjustLogicStep(factor);
}
}

We're calculating the factor between which will tell us how close or far it is from the next logic step. If the factor is 0, it means we're just at the exact time of the logic step we've just executed. If the factor is 0.5, it means we're halfway between the current step and the next one and if the factor is 0.8, we're almost at the next logic step and precisely 80% of time passed since the previous step. The way to smooth the transition between one logic step and the next is to interpolate using this factor, but to be able to do so, first we need to calculate the values of the next step as well. Let's change the logicTick() method to implement this change:

float nextX; 
float nextY; 
float nextVX; 
float nextVY; 
 
void logicTick(int width, int height) { 
    ttl--; 
 
    if (ttl > 0) { 
        x = nextX; 
        y = nextY; 
        vx = nextVX; 
        vy = nextVY; 
 
        nextVX = nextVX * 0.95f; 
        nextVY = nextVY + 0.2f; 
 
        nextX += nextVX; 
        nextY += nextVY; 
 
        if (nextY < 0) { 
            nextY = 0; 
            nextVY = -nextVY * 0.8f; 
        } 
 
        if (nextX < 0) { 
            nextX = 0; 
            nextVX = -nextVX * 0.8f; 
        } 
 
        if (nextX >= width) { 
            nextX = width - 1; 
            nextVX = -nextVX * 0.8f; 
        } 
    } 
} 

Now, at every logic step we're assigning the values of the next logic step to the current variables to avoid recalculating them, and calculating the next logic step. This way, we've got both values; the current and the new values after the next logic step is executed.

As we'll be using some intermediate values between x, y, and nextX, nextY, we'll calculate these values on new variables as well:

float drawX; 
float drawY; 
 
void adjustLogicStep(float factor) { 
    drawX = x * (1.f - factor) + nextX * factor; 
    drawY = y * (1.f - factor) + nextY * factor; 
} 

As we can see, drawX and drawY will be an intermediate state between the current logic step and the next one. If we apply the previous example values to this factor, we'll see how this method works.

If factor is 0drawX and drawY are exactly x and y. On the contrary, if factor is 1, drawX and drawY are exactly nextX and nextY, although this should never happen as another logic step would have been triggered.

In the case of factor being 0.8, drawX and drawY values are a linear interpolation weighed at 80% the values of the next logic step and 20% of the current one, allowing a smooth transition between states.

You can find the whole example source code in the Example28-FixedTimestep folder in the GitHub repository. The fixed timestep is covered with more details in the fix your timestep artiche on the Gaffer On Games blog.