Hello
After reading the famous article Fix your timestep, I'm trying to implement 2D sprite interpolation in my engine
This is my sprite state struct
// Sprite state for the interpolation
struct SpriteState
{
Vec2 position;
float angle;
};
My interpolation functions
// Return an interpolated position between previous and current state
// Scalar
inline auto calcInterpolation(float previous, float current, float alpha) -> float
{
return current * alpha + previous * (1.0f - alpha);
}
// Vectors
inline auto calcInterpolation(const Vec2& previous, const Vec2& current, float alpha) -> Vec2
{
return { current * alpha + previous * (1.0f - alpha) };
}
My Sprite class looks like this
class Sprite
{
public:
// ctor
Sprite(const Texture2D& texture, const AABB<int>& rect, const AnimationState& animState = {});
/// just to test
void useInterpolation(bool active = true);
/// just to test
void setPosition(const Vec2& pos);
void setRotation(float angle);
void update(float dt);
void render(QuadRenderer& quadRenderer, float alpha);
private:
/// just to test
bool m_usingInterpolation = true;
/// just to test
// Sprite states for the interpolation
// (position and rotation)
SpriteState m_currState, m_prevState;
// Other transformations
Transformations m_transform;
// Animation state for this sprite
AnimationState m_animState;
// Sprite coordinates from the Texture Atlas
AABB<int> m_spriteRect;
// Normalized texture coordinates
AABB<float> m_uvCoords;
// Texture data
const Texture2D& m_texture;
// Sprite color
Color m_color;
};
Sprite update and render functions
void Sprite::update(float dt)
{
m_prevState = m_currState;
/// TODO: if animated, handle animation
}
void Sprite::render(QuadRenderer& quadRenderer, float alpha)
{
SpriteState state;
/// just to test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
if (m_usingInterpolation)
{
// Handle interpolation
state.position = calcInterpolation(m_prevState.position, m_currState.position, alpha);
state.angle = calcInterpolation(m_prevState.angle, m_currState.angle, alpha);
}
else
{
// If not using interpolation, just take the current state
state = m_currState;
}
/// just to test !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// Update matrix
m_transform.updateMatrix(state);
// Create a rectangle with the sprite dimensions,
// starting from the origin of the coordinate system
AABB<float> aabb =
{
0.0f, 0.0f,
static_cast<float>(m_spriteRect.width),
static_cast<float>(m_spriteRect.height)
};
// Add this sprite to the renderer
quadRenderer.transformQuad(aabb, m_uvCoords, m_transform.getMatrix(), m_texture, m_color);
}
Transformation code
// Update the internal matrix
void Transformations::updateMatrix(const SpriteState& state)
{
// Baking a matrix
// http://totologic.blogspot.com.ar/2015/02/2d-transformation-matrices-baking.html
float angle = math::degToRad(-state.angle);
float cosine = static_cast<float>(std::cos(angle));
float sine = static_cast<float>(std::sin(angle));
float sxc = m_scale.x * cosine;
float syc = m_scale.y * cosine;
float sxs = m_scale.x * sine;
float sys = m_scale.y * sine;
float tx = -m_origin.x * sxc - m_origin.y * sys + state.position.x;
float ty = m_origin.x * sxs - m_origin.y * syc + state.position.y;
m_matrix =
{
sxc, sys, tx,
-sxs, syc, ty,
0.0f, 0.0f, 1.0f
};
}
And this is my game loop code
const int updateFPS = 30;
const float dt = 1.0f / updateFPS;
float accumulator = 0.0f;
Uint32 currentTime = SDL_GetTicks();
// Main Loop
bool close = false;
while(!close)
{
// Process system events
SDL_Event event;
while(SDL_PollEvent(&event))
{
switch(event.type)
{
case SDL_QUIT:
close = true;
break;
case SDL_KEYDOWN:
if (event.key.keysym.sym == SDLK_LEFT)
{
sprite1.moveH(-spriteVel * dt);
sprite2.moveH(-spriteVel * dt);
}
if (event.key.keysym.sym == SDLK_RIGHT)
{
sprite1.moveH(spriteVel * dt);
sprite2.moveH(spriteVel * dt);
}
default:
break;
}
} // events
// update
// Get frame time in seconds
const Uint32 newTime = SDL_GetTicks();
float frameTime = static_cast<float>(newTime - currentTime) / 1000.0f;
// Avoid "Spiral of death"
// (That is what happens when your physics simulation
// cannot keep up with the steps it’s asked to take)
frameTime = std::min(frameTime, 0.25f);
currentTime = newTime;
accumulator += frameTime;
// Logic update
while (accumulator >= dt)
{
sprite1.update(dt);
sprite2.update(dt);
accumulator -= dt;
}
// render
float alpha = accumulator / dt;
quadRenderer.begin();
sprite1.render(quadRenderer, alpha);
sprite2.render(quadRenderer, alpha);
quadRenderer.end();
swapBuffers();
}
Quad renderer is my sprite batcher. It works fine
I'm creating two sprites to test this code, I activated interpolation in the first one, to see the difference
Sprite sprite1(playerAtlas, { 0, 0, 64, 64 });
sprite1.setOrigin({ 32.0f, 63.0f });
sprite1.setPosition({ 512.0f, 384.0f });
sprite1.useInterpolation(true);
Sprite sprite2(playerAtlas, { 0, 0, 64, 64 });
sprite2.setOrigin({ 32.0f, 63.0f });
sprite2.setPosition({ 512.0f, 318.0f });
sprite2.useInterpolation(false);
Now, the questions I have:
1) Is it ok to get the time as unsigned integers, or should I use something like std::chrono? SDL2 only returns time as Uint32
2) Why the author of the article is using this hardcoded number? if ( frameTime > 0.25 )
3) Is my system events handling code in the right place? or should it be inside the while (accumulator >= dt) code?
4) Why I dont see any difference at all between the two sprites? the one using the interpolation method and the other one
Thanks for reading... any tips for improving my code will be welcome