Hello all and welcome to the first part in a series of articles about my personal game engine I’m currently developing in my spare time. I called it BlackOmen like the last dungeon in Chrono Trigger. It is coded in C++11, the latest standard of C++.
My goal with this series is to teach you what I learned along the way developing my engine. Even if I work professionally in the game industry, I still have much to learn about engine programming so feel free to give me constructive comments.
First of all, I want to talk to you about a fundamental header I’ve seen in many libraries, the platform definition header. This header contains type definition and macro for identify which platform your code is currently being compiled. It is very useful to make your code more portable.
What’s the big fuzz about types ?
Types are a fundamental part on any programming language. They help you tell the compiler how you want your variable to be stored and interpreted in memory. In a game, you are dealing with lots of data and knowing how your data is stored in memory can be critical in certain situations. Types are defined by the kind of data they represent and their size. They also have different size depending on which compiler and/or platform you are using.
As an example, the signed types in the Microsoft C++ compiler are defined like this:
|Type name||Size in memory (bytes)||Range|
|int||4||–2,147,483,648 to 2,147,483,647|
|bool||1||false or true|
|char||1||–128 to 127 by default
0 to 255 when compiled with /J
|short||2||–32,768 to 32,767|
|long||4||–2,147,483,648 to 2,147,483,647|
|float||4||3.4E +/- 38 (7 digits)|
|double||8||1.7E +/- 308 (15 digits)|
I want to point you to the char type. It says that depending on a compiler switch, it can be signed or unsigned. If you are using char to store a character, it’s fine. But if you use char naively to store a number of 1 byte size, you can expect different results depending of which platform you are and can cause weird bugs. If you want consistent behavior across the platforms you are supporting, you need to create typedef for each kind and size of type you want to use.
My platform definition header
In my engine, I created a file called platform_types.h. It contains all the required typedef and macro for platform support.
It starts with this:
#if defined(_WIN32) || defined(_WIN64) || defined(__CYGWIN__) #include "platform_windows.h" #else #error "Please define a platform" #endif
To ease maintenance, I decided to split platform support in a separate header so I can handle the various compilers for that platform. To identify the platform, I use macros that I’m 100% sure that are defined for the given platform. On Windows, if _WIN32 or _WIN64 is defined, you are pretty sure you are compiling on Windows. Notice also the
__CYGWIN__ test to make sure I support Cygwin also. I still only support Windows in my engine but I included an #error preprocessor notification to remind me to create the platform support header only I port my engine to another platform.
Windows platform header
Here’s how platform_windows.h looks like
#define BLACKOMEN_PLATFORM_WINDOWS #if defined(_MSC_VER) #if defined(_WIN32) #define BLACKOMEN_COMPILER_VISUALCPP_X86 #elif defined(_WIN64) #define BLACKOMEN_COMPILER_VISUALCPP_X64 #endif #define BLACKOMEN_COMPILER_VISUALCPP #pragma warning(disable: 4127) // Disable warning C4127: conditional expression is constant #elif defined(__CYGWIN__) #define BLACKOMEN_COMPILER_CYGWIN #else #error "Define the compiler typedef" #endif #if defined(BLACKOMEN_COMPILER_VISUALCPP_X86) typedef unsigned char byte; typedef signed char sbyte; typedef unsigned int uint; typedef unsigned int uint32; typedef signed int sint; typedef float float32; typedef double float64; typedef long long int64; typedef unsigned long long uint64; #elif defined(BLACKOMEN_COMPILER_VISUALCPP_X64) typedef unsigned char byte; typedef signed char sbyte; typedef unsigned int uint; typedef unsigned int uint32; typedef signed int sint; typedef float float32; typedef double float64; typedef long long int64; typedef unsigned long long uint64; #else #error "Please define the types for your compiler #endif #if defined(BLACKOMEN_COMPILER_VISUALCPP) #define ForceBreakpoint() __debugbreak() #else #error "Please define the ForceBreakpoint for your compiler" #endif
First of all I created my own define BLACKOMEN_PLATFORM_WINDOWS to allow me to put Windows specific code inside a define if I want to.
After that, I try to identify which compiler I use to create my typedef of the basic types. I also use this occasion to disable some annoying false positive warnings per compiler. When you are compiling with warning-as-errors, those false positives become quite annoying. As before, I add #error to remind me myself to add support for additional compilers if required.
Then come the meat of the header, The typedef. For each basic type, I create a new name with the size of the type specified for it, expect for byte. I decided also to name double float64 to emphasis the fact it’s a float but with greater size.
And at last, I add a nifty macro which can be handy while debugging called ForceBreakpoint(). It forces the debugger to break at that exact line. Useful when you want to debug a specific object and the conditional debugging is too slow. I might move this macro outside of platform specific headers in the future thought.
Next in my header is the target macro definition.
#if defined(DEBUG) || defined(_DEBUG) || defined(_BLACKOMEN_DEBUG) #define BLACKOMEN_DEBUG #elif defined(_BLACKOMEN_FINAL) #define BLACKOMEN_FINAL #elif defined(NDEBUG) || defined(_BLACKOMEN_RELEASE) #define BLACKOMEN_RELEASE #endif
It is used to create specific code depending of which target I’m compiling. For example, my debug info code is not compiled for the Final target. The target I’m using are Debug, Release and Final. Debug is the version with no optimization. Release is the version with full optimization but with debug information on. Final is the final release of the game with full optimization and no debug information.
I use the C++ preprocessor definitions in the project settings to set _BLACKOMEN_DEBUG, _BLACKOMEN_FINAL and _BLACKOMEN_RELEASE depending of the target.
DLL visibility macros
Next up is the DLL visibility macros. In a shared dynamic library, you can set the visibility of any function or class (symbols) to be public or private. This is done using various compiler specific attribute you can’t afford to use directly if you want portable code.
#if defined BLACKOMEN_PLATFORM_WINDOWS #define BLACKOMEN_DLL_EXPORT __declspec(dllexport) #define BLACKOMEN_DLL_IMPORT __declspec(dllimport) #define BLACKOMEN_DLL_LOCAL #elif __GNUC__ >= 4 || defined __clang__ #define BLACKOMEN_DLL_EXPORT __attribute__ ((visibility("default"))) #define BLACKOMEN_DLL_IMPORT __attribute__ ((visibility("default"))) #define BLACKOMEN_DLL_LOCAL __attribute__ ((visiblity("hidden"))) #else #define BLACKOMEN_DLL_EXPORT #define BLACKOMEN_DLL_IMPORT #define BLACKOMEN_DLL_LOCAL #endif #ifdef BLACKOMEN_DLL #ifdef BLACKOMEN_BUILDING_DLL #define BLACKOMEN_PUBLIC BLACKOMEN_DLL_EXPORT #else #define BLACKOMEN_PUBLIC BLACKOMEN_DLL_IMPORT #endif #define BLACKOMEN_LOCAL BLACKOMEN_DLL_LOCAL #else #define BLACKOMEN_PUBLIC #define BLACKOMEN_LOCAL #endif
On a Windows system, you need two attribute to make DLL visibility works. The first one is BLACKOMEN_DLL_EXPORT when you are buidling the DLL itself which tells the linker to make the symbols (functions or class) public. And when you are using code from the DLL, you need to specific BLACKOMEN_DLL_IMPORT for the linker to correctly import the symbols. This is why I use BLACKOMEN_BUILDING_DLL to switch between the two. On Windows BLACKOMEN_DLL_LOCAL does nothing.
On an UNIX/Linux system, either you set the visibility to default or to hidden. BLACKOMEN_DLL_EXPORT and BLACKOMEN_DLL_IMPORT are the same here. BLACOMEN_DLL_LOCAL tells the linker to hide the symbol.
In any case, if I want my symbols to be public, I use the macro BLACKOMEN_PUBLIC.
The last one is the BLACKOMEN_DLL define, it allows me to support static or dynamic library with the same code.
I don’t compile my engine in a DLL yet but I just put the macros there just in case I need them one day.
Engine specific types defitions
The last section on my platform definiton file is where I defined useful typedef to use for my game engine.
typedef const char* cstring; typedef const float32 timestep_t; typedef int64 system_tick_t; typedef float64 gametime_t;
cstring is just an alias for a C-style string. timestep_t is the delta time between frames, I wanted this to be explicit in the code so that’s why I created a typedef. Same for system_tick_t and gametime_t. system_tick_t is the system time from a high-precision timer and gametime_t is the elapsed time since the start of the game.
How I include this header
Instead of including this header is every file that I use, I found a compiler switch that does it for me.
In GCC and clang, it’s the flag –include that does this trick.
I agree it’s not the most portable solution but it’s works for me for now.
Having a platform defintion header is a key part in making your software portable across different compilers and systems. It is also a handy place to define DLL visibility macros and common types you use in your library/engine.