Bounding Boxes

Discover bounding boxes: what they are, why we use them, and how to create them

Ryan McCombe
Published

So far, our game objects exist only as single points (Vec2 Position). While great for movement, it's not enough for interactions like checking if a projectile hits a character.

This lesson tackles that by introducing bounding boxes - simple rectangular shapes that represent the space an object occupies. We'll focus on Axis-Aligned Bounding Boxes (AABBs), implement a BoundingBox class using SDL_FRect, add it to our GameObject, and learn how to draw these boxes for debugging purposes.

In the next lesson, we'll learn how to use these bounding boxes to detect when they intersect, and some practical reasons why that is useful.

Finally, we'll end the chapter by revisiting our physics system and use our new bounding boxes to let our objects physically interact with each other.

Bounding Boxes / Volumes

Let's imagine we want to add an enemy to our game that can fire projectiles that our player needs to dodge. When our player's character is represented by a simple point in space, we can't easily determine if a projectile hit them:

Unfortunately, detecting which objects in our scene are colliding with each other is not a something we can do directly. in real-world games, each object can have a complex shape comprising thousands of pixels (or vertices, in a 3D game) and our scene may have thousands of objects. Understanding how all of these complex shapes might be interacting on every frame is unreasonably expensive.

To help with this, we use bounding volumes. These are much simpler shapes that "bound" (or contain) all the components of a more complex object. They're not visible to players but, behind the scenes, we use them to understand how the objects in our scene are interacting.

The most common bounding volume we use is a bounding box, which is a simple rectangle in 2D, or a cuboid in 3D:

A bounding box rendered in Unreal Engine

Now, to understand if our player was hit by a fireball, we just need to do the much simpler calculation of checking whether two rectangles are overlapping:

Axis-Aligned Bounding Boxes (AABBs)

In 2D games, bounding boxes are simple rectangles. Axis-aligned bounding boxes, or AABBs, are boxes whose edges are parallel to the axes of our space. More simply, we can think of AABBs as being rectangles that have not been rotated:

A bounding box that can be rotated is typically called an oriented bounding box, or OBB.

AABBs are friendlier and faster to work than OBBs as the lack of rotation simplifies a lot of the calculations required to determine things like whether two bounding boxes intersect.

AABBs are also easier and more memory-efficient to represent, requiring only four scalar values in 2D, or six in 3D.

We're working with 2D for now, and there are two main ways to represent an AABB in two dimensions:

  • The top-left corner of the box, its width, and its height. These values are typically labelled xx, yy, ww, and hh.
  • The top-left corner and bottom-right corner of the box. These use values typically labelled x1x_1, y1y_1, x2x_2, and y2y_2.

As we've seen, SDL_Rect uses the first of these conventions, with members called xx, yy, ww, and hh. Often, we need to use both conventions within the same program, but it's relatively easy to convert one to the other.

We can calculate the bottom right corner (x2,y2x_2, y_2) of an SDL_Rect through addition:

x2=x+wy2=y+h x_2 = x + w \\ y_2 = y + h \\

If we have a box defined by x1x_1, y1y_1, x2x_2, and y2y_2 values, we can calculate its width and height through subtraction:

w=x2x1h=y2y1 w = x_2 - x_1 \\ h = y_2 - y_1 \\

Implementing AABBs

Our bounding boxes are defined in world space, so we'll represent them using an SDL_FRect, which works in the same way as an SDL_Rect except that the x, y, w, and h values are stored as floating point numbers instead of integers:

// BoundingBox.h
#pragma once
#include <SDL.h>

class BoundingBox {
public:
  BoundingBox(const SDL_FRect& InitialRect)
  : Rect{InitialRect} {}

private:
  SDL_FRect Rect;
};

We'll also add a SetPosition() method to move our bounding box by setting the x and y values of the SDL_FRect:

// BoundingBox.h
#pragma once
#include <SDL.h>
#include "Vec2.h"

class BoundingBox {
public:
  BoundingBox(const SDL_FRect& InitialRect)
  : Rect{InitialRect} {}

  void SetPosition(const Vec2& Position) {
    Rect.x = Position.x;
    Rect.y = Position.y;
  }

private:
  SDL_FRect Rect;
};

Let's update our GameObject class to give each object a BoundingBox. Our GameObject instances already have a Position variable that can be used to set the top-left corner of the bounding box. We also need to specify its width and height, so let's add those as constructor parameters:

// GameObject.h
// ...
#include "BoundingBox.h"
// ...

class GameObject {
 public:
  GameObject(
    const std::string& ImagePath,
    const Vec2& InitialPosition,
    float Width,
    float Height,
    const Scene& Scene
  ) : Image{ImagePath},
      Position{InitialPosition},
      Scene{Scene},
      Bounds{SDL_FRect{
        InitialPosition.x, InitialPosition.y,
        Width, Height
      }}
    {}
    
  // ...
  
 private:
  // ...
  BoundingBox Bounds;
};

Once we've calculated our object's new Position at the end of each Tick() function, we'll notify our bounding box:

// GameObject.h
// ...

class GameObject {
 public:
  // ...
  void Tick(float DeltaTime) {
    ApplyForce(FrictionForce(DeltaTime));
    ApplyForce(DragForce());
    Velocity += Acceleration * DeltaTime;
    Position += Velocity * DeltaTime;

    Acceleration = {0, -9.8};

    // Don't fall through the floor
    if (Position.y < 2) {
      Position.y = 2;
      Velocity.y = 0;
    }
    Bounds.SetPosition(Position); 
    Clamp(Velocity);
  }
  // ...
};

Finally, let's update our scene to include the width and height of our objects, bearing in mind that our world space uses meters as its unit of distance.

In this case, the 1.9 and 1.7 values were chosen based on the size of the image we're using to represent our character:

// Scene.h
// ...

class Scene {
public:
  Scene() {
    Objects.emplace_back(
      "dwarf.png", Vec2{6, 2}, 1.9, 1.7, *this);
  }
  // ...
};

Drawing Bounding Boxes

Normally, bounding boxes are not rendered to the screen but, when developing a complex game, it's useful to have that capability to help us understand what's going on.

To enable this, we need to be able to convert our world space bounding box to an equivalent screen space version. Our Scene class already has a ToScreenSpace() function for converting Vec2 objects from world space to screen space. We'll overload this function with a variation that works with SDL_FRect objects.

To convert the rectangle's top-left corner (x, y), we can use our existing ToScreenSpace(Vec2) function. For the width (w) and height (h), we only need to scale them by the horizontal and vertical scaling factors, respectively:

// Scene.h
// ...

class Scene {
public:
  // ...
Vec2 ToScreenSpace(Vec2& Pos) {/*...*/} SDL_FRect ToScreenSpace(const SDL_FRect& Rect) const { Vec2 ScreenPos{ToScreenSpace(Vec2{Rect.x, Rect.y})}; float HorizontalScaling{Viewport.w / WorldSpaceWidth}; float VerticalScaling{Viewport.h / WorldSpaceHeight}; return { ScreenPos.x, ScreenPos.y, Rect.w * HorizontalScaling, Rect.h * VerticalScaling }; } // ... };

Next, we'll add a Render() function to our BoundingBox class. We'll need to access our Scene for the ToScreenSpace() function we just created.

We need access to the Scene object within our bounding box's Render() method, but we can't #include the Scene header file directly in BoundingBox.h. Doing so would create a circular dependency because Scene.h includes GameObject.h, which in turn includes BoundingBox.h.

To resolve this, we can forward-declare Scene in BoundingBox.h. We then include "Scene.h" in a new implementation file - BoundingBox.cpp - where the Render() method is actually defined and needs the full Scene definition.

// BoundingBox.h
// ...

class Scene; 
class BoundingBox {
public:
  // ...

 void Render(
   SDL_Surface* Surface, const Scene& Scene);

  // ...
};
// BoundingBox.cpp
#include <SDL.h>
#include "Scene.h"
#include "BoundingBox.h"

void BoundingBox::Render(
  SDL_Surface* Surface, const Scene& Scene
) {
  // ...
}

In addition to converting our bounding box's SDL_FRect to screen space, we also need to round its x, y, w, and h floating point numbers to integers to specify which pixels they occupy on the SDL_Surface.

To do this rounding, we'll create a function for converting SDL_FRect objects to SDL_Rects.

This rounding conversion is a general utility function. Often, you'd place such utilities in a separate header file. For simplicity in this lesson, we'll add it directly to our BoundingBox class:

// BoundingBox.h
// ...

class BoundingBox {
public:
  // ..
  static SDL_Rect Round(const SDL_FRect& Rect) {
    return {
      static_cast<int>(std::round(Rect.x)),
      static_cast<int>(std::round(Rect.y)),
      static_cast<int>(std::round(Rect.w)),
      static_cast<int>(std::round(Rect.h))
    };
  }
  // ...
};

Since the Round() function doesn't need access to any specific BoundingBox object's data (like Rect), we can declare it as static.

This makes it usable even in situations where we don't have a bounding box instance - we can invoke it using BoundingBox::Round() in those scenarios.

We can now combine our ToScreenSpace() and Round() functions to get our bounding box in screen space, and with its components as rounded int values:

// BoundingBox.cpp
// ...

void BoundingBox::Render(
  SDL_Surface* Surface, const Scene& Scene
) {
  auto [x, y, w, h]{
    Round(Scene.ToScreenSpace(Rect))};
    
  // ...
};

Unlike previous examples, we don't want the full area of our rectangle to be filled with a solid color. Instead, we just want to draw it as a rectangular border:

To do this, we can draw four lines - one for each of the top, bottom, left, and right edges. To draw each line, we can simply render a thin rectangle.

For example, our top border will start at the top left of our bounding box and span its full width. As such, it will share its x, y, and w values. However, the height (h) of this rectangle will be much shorter, representing the thickness of our line.

Let's set that up:

// BoundingBox.cpp
// ...

void BoundingBox::Render(
  SDL_Surface* Surface, const Scene& Scene
) {
  auto [x, y, w, h]{
    Round(Scene.ToScreenSpace(Rect))};
    
  int LineWidth{4};
  SDL_Rect Top{x, y, w, LineWidth};
  // ...
};

We can draw the rectangle representing our line in the usual way, passing the surface, rectangle, and color to SDL_FillRect():

// BoundingBox.cpp
// ...

void BoundingBox::Render(
  SDL_Surface* Surface, const Scene& Scene
) {
  auto [x, y, w, h]{
    Round(Scene.ToScreenSpace(Rect))};
    
  int LineWidth{4};
  SDL_Rect Top{x, y, w, LineWidth};
    
  Uint32 LineColor{SDL_MapRGB(
    Surface->format, 220, 0, 0)};  
  SDL_FillRect(Surface, &Top, LineColor); 
}

Let's draw our other edges by following the same logic. Because each SDL_Rect defines the top left corner of where the rectangle will be drawn, the right edge will draw slightly outside of our bounding box if we don't intervene.

To fix this, we move it left by reducing its x value based on our LineWidth:

The bottom edge will have a similar problem where it is rendered below our bounding box, so we need to move it up by the LineWidth value. Because we're working with screen space values at this point in our function, moving up corresponds to reducing the y value.

Putting everything together looks like this:

// BoundingBox.cpp
// ...

void BoundingBox::Render(
  SDL_Surface* Surface, const Scene& Scene
) {
  auto [x, y, w, h]{
    Round(Scene.ToScreenSpace(Rect))};
    
  int LineWidth{4};
  Uint32 LineColor{SDL_MapRGB(
    Surface->format, 220, 0, 0)};
    
  SDL_Rect Top{x, y, w, LineWidth};
  SDL_Rect Left{x, y, LineWidth, h};
  SDL_Rect Bottom{
    x, y + h - LineWidth, w, LineWidth};
  SDL_Rect Right{
    x + w - LineWidth, y, LineWidth, h};

  SDL_FillRect(Surface, &Top, LineColor);
  SDL_FillRect(Surface, &Left, LineColor);
  SDL_FillRect(Surface, &Bottom, LineColor);
  SDL_FillRect(Surface, &Right, LineColor);
}

Finally, we need to update our GameObject class to call the Render() method on our bounding box. We only want the bounding boxes to be drawn when we're debugging our program, so this is a scenario where we might want to use a preprocessor directive:

// GameObject.cpp
// ...
#include "BoundingBox.h" 

#define DRAW_BOUNDING_BOXES 

void GameObject::Render(SDL_Surface* Surface) {
  Image.Render(
    Surface, Scene.ToScreenSpace(Position)
  );
#ifdef DRAW_BOUNDING_BOXES 
  Bounds.Render(Surface, Scene); 
#endif 
}

// ...

If we run our program, we should now see our character's bounding box drawn, and its position updates as our character moves:

Note that our screenshots include additional trajectory lines to show how objects move. The code to render these lines is included in the GameObject.cpp file below for those interested.

Complete Code

A complete version of our BoundingBox class is included below. We have also provided the GameObject and Scene classes with the code we added in this lesson highlighted:

Summary

In this lesson, we introduced axis-aligned bounding boxes (AABBs) as a simple way to represent the space occupied by game objects. We created a BoundingBox class using SDL_FRect to store its position and dimensions in world space, integrated it into our GameObject, and updated the bounding box's position each frame.

We also implemented a rendering function to draw the bounding box outlines for debugging, requiring coordinate space conversion and rounding. Key Takeaways:

  • Bounding Boxes: Simple shapes (like rectangles) used to approximate complex object boundaries for efficient interaction checks.
  • AABBs: Axis-Aligned Bounding Boxes; rectangles that aren't rotated, simplifying calculations.
  • Representation: AABBs can be defined by top-left corner and dimensions (x, y, w, h) or by two opposing corners. SDL_FRect uses the former with floats.
  • Implementation: We created a BoundingBox class storing an SDL_FRect and added it to GameObject.
  • Updating: The bounding box position must be updated whenever the GameObject's position changes.
  • Rendering: Drawing bounding boxes involves converting world coordinates to screen coordinates and rounding to integer pixel values (from SDL_FRect to SDL_Rect) before drawing.
Next Lesson
Lesson 109 of 129

Intersections and Relevancy Tests

Optimize games by checking object intersections with functions like SDL_HasIntersection().

Have a question about this lesson?
Purchase the course to ask your own questions