Tutorial 27: Scrolling Backgrounds and Parallax
What you’ll learn
- Layering backgrounds and giving each its own scroll speed.
- Tiling a layer infinitely, including the wrap-address approach.
- Driving all layers from a single camera position.
Before you start — Tutorial 17: 2D Camera and Viewport — the layers scroll from the Camera2D built there.
What is Parallax?
Parallax scrolling is a technique where background layers move at different speeds relative to the camera, creating a convincing illusion of depth. Objects further away appear to move more slowly than objects close to the viewer — the same effect you see when driving past a landscape where distant hills scroll slowly and nearby fences fly past.
It is one of the most cost-effective visual tricks in 2D games. A side-scrolling platformer can go from flat to atmospheric with just three or four well-chosen layers: a static sky, slowly drifting clouds, mid-distance mountains, and a faster foreground treeline.
The rule of thumb: slower scroll = further away. A layer that doesn't scroll at all sits at infinity (like a sky gradient). A layer that scrolls at the same speed as the camera is at the same depth as the player.
Layer System
Model each layer as a self-contained object with three data members:
- texture — the
Texture2Dimage for this layer. - scrollFactor — a multiplier in the range 0.0–1.0. 0.0 = stationary, 1.0 = moves with the camera.
- offset — accumulated horizontal scroll position in pixels, tracked per layer.
Layers are drawn back-to-front (sky first, foreground last). You can also stack vertical layers for top-down games or combine horizontal and vertical scrolling for diagonal perspectives.
struct ParallaxLayer {
Texture2D* texture;
float scrollFactor; // 0.0 = fixed, 1.0 = full camera speed
float offset; // current horizontal offset in pixels
int screenHeight; // draw at full screen height
};
Scroll Speed per Layer
Each frame, update the layer's offset based on how fast the camera is moving and the layer's scroll factor:
// dt = delta time in seconds, cameraSpeedX = camera pixels/sec
layer.offset += cameraSpeedX * layer.scrollFactor * dt;
Typical scroll factor values:
- Sky / distant clouds:
0.05–0.15 - Far mountains:
0.3–0.4 - Mid-ground trees:
0.6–0.7 - Near foreground:
0.85–0.95
The camera itself (the gameplay layer the player sees) scrolls at factor 1.0 — but that layer is usually not a parallax layer, it's just the tile map or level geometry.
Infinite Tiling Technique
Parallax layers need to tile horizontally without a visible seam. The trick is to keep the offset in the range [0, textureWidth) using fmod, then draw the texture twice: once at -offset and once at textureWidth - offset. At least one copy is always visible no matter the scroll position.
void Update(float cameraSpeedX, float dt) {
offset += cameraSpeedX * scrollFactor * dt;
// Wrap offset so it never grows unbounded
offset = std::fmod(offset, static_cast<float>(texture->getWidthProperty()));
}
void Draw(SpriteBatch& sb) {
float w = static_cast<float>(texture->getWidthProperty());
// Draw two copies to cover the seam
sb.Draw(*texture, Vector2(-offset, 0.0f), Color::White);
sb.Draw(*texture, Vector2(w - offset, 0.0f), Color::White);
}
If your texture is narrower than the screen you may need to draw a third copy. In practice, layer textures are usually made wide enough (e.g. 1920 px) to avoid this.
SpriteBatch with Wrap Address
An alternative to drawing two copies manually is to set the sampler to TextureAddressMode::Wrap for SpriteBatch::Begin and then draw a destination rectangle wider than the texture using a source rectangle that reaches past the texture’s right edge, so the U coordinate goes beyond 1.0 and wraps. CNA exposes this through SamplerState, whose properties are set with accessors:
// Custom sampler with wrapping enabled. Configure it completely BEFORE its
// first use: as in XNA, a state that has been bound to the device is immutable.
SamplerState wrapSampler;
wrapSampler.setAddressUProperty(TextureAddressMode::Wrap);
wrapSampler.setAddressVProperty(TextureAddressMode::Clamp);
// Sort mode, blend state, then const SamplerState*, depth-stencil, rasterizer
spriteBatch->Begin(SpriteSortMode::Deferred, BlendState::AlphaBlend,
&wrapSampler, nullptr, nullptr);
// The source rectangle is as wide as the screen (wider than a narrow tile
// texture); with Wrap addressing the texture repeats to fill it.
Rectangle source(static_cast<int>(offset), 0, screenWidth, layer.texture->getHeightProperty());
Rectangle dest(0, 0, screenWidth, screenHeight);
spriteBatch->Draw(*layer.texture, dest, source, Color::White);
spriteBatch->End();
The manual two-copy approach is simpler and does not depend on how a renderer implements wrap addressing (XNA’s own Reach profile even restricts wrapped textures to power-of-two sizes), so prefer it unless you need to tile a very narrow texture many times.
Camera Integration
Rather than tracking an explicit camera speed, you can drive parallax directly from the camera's world-space X position. Each frame, compute the layer offset as camera.X * scrollFactor — no accumulation needed, no drift:
The examples on this page use a tiny stand-in Camera2D that holds only a position. If you use the fuller Camera2D class from Tutorial 17, read the same value with camera.getPositionProperty().X.
struct Camera2D {
Vector2 position; // world-space camera centre
};
// In ParallaxLayer::Draw, derive offset from camera directly:
void Draw(SpriteBatch& sb, const Camera2D& cam, int screenH) {
float w = static_cast<float>(texture->getWidthProperty());
float off = std::fmod(cam.position.X * scrollFactor, w);
sb.Draw(*texture, Vector2(-off, 0.0f), Color::White);
sb.Draw(*texture, Vector2(w - off, 0.0f), Color::White);
}
This approach is deterministic: if you teleport the camera, the background instantly snaps to the correct position with no accumulated error.
Load layer textures through ContentManager so they participate in the standard asset pipeline. Call getContentProperty().Load<Texture2D>("backgrounds/sky") in LoadContent() and the content manager caches the asset for you. Load<T> returns the texture by value, so hold it in a Texture2D member and give ParallaxLayer a pointer to that member. The full example below loads the PNGs straight from disk instead, to stay self-contained.
Full Example
A complete three-layer parallax background. Press the right arrow key to scroll the camera.
#include <cmath>
#include <array>
#include <memory>
#include <optional>
#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/Color.hpp"
#include "Microsoft/Xna/Framework/Input/Keyboard.hpp"
#include "Microsoft/Xna/Framework/Input/Keys.hpp"
#include "Microsoft/Xna/Framework/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Graphics/SpriteBatch.hpp"
#include "Microsoft/Xna/Framework/Graphics/Texture2D.hpp"
using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Graphics;
using namespace Microsoft::Xna::Framework::Input;
// -------------------------------------------------------
struct Camera2D {
Vector2 position{ 0.0f, 0.0f };
};
// -------------------------------------------------------
class ParallaxLayer {
public:
ParallaxLayer(Texture2D* tex, float factor)
: texture_(tex), scrollFactor_(factor) {}
void Draw(SpriteBatch& sb, const Camera2D& cam, int screenH) {
float w = static_cast<float>(texture_->getWidthProperty());
float h = static_cast<float>(texture_->getHeightProperty());
float off = std::fmod(cam.position.X * scrollFactor_, w);
if (off < 0.0f) off += w;
// Scale height to fill the screen
float scaleY = static_cast<float>(screenH) / h;
Vector2 scale{ 1.0f, scaleY };
sb.Draw(*texture_, Vector2(-off, 0.0f), std::nullopt, Color::White,
0.0f, Vector2::Zero, scale, SpriteEffects::None, 0.0f);
sb.Draw(*texture_, Vector2(w - off, 0.0f), std::nullopt, Color::White,
0.0f, Vector2::Zero, scale, SpriteEffects::None, 0.0f);
}
private:
Texture2D* texture_;
float scrollFactor_;
};
// -------------------------------------------------------
class ParallaxBackground {
public:
void Init(Texture2D* sky, Texture2D* mountains, Texture2D* trees) {
layers_[0] = std::make_unique<ParallaxLayer>(sky, 0.10f);
layers_[1] = std::make_unique<ParallaxLayer>(mountains, 0.40f);
layers_[2] = std::make_unique<ParallaxLayer>(trees, 0.80f);
}
void Draw(SpriteBatch& sb, const Camera2D& cam, int screenH) {
for (auto& layer : layers_)
layer->Draw(sb, cam, screenH);
}
private:
std::array<std::unique_ptr<ParallaxLayer>, 3> layers_;
};
// -------------------------------------------------------
class ParallaxGame final : public Game {
public:
ParallaxGame() : graphics_(this) {
graphics_.setPreferredBackBufferWidthProperty(800);
graphics_.setPreferredBackBufferHeightProperty(480);
}
protected:
void LoadContent() override {
spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());
texSky_ = std::make_unique<Texture2D>("assets/bg_sky.png",
getGraphicsDeviceProperty());
texMountains_ = std::make_unique<Texture2D>("assets/bg_mountains.png",
getGraphicsDeviceProperty());
texTrees_ = std::make_unique<Texture2D>("assets/bg_trees.png",
getGraphicsDeviceProperty());
background_.Init(texSky_.get(), texMountains_.get(), texTrees_.get());
}
void Update(GameTime& gameTime) override {
float dt = static_cast<float>(gameTime.getElapsedGameTimeProperty().getTotalSecondsProperty());
auto kb = Keyboard::GetState();
const float cameraSpeed = 200.0f; // pixels per second
if (kb.IsKeyDown(Keys::Right)) camera_.position.X += cameraSpeed * dt;
if (kb.IsKeyDown(Keys::Left)) camera_.position.X -= cameraSpeed * dt;
if (camera_.position.X < 0.0f) camera_.position.X = 0.0f;
}
void Draw(const GameTime&) override {
auto& gd = getGraphicsDeviceProperty();
gd.Clear(Color(135, 206, 235, 255)); // sky blue fallback
// The PNGs are loaded straight from disk (straight alpha), so draw them
// with NonPremultiplied blending (see Tutorial 22).
spriteBatch_->Begin(SpriteSortMode::Deferred, BlendState::NonPremultiplied);
background_.Draw(*spriteBatch_, camera_, 480);
spriteBatch_->End();
// Game::EndDraw() presents the frame after Draw() returns.
}
private:
GraphicsDeviceManager graphics_;
std::unique_ptr<SpriteBatch> spriteBatch_;
std::unique_ptr<Texture2D> texSky_;
std::unique_ptr<Texture2D> texMountains_;
std::unique_ptr<Texture2D> texTrees_;
ParallaxBackground background_;
Camera2D camera_;
};
int main() {
ParallaxGame game;
game.Run();
return 0;
}