Lazy Foo' Productions

SDL Forums external link SDL Tutorials Articles OpenGL Tutorials OpenGL Forums external link
Follow BlueSky Follow Facebook Follow Twitter Follow Threads
Donate
News FAQs Contact Bugs

Multiple Source Files

Last Updated: Aug 1st, 2022

As you saw with the last article, program source codes can get pretty big. Here you'll learn how to break your program's source into smaller, easier to manage pieces.
The state machine demo had over 1500 lines of code. Any of you that have tried know it's a pain having to search through one big source file. By having your program's source split into multiple source files, you no longer have to sift through one big chunk of code.

When dealing with multiple source files there's basically two types of files you're going to be dealing with: source files and header files. Standard *.cpp source files you already know as you've been using them since you started programming. Headers, however, are a bit tricky.

To try to understand what headers do, look in the SDL_image.h file. Inside, you'll see a bunch of declarations of the functions in the SDL_image API like IMG_Load(), IMG_GetError(), IMG_isPNG(), etc, but you don't actually see the definitions of the functions that load the images, get the errors, and such. Is all that headers do is declare constants/classes/functions/variables?

The answer is yes. All SDL_image.h does is tell the compiler about the functions in SDL_image so it can compile your source file. Is that all it needs? Well, when you try to build your executable, the compiler will compile your source file, then the linker will try to link everything together into one binary. After it compiles your .cpp file, it will try to look up the actual definitions for the SDL_image functions. When the linker doesn't find the function definitions in one of the *.cpp files in your project, it will complain that it can't find the definitions and abort.

Well where is the actual code for SDL_image functions? The functions are compiled inside of the SDL_image binary (*.dll on windows and *.so on *nix). To get the linker to stop complaining, we give it a lib file which tells the linker where function definitions are in the SDL_image binary so it can link dynamically at the program's run time.

This might not make much sense now, but it will after we get our hands dirty by splitting up the motion tutorial source code into mulitple files.
From Constants.h
#ifndef CONSTANTS_H
#define CONSTANTS_H

//Screen dimension constants
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;

#endif 
Here's the code inside of the constants.h file. This header file declares the constants we're going to use in the program.

The preprocessors at the top and the bottom might be new to you. For those of you who don't know what a preprocessor is, it's basically a way to talk to the compiler. For example the #include preprocessor tells the compiler to include a file into the code.

What "#ifndef CONSTANTS_H" does is asks if CONSTANTS_H is not defined. If CONSTANTS_H is not defined, the next line defines CONSTANTS_H. Then we continue on with the code that defines the constants. Then #endif serves as the end of the "#ifndef CONSTANTS_H" condition block.

Now why did we do that?
#include "Constants.h"
#include "Constants.h"
Let's say we had a situation where we included the same file twice.

In the first line where we include Constants.h, the compiler will check if CONSTANTS_H is defined. Since it's not, it will define CONSTANTS_H and use the constants code inside the Constants.h normally.

In the second line when we try to include Constants.h, the compiler will try check if CONSTANTS_H is defined. Because CONSTANTS_H was defined already, it will skip past the code that defines the constants. This prevents the constants from being defined twice and causing a conflict.

So now you see how this simple but effective safeguard works.
#ifndef BACON
#define BACON

//The screen attributes
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
const int SCREEN_BPP = 32;

//The frame rate
const int FRAMES_PER_SECOND = 20;

//The dimensions of the dot
const int DOT_WIDTH = 20;
const int DOT_HEIGHT = 20;

#endif
Just for the sake of information, what definition you check for doesn't matter. The above code will work perfectly. The thing is, using FILENAME_H is the common naming standard and as I have mentioned before it's important to use a naming standard.
From Dot.h
#ifndef DOT_H
#define DOT_H

#include <SDL.h>

//The dot that will move around on the screen
class Dot
{
public:
    //The dimensions of the dot
    static const int DOT_WIDTH = 20;
    static const int DOT_HEIGHT = 20;

    //Maximum axis velocity of the dot
    static const int DOT_VEL = 10;

    //Initializes the variables
    Dot();

    //Takes key presses and adjusts the dot's velocity
    void handleEvent( SDL_Event& e );

    //Moves the dot
    void move();

    //Shows the dot on the screen
    void render();

private:
    //The X and Y offsets of the dot
    int mPosX, mPosY;

    //The velocity of the dot
    int mVelX, mVelY;
};

#endif 
Here's the Dot.h file which declares the Dot class. The general rule is you should have a header file associated with each source file and one class per header/source pair. It's a loose rule that often gets broken in real professional games, but I recommend sticking to it when you can.
From Util.h
#ifndef UTIL_H
#define UTIL_H

//Starts up SDL and creates window
bool init();

//Loads media
bool loadMedia();

//Frees media and shuts down SDL
void close();

#endif 
Here are the declarations for our utility functions. Having a bunch of stray functions can be awkward to manage which is why functions are typically tied to some class.
From Globals.h
#ifndef GLOBALS_H
#define GLOBALS_H

#include "LTexture.h"

//The window we'll be rendering to
extern SDL_Window* gWindow;

//The window renderer
extern SDL_Renderer* gRenderer;

//Scene textures
extern LTexture gDotTexture;

#endif 
Here's the Globals.h which contains the declarations for the global variables in the program. At the top you see we included LTexture.h because the header needs to know what an LTexture is.

Now the extern keyword you see infront of the global variables might be new to you. Well remember that header files are just supposed to inform the compiler that something exists because the compiler compiles each cpp file independently of one another. If we didn't have extern infront of the globals, when the header is included in a source file the compiler will create a copy of the variable for that source file. Then when the linker tries to link everything together, it will find multiple copies of the same variable and it will complain and abort.

The extern keyword just tells the compiler the variable exists somewhere. Now you won't have multiple definitions of the same variable, but where are the actual globals located?
From Globals.cpp
#include "Globals.h"
#include "LTexture.h"

//The window we'll be rendering to
SDL_Window* gWindow = NULL;

//The window renderer
SDL_Renderer* gRenderer = NULL;

//Scene textures
LTexture gDotTexture;
When the linker looks for the definitions for the of the globals, it'll find them in the Globals.cpp source file. Also notice this is where we initialize the globals which makes sense since these are the actual variables and not just a declaration.

Just a tip: in case you mess up with the globals and get a multiple definiton error and then you fix the source code but the linker still complains, try rebuilding the whole project. To save time, the compiler will only recompile the source codes that have been changed, and since the source files haven't been changed, the linker stll has them compiled with multiple definitons. Rebuilding them will get rid of the old compiled code.
From Dot.h
#include "Dot.h"
#include "Globals.h"
#include "Constants.h"

Dot::Dot()
{
    //Initialize the offsets
    mPosX = 0;
    mPosY = 0;

    //Initialize the velocity
    mVelX = 0;
    mVelY = 0;
}
Here's the top of the Dot.cpp file which defines the Dot class.

As a general rule, make sure to only include the files you need to. Just including everything can cause unnecessary overhead and dependencies when compiling.
From Util.cpp
#include "Util.h"
#include "Globals.h"
#include "Constants.h"

bool init()
{
    //Initialization flag
    bool success = true;

    //Initialize SDL
    if( SDL_Init( SDL_INIT_VIDEO ) < 0 )
    {
        printf( "SDL could not initialize! SDL Error: %s\n", SDL_GetError() );
        success = false;
    }
    else
    {
        //Set texture filtering to linear
        if( !SDL_SetHint( SDL_HINT_RENDER_SCALE_QUALITY, "1" ) )
        {
            printf( "Warning: Linear texture filtering not enabled!" );
        }

        //Create window
        gWindow = SDL_CreateWindow( "SDL Tutorial", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN );
        if( gWindow == NULL )
        {
            printf( "Window could not be created! SDL Error: %s\n", SDL_GetError() );
            success = false;
        }
        else
        {
            //Create vsynced renderer for window
            gRenderer = SDL_CreateRenderer( gWindow, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC );
            if( gRenderer == NULL )
            {
                printf( "Renderer could not be created! SDL Error: %s\n", SDL_GetError() );
                success = false;
            }
            else
            {
                //Initialize renderer color
                SDL_SetRenderDrawColor( gRenderer, 0xFF, 0xFF, 0xFF, 0xFF );

                //Initialize PNG loading
                int imgFlags = IMG_INIT_PNG;
                if( !( IMG_Init( imgFlags ) & imgFlags ) )
                {
                    printf( "SDL_image could not initialize! SDL_image Error: %s\n", IMG_GetError() );
                    success = false;
                }
            }
        }
    }

    return success;
}
Here's the top of Util.cpp file. Another common convention to follow is to include the matching header file at the top of the include block, and then include the supplementary headers afterward. Remember how header files are included and processed sequentially, so you want to maintain a consistent order to prevent inconsistent behavior.
From Main.cpp
#include "Util.h"
#include "Dot.h"
#include "Globals.h"

int main( int argc, char* args[] )
{
    //Start up SDL and create window
    if( !init() )
    {
        printf( "Failed to initialize!\n" );
    }
    else
    {
        //Load media
        if( !loadMedia() )
        {
            printf( "Failed to load media!\n" );
        }
        else
        {    
            //Main loop flag
            bool quit = false;

            //Event handler
            SDL_Event e;

            //The dot that will be moving around on the screen
            Dot dot;

            //While application is running
            while( !quit )
            {
                //Handle events on queue
                while( SDL_PollEvent( &e ) != 0 )
                {
                    //User requests quit
                    if( e.type == SDL_QUIT )
                    {
                        quit = true;
                    }

                    //Handle input for the dot
                    dot.handleEvent( e );
                }

                //Move the dot
                dot.move();

                //Clear screen
                SDL_SetRenderDrawColor( gRenderer, 0xFF, 0xFF, 0xFF, 0xFF );
                SDL_RenderClear( gRenderer );

                //Render objects
                dot.render();

                //Update screen
                SDL_RenderPresent( gRenderer );
            }
        }
    }

    //Free resources and close SDL
    close();

    return 0;
}
Finally The main function itself is exactly the same as its single source file counterpart. Even though this could be considered a utility function, it is convention to put the main function in its own source file.