Tutorial 16: Basic Collision Detection

Geometry  ·  Rectangle  ·  BoundingBox  ·  BoundingSphere

CNA provides the same geometric intersection utilities as XNA 4.0. 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.
}

// Intersects(Rectangle) also has an out-parameter overload
// that returns a bool and fills an output Rectangle with the overlap:
Rectangle overlap;
bool hit = player.Intersects(enemy, overlap);
// overlap now holds the intersection rectangle

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
}

// Vector2 overload (rounds to nearest integer internally)
Vector2 projectile(350.7f, 200.3f);
if (room.Contains(projectile)) { }

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;
    Single  radius;

    bool Intersects(const Circle& other) const {
        Single 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, Single radius, Rectangle rect) {
    Single closestX = std::clamp(circleCenter.X,
                                 (Single)rect.X,
                                 (Single)(rect.X + rect.Width));
    Single closestY = std::clamp(circleCenter.Y,
                                 (Single)rect.Y,
                                 (Single)(rect.Y + rect.Height));
    Single dx = circleCenter.X - closestX;
    Single 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));

ContainmentType ct = boxA.Contains(boxB);
// ContainmentType::Disjoint / Intersects / Contains

bool overlaps = (boxA.Intersects(boxB) != ContainmentType::Disjoint);

// BoundingSphere — centre + radius
BoundingSphere sphere(Vector3(0, 0, 0), 1.5f);
bool sphereHitsBox = (sphere.Intersects(boxA) != ContainmentType::Disjoint);

// Sphere vs sphere
BoundingSphere enemy(Vector3(2, 0, 0), 0.5f);
bool sphereHit = sphere.Intersects(enemy);

You can also create a BoundingSphere that tightly encloses an array of points, or a BoundingBox from a list of Vector3 corners — useful for computing bounds from loaded mesh data.

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:

  1. First run a Rectangle.Intersects() test as a broad phase. Skip pixel checks if bounding boxes do not overlap.
  2. Read the texture data with Texture2D::GetData() into a std::vector<Color>.
  3. 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.getWidth() * texA.getHeight());
texA.GetData(pixelsA.data(), pixelsA.size());

// Narrow-phase check inside the bounding-box intersection
Rectangle inter;
if (!rectA.Intersects(rectB, inter)) return false;

for (intcs y = inter.Y; y < inter.Y + inter.Height; ++y) {
    for (intcs x = inter.X; x < inter.X + inter.Width; ++x) {
        intcs idxA = (x - rectA.X) + (y - rectA.Y) * rectA.Width;
        intcs idxB = (x - rectB.X) + (y - rectB.Y) * rectB.Width;
        if (pixelsA[idxA].A > 10 && pixelsB[idxB].A > 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 <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/Graphics/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_.setPreferredBackBufferWidth(800);
        graphics_.setPreferredBackBufferHeight(600);
    }

protected:
    void LoadContent() override {
        spriteBatch_ = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());
        playerTex_   = Content.Load<Texture2D>("Sprites/player");
        enemyTex_    = Content.Load<Texture2D>("Sprites/enemy");
        ballTex_     = Content.Load<Texture2D>("Sprites/ball");

        playerPos_ = Vector2(375.0f, 500.0f);

        // Place a row of enemies
        for (intcs 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();
        Single dt = (Single)gt.getElapsedGameTime().TotalSeconds();

        // 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((intcs)playerPos_.X, (intcs)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_ += 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((Single)e.bounds.X, (Single)e.bounds.Y), Color::White);
        }
        spriteBatch_->Draw(*ballTex_, ballPos_, Color::Yellow);
        spriteBatch_->End();

        gd.Present();
    }

private:
    GraphicsDeviceManager         graphics_;
    std::unique_ptr<SpriteBatch>  spriteBatch_;
    std::unique_ptr<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 dataTexture2D::GetData() is slow. Read it once in LoadContent() and store in a std::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.