Spacecrash day 1 of 7: game skeleton
Time to get started. Today I am going to set up the main project skeleton, project directory, get it to compile on both Windows and OS X, set up loading textures, and get a spaceship to fly over some kind of scrolling background. Plus possibly have a few things flying around.
Setting things up will be a bit painful, since there are a lot of small things to configure correctly. Projects, modules, etc… if you are not all that fluent in C/C++, and something isn’t easy to understand, don’t despair: follow the instructions to set it up, ask your doubts on the comments, and remember that we will only be doing this type of stuff today, since once things are set up we will be concentrating in the game code. I am going quickly here, so you will be able to put in the time and effort after this week, and be able to understand all the details of today’s work when you look at it more slowly.
Good way to organize a project: create directory for the project wherever you like to work and call it with the project’s name (“spacecrash” here). Create three subdirectories: “src” for the source code, “data” for the resources that will be loaded by the game, and “prod” for intermediate production files.
Here’s how we’ll organize the code for our game: there will be a game.cpp file with the main logic of the game (remember, we’ll use C, but compiling as C++, thus the cpp extension). We will have a module called sys with two versions: sys_osx.cpp or sys_win.cpp for the OS-dependent part. If you are only going to do one OS, you can have just one. I will write both version here, to compile on both Windows and OS X, and the project on each platform will only include the relevant version. The trick is that both sys_osx.cpp and sys_win.cpp implement exactly the same functions, then there is a sys.h file with the declarations of these, and the rest of the program can call the functions in this module, thus becoming platform-independent. No game.h is necessary, since the other modules won’t be calling anything from game.cpp (game.cpp is a user of the other modules, not the other way around).
We’ll use prefixes for module functions: SYS_ for those in the platform layer (sys_win.cpp or sys_osx.cpp). It’s good practice and common in C, where there is no support for modules, classes or similar mechanisms.
The sys module will contain the OS-dependent logic, but the main loop will be in game.cpp, inside a function that I have decided to call “Main” (with uppercase “M”). This function has to perform the main game loop, calling the SYS_ functions, and return when the game has finished. The system-dependent sys_osx.cpp or sys_win.cpp is responsible of calling Main() to get everything started.
In case this is your first game dev experience, every game is centered around a “game loop”: this is a long running loop that does the following:
- It draws the game on the display for the player to see it
- It reads player input and processes it
- It runs the game logic (characters and enemies advancing, etc…)
- …and then just starts all over again!
This way, the player sees a continuously animated world, which is just the result of seeing many individual images per second. They can press keys that are checked every time around the loop. The game doesn’t wait for input: if no input is ready, it keeps looping and advancing the game, as is necessary for the game to progress independently of the player.
Files common to all platforms
You need several files which will be common for all platforms. First of all, it’s quite common in C/C++ projects to have a base.h file that is included everywhere, declaring some useful basic data types. Here it is:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// base.h #ifndef _BASE_H_ #define _BASE_H_ //----------------------------------------------------------------------------- // Basic types typedef unsigned char byte; typedef unsigned short int word; typedef unsigned int dword; typedef int sdword; typedef unsigned int uint; //----------------------------------------------------------------------------- // Useful macros #define SIZE_ARRAY(__a__) (sizeof(__a__)/sizeof(__a__[0])) //----------------------------------------------------------------------------- struct ivec2 { int x, y; }; //----------------------------------------------------------------------------- struct vec2 { float x, y; }; inline vec2 vmake (float x, float y) { vec2 r; r.x = x; r.y = y; return r; } inline vec2 vadd (vec2 v1, vec2 v2) { return vmake(v1.x + v2.x, v1.y + v2.y); } inline vec2 vsub (vec2 v1, vec2 v2) { return vmake(v1.x - v2.x, v1.y - v2.y); } inline vec2 vscale(vec2 v, float f) { return vmake(v.x * f, v.y * f); } inline float vlen2 (vec2 v) { return v.x * v.x + v.y * v.y; } inline float vlen (vec2 v) { return (float)sqrt(vlen2(v)); } inline float vdot (vec2 v1, vec2 v2) { return v1.x * v2.x + v1.y * v2.y; } inline vec2 vunit (float angle) { return vmake((float)cos(angle), (float)sin(angle)); } #endif |
Short explanation of the elements: the #ifndef/#define trick avoids errors if you include the file multiple times (common C/C++ idiom). The typedefs above define low-level types such as “byte”, “word”, etc… which do not exist in C/C++ by default. The SIZE_ARRAY macro is just for utility. We define an ivec2 structure type to contain integer x,y coordinate pairs (we will use it for mouse coordinates). And the last piece is our vector library: we define a vec2 structure type to hold a floating-point 2D vector, and functions to operate with them: addition, subtraction, length, do products, etc…
Apart from this, we need a sys.h file that contains the declarations of the system-dependent functions, which will be then defined differently on the different platforms:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
// sys.h #ifndef _SYS_H_ #define _SYS_H_ //======================================================================================== // Resolution (real & virtual) // Portrait 2:3 aspect ratio #define SYS_WIDTH 512 #define SYS_HEIGHT 768 #define SYS_FULLSCREEN 0 // Main game coordinate system: width is 1000, height is 1500. Origin bottom left #define G_WIDTH 1000 #define G_HEIGHT 1500 //======================================================================================== // Platform layer void SYS_Pump(); void SYS_Show(); bool SYS_GottaQuit(); void SYS_Sleep(int ms); bool SYS_KeyPressed(int key); ivec2 SYS_MousePos(); bool SYS_MouseButonPressed(int button); //----------------------------------------------------------------------------- #ifdef _WINDOWS #define SYS_KEY_UP VK_UP #define SYS_KEY_DOWN VK_DOWN #define SYS_KEY_LEFT VK_LEFT #define SYS_KEY_RIGHT VK_RIGHT #define SYS_MB_LEFT VK_LBUTTON #define SYS_MB_RIGHT VK_RBUTTON #define SYS_MB_MIDDLE VK_MBUTTON //----------------------------------------------------------------------------- #elif defined(__APPLE__) #include "TargetConditionals.h" #if defined(__MACH__) && TARGET_OS_MAC && !TARGET_OS_IPHONE #define SYS_KEY_UP GLFW_KEY_UP #define SYS_KEY_DOWN GLFW_KEY_DOWN #define SYS_KEY_LEFT GLFW_KEY_LEFT #define SYS_KEY_RIGHT GLFW_KEY_RIGHT #define SYS_MB_LEFT GLFW_MOUSE_BUTTON_LEFT #define SYS_MB_RIGHT GLFW_MOUSE_BUTTON_RIGHT #define SYS_MB_MIDDLE GLFW_MOUSE_BUTTON_MIDDLE #endif //defined(__MACH__) && TARGET_OS_MAC && !TARGET_OS_IPHONE #endif //defined(__APPLE__) //----------------------------------------------------------------------------- #endif |
As you can see, sys.h declares some functions that do system chores. The main game loop will call the various SYS functions to make sure the system goes running. The actual functions will be written differently in Windows and OS X, but they will do the same work, so that the game itself can be written just once.
You can see some global constants (width/height of the game window, width and height of the game coordinates we will use to refer to it, and you can see how some #ifdef trickery is used to define the constants for keys differently on Windows or OS X).
Here is the first version of the game code to test this set-up:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
// game0.cpp #include "stdafx.h" #include "base.h" #include "sys.h" //----------------------------------------------------------------------------- void Render() { glClear( GL_COLOR_BUFFER_BIT ); } //----------------------------------------------------------------------------- void StartGame() { } //----------------------------------------------------------------------------- void RunGame() { } //----------------------------------------------------------------------------- void ProcessInput() { } //----------------------------------------------------------------------------- // Game state (apart from entities & other stand-alone modules) float g_time = 0.f; //----------------------------------------------------------------------------- // Main int Main(void) { // Start things up & load resources --------------------------------------------------- StartGame(); // Set up rendering --------------------------------------------------------------------- glViewport(0, 0, SYS_WIDTH, SYS_HEIGHT); glClearColor( 0.0f, 0.1f, 0.3f, 0.0f ); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho( 0.0, G_WIDTH, 0.0, G_HEIGHT, 0.0, 1.0); glEnable(GL_TEXTURE_2D); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Main game loop! ====================================================================== while (!SYS_GottaQuit()) { Render(); ProcessInput(); RunGame(); SYS_Show(); SYS_Pump(); SYS_Sleep(16); g_time += 1.f/60.f; } return 0; } |
This contains the main game loop, already structured in calls to do the separate tasks (Render, ProcessInput, etc…). You can see how it calls the SYS abstraction layer to do some basic tasks: show the results after drawing, etc… and it uses OpenGL calls directly for others. It calls SYS_GottaQuit() to check whether to exit. And you can see some poor-man’s attempt at controlling the speed of the game: it sleeps for 16 milliseconds each turn, assuming the rest of the code hardly takes any time at all, this will get it to run at 60 frames per second.
We are also going to need to configure a module for “precompiled headers”. Since including many files with #include in C or C++ can make compilation slow, it’s common to configure all of them in a single module so that it is processed just once. We are going to call it “stdafx.h” and “stdafx.cpp”, as it’s usually called under Windows (we could call it anything for OS X too). Here is the required content:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
// stdafx.h #ifndef _STDAFX_H_ #define _STDAFX_H_ //============================================================================= #ifdef _WINDOWS #pragma pack(1) #pragma warning(disable:4996) // Using open/close/read... for file access #include <SDKDDKVer.h> #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <gl/gl.h> #include <GL/glu.h> #include <al.h> #include <alc.h> #include <io.h> //============================================================================= #elif defined(__APPLE__) #include "TargetConditionals.h" #if defined(__MACH__) && TARGET_OS_MAC && !TARGET_OS_IPHONE #include <unistd.h> #include <GL/glfw.h> #define GL_BGRA_EXT GL_BGRA #include <OpenAL/al.h> #include <OpenAL/alc.h> #endif #endif //============================================================================= // Common includes #include <stdlib.h> #include <stdio.h> #include <limits.h> #include <string.h> #include <sys/stat.h> #include <fcntl.h> #include <math.h> #endif |
1 2 |
// stdafx.cpp #include "stdafx.h" |
Setting it up on OS X
Finally, you need sys_osx.cpp implementing the OS X specific code, here it is:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
// sys_osx.cpp #include "stdafx.h" #include "base.h" #include "sys.h" //======================================================================================== // Has to be provided by the game extern int Main(void); //======================================================================================== // Platform layer implementation //----------------------------------------------------------------------------- int main(int argc, char *argv[]) { int retval = -1; if (glfwInit() == GL_TRUE) { if (glfwOpenWindow(SYS_WIDTH, SYS_HEIGHT, 0, 0, 0, 0, 8, 0, SYS_FULLSCREEN ? GLFW_FULLSCREEN : GLFW_WINDOW) == GL_TRUE) /* rgba, depth, stencil */ { retval = Main(); glfwCloseWindow(); } glfwTerminate(); } return retval; } //----------------------------------------------------------------------------- void SYS_Pump() { // GLFW takes care... } //----------------------------------------------------------------------------- void SYS_Show() { glfwSwapBuffers(); } //----------------------------------------------------------------------------- bool SYS_GottaQuit() { return glfwGetKey(GLFW_KEY_ESC) || !glfwGetWindowParam(GLFW_OPENED); } //----------------------------------------------------------------------------- void SYS_Sleep(int ms) { usleep(1000 * ms); } //----------------------------------------------------------------------------- bool SYS_KeyPressed(int key) { return glfwGetKey(key); } //----------------------------------------------------------------------------- ivec2 SYS_MousePos() { int x, y; ivec2 pos; glfwGetMousePos(&x, &y); pos.x = x; pos.y = SYS_HEIGHT - y; return pos; } //----------------------------------------------------------------------------- bool SYS_MouseButonPressed(int button) { return glfwGetMouseButton(button); } |
As you can see, the SYS_ functions are implemented just by calling glfw or Unix-specific code (usleep). It also features the function main(), the real entry point in a Unix/Linux/OS X application, which starts GLFW and calls our custom Main() game function which resides in game.cpp.
Now, create a new OS X project (“Command Line Tool”). Call it “scosx”, C++ type, and select the “spacecrash” root project directory as the location (not the “src” subdirectory). Once created, delete the “main.cpp” and “scosx.1” files Xcode creates by default, and add our source files to the project: game.cpp, sys_osx.cpp and sys.h, stdafx.h and stdafx.cpp. You will need to do the same adjustments we did for the test program: add the frameworks OpenGL, OpenAL, Cocoa and IOKit, the linker flag “-lglfw”, and the search paths “/usr/local/include” and “/usr/local/lib” for headers and libraries. After this is done, you should be able to run the sample app with Cmd-R. It should just show a portrait window with a dark-blue background.
Setting it up on Windows
We are going to share most files with Windows: base.h, sys.h, stdafx.h, stdafx.cpp, and mainly, game.cpp, which is the main module, containing the actual game. We will need another file: sys_win.cpp, implementing the SYS functions for Windows. Here you have it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
// sys_win.cpp #include "stdafx.h" #include "base.h" #include "sys.h" extern int Main(void); HINSTANCE WIN_hInst = 0; int WIN_nCmdShow = 0; HWND WIN_hWnd = 0; HDC WIN_hDC = 0; HGLRC WIN_hGLRC = 0; bool WIN_bGottaQuit = false; //----------------------------------------------------------------------------- LRESULT CALLBACK WIN_WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { PAINTSTRUCT ps; HDC hdc; switch (message) { case WM_PAINT: hdc = BeginPaint(hWnd, &ps); EndPaint(hWnd, &ps); break; case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0; } //----------------------------------------------------------------------------- ATOM WIN_RegisterClass() { WNDCLASS wc; wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = WIN_WndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = WIN_hInst; wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); wc.lpszMenuName = NULL; wc.lpszClassName = "WIN_WindowClass"; return RegisterClass(&wc); } //----------------------------------------------------------------------------- bool WIN_InitInstance() { RECT r; r.left = 0; r.right = SYS_WIDTH; r.top = 0; r.bottom = SYS_HEIGHT; AdjustWindowRect(&r, WS_OVERLAPPEDWINDOW, FALSE); WIN_hWnd = CreateWindow("WIN_WindowClass", "Gamecrash Window", WS_OVERLAPPEDWINDOW, 0, 0, r.right-r.left, r.bottom-r.top, NULL, NULL, WIN_hInst, NULL); if (!WIN_hWnd) return false; ShowWindow(WIN_hWnd, WIN_nCmdShow); UpdateWindow(WIN_hWnd); return true; } //----------------------------------------------------------------------------- void WIN_EnableOpenGL() { PIXELFORMATDESCRIPTOR pfd; int format; WIN_hDC = GetDC( WIN_hWnd ); // Set the pixel format for the DC ZeroMemory( &pfd, sizeof( pfd ) ); pfd.nSize = sizeof( pfd ); pfd.nVersion = 1; pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER; pfd.iPixelType = PFD_TYPE_RGBA; pfd.cColorBits = 24; pfd.cDepthBits = 16; pfd.iLayerType = PFD_MAIN_PLANE; format = ChoosePixelFormat( WIN_hDC, &pfd ); SetPixelFormat( WIN_hDC, format, &pfd ); // Create and enable the render context (RC) WIN_hGLRC = wglCreateContext( WIN_hDC ); wglMakeCurrent( WIN_hDC, WIN_hGLRC ); } //----------------------------------------------------------------------------- void WIN_DisableOpenGL() { wglMakeCurrent( NULL, NULL ); wglDeleteContext( WIN_hGLRC ); ReleaseDC( WIN_hWnd, WIN_hDC ); } //----------------------------------------------------------------------------- int APIENTRY WinMain(HINSTANCE hI, HINSTANCE hPrevI, LPSTR lpCmdLine, int nCS) { WIN_hInst = hI; WIN_nCmdShow = nCS; if (!WIN_RegisterClass()) return -1; if (!WIN_InitInstance ()) return -1; WIN_EnableOpenGL(); int retval = Main(); WIN_DisableOpenGL(); return retval; } //----------------------------------------------------------------------------- void SYS_Pump() { MSG msg; // Process messages if there are any while ( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) ) { if (msg.message == WM_QUIT) WIN_bGottaQuit = true; else { TranslateMessage(&msg); DispatchMessage(&msg); } } } //----------------------------------------------------------------------------- void SYS_Show() { glFlush(); SwapBuffers( WIN_hDC ); } //----------------------------------------------------------------------------- bool SYS_GottaQuit() { return WIN_bGottaQuit; } //----------------------------------------------------------------------------- void SYS_Sleep(int ms) { Sleep(ms); } //----------------------------------------------------------------------------- bool SYS_KeyPressed(int key) { return GetFocus() == WIN_hWnd && (GetAsyncKeyState(key) & 0x8000) != 0; } //----------------------------------------------------------------------------- ivec2 SYS_MousePos() { POINT pt; GetCursorPos(&pt); ScreenToClient(WIN_hWnd, &pt); ivec2 r = { pt.x, SYS_HEIGHT - pt.y }; return r; } //----------------------------------------------------------------------------- bool SYS_MouseButonPressed(int button) { return GetFocus() == WIN_hWnd && (GetAsyncKeyState(button) & 0x8000) != 0; } |
It is more complex than the one for OS X, as we are doing everything “by hand” instead of using GLFW. What you see above is actually the minimum set of things an app has to do under Windows to create an OpenGL window and keep it running. Also here, the WinMain() function which is the actual entry point in a Windows application.
When you move to another platform, this is the kind of code you have to write to get started, and then you can get to programming the game in the same way we are going to do. This is the same for OS X, Linux, Android, iOS, and many other platforms.
Here are the steps you need to take to set up the project under Windows: open VS, select “New Project”, “Visual C++”, “Win32 project”. Select “spacecrash” (the root directory of the project), and name the new project “scwin”. Once created, right-click and remove from the project all the files Visual Studio created, and add our source files. You will also need to right-click and change the properties of stdafx.cpp, select “Precompiled Headers”, and set to “Create” instead of “Use”. Also, the same changes we had to do in the test: “Use Multibyte Character Set”, add “opengl32.lib;openal32.lib;” to the extra libraries, and add the OpenAL directories to the VC++ Directories for Include and Library files.
By the way: VS, the same as other IDEs, allows multiple work configurations for a project. It has two by default: “Debug” and “Release”. When you change settings, by default it only applies to the currently active one (“Debug” by default). To apply the changes to all configurations, after right-clicking “Properties”, select “Configuration: All Configurations” in the combo-box at the top-left. Try to do this before changing the settings.
Once this is done, running with F5 should show you a nice dark-blue background portrait window.
Setting up on Linux
I can’t go into details, but I believe you can use exactly the same source code as the OS X version, and compile using gcc from the command-line. Please post in the comments if there is any issue with this.
Loading graphics
Now we have an application skeleton, both for Windows and OS X, which sets up a main game window, and enters a game loop, drawing and calculating each frame. It is very static now since we haven’t added anything yet. But we should now start drawing game elements… let’s get on to that.
We need to load some graphics into memory. Since I didn’t want to use any external library, I have chosen a graphics file format which is very easy to read: 32 bits-per-pixel BMP with alpha. This format is not compressed, thus, it can be read as a sequence of bytes from the file, and then sent over easily to OpenGL. OpenGL wants the pixel data in raw uncompressed format – if you load PNG or some other file, you need to decode it, which is non-trivial. When using PNG or other formats, the common thing is not to write your own loader, but to use some nice library, of which there are many. But you should learn to read a file format by hand for instructional purposes, before using libraries to do anything! So here we go.
One other important thing is that OpenGL only supports textures in sizes that are a power of two (32, 64, 128, 256, 512, etc…). To load images of arbitrary size, we will round up to the nearest power of two and only use a part of the texture.
Finally, to hold this code, and other types of similar code, we will create a new module: the core module. This will contain code that is common between platforms, but which is not really game-specific. Texture support, graphics utilities, sounds support… this is all part of the “engine”: we will declare things in core.h and define them in core.cpp. We will use the prefix CORE_ for the functionality in this module. Here is the first version with the texture-loading code (add these modules to your “src” directory and to your VS or Xcode project):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// core.h #ifndef _CORE_H_ #define _CORE_H_ // Bitmap/texture functions int CORE_LoadBmp(const char filename[], bool wrap); ivec2 CORE_GetBmpSize(int texix); void CORE_UnloadBmp(int texix); void CORE_RenderCenteredSprite(vec2 pos, vec2 size, int texix); #endif |
As you see, this declares a function to which you can pass a filename (as an array of chars, the C concept of a string), and returns an identifier to use it later on. You can call GetBmpSize() to get the pixel size of the bitmap. There is also a function to unload the texture and release the resources, which you only need to call once it is not going to be used any more. Finally, we have added a function to draw a rectangle with a given texture, passing in the coordinates, size, and what texture to use (a returned by CORE_LoadBmp()).
Here is the implementation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
// core.cpp #include "stdafx.h" #include "base.h" #include "core.h" //======================================================================================== // Loading textures (from BMP files) #define MAX_TEXTURES 32 struct Texture { bool used; float w, h; // In 0..1 terms int pixw, pixh; // In pixels GLuint tex; } g_textures[MAX_TEXTURES] = { 0 }; int CORE_LoadBmp (const char filename[], bool wrap); void CORE_UnloadBmp (int texid); //----------------------------------------------------------------------------- struct CORE_BMPFileHeader { byte mark[2]; byte filesize[4]; byte reserved[4]; byte pixdataoffset[4]; byte hdrsize[4]; byte width[4]; byte height[4]; byte colorplanes[2]; byte bpp[2]; byte compression[4]; byte pixdatasize[4]; byte hres[4]; byte vres[4]; byte numcol[4]; byte numimportantcolors[4]; }; //----------------------------------------------------------------------------- #define READ_LE_WORD(a) (a[0] + a[1]*0x100) #define READ_LE_DWORD(a) (a[0] + a[1]*0x100 + a[2]*0x10000 + a[3]*0x1000000) //----------------------------------------------------------------------------- // compute the next highest power of 2 of 32-bit v dword hp2(dword v) { v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++; return v; } //----------------------------------------------------------------------------- static byte pixloadbuffer[2048 * 2048 * 4]; int CORE_LoadBmp(const char filename[], bool wrap) { GLint retval = -1; struct CORE_BMPFileHeader hdr; int fd = open(filename, O_RDONLY); if (fd != -1) { read(fd, &hdr, sizeof(hdr)); if (hdr.mark[0] == 'B' && hdr.mark[1] == 'M') { // Find an empty texture entry for (int i = 0; i < MAX_TEXTURES; i++) { if (!g_textures[i].used) { retval = i; break; } } if (retval == -1) return retval; dword width = READ_LE_DWORD(hdr.width); sdword height = READ_LE_DWORD(hdr.height); dword offset = READ_LE_DWORD(hdr.pixdataoffset); dword pixdatasize = READ_LE_DWORD(hdr.pixdatasize); if (!pixdatasize) pixdatasize = (width * abs(height) * READ_LE_WORD(hdr.bpp) / 8); lseek(fd, offset, SEEK_SET); if (height > 0) read(fd, pixloadbuffer, pixdatasize); else { // Reverse while loading int nrows = -height; for (int i = 0; i < nrows; i++) read(fd, pixloadbuffer + (nrows-i-1) * width * 4, (pixdatasize / nrows)); } GLuint texid = 1; glGenTextures( 1, &texid ); glBindTexture( GL_TEXTURE_2D, texid ); //glTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE ); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST ); // GL_LINEAR_MIPMAP_NEAREST glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST ); // GL_LINEAR); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrap ? GL_REPEAT : GL_CLAMP ); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrap ? GL_REPEAT : GL_CLAMP ); //gluBuild2DMipmaps( GL_TEXTURE_2D, GL_RGBA8, width, height, GL_BGRA_EXT, GL_UNSIGNED_BYTE, pixloadbuffer ); height = abs((int)height); dword width_pow2 = hp2(width); dword height_pow2 = hp2(height); glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA, width_pow2, height_pow2, 0, GL_BGRA_EXT, GL_UNSIGNED_BYTE, NULL); glTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, width, height, GL_BGRA_EXT, GL_UNSIGNED_BYTE, pixloadbuffer); g_textures[retval].used = true; g_textures[retval].tex = texid; g_textures[retval].pixw = width; g_textures[retval].pixh = height; g_textures[retval].w = width/(float)width_pow2; g_textures[retval].h = height/(float)height_pow2; } close(fd); } return retval; } //----------------------------------------------------------------------------- void CORE_UnloadBmp(int texix) { glDeleteTextures( 1, &g_textures[texix].tex ); g_textures[texix].used = false; } //----------------------------------------------------------------------------- ivec2 CORE_GetBmpSize(int texix) { ivec2 v; v.x = g_textures[texix].pixw; v.y = g_textures[texix].pixh; return v; } //----------------------------------------------------------------------------- void CORE_RenderCenteredSprite(vec2 pos, vec2 size, int texix) { vec2 p0 = vsub(pos, vscale(size, .5f)); vec2 p1 = vadd(pos, vscale(size, .5f)); glBindTexture( GL_TEXTURE_2D, g_textures[texix].tex ); glBegin( GL_QUADS ); glTexCoord2d(0.0, 0.0); glVertex2f(p0.x, p0.y); glTexCoord2d(g_textures[texix].w, 0.0); glVertex2f(p1.x, p0.y); glTexCoord2d(g_textures[texix].w, g_textures[texix].h); glVertex2f(p1.x, p1.y); glTexCoord2d(0.0, g_textures[texix].h); glVertex2f(p0.x, p1.y); glEnd(); } |
I won’t go into much detail about this code, although you would do very, very well in understanding it if you want to become a proficient C/C++ games developer. The CORE_LoadBmp() function reads the file header to get the main information, and then reads the pixel data. BMP files may store the info bottom-to-top or top-to-bottom, and it has to adapt it to the common BMP file format (which we take as being bottom-to-top). It computes the nearest higher power-of-two (hp2()), and creates a texture of that size, passing in the pixel data to that texture. It sets the texture parameters, and then saves both the texture identifier and the information about what part of the texture is actually used, in a g_textures array which is the array where we will keep the information of all loaded textures.
To test the image loading and drawing, I am going to prepare the graphics for the ship you will be controlling. Here are the ones I have selected from the Tyrian graphics:
We can’t use these graphics directly. We need to extract each animation frame into a separate sprite, making sure it is properly aligned, and prepare it with alpha transparency for the area in green. They way I do it: I use Paint.NET (you can use your favorite tool), copy and paste a single image, use the magic wand to select the green area, and remove it. Save the result as a 32-bpp BMP with alpha, which is what we are able to load. I actually create a single file with multiple layers, and put each animation frame in one layer, that way I can check alignment. Then, export each layer separately a single BMP file (hide all layers but the one you want, “Save as”, and choose BMPX, which will flatten the image – use “Undo” afterwards to restore all layers). I put the intermediate files in my “prod” directory (that’s what it is for), and the actual sprites in the “data” directory (I’m calling them “ShipLL.bmp”, “ShipL.bmp”, “ShipC.bmp”, “ShipR.bmp” and “ShipRR.bmp”).
I am also going to export a simple background graphic just for some early testing, here it is:
I have uploaded these graphics for you, you can get them here:
http://jonbho.net/wp-content/uploads/2013/08/sc-gfx-0.zip
Now that we have these, they should be exracted to the “data” directory, and let’s make the main game load them and draw a simple scene with them:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
// game1.cpp #include "stdafx.h" #include "base.h" #include "sys.h" #include "core.h" #define SHIP_W 250 #define SHIP_H 270 int g_ship_LL, g_ship_L, g_ship_C, g_ship_R, g_ship_RR; int g_bkg; //----------------------------------------------------------------------------- void StartGame() { g_ship_LL = CORE_LoadBmp("data/ShipLL.bmp", false); g_ship_L = CORE_LoadBmp("data/ShipL.bmp" , false); g_ship_C = CORE_LoadBmp("data/ShipC.bmp" , false); g_ship_R = CORE_LoadBmp("data/ShipR.bmp" , false); g_ship_RR = CORE_LoadBmp("data/ShipRR.bmp", false); g_bkg = CORE_LoadBmp("data/bkg0.bmp" , false); } //----------------------------------------------------------------------------- void EndGame() { CORE_UnloadBmp(g_ship_LL); CORE_UnloadBmp(g_ship_L); CORE_UnloadBmp(g_ship_C); CORE_UnloadBmp(g_ship_R); CORE_UnloadBmp(g_ship_RR); CORE_UnloadBmp(g_bkg); } //----------------------------------------------------------------------------- void Render() { glClear( GL_COLOR_BUFFER_BIT ); CORE_RenderCenteredSprite(vmake(G_WIDTH/2.0f,G_HEIGHT/2.0f), vmake(G_WIDTH,G_HEIGHT), g_bkg); if (SYS_KeyPressed(SYS_KEY_LEFT)) CORE_RenderCenteredSprite(vmake(G_WIDTH/2.0f, 200.f), vmake(SHIP_W,SHIP_H), g_ship_LL); else if (SYS_KeyPressed(SYS_KEY_RIGHT)) CORE_RenderCenteredSprite(vmake(G_WIDTH/2.0f, 200.f), vmake(SHIP_W,SHIP_H), g_ship_RR); else CORE_RenderCenteredSprite(vmake(G_WIDTH/2.0f, 200.f), vmake(SHIP_W,SHIP_H), g_ship_C); } //----------------------------------------------------------------------------- void RunGame() { } //----------------------------------------------------------------------------- void ProcessInput() { } //----------------------------------------------------------------------------- // Game state (apart from entities & other stand-alone modules) float g_time = 0.f; //----------------------------------------------------------------------------- // Main int Main(void) { // Start things up & load resources --------------------------------------------------- StartGame(); // Set up rendering --------------------------------------------------------------------- glViewport(0, 0, SYS_WIDTH, SYS_HEIGHT); glClearColor( 0.0f, 0.1f, 0.3f, 0.0f ); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho( 0.0, G_WIDTH, 0.0, G_HEIGHT, 0.0, 1.0); glEnable(GL_TEXTURE_2D); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Main game loop! ====================================================================== while (!SYS_GottaQuit()) { Render(); SYS_Show(); ProcessInput(); RunGame(); SYS_Pump(); SYS_Sleep(16); g_time += 1.f/60.f; } EndGame(); return 0; } |
You can see how we use global variables to keep the graphics, and how our Render function now actually draws a scene. I have also made it check the state of the LEFT/RIGHT keys and draw something a bit different.
See how we are loading the resources: we are loading files with the name “data/*.bmp”. These are called “relative paths”, and it will only work if the current working directory for the program is the root project directory. To ensure this, on OS X we have to select “Edit Scheme” (click where it says “scosx > My Mac 64-bit” control on the top left of the Xcode Window)m check “Use custom working directory”, and select the “spacecrash” project root directory. In VS, you need to right-click on the project, “Properties”, select “Debugging”, “Working Directory”, and browse to the project root file.
Once you run it, it should look like this (press left/right to see how the ship graphic changes!):
Starting the actual game
It’s been a bit of a mouthful to get all the above working, but we now have all the graphical & input tools necessary to start building the game! Let’s get on to doing some game dynamics.
[Important note for those who started with an early version of this article: I have added extra functionality to the Core module, it now remembers the pixel width & height of a texture, and you can request it with CORE_GetBmpSize(). I’ve added the code in the modules above. Be sure you copy/paste it again if you had done so before this functionality was there.]
First thing, I’ve added some rock graphics. Download the new package from here:
http://jonbho.net/wp-content/uploads/2013/08/sc-gfx-1.zip
Now, in order to have some game dynamics, I did two things:
1. I decided on a coordinate system: entities and all things in the game world will be stored in world-coordinates (not in screen coordinates). The ship will start flying upwards from the bottom of the level (y = 0), and will advance boundlessly (y will grow) until the end of the level is reached. We will have a “g_camera_offset” variable controlling what part of the level is seen, which will be set on each frame according to the coordinates of the ship.
2. I have created an “Entity” struct type for game elements, stored in a global array. There is an “EntityType” enum for the different game element types. There is an E_NULL entity type, used for empty slots in the entities array.
Without further ado, here is the code that does this (only game.cpp needs to change):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
// game2.cpp #include "stdafx.h" #include "base.h" #include "sys.h" #include "core.h" #define SPRITE_SCALE 8.f #define MAINSHIP_ENTITY 0 #define MAINSHIP_RADIUS 100.f #define ROCK_RADIUS 100.f #define SHIP_W 250.f #define SHIP_H 270.f #define VERTICAL_SHIP_VEL 20.f #define HORIZONTAL_SHIP_VEL 10.f float g_camera_offset = 0.f; //============================================================================= // Textures int g_ship_LL, g_ship_L, g_ship_C, g_ship_R, g_ship_RR; int g_bkg; int g_rock[5]; //============================================================================= // Entities enum EType { E_NULL, E_MAIN, E_ROCK }; #define MAX_ENTITIES 64 struct Entity { EType type; vec2 pos; vec2 vel; float radius; int gfx; }; Entity g_entities[MAX_ENTITIES]; //----------------------------------------------------------------------------- void InsertEntity(EType type, vec2 pos, vec2 vel, float radius, int gfx) { for (int i = 0; i < MAX_ENTITIES; i++) { if (g_entities[i].type == E_NULL) { g_entities[i].type = type; g_entities[i].pos = pos; g_entities[i].vel = vel; g_entities[i].radius = radius; g_entities[i].gfx = gfx; break; } } } //----------------------------------------------------------------------------- void StartGame() { // Resources g_ship_LL = CORE_LoadBmp("data/ShipLL.bmp", false); g_ship_L = CORE_LoadBmp("data/ShipL.bmp" , false); g_ship_C = CORE_LoadBmp("data/ShipC.bmp" , false); g_ship_R = CORE_LoadBmp("data/ShipR.bmp" , false); g_ship_RR = CORE_LoadBmp("data/ShipRR.bmp", false); g_bkg = CORE_LoadBmp("data/bkg0.bmp" , false); g_rock[0] = CORE_LoadBmp("data/Rock0.bmp" , false); g_rock[1] = CORE_LoadBmp("data/Rock1.bmp" , false); g_rock[2] = CORE_LoadBmp("data/Rock2.bmp" , false); g_rock[3] = CORE_LoadBmp("data/Rock3.bmp" , false); g_rock[4] = CORE_LoadBmp("data/Rock4.bmp" , false); // Logic InsertEntity(E_MAIN, vmake(G_WIDTH/2.0, G_HEIGHT/8.f), vmake(0.f, VERTICAL_SHIP_VEL), MAINSHIP_RADIUS, g_ship_C); } //----------------------------------------------------------------------------- void EndGame() { CORE_UnloadBmp(g_ship_LL); CORE_UnloadBmp(g_ship_L); CORE_UnloadBmp(g_ship_C); CORE_UnloadBmp(g_ship_R); CORE_UnloadBmp(g_ship_RR); CORE_UnloadBmp(g_bkg); CORE_UnloadBmp(g_rock[0]); CORE_UnloadBmp(g_rock[1]); CORE_UnloadBmp(g_rock[2]); CORE_UnloadBmp(g_rock[3]); CORE_UnloadBmp(g_rock[4]); } //----------------------------------------------------------------------------- void Render() { glClear( GL_COLOR_BUFFER_BIT ); // Render background, only tiles within the camera view int first_tile = (int)floorf(g_camera_offset / G_HEIGHT); for (int i = first_tile; i < first_tile + 2; i++) { CORE_RenderCenteredSprite( vsub(vadd(vmake(G_WIDTH/2.0f,G_HEIGHT/2.0f), vmake(0.f, (float)i * G_HEIGHT)), vmake(0.f,g_camera_offset)), vmake(G_WIDTH,G_HEIGHT), g_bkg); } // Draw entities for (int i = MAX_ENTITIES - 1; i >= 0; i--) { if (g_entities[i].type != E_NULL) { ivec2 sz = CORE_GetBmpSize(g_entities[i].gfx); vec2 pos = g_entities[i].pos; pos.x = (float)((int)pos.x); pos.y = (float)((int)pos.y); CORE_RenderCenteredSprite( vsub(pos, vmake(0.f,g_camera_offset)), vmake(sz.x * SPRITE_SCALE, sz.y * SPRITE_SCALE), g_entities[i].gfx ); } } } //----------------------------------------------------------------------------- void RunGame() { // Move entities for (int i = MAX_ENTITIES - 1; i >= 0; i--) { if (g_entities[i].type != E_NULL) { g_entities[i].pos = vadd(g_entities[i].pos, g_entities[i].vel); // Remove entities that fell off the screen if (g_entities[i].pos.y < g_camera_offset - G_HEIGHT) g_entities[i].type = E_NULL; } } // Dont let steering off the screen! if (g_entities[MAINSHIP_ENTITY].pos.x < MAINSHIP_RADIUS) g_entities[MAINSHIP_ENTITY].pos.x = MAINSHIP_RADIUS; if (g_entities[MAINSHIP_ENTITY].pos.x > G_WIDTH - MAINSHIP_RADIUS) g_entities[MAINSHIP_ENTITY].pos.x = G_WIDTH - MAINSHIP_RADIUS; // Possibly insert new rock if (rand() < (RAND_MAX / 40)) InsertEntity(E_ROCK, vmake((rand() * (float)G_WIDTH)/RAND_MAX, g_camera_offset + G_HEIGHT + 400.f), vmake(0.f, 0.f), ROCK_RADIUS, g_rock[rand()%5U]); // Set camera to follow the main ship g_camera_offset = g_entities[MAINSHIP_ENTITY].pos.y - G_HEIGHT/8.f; } //----------------------------------------------------------------------------- void ProcessInput() { if (SYS_KeyPressed(SYS_KEY_LEFT)) g_entities[MAINSHIP_ENTITY].vel.x = -HORIZONTAL_SHIP_VEL; else if (SYS_KeyPressed(SYS_KEY_RIGHT)) g_entities[MAINSHIP_ENTITY].vel.x = +HORIZONTAL_SHIP_VEL; else g_entities[MAINSHIP_ENTITY].vel.x = 0.f; } //----------------------------------------------------------------------------- // Game state (apart from entities & other stand-alone modules) float g_time = 0.f; //----------------------------------------------------------------------------- // Main int Main(void) { // Start things up & load resources --------------------------------------------------- StartGame(); // Set up rendering --------------------------------------------------------------------- glViewport(0, 0, SYS_WIDTH, SYS_HEIGHT); glClearColor( 0.0f, 0.1f, 0.3f, 0.0f ); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho( 0.0, G_WIDTH, 0.0, G_HEIGHT, 0.0, 1.0); glEnable(GL_TEXTURE_2D); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Main game loop! ====================================================================== while (!SYS_GottaQuit()) { Render(); SYS_Show(); ProcessInput(); RunGame(); SYS_Pump(); SYS_Sleep(16); g_time += 1.f/60.f; } EndGame(); return 0; } |
There are a few of things worthy of mention: rand() is the C way of getting a random number, between 0 and RAND_MAX. If you divide rand() by RAND_MAX (using floating point), you will get a random value between 0.0 and 1.0. Also, checking (rand() < RAND_MAX/10) returns true only one tenth of the time. I'm using rand() to generate new rocks only once in a while, also to calculate the position where to create them, and also to choose what rock type to calculate (since there are 5 rock types, I need a random number between 0 and 4 - I do (rand() % 5U), where 'U' means 'unsigned, to get the positive reminder of dividing by 5). Also, to render the background, imagine that there is an infinite number of instances of the background image, but we don't want to render them all, only the two that are visible at the current camera position. I use floor() to find which one to render, and I render this one and the one on top. As you can see now, the game running and drawing is now centered around entities, which is a very common pattern. And here is a video showing how the above results:
Spacecrash: Day 1 from jonbho on Vimeo.
Not bad for one day. Hope you like it, and see you tomorrow.
And if you want to be notified when new articles to help you become a games developer are ready, just fill in the form below:
I think there is a HTML formatting issue in some of the code here, e.g. :
glfwGetMousePos(&x, &y);
& is the entity name for “ampersand” according to : http://www.w3schools.com/tags/ref_entities.asp
Looks like my post got formatted.
What I’m talking about that is in the code is in the “Reserved Characters” table -> “Entity Name” column -> “Ampersand” row at the following link.
http://www.w3schools.com/tags/ref_entities.asp
Yup, sorry, I think it’s fixed it now. Thanks!
@antisyzygy That would explain a lot. I started to wonder if I ever understood C/C++! :\
Hopefully everything is clear now!
In VS 2012 (Win 7) I was unable to get the #pragma warning(disable:4996) to work, it came back as an error…
To fix this I had to add _CRT_SECURE_NO_WARNINGS to project setting > C/C++ > preprocessor > preprocessor definitions
And add
#define open _open
#define read _read
#define close _close
#define lseek _lseek
to the windows section of the stdafx.h. Any better way around this?
Alaws, I am using VS2010, that’s why I haven’t stumbled into that. I have added “#define _CRT_SECURE_NO_WARNINGS” in stdafx.h. The only other thing necessary would be to make sure that the setting “Disable Language Extensions”, in Project Properties -> C/C++ -> Language is set to “No”.
I am getting an error saying that the “open” function is deprecated in VS 2012.
Error 1 error C4996: ‘open’: The POSIX name for this item is deprecated. Instead, use the ISO C++ conformant name: _open. See online help for details. path\spacecrash\src\core.cpp 57 1 scwin
What would you recommend?
This is same problem as alaws by the way.
Look at my response to alaws above. Adding a #define at the top of stdafx.h and making sure language extensions are enabled should fix it.
I had the same problem and the solution proposed by alaws didn’t work for me, so I changed all “open” with “_open” (via edit->find and replace) in game2.cpp. Then I had to do the same for “read” and “close”, replacing them with “_read” and “_close”.
Once I did all of that the game loaded smoothly.
Good to know, and sorry about that. I would have used the more-standard fopen/fread/fclose in normal conditions, which are also usually more efficient, but that required using pointers, so I had to use ye-olde file functions.
I have all the same coding and setup, but for some reason it seems to be showing nothing but a blank window…I running on windows vista if that helps…
If your graphics never load, double check the path\permissions of the image files.
anc_k, thanks for the heads-up. Indeed, if the BMP loading routine can’t find the file, it won’t fail, the textures will just be white. Jeremy, check that the working directory for debugging is the root project directory. You can also set a breakpoint in the call to “open” in core.cpp and try stepping through it to see if the problem is there, which seems likely.
I’m having the same issue, but when I step through the CORE_LoadBmp method, it seems to work. The bmp files are found, the data loads, it gets added to g_texture, the works. But the screen is still blank white. Could it be an issue with the bmp files themselves, like could they be in the wrong format?
Ok, I spoke too soon — it was the bmps I made that somehow where messed up. I did them in photoshop, and they appeared to be fine, but they didn’t work. When I dropped your files in from the zip, it started working. Why do you think that would be?
Ah ok, it makes sense. Photoshop probably can’t write 32-bpp BMP files with alpha. That’s why I was recommending Paint.NET (which is free btw), you install the BMPX plugin and you can generate those files.
In the future, it will make sense to switch to loading PNGs using a library or so, but, for this project, I wanted to go really simple, and there’s nothing as simple as loading uncompressed bitmaps.
If you want to restore the steering animation in game2.cpp just change process inputs to
void ProcessInput()
{
if (SYS_KeyPressed(SYS_KEY_LEFT)) {
g_entities[MAINSHIP_ENTITY].vel.x = -HORIZONTAL_SHIP_VEL;
g_entities[MAINSHIP_ENTITY].gfx = g_ship_LL;
}
else if (SYS_KeyPressed(SYS_KEY_RIGHT)) {
g_entities[MAINSHIP_ENTITY].vel.x = +HORIZONTAL_SHIP_VEL;
g_entities[MAINSHIP_ENTITY].gfx = g_ship_RR;
}
else {
g_entities[MAINSHIP_ENTITY].vel.x = 0.f;
g_entities[MAINSHIP_ENTITY].gfx = g_ship_C;
}
}
I also made a cmake project for linux users, it might actually work in windows and mac as well since glfw actually works in windows. Here is a link to that https://www.dropbox.com/sh/0vpp621wxfli9ja/HufWeL8EE2
I forgot to mention, to get this to work you need all dependencies and cmake. Delete the directory build and just
mkdir build
cd build
cmake ..
make
and then just run the app
Thanks Oeslian, very useful. I will definitely get to set up my Linux dev env myself after the week is over (no time now), and include Linux in my workflow.
I seem to have run into a problem, right off the bat. I went through the first portion of the first day of this project (thank you very much for taking the time and effort) but it seems OpenGL does not like my setup (VS2012). Every reference to OpenGL returns an error similar to “‘GL_COLOR_BUFFER_BIT’ : undeclared identifier” (first error on the long list). OpenGL is included in the projects additional dependencies. My background is in networking, so I am probably making a silly mistake, but any suggestions would be greatly appreciated.
Again, thank you for this project. I hope to take in a lot from it.
OK, here is an update. I have added the follow to game0.cpp:
#include
#include
Both are needed or I get errors. Only problem is that now I get another error:
LINK : fatal error LNK1561: entry point must be defined
I am left even more confused, because main() is right there. I have a feeling the problem is with my IDE setup, even though I following the preparation post before this. More research is needed.
Apparently the site didn’t like how I wrote this. Here is what the includes above are.
#include (less-than symbol)windows.h(greater-than symbol)
#include (less-than symbol)gl/gl.h(greater-than symbol)
Please excuse the barbaric work-around.
Those includes are actually included in stdafx.h, inside an #ifdef _WINDOWS section, so they should they should have been included to begin with! Maybe you can check why they weren’t. Here is the chain of logic:
1) game.cpp includes stdafx.h first thing in the module
2) stdafx.h has an #ifndef _STDAFX_H_ that passes
3) stdafx.h has an #ifdef _WINDOWS that should pass, too
4) The includes are there! So it should work.
I know adding them directly fixes it, but it’s good to understand the real underlying cause, because one day, quick hacks stop working… and you have to understand the nuts and bolts!
I had the same issue in VS 2013, turned out to be because _WINDOWS wasn’t defined. Changing stdafx.h to check for _WIN32 instead sorted it.
at first I want to say what a great idea I find this article. I really love articles about game development, even when I’m not the target audience, but I like to see how other people write stuff to learn new things…
when it’s allowed I have some small review points I want to mention to help the beginners:
– you use bool which is a c++ only feature. in c99 there’s stdbool, but that is not working in visual studio. just a small hint for c beginners
– you use word and dword(which comes from the windows-world), but I find it much easier to use int16 and int32 to make the size clear to others (matter of style)
– to generate a random number you have to be really careful when you use mod because you generate random numbers with a slightly bias. let’s say (for this example) rand_max is 7 and you use rand()%5 you get 0, 1, 2 with a different probability than 3 and 4. random numbers are a really complex beast and needs special treatment (for example you normally throw away the lower bits and stuff like that).
I have some more advanced stuff, but I don’t want to make it too complicated for the beginners or hijack your articles, maybe you could do after the week a review-article what should be changed and why (for example subpixel-exact movement, correct mainloop with correct timesteps (physics!), use bitmap stripes to not have to cut every animation image by image, stb_image, …)
keep up writing interesting articles 🙂
Thanks for the positive words, and for the comments that clarify the finer points. Indeed, as you say, the project stays at the basic stage of many tech aspects. This is on purpose, getting from zero to a completed game in one week required some hard choices. Both a fixed-step loop and the stock rand() are enough for a start. People can then delve as deep as they want later on…
I see too many people with great knowledge and control of a lot of advanced techniques, but completely unable to complete a game. That is my goal in this project.
I was tempted to go 100% C and only two main things prevented me from doing it: having to prepend “enum” or “struct” to type names (I could use “typedef” but it seemed too much), and the C “inline” functions mess. People should really move on to C++ quickly afterwards though so I think this is the way to go.
Ah, and the DWORD terminology definitely predates Windows, I remember using DB/DW/DD to enter 8-bit/16-bit/32-bit data in assembly language long before Windows even existed! 🙂
I get this error :
game0.obj : error LNK2019: unresolved external symbol “void __cdecl SYS_Show(void)” (?SYS_Show@@YAXXZ) referenced in function “int __cdecl Main(void)” (?Main@@YAHXZ)
Can anyone help ?I guess it’s something about linking the functions from sys_win.cpp into game0.cpp
Do you have sys_win.cpp in the project? Does it have a SYS_Show function in it, without typos? Can you check that the SYS_Show declaration in sys.h and the definition in sys_win.cpp match?
It works now ! Thank you!
sys_win.cpp in the project – checked
contains SYS_Show – checked
About the third thing ,I’m quite the newbie and I forgot I have to complete the sys.h file depending on the OS ,I’ll come back in 5 minutes with an answer .Thanks
I have run into a strange problem when trying to load my .bmp images exported from gimp. It looks like the green and blue color channels have swapped and the image which is supposed to be closer to green appears more blue. I am not aware of any GL_GBRA swaps or anything like that.
It is likely that the BMP files that gimp outputs are not compatible with the file reading routine above. That’s why I recommended Paint.NET on Windows or Paintbrush on Mac OS X, since I have tested those. Does alpha work on those images? Can you post a capture of how the bitmap looks for you? If you send me a copy of the file I can try to see what’s happening (as soon as I have a bit of time, which is not too easy). My email address is on the About page.
I get a white screen. Also, is it ok to get a dozen messages that say ‘Cannot find or open the PDB file’ ?
If the BMP loading routine can’t find the texture files, it won’t fail, the textures will just be white. Check that the working directory for debugging is the root project directory, so that “data/whatever.bmp” opens the right file. You can also set a breakpoint in the call to “open” in core.cpp and try stepping through it to see if the problem is there, which seems likely.
Please post the PDB message in detail, when do you get it, when compiling/linking or when running the app?
build is fine. No errors there. After I hit run I get a dozen or so of those messages kind of like this:
Loaded ‘C:\WINDOWS\system32\kernel32.dll’, Cannot find or open the PDB file
The working directory is my project folder ( which is what I am assuming is what you mean by root project directory, if not than I’m not sure what you mean). I have my bmp files in a data folder in the project folder. Pretty sure I might be making some silly mistake and I can’t see it.
I added the breakpoint at the “open” call and it opened the game window, but it crashed.
Ok, those messages are normal.
Can you describe the crash in more detail? Where does it happen? What message do you get? Does execution ever reach the file “open” call?
Thanks for all the effort, Jon! Seems today is time to read OpenGL API and learn what do all those functions do and how do they do it.
I’ve setup a github repo with a Linux-only environment here: https://github.com/alkazar/spacecrash
I’ll be updating and tagging for each day.
Thanks Alkazar. I thought about doing it myself, but I thought it would be better to have people preparing their own source files & projects, etc… for practice purposes. It’s too easy to just click “Build” and “Run”, instead of seeing how all pieces are put together!
On a side note I’ve been wondering what license you want to release this code as?
This is a great idea actually. Later, people can look up the commit history to see what changed over the course of development.
Hi,
I followed everything through (up to the point where it should be “playable”), however when I run it, I just get a window at the left side of my screen that stretches from the top of the screen to the bottom. Changing SYS_HEIGHT does not affect the height of the window, but changing SYS_WIDTH affects the width. There are also no images shown, it is still just the blue background.
Sorry if I am being extremely stupid and the answer is simple 😛
What system are you using, OS X or Windows? If it’s OS X, check whether SYS_FULLSCREEN is set to true, it shouldn’t. Apart from that, check that the working directory is the right one (the root directory, which should have the bitmaps in a “data” subdirectory).
Hi,
I am running on Windows right now. I have done as you said, and it is still doing exactly the same. I can only hazard a guess that I have screwed up in one of the render calls somewhere?
Something which I missed was I kept executing the game0.cpp even after adding game1.cpp. Took me a couple of mins to see what was wrong. lolz!
Okay, I found out why the screen size was not changing. I had not set r.top to 0, I had just put r.top; by accident.
It is still not showing any of the bmp’s though 🙁
Is there a tutorial on how to convert images into 32 bits-per-pixel BMP with alpha?
Most of the images show up blue, which I am guessing is because it is not able to decode the file.
The way to convert them is to use Paint.NET or Paintbrush, and just save images as 32bpp BMP files. In any case, I recommend you just download the ZIP file with the BMP files I have prepared, to remove that as a doubt. Try that and post again.
Thanks so much for this great tutorial. I find myself stuck right here:
You will need to do the same adjustments we did for the test program: add the frameworks OpenGL, OpenAL, Cocoa and IOKit, the linker flag “-lglfw”, and the search paths “/usr/local/include” and “/usr/local/lib” for headers and libraries.
I’m sure I’m missing something, but I can’t seem to figure out what this step is asking me to do. Thanks in advance for any help.
I am referring to the instructions followed when preparing the set-up (http://jonbho.net/2013/08/24/one-week-game-getting-ready/). These were the steps:
– In Build Settings, and enter “/usr/local/include” for Header Search Paths and “/usr/local/lib” for Library Search Paths (for Xcode to find the GLFW library)
– Also add the options “-lglfw” to the “Other Linker Flags” setting
– Select the “Build Phases” tab, open “Link Binary With Libraries”, and add the following frameworks: OpenGL, OpenAL, IOKit and Cocoa
Ah Ha! I knew I was just overlooking something.. Thanks for the help!
Hi Jon, I’m getting these two errors:
Error 1 error C2440: ‘=’ : cannot convert from ‘const char [16]’ to ‘LPCWSTR’ c:\users\ivan\desktop\spacecrash\sscccwin\sscccwin\sys_win.cpp 45 1 sscccwin
and
Error 2 error C2664: ‘CreateWindowExW’ : cannot convert parameter 2 from ‘const char [16]’ to ‘LPCWSTR’ c:\users\ivan\desktop\spacecrash\sscccwin\sscccwin\sys_win.cpp 55 1 sscccwin
What can I do to fix them?
Hi,
If you right click your solution and go into Properties. Under Configuration Properties -> General there is a setting for Character Set. This will be on Unicode. If you change this to “Use Multi-Byte Character Set” your errors will be fixed 🙂
Sospitas thanks! Indeed that is the most likely issue, Ivan is compiling in Unicode mode.
Thanks, now it works!!
Hi,
I am getting stuck very early on. Just after we create an Xcode Project.
I deleted “main.cpp” and “scosx.1” and added our source files to the project. Made the same changes as for the test program and tried to build.
I get an error saying that there is no such directory for the path of game.cpp and stdafx.h and stdafx.cpp. Am I not adding our source files to the correct path? Do I need to add them to the exact same place where “main.cpp” and “scosx.1” were?
Thank a lot in advance!!
How are you adding the files? What you describe is very weird. I just use right click -> Add Files to “project”…
Also where do you get the error? When compiling, or when adding the files?
Hi, I was copying it to the folder where the project is.
I will try to the right click and add them to the project (not very familiar with Xcode).
I get the error when compiling.
Thanks for the reply I will try again and get back with the result. Take care!
Good luck, and feel free to ask again if you can’t get it to work!
Wow. What’s the name of this color scheme for the coding? Great tut. Thanks!
It’s whatever the Crayon WordPress plugin does, but indeed, it’s nice!! And glad you’re enjoying the tutorial.
I get these errors when debugging after adding game1.cpp and its content:
1>game1.obj : error LNK2005: “void __cdecl StartGame(void)” (?StartGame@@YAXXZ) already defined in game.obj
1>game1.obj : error LNK2005: “void __cdecl EndGame(void)” (?EndGame@@YAXXZ) already defined in game.obj
1>game1.obj : error LNK2005: “void __cdecl Render(void)” (?Render@@YAXXZ) already defined in game.obj
1>game1.obj : error LNK2005: “void __cdecl RunGame(void)” (?RunGame@@YAXXZ) already defined in game.obj
1>game1.obj : error LNK2005: “void __cdecl ProcessInput(void)” (?ProcessInput@@YAXXZ) already defined in game.obj
1>game1.obj : error LNK2005: “int __cdecl Main(void)” (?Main@@YAHXZ) already defined in game.obj
1>game1.obj : error LNK2005: “int g_ship_RR” (?g_ship_RR@@3HA) already defined in game.obj
1>game1.obj : error LNK2005: “int g_ship_LL” (?g_ship_LL@@3HA) already defined in game.obj
1>game1.obj : error LNK2005: “int g_bkg” (?g_bkg@@3HA) already defined in game.obj
1>game1.obj : error LNK2005: “int g_ship_C” (?g_ship_C@@3HA) already defined in game.obj
1>game1.obj : error LNK2005: “int g_ship_L” (?g_ship_L@@3HA) already defined in game.obj
1>game1.obj : error LNK2005: “int g_ship_R” (?g_ship_R@@3HA) already defined in game.obj
1>game1.obj : error LNK2005: “float g_time” (?g_time@@3MA) already defined in game.obj
1>LINK : fatal error LNK1123: failure during conversion to COFF: file invalid or corrupt
========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========
How do I go about fixing it?
[…] Spacecrash day 1 of 7: game skeleton […]
For those still learning and strugling to make this work in linux.
I know there are a lot of other answers but here is my tip.
#1 Don’t forget to add -lglfw and -lopenal to your linker if you’re working with Eclipse CDT or something like that.
#2 Use this stadfx.h (http://pastebin.kde.org/p5iatzdhs) which I had to tweak to include Linux support in it.
AS you can see I’ve added a defined(__linux__) directive to check upon linux and include the missing libraries.
Hope this helps as it did help me.
Cheerz
Hi Jon. This stuff is great. I went through it all, following along in VS2010, and I’m now going back through and adapting this approach to a different style of game (simple snake clone). A quick question, if you find the time: In game.cpp, in the Render() function, after retrieving an element’s position, why cast (int) and back to (float)? (I think that’s what you’re doing, at least!) Is the data not already float type, as per the pos definition in the element struct above? Is it just a sneaky way to intentionally concatenate the value? If so, why?
Thank you SO MUCH for doing this for all of us. I am exactly your target audience! We’ll see if I can finish my snake game…. 🙂
Michael