SDL_HasIntersection()
.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.
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:
The mechanism that checks if a specific object or behavior should currently be active is often called a relevancy test.
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:
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 , , , and 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 , , , and 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:
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.
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 meters away from the player. The player’s physics is still calculated, because the distance from the player to the player is inherently :
Calculating Physics
Skipping Physics
Calculating Physics
Skipping Physics
Calculating Physics
Skipping Physics
...
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.
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:
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
...
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.
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.
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:
y
position reduced by their height (h
)SDL_IntersectFRect()
to calculate the intersection of these rectangles within SDL’s coordinate system.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;
}
};
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;
}
}
}
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;
};
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:
SDL_HasIntersection()
, SDL_IntersectRect()
, SDL_HasIntersectionF()
, SDL_IntersectFRect()
.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.Optimize games by checking object intersections with functions like SDL_HasIntersection()
.
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games