Hello and welcome to part 2 of my tutorial on 2D Frustum Culling.
Before we go any further with the tutorial, if you’re reading this and have not yet read part 1, it is definitely worth your while to go back and read it. It lays the foundations that we need before we can write our 2D Camera class, which will contain the frustum and frustum checking code.
If you’ve read part 1 and have either implemented its Vector2 and BoxCollider code or are using your own (or 3rd party equivalent), then you’re all set to continue into part 2!
2D Camera – Background theory:
Yes, theory…hooray! This is important though and good to know before we write code for the camera. First thing’s first, this is NOT an actual camera as you may think! No rendering, no fancy visual effects, nothing. Stripped down, it is simply a Vector2 which represents the “Position” if you like, of the ‘camera’.
This position is then used to offset all sprites (eg. player, enemies, bullets) as they are drawn, giving the illusion of a camera being used. Simply put, you can call this technique ‘side-scrolling’ or even ‘faked-scrolling’, as that’s essentially what this ‘camera’ class does – changes a position, then subtracts that position from the position of everything you want to scroll.
I’ve used this technique in several games and as simple as it is, it does the job quite well. If the explanation of how this camera will work didn’t make sense, the following code should clear things up. Create a new C++ header (‘.h’) file and name it ‘Camera‘.
Simple 2D ‘Camera’ class:
#ifndef CAMERA_H #define CAMERA_H #include "Collision.h" // Simple 2D 'Camera' class class Camera { public: // Function prototypes //---------------------// Camera(); // Default constructor // Set up frustum box using specified screen margins void InitFrustum(int a_iMargin_X = 20, int a_iMargin_Y = 20); // Return true if specified collider is within camera frustum bool InFrustum(BoxCollider* a_pCollider, bool a_bOverlap = true); // Member variables //---------------------// Vector2 m_Position; // 'Camera' offset position private: // Frustum box struct FrustumBox { // Edges of the frustum box float Left, Right, Top, Bottom; }Frustum; }; #endif // CAMERA_H // Externally define the 'Camera' class as 'camera' extern Camera camera;
The structure of the Camera class is done. Our frustum is simply a struct called ‘FrustumBox‘ (defined for use as ‘Frustum‘), which contains four float values. Each value represents a respective on-screen boundary, imagine the four boundaries together as a rectangle. Yes, that is our frustum ‘box’.
Create a new C++ source file (‘.cpp’) and name it ‘Camera‘.
‘InitFrustum’ function:
#include "Camera.h" // Define the 'Camera' class as 'camera' Camera camera; // Set up frustum box using specified screen margins void Camera::InitFrustum(int a_iMargin_X, int a_iMargin_Y) { Frustum.Left = SCREEN_W - (SCREEN_W - a_iMargin_X); Frustum.Right = SCREEN_W - a_iMargin_X; Frustum.Top = SCREEN_H - (SCREEN_H - a_iMargin_Y); Frustum.Bottom = SCREEN_H - a_iMargin_Y; }
You may have noticed in the function prototype, both the ‘a_iMargin_X‘ and ‘a_iMargin_Y‘ parameters are set to 20 by default. This will give you a margin of 20 on each side of the frustum box, between it and the respective edge of the screen. By passing other values into this function, you can control how big you want the frustum to be with ease. I found 20 was a decent starting value, which is why I used that.
Note that ‘SCREEN_W‘ and ‘SCREEN_H‘ are just example variables I made up, to represent the width and height of the screen. Replace these with your own variables or if you like, just straight numbers (eg. 1280, 720).
‘InFrustum’ function:
// Return true if specified collider is within camera frustum bool Camera::InFrustum(BoxCollider* a_pCollider, bool a_bOverlap) { // Set frustum boundaries to use, based on 'a_bOverlap' value float fFrustum_Left = a_bOverlap ? Frustum.Left - a_pCollider->Size.X : Frustum.Left; float fFrustum_Right = a_bOverlap ? Frustum.Right + a_pCollider->Size.X : Frustum.Right; float fFrustum_Top = a_bOverlap ? Frustum.Top - a_pCollider->Size.Y : Frustum.Top; float fFrustum_Bottom = a_bOverlap ? Frustum.Bottom + a_pCollider->Size.Y : Frustum.Bottom; if (a_pCollider->GetLeft() >= fFrustum_Left // Within left side of frustum && a_pCollider->GetRight() <= fFrustum_Right // Within right side of frustum && a_pCollider->GetTop() >= fFrustum_Top // Below top side of frustum && a_pCollider->GetBottom() <= fFrustum_Bottom) // Above bottom of frustum { // Collider is inside camera frustum return true; } // Collider is outside camera frustum return false; }
The return value of boolean parameter ‘a_bOverlap‘, determines whether we want to allow the collider to be PARTIALLY (overlapping) inside the frustum, in which case we would pass ‘true‘, or whether we want to make sure the collider is ENTIRELY inside the frustum, in which case we would pass ‘false‘.
The four temporary frustum values, are then either directly assigned to the default frustum values (‘a_bOverlap‘ is ‘false‘) or are assigned to the default frustum values with the collider’s width and height added or subtracted (‘a_bOverlap‘ is ‘true‘).
Note that the ‘a_bOverlap‘ parameter is set to ‘true‘ by default.
Example – putting it all together:
Now that we’ve finished the Camera class, it’s time for an example of putting it to use!
#include "Camera.h" BoxCollider myCollider; // BoxCollider for this example // Set up our collider (use your own values if you like) myCollider.Size = Vector2(60); myCollider.Position = Vector2(50); // INSIDE GAME LOOP (update/draw each frame) //------------------------------------------// if (camera.InFrustum(&myCollider)) { // The collider is inside the camera frustum! // Reset collider velocity myCollider.Velocity = Vector2(0); // Move collider left // If the 'A' key was pressed // myCollider.Velocity.X -= 40.0f; // Move collider right // If the 'D' key was pressed // myCollider.Velocity.X += 40.0f; // Apply velocity to collider position myCollider.Position += myCollider.Velocity; // Eg. Check for collision... // Eg. Draw object using this collider... }
Several comments in this example are pseudocode, because this tutorial doesn’t cover using a 3rd-party (or making one) engine for rendering and input. They still get the idea across of how you could go about updating the collider.
Part 2 Conclusion:
And there you have it, a complete simple, yet effective 2D Camera system with frustum culling functionality. This system can quite easily be built upon to allow for more advanced functionality, but as is, it’s all one needs for simple 2D culling (and even side-scrolling, as I mentioned earlier). On a side note, this tutorial has laid all the foundations needed for an upcoming tutorial I have planned!
If this tutorial has been helpful, I truly appreciate any thanks or feedback. You could share a link to this tutorial on another site, follow this blog or list this blog for thanks/special thanks in the credits of your game – any of these would be greatly appreciated!