Skip to content
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
110 lines (94 sloc) 3.71 KB
//! By default, Tetra runs updates at a fixed tick rate, decoupled from the speed at which
//! the screen is rendering. This means your game's physics will behave consistently regardless
//! of how powerful the player's device is, which is nice! However, it does mean that if the
//! update and render rates don't line up nicely, your game might look a bit choppy.
//! There are two ways of handling this:
//! * Interpolation
//! * Store both the previous state and the current state, and render somewhere in between the two
//! * Pros: Accurate, no guesswork
//! * Cons: Complex, introduces a tiny bit of input lag as you're updating one frame ahead of rendering
//! * Extrapolation
//! * Store only the current state, and guess what the next state will look like
//! * Pros: Simple to implement, doesn't cause lag
//! * Cons: Looks weird when the extrapolated state doesn't end up matching reality
//! The example below shows how to implement very naive forms of these techniques, with the
//! tick rate set extremely low for demonstration purposes.
//! For more information, see these articles:
//! *
//! *
use tetra::graphics::{self, Color, Texture};
use tetra::math::{self, Vec2};
use tetra::{Context, Settings, State};
struct GameState {
texture: Texture,
velocity: Vec2,
position_none: Vec2,
position_ex: Vec2,
position_in_prev: Vec2,
position_in_curr: Vec2,
impl GameState {
fn new(ctx: &mut Context) -> tetra::Result<GameState> {
Ok(GameState {
texture: Texture::new(ctx, "./examples/resources/player.png")?,
velocity: Vec2::new(16.0, 0.0),
position_none: Vec2::new(16.0, 16.0),
position_ex: Vec2::new(16.0, 32.0),
position_in_prev: Vec2::new(16.0, 48.0),
position_in_curr: Vec2::new(16.0, 48.0),
impl State for GameState {
fn update(&mut self, _: &mut Context) -> tetra::Result {
// Without special handling, or with extrapolation, we can just
// update normally.
self.position_none += self.velocity;
self.position_ex += self.velocity;
// For interpolation, we have to store the previous state as well.
// We're effectively running the simulation 1 tick ahead of the
// renderer.
self.position_in_prev = self.position_in_curr;
self.position_in_curr += self.velocity;
fn draw(&mut self, ctx: &mut Context, dt: f64) -> tetra::Result {
graphics::clear(ctx, Color::rgb(0.769, 0.812, 0.631));
// `dt` is a number between 0.0 and 1.0 which represents how
// far between ticks we currently are. For example, 0.0 would
// mean the update just ran, 0.99 would mean another update is
// about to run.
// No special handling - looks choppy!
graphics::draw(ctx, &self.texture, self.position_none);
// With extrapolation - just guess where the position should be
// based on the object's velocity.
self.position_ex + (self.velocity * dt as f32),
// With interpolation - we draw at a fixed point between the
// two stored states.
math::lerp(&self.position_in_prev, &self.position_in_curr, dt as f32),
fn main() {
&Settings::new("Interpolation", 640, 480)
You can’t perform that action at this time.