Tutorial 16: Basic Collision Detection
What you’ll learn
- Rectangle overlap and containment tests with
Intersects()andContains(). - Doing circle collision by hand when a rectangle is the wrong shape.
- Where
BoundingBoxandBoundingSpherefit in. - Keeping broad-phase costs down as object counts grow.
Before you start — Tutorial 12: Moving Sprites and Basic Animation — you need something moving before collision means anything.
CNA provides the same geometric intersection utilities as XNA 4.0 (Rectangle, BoundingBox, BoundingSphere and friends live in Microsoft::Xna::Framework). For 2D games Rectangle AABB tests cover most use cases. For 3D, BoundingBox and BoundingSphere handle axis-aligned and spherical volumes. This tutorial covers all three plus a manual circle-circle test and an overview of pixel-perfect collision.
Rectangle.Intersects()
Rectangle is an integer-coordinate AABB defined by X, Y, Width, and Height. The Intersects() method returns true when the rectangles overlap by at least one pixel:
#include "Microsoft/Xna/Framework/Rectangle.hpp"
using namespace Microsoft::Xna::Framework;
Rectangle player(100, 200, 48, 64); // x, y, width, height
Rectangle enemy (120, 220, 48, 64);
if (player.Intersects(enemy)) {
// Collision! Handle damage, bounce, etc.
}
// To get the overlapping region itself, use the static Rectangle::Intersect().
// It returns the intersection, or an empty Rectangle (0,0,0,0) if there is none:
Rectangle overlap = Rectangle::Intersect(player, enemy);
if (!overlap.getIsEmptyProperty()) {
// overlap now holds the shared area (here 28 x 44 pixels)
}
// (There is also an out-parameter form, Intersects(other, bool& result),
// kept for XNA compatibility. It reports the bool only, not the region.)
The integer coordinates map naturally to sprite pixel positions. If your player is drawn at (px, py) and is 48x64 pixels, build its rectangle as Rectangle(px, py, 48, 64). Remember that Y increases downward in screen space.
Rectangle.Contains()
Contains() tests whether a point or another rectangle is fully inside:
Rectangle room(0, 0, 800, 600);
Point mousePos(350, 400);
// Point containment
if (room.Contains(mousePos)) {
// mouse is inside the room
}
// Rectangle containment — true only if 'item' is fully inside 'room'
Rectangle item(10, 10, 100, 50);
if (room.Contains(item)) {
// item fits completely within room
}
// There is no Vector2 overload (XNA 4.0 has none): convert to a Point first.
// Casting truncates toward zero; use std::lround if you want rounding.
Vector2 projectile(350.7f, 200.3f);
if (room.Contains(Point(static_cast<int>(projectile.X),
static_cast<int>(projectile.Y)))) { }
Circle-based collision (manual)
XNA does not have a 2D Circle type, but circle-circle overlap is a one-liner using Vector2::Distance():
struct Circle {
Vector2 center;
float radius;
bool Intersects(const Circle& other) const {
float dist = Vector2::Distance(center, other.center);
return dist < (radius + other.radius);
}
};
// Usage
Circle player { Vector2(100.0f, 200.0f), 20.0f };
Circle bullet { Vector2(115.0f, 210.0f), 6.0f };
if (player.Intersects(bullet)) {
// hit!
}
For circle vs rectangle, test whether the closest point on the rectangle to the circle center is within the circle radius:
bool CircleVsRect(Vector2 circleCenter, float radius, Rectangle rect) {
float closestX = std::clamp(circleCenter.X,
static_cast<float>(rect.X),
static_cast<float>(rect.X + rect.Width));
float closestY = std::clamp(circleCenter.Y,
static_cast<float>(rect.Y),
static_cast<float>(rect.Y + rect.Height));
float dx = circleCenter.X - closestX;
float dy = circleCenter.Y - closestY;
return (dx * dx + dy * dy) < (radius * radius);
}
BoundingBox and BoundingSphere (3D)
For 3D games CNA provides axis-aligned bounding boxes and spheres in Microsoft::Xna::Framework:
#include "Microsoft/Xna/Framework/BoundingBox.hpp"
#include "Microsoft/Xna/Framework/BoundingSphere.hpp"
// BoundingBox — defined by min and max corners
BoundingBox boxA(Vector3(-1, -1, -1), Vector3(1, 1, 1));
BoundingBox boxB(Vector3( 0, 0, 0), Vector3(2, 2, 2));
// Contains() answers "how much of B is inside A?" with a ContainmentType:
ContainmentType ct = boxA.Contains(boxB);
// ContainmentType::Disjoint / Intersects / Contains
// Intersects() is the plain yes/no overlap test and returns a bool:
bool overlaps = boxA.Intersects(boxB);
// BoundingSphere — centre + radius
BoundingSphere sphere(Vector3(0, 0, 0), 1.5f);
bool sphereHitsBox = sphere.Intersects(boxA);
// Sphere vs sphere
BoundingSphere enemySphere(Vector3(2, 0, 0), 0.5f);
bool sphereHit = sphere.Intersects(enemySphere);
You can also create a BoundingSphere that encloses a std::vector<Vector3> of points with BoundingSphere::CreateFromPoints(), or a BoundingBox with BoundingBox::CreateFromPoints() — useful for computing bounds from loaded mesh data. Both also intersect Rays (returning a std::optional<float> distance), which is what picking code needs.
Pixel-perfect collision overview
Pixel-perfect collision checks every non-transparent pixel pair between two sprites. It is more accurate but much slower than bounding-box tests. The general approach in CNA:
- First run a
Rectangle::Intersects()test as a broad phase. Skip pixel checks if bounding boxes do not overlap. - Read the texture data with
Texture2D::GetData()into astd::vector<Color>(oneColorper pixel). - Iterate over the overlapping pixel region. For each pixel in the intersection rectangle, check whether both sprites have non-zero alpha (alpha > threshold).
// Read sprite pixels once (expensive — cache this in LoadContent)
std::vector<Color> pixelsA(texA.getWidthProperty() * texA.getHeightProperty());
texA.GetData(pixelsA.data(), static_cast<int>(pixelsA.size()));
// ... the same for pixelsB / texB ...
// Narrow-phase check inside the bounding-box intersection
Rectangle inter = Rectangle::Intersect(rectA, rectB);
if (inter.getIsEmptyProperty()) return false; // broad phase says no
for (int y = inter.Y; y < inter.Y + inter.Height; ++y) {
for (int x = inter.X; x < inter.X + inter.Width; ++x) {
int idxA = (x - rectA.X) + (y - rectA.Y) * rectA.Width;
int idxB = (x - rectB.X) + (y - rectB.Y) * rectB.Width;
if (pixelsA[idxA].getAProperty() > 10 && pixelsB[idxB].getAProperty() > 10)
return true; // pixel-level hit
}
}
return false;
For most games rectangle tests are sufficient. Reserve pixel-perfect for cases where sprite silhouettes are unusual (e.g. a curved ship outline) and accuracy is critical.
Complete example: player vs enemies, bouncing ball
#include <algorithm>
#include <cmath>
#include <memory>
#include <vector>
#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/Color.hpp"
#include "Microsoft/Xna/Framework/Rectangle.hpp"
#include "Microsoft/Xna/Framework/Vector2.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 Enemy {
Rectangle bounds;
bool alive = true;
};
class CollisionDemo final : public Game {
public:
CollisionDemo() : graphics_(this) {
graphics_.setPreferredBackBufferWidthProperty(800);
graphics_.setPreferredBackBufferHeightProperty(600);
}
protected:
void LoadContent() override {
spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());
// Load<T> returns each texture by value; the members are plain Texture2D values.
playerTex_ = getContentProperty().Load<Texture2D>("Sprites/player");
enemyTex_ = getContentProperty().Load<Texture2D>("Sprites/enemy");
ballTex_ = getContentProperty().Load<Texture2D>("Sprites/ball");
playerPos_ = Vector2(375.0f, 500.0f);
// Place a row of enemies
for (int i = 0; i < 5; ++i) {
enemies_.push_back({ Rectangle(100 + i * 130, 100, 48, 48), true });
}
// Ball starts at centre, moving diagonally
ballPos_ = Vector2(400.0f, 300.0f);
ballVel_ = Vector2(200.0f, 150.0f); // pixels per second
}
void Update(GameTime& gt) override {
auto kb = Keyboard::GetState();
float dt = static_cast<float>(gt.getElapsedGameTimeProperty().getTotalSecondsProperty());
// Move player left/right
if (kb.IsKeyDown(Keys::Left)) playerPos_.X -= 200.0f * dt;
if (kb.IsKeyDown(Keys::Right)) playerPos_.X += 200.0f * dt;
Rectangle playerRect(static_cast<int>(playerPos_.X), static_cast<int>(playerPos_.Y), 48, 64);
// Check player vs each enemy
for (auto& e : enemies_) {
if (!e.alive) continue;
if (playerRect.Intersects(e.bounds)) {
e.alive = false; // destroy enemy on touch
}
}
// Update bouncing ball
ballPos_ = ballPos_ + ballVel_ * dt;
// Bounce off walls
if (ballPos_.X < 0 || ballPos_.X > 784) {
ballVel_.X = -ballVel_.X;
ballPos_.X = std::clamp(ballPos_.X, 0.0f, 784.0f);
}
if (ballPos_.Y < 0 || ballPos_.Y > 584) {
ballVel_.Y = -ballVel_.Y;
ballPos_.Y = std::clamp(ballPos_.Y, 0.0f, 584.0f);
}
// Ball vs player (circle approximation)
Vector2 ballCenter = ballPos_ + Vector2(8.0f, 8.0f);
Vector2 playerCenter = playerPos_ + Vector2(24.0f, 32.0f);
if (Vector2::Distance(ballCenter, playerCenter) < 36.0f) {
ballVel_.Y = -std::abs(ballVel_.Y); // bounce up
}
}
void Draw(const GameTime&) override {
auto& gd = getGraphicsDeviceProperty();
gd.Clear(Color::DarkSlateBlue);
spriteBatch_->Begin();
spriteBatch_->Draw(playerTex_, playerPos_, Color::White);
for (auto& e : enemies_) {
if (e.alive)
spriteBatch_->Draw(enemyTex_,
Vector2(static_cast<float>(e.bounds.X), static_cast<float>(e.bounds.Y)),
Color::White);
}
spriteBatch_->Draw(ballTex_, ballPos_, Color::Yellow);
spriteBatch_->End();
// Game::EndDraw() presents the frame after Draw() returns.
}
private:
GraphicsDeviceManager graphics_;
std::unique_ptr<SpriteBatch> spriteBatch_;
Texture2D playerTex_, enemyTex_, ballTex_;
Vector2 playerPos_;
Vector2 ballPos_, ballVel_;
std::vector<Enemy> enemies_;
};
int main() { CollisionDemo game; game.Run(); }
Performance tips
- Broad phase first — always do a cheap rectangle or sphere test before any narrow-phase check.
- Avoid O(n²) for large entity counts — partition space with a grid or quadtree when you have more than ~50 collidable objects per frame.
- Cache pixel data —
Texture2D::GetData()is slow. Read it once inLoadContent()and store in astd::vector<Color>. - Separate collision from rendering bounds — your collision rectangle does not need to match the sprite art exactly. Shrink it by 20–30% for a more forgiving feel.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- A first CNA game, read line by line — A minimal CNA game and its extension to movement, edge clamping, sound and rectangle collision, explaining each framework contract and where CNA conveniences differ from portable XNA code.