Intersections and Relevancy Tests

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

Ryan McCombe
Published

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:

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:

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:

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.

Using 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

Using SDL_IntersectRect()

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

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

Using 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
...

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
...

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:

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

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:

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.
Next Lesson
Lesson 110 of 129

Handling Object Collisions

Implement bounding box collision detection and response between game objects

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