Intersections and Relevancy Tests

Optimize games by checking object intersections with functions like SDL_HasIntersection().
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Get Started for Free
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Posted

In this lesson, we'll learn how to determine if shapes overlap in our game world, a process often called intersection testing. We'll focus on using SDL's built in functions like SDL_HasIntersection() and SDL_IntersectRect() to check if SDL_Rect and SDL_FRect objects intersect.

We'll also see how these tests are crucial for relevancy testing - optimizing our game by only updating objects that are currently important, like those near the player or visible on screen.

Relevancy Testing

As our worlds get bigger, our players will only be interacting with a small portion of the level at any given time. Because of this, objects that are not close to the player’s location, or objects that are not in view of the camera, become less important.

To optimize our game and keep everything running quickly, we can "turn off" those objects (or turn off some of their behaviors) until they become relevant again. For example:

  • An object that is not on the screen does not need to be animated
  • An object that is not near any player does not need to be simulating physics or AI behaviors
  • In a multiplayer game, a player on one side of the map does not need to be sending network updates describing their actions to a player on the opposite end of the map who can’t see them

The mechanism that checks if a specific object or behavior should currently be active is often called a relevancy test.

Intersections and Distance

In a 2D game, relevancy testing is often done by checking if two rectangles intersect. This is because objects typically have rectangular bounding boxes. Our viewport is also a rectangle, so we can determine if an object is currently visible by determining if its bounding box rectangle intersects with the viewport rectangle:

Diagram showing the intersection of bounding boxes with the viewport

We don’t necessarily want to turn every behavior off just because an object isn’t currently visible. For example, if we put objects to sleep when they’re not on the screen, the player could escape from enemies simply by turning their camera away from them.

Equally, however, we don’t want to unnecessarily calculate AI behaviors for an enemy that is nowhere near the player. So, our AI system might implement a relevancy check that uses a world space distance comparison, instead:

Diagram showing examples of world space distance comparisons

Rectangular Intersections

The way we’re representing rectangles in this course is with SDL’s built-in SDL_Rect and SDL_FRect structures. As we’ve seen, these use the xx, yy, ww, and hh convention to represent a rectangle:

Diagram showing the SDL_Rect implementation of a bounding box

As a logic puzzle, you may want to consider how we could write an algorithm to determine if two SDL_Rect objects intersect by comparing their xx, yy, ww, and hh values.

However, we don’t need to, as SDL has already provided some helpful functions to do just this.

SDL_HasIntersection()

We can use the SDL_HasIntersection() function to check if two SDL_Rect objects intersect.

This function accepts two pointers to the relevant rectangles, and returns SDL_TRUE if there is any intersection, or SDL_FALSE otherwise:

#include <iostream>
#include <SDL.h>

int main(int argc, char** argv) {
  SDL_Rect A{100, 100, 100, 100};
  SDL_Rect B{150, 150, 100, 100};
  if (SDL_HasIntersection(&A, &B)) {
    std::cout << "Those are intersecting";
  }

  return 0;
}
Those are intersecting

SDL_IntersectRect()

The intersection of two rectangles is, itself, a rectangle:

Diagram showing the intersection of two rectangles

For more complex interactions, rather than the simple true or false returned by SDL_HasIntersection(), it may be helpful to retrieve this rectangle so we can analyze the exact nature of the intersection.

To do this, we have the SDL_IntersectRect() function. This function works in the same way as SDL_HasIntersection(), except we can provide a third pointer:

#include <iostream>
#include <SDL.h>

int main(int argc, char** argv) {
  SDL_Rect A{100, 100, 100, 100};
  SDL_Rect B{150, 150, 100, 100};
  SDL_Rect C;
  if (SDL_IntersectRect(&A, &B, &C)) {
    std::cout << "Those are intersecting"
      << "  x = " << C.x
      << ", y = " << C.y
      << ", w = " << C.w
      << ", h = " << C.h;
  }

  return 0;
}

The SDL_Rect pointed at by the third argument will be updated with the intersection rectangle:

Those are intersecting  x = 150, y = 150, w = 50, h = 50

Note that if the third argument is a nullptr, then SDL_IntersectRect() will return SDL_FALSE, even if the first two rectangles intersect:

#include <iostream>
#include <SDL.h>

int main(int argc, char** argv) {
  SDL_Rect A{100, 100, 100, 100};
  SDL_Rect B{150, 150, 100, 100};
  if (SDL_IntersectRect(&A, &B, nullptr)) {
    // ...
  } else {
    std::cout << "No intersection";
  }

  return 0;
}
No intersection

If we're not sure whether our third argument is a nullptr, we should proactively check and switch to SDL_HasIntersection() if it is:

#include <iostream>
#include <SDL.h>

int main(int argc, char** argv) {
  SDL_Rect A{100, 100, 100, 100};
  SDL_Rect B{150, 150, 100, 100};
  SDL_Rect* C{nullptr};

  if (C && SDL_IntersectRect(&A, &B, C)) {
    std::cout << "Those are intersecting and "
    "I have updated C";
  } else if (SDL_HasIntersection(&A, &B)) {
    std::cout << "Those are intersecting";
  }

  return 0;
}
Those are intersecting

SDL_HasIntersectionF() and SDL_IntersectFRect()

Variations of SDL_HasIntersection and SDL_IntersectRect() are available to compare SDL_FRect objects.

SDL_HasIntersectionF() and SDL_IntersectFRect() work in the same way as their SDL_Rect counterparts, except all the arguments are pointers to SDL_FRect objects instead.

Distance-Based Relevancy Tests

Let’s see an example of adding some relevancy testing to our scene. We’ll implement a relevancy check for our physics system, and skip physics simulations for objects that are more than 20 meters away from the player character.

Let’s update our Scene class with a second object, as well as a function that gets the player character. We’ll just assume the first object in our scene is the player character:

// Scene.h
// ...

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

  const GameObject& GetPlayerCharacter() const {
    return Objects[0];
  }

  // ...
};

In our GameObject class, we’ll add a public getter to grant access to the object’s current position:

// GameObject.h
// ...

class GameObject {
public:
  const Vec2& GetPosition() const {
    return Position;
  }
  // ...
private:
  Vec2 Position{0, 0};
  // ...
};

Finally, in our GameObject::Tick() function, we’ll calculate how far away the object is from the player, and return early if the distance is over our 20-meter threshold:

// GameObject.cpp
// ...

void GameObject::Tick(float DeltaTime) {
  if (Scene.GetPlayerCharacter().GetPosition()
           .GetDistance(Position) > 20) {
    std::cout << "Skipping Physics\n";
    return;
  }
  std::cout << "Calculating Physics\n";
  // ...
}

Now, each frame skips the physics calculation for our dragon because it’s more than 2020 meters away from the player. The player’s physics is still calculated, because the distance from the player to the player is inherently 00:

Calculating Physics
Skipping Physics
Calculating Physics
Skipping Physics
Calculating Physics
Skipping Physics
...

Updating Class Code to use Getters

When we add a getter to a class, it is worth considering whether we should update the functions within that class to use this getter too, rather than accessing the member directly. For example:

// GameObject.h
// ...

class GameObject {
// ...
private:
  float GetFrictionCoefficient() const {
    // Before:
    return Position.y > 2 ? 0 : 0.5;
    // After:
    return GetPosition().y > 2 ? 0 : 0.5;
  }
  // ...
};

Standardizing how a value is accessed means that future changes are easier, and can reduce the risk of bugs. For example, determining the Position of an object may get more complex in the future, and we might decide to implement that logic in the getter:

// GameObject.h
// ...

class GameObject {
public:
  // ...  
  const Vec2& GetPosition() const {
    return isLoading ? LoadingPosition : Position; 
  }
  // ...
};

However, code that is bypassing our getter will also bypass that logic, and we may not notice the bug.

Intersection-Based Relevancy Tests

In the following example, we implement an imaginary animation system, but the animation is only relevant if the object is on the screen. We test if an object is currently visible by comparing the screen space coordinates of its bounding box to the viewport’s rectangle, which we’re currently storing on the Scene object.

Let’s update our BoundingBox to make its Rect accessible:

// BoundingBox.h
// ...

class BoundingBox {
public:
  // ...
  const SDL_FRect GetRect() const {
    return Rect;
  }

private:
  SDL_FRect Rect;
};

We’ll also update our Scene to make its Viewport accessible:

// Scene.h
// ...

class Scene {
public:
  // ...
  const SDL_Rect& GetViewport() const {
    return Viewport;
  }

private:
  SDL_Rect Viewport;
  // ...
};

We’ll add a CalculateAnimation() function to our GameObject class, and call it on every Tick():

// GameObject.h
// ...

class GameObject {
// ...
private:
  void CalculateAnimation();
  // ...
};
// GameObject.cpp
// ...

void GameObject::CalculateAnimation() {
  // ....
}

void GameObject::Tick(float DeltaTime) {
  CalculateAnimation();
  // ...
}

// ...

Before we can check if our bounding box and viewport rectangle are intersecting, we need to address two problems:

  1. Our bounding boxes are in world space, while the viewport is in screen space
  2. Our bounding boxes use floating point numbers (SDL_FRect) while our viewport uses integers (SDL_Rect).

We can address these problems by using the Scene.ToScreenSpace() and BoundingBox::Round() functions we added in the previous lesson:

// GameObject.cpp
// ...

void GameObject::CalculateAnimation() {
  SDL_Rect BoundingBox{BoundingBox::Round(
    Scene.ToScreenSpace(Bounds.GetRect())
  )};

  // ....
}

// ...

Finally, we can check if our object is currently visible using SDL_HasIntersection(), and skip animating if it isn’t:

// GameObject.cpp
// ...

void GameObject::CalculateAnimation() {
  SDL_Rect BoundingBox{BoundingBox::Round(
    Scene.ToScreenSpace(Bounds.GetRect())
  )};

  if (!SDL_HasIntersection(
    &Scene.GetViewport(), &BoundingBox
  )) {
    std::cout << "Not visible - skipping\n";
  }

  std::cout << "Calculating animation\n";
  // ....
}
// ...
Calculating animation
Not visible - skipping
Calculating animation
Not visible - skipping
Calculating animation
Not visible - skipping
...

Tick Dependencies and Off-By-One Logic

As we progressively add more and more logic to be executed on each frame (that is, each iteration of our game loop), things can get a little complicated. This is because, at any given time within the frame, we may not be entirely clear which code has already run.

If ObjectA's Tick() function is pulling data from ObjectB, has ObjectB’s Tick() function been called yet for this frame? If not, ObjectA is using the state of ObjectB from the previous frame, effectively meaning that logic is running one frame behind.

In this case, our Scene is updating its Viewport value within the Render() function. However, our Render() functions are called after our Tick() function. This means that, if the viewport has changed, our GameObject is using values from the previous frame.

In many cases, something running one frame behind is not noticeable. However, where possible, we typically want to use the latest state. This is particularly true for logic that is performed as a result of player input, as we want our game to feel as responsive as possible.

Creating a system to manage these per-frame dependencies can be quite complex to set up. It may involve adding additional tick functions, such as a PostPhysicsTick() that runs after the physics simulations. This means that if some logic depends on the latest positional data, we can put it in PostPhysicsTick() so it runs after our physics code has updated every object.

Even within tick functions, we can still have inter-object dependencies. For example, if ObjectA’s frame-by-frame updates depend on the state of ObjectB, we typically want to ensure that ObjectB gets updated before ObjectA queries it.

As an example, this article explains Unreal Engine’s design for dealing with dependencies in the per-frame update process.

Checking Relevancy when Rendering

The most obvious optimization we can make for an object that is not within the viewport is to not render that object. So, we may be tempted to do something like this within our Render() function:

void GameObject::Render(SDL_Surface* Surface) {
  if (!isInViewport()) return;
  // ...
}

This is a good instinct but in this case, and most cases in general, it is unnecessary. The underlying APIs we use for rendering typically already include this optimization.

In our examples, we’re using SDL_BlitSurface() and SDL_BlitSurfaceScaled() for rendering. These functions already perform a very similar check internally to minimize unnecessary work if the relevant rectangles do not intersect.

World Space Intersections

SDL’s built-in rectangle intersection functions like SDL_HasIntersection() and SDL_IntersectFRect() assume that the provided rectangles use the "y-down" convention. That is, they assume that increasing y values correspond to moving vertically down within the space.

That has been true of our previous examples, as we were comparing rectangles defined in screen space, or rectangles converted to screen space. However, for rectangles defined in our definition of world space, where increasing y values correspond to moving up, we need an alternative.

We can create these "y-up" intersection functions from scratch if we want, or we can adapt how we use the SDL functions.

If we wanted to use SDL’s functions, we have four steps:

  1. Start with rectangles defined using the y-up convention.
  2. Create copies of these rectangles that have their y position reduced by their height (h)
  3. Use an SDL function like SDL_IntersectFRect() to calculate the intersection of these rectangles within SDL’s coordinate system.
  4. If we need the intersection rectangle, increase its y value by its height to convert it to the y-up representation.

To understand why this works, we can step through the logic visually:

Diagram showing the process of converting between y-up and y-down representations

Let’s add a function to our BoundingBox class that implements this procedure to calculate world-space intersections between our bounding boxes:

// BoundingBox.h
// ...

class BoundingBox {
public:
  // ...
  bool GetIntersection(
    const BoundingBox& Other,
    SDL_FRect* Intersection
  ) {
    if (!Intersection) return false;
    
    // Step 1
    SDL_FRect A{GetRect()};
    SDL_FRect B{Other.GetRect()};
    
    // Step 2
    A.y -= A.h;
    B.y -= B.h;
    
    // Step 3
    SDL_IntersectFRect(&A, &B, Intersection);
    
    // If the intersection rectangle has no area,
    // there was no intersection
    if (Intersection->w <= 0 ||
        Intersection->h <= 0) {
      return false;
    }
    
    // Step 4
    Intersection->y += Intersection->h;
    
    return true;
  }
};

SDL_RectEmpty() and SDL_FRectEmpty()

Our previous example checks if a rectangle is "empty" by checking if it has no area. That is, if it has either no height (h) or no width (w).

SDL provides SDL_RectEmpty() and SDL_FRectEmpty() functions for this, which we can use if we prefer. Our previous code could be written like this:

// BoundingBox.h
// ...

class BoundingBox {
public:
  // ...
  bool GetIntersection(
    const BoundingBox& Other,
    SDL_FRect* Intersection
  ) {
if (Intersection->w <= 0 || Intersection->h <= 0) { return false; } if (SDL_FRectEmpty(Intersection)) { return false; }
} }

Complete Code

To keep things simple, we’ll remove our relevancy tests for now, but we’ll keep the getters we added in this lesson.

Our updated Scene, GameObject, and BoundingBox classes are provided below, with the code we added in this lesson highlighted:

#pragma once
#include <SDL.h>
#include <vector>
#include "GameObject.h"

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

  const GameObject& GetPlayerCharacter() const {
    return Objects[0];
  }
  
  const SDL_Rect& GetViewport() const {
    return Viewport;
  }

  Vec2 ToScreenSpace(const Vec2& Pos) const {
    auto [vx, vy, vw, vh]{Viewport};
    float HorizontalScaling{vw / WorldSpaceWidth};
    float VerticalScaling{vh / WorldSpaceHeight};

    return {
      vx + Pos.x * HorizontalScaling,
      vy + (WorldSpaceHeight - Pos.y)
      * VerticalScaling
    };
  }

  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
    };
  }

  Vec2 ToWorldSpace(const Vec2& Pos) const {
    auto [vx, vy, vw, vh]{Viewport};
    float HorizontalScaling{
      WorldSpaceWidth / vw};
    float VerticalScaling{
      WorldSpaceHeight / vh};

    return {
      (Pos.x - vx) * HorizontalScaling,
      WorldSpaceHeight - (Pos.y - vy)
      * VerticalScaling
    };
  }

  void HandleEvent(SDL_Event& E) {
    for (GameObject& Object : Objects) {
      Object.HandleEvent(E);
    }
  }

  void Tick(float DeltaTime) {
    for (GameObject& Object : Objects) {
      Object.Tick(DeltaTime);
    }
  }

  void Render(SDL_Surface* Surface) {
    SDL_GetClipRect(Surface, &Viewport);
    for (GameObject& Object : Objects) {
      Object.Render(Surface);
    }
  }

private:
  SDL_Rect Viewport;
  std::vector<GameObject> Objects;
  float WorldSpaceWidth{14}; // meters
  float WorldSpaceHeight{6}; // meters
};
// GameObject.h
#pragma once
#include <SDL.h>

#include "BoundingBox.h"
#include "Vec2.h"
#include "Image.h"

class Scene;

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
        }
      } {}

  void HandleEvent(const SDL_Event& E);

  const Vec2& GetPosition() const {
    return Position;
  }

  void Tick(float DeltaTime);

  void Render(SDL_Surface* Surface);

  void ApplyForce(const Vec2& Force) {
    Acceleration += Force / Mass;
  }

private:
  Image Image;
  const Scene& Scene;

  Vec2 Position{0, 0};
  Vec2 Velocity{0, 0};
  Vec2 Acceleration{0, -9.8};
  float Mass{70};
  BoundingBox Bounds;

  float DragCoefficient{0.2};

  Vec2 DragForce() const {
    return -Velocity * DragCoefficient
      * Velocity.GetLength();
  }

  float GetFrictionCoefficient() const {
    if (Position.y > 2) { return 0; }

    return 0.5;
  }

  Vec2 FrictionForce(float DeltaTime) const {
    float MaxMagnitude{
      GetFrictionCoefficient()
      * Mass * -Acceleration.y};
    if (MaxMagnitude <= 0) return Vec2(0, 0);

    float StoppingMagnitude{
      Mass *
      Velocity.GetLength() / DeltaTime};

    return -Velocity.Normalize() * std::min(
      MaxMagnitude, StoppingMagnitude);
  }

  void Clamp(Vec2& V) const {
    V.x = std::abs(V.x) > 0.01 ? V.x : 0;
    V.y = std::abs(V.y) > 0.01 ? V.y : 0;
  }

  void ApplyImpulse(const Vec2& Impulse) {
    Velocity += Impulse / Mass;
  }

  void ApplyPositionalImpulse(
    const Vec2& Origin, float Magnitude
  ) {
    Vec2 Displacement{Position - Origin};
    Vec2 Direction{Displacement.Normalize()};
    float Distance{Displacement.GetLength()};
    float AdjustedMagnitude{
      Magnitude /
      ((Distance + 0.1f) * (Distance + 0.1f))};

    ApplyImpulse(Direction * AdjustedMagnitude);
  }
};
#pragma once
#include <SDL.h>
#include "Vec2.h"

class Scene;

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

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

  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))
    };
  }
  
  bool GetIntersection(
    const BoundingBox& Other,
    SDL_FRect* Intersection
  ) {
    if (!Intersection) return false;
    SDL_FRect A{GetRect()};
    A.y -= A.h;
    SDL_FRect B{Other.GetRect()};
    B.y -= B.h;
    SDL_IntersectFRect(&A, &B, Intersection);

    if (SDL_FRectEmpty(Intersection)) {
      return false;
    }

    Intersection->y += Intersection->h;
    return true;
  }

  const SDL_FRect GetRect() const {
    return Rect;
  }

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

private:
  SDL_FRect Rect;
};

Summary

This lesson covered techniques for detecting intersections between SDL_Rect and SDL_FRect objects. We discussed the importance of these checks for implementing relevancy tests, which help make games run faster by skipping updates for non relevant objects.

We explored both distance based and intersection based relevancy and learned how to use SDL's specific functions for these tasks. Key Takeaways:

  • Games use intersection tests to see if objects (like bounding boxes) overlap.
  • SDL offers functions: SDL_HasIntersection(), SDL_IntersectRect(), SDL_HasIntersectionF(), SDL_IntersectFRect().
  • Relevancy testing optimizes performance by selectively updating game elements.
  • Common relevancy methods include checking distance to the player or intersection with the screen viewport.
  • Be mindful of coordinate systems (y up vs. y down) when performing intersection tests.
  • Third-party libraries like SDL’s SDL_BlitSurface() often perform their own relevancy checks internally, so it’s not always necessary for us to perform checks before using them. We can check the documentation to make sure.
Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Ryan McCombe
Ryan McCombe
Posted
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Get Started for Free
Motion and Collisions
Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Contact|Privacy Policy|Terms of Use
Copyright © 2025 - All Rights Reserved