Linux Game Programming Tutorial 1: Introduction/First SDL
原文 http://www.dreamincode.net/forums/topic/290187-linux-game-programming-tutorial-1-introduction-first-sdl/
添加到推刊
Linux Game Programming Tutorial
1: Introduction
written by WolfCoder (2012)
Related source code and files are attached to this tutorial: tutorial.1.tar (60K)
Number of downloads: 41
I'm back~! Lots have changed since my old series of tutorials on Linux.. with Linux itself included. If you're here, there's a good chance it had something to do with Microsoft deciding every home and business needs a giant oversized smart phone, or Valve's recent plans to migrate their engine and Steam to Linux, ..-or perhaps you just wanted to learn how to write video games from scratch and I happen to be targeting Linux- any reason is a good reason to get started!
Introduction
The Games for Linux movement hasn't actually been invented yet after these years, it all starts with you~! I've stumbled around this confusing wasteland of Linux MAN pages trying to assemble a familiar ground for game development so you won't have to. I'll write about all the fundamental building blocks you'll need to create whatever it is you'll need to make your ideas. As far as this being your first experience writing game engines, it's actually going to be easier than if you started with Windows- OpenGL, SDL, and the basic Linux OS system functions are very simple in comparison. It actually makes the experience of game engineering kind of fun~
The major difference between this new series of tutorials and the old ones is the focus on being entirely inside the Linux environment. The techniques inside will also be written without much knowledge of the particular distribution of Linux or desktop environment you're using. If you don't know what either of those quite mean, don't worry! If you're migrating from a long experience writing games for Windows, you'll be OK as you share my aversion to using the command line "way too much". I'll try my best to avoid these common problems found in many Linux-related tutorials.
Finally, and importantly, here's what I assume you already know . You know how to use your computer, you've already installed a version of Debian Linux (Ubuntu, etc.), you know what a video game is, and have some knowledge of how to program in C. You don't need to be too experienced in C, but it will help. You'll quickly find out that I expect you to know how to use whatever desktop environment you chose (different distros have different desktop environments as default) since I can't be specific when it comes to finding particular things.
No more delay, let's begin with some basic Linux things and our first video program~!
Compile a C Program
Before you can write the game, you need to get a grasp on C. Let's get around to compiling and testing a C program using the basic tools that come with most Linux distros. Here is the basic Hello World program:
You can just save this somewhere (remember where you put it!) as a .c file (I named mine hello.c). There, you've written a program, but you probably knew that.. What's probably different from your usual environment is how to build and run it. You'll need to open a Terminal window. If you're using the default Ubuntu (Gnome 3) or something similar, I have no idea how to help you- you'll need it if you followed the directions for installing MATE anyways. You're looking for the black box with >_ inside it, probably under all programs or Accessories. You'll need to navigate to wherever you saved your .c file.. Navigate.. Yes! If you're new, you can use ls to show all the files and folders in your current directory and cd to change your current directory. cd <folder> will ENTER that directory, cd .. will go "up" out of wherever you are, and you can ls after each step to see the effect. You'll be cd 'ing directly into where you want like a pro once you get the hang of it. On some systems, you can type the first part of what you want and hit TAB until the actual name appears to auto-complete. Handy for way-to-long file and folder names. Anyway, navigate until you see your .c file appear when you enter ls . We'll make this all easier later.
So now.. We compile the file! We're going to use the GNU C Compiler which should already be installed, but do know there's others out there. To compile, enter:
And if everything works, you should have these files now:
Great so we.. can't run it yet-! You need to "link" them together even though there's only one .o file. The .o files are compiled, but not yet "linked" together into your actual ELF binary, which is the program we can run. To do that:
And then the program will appear:
There's no .exe at the end, programs in linux are defined instead by permissions. Don't worry about that now. If you type:
Then the familiar message should appear:
And there you go! That's how programs are written. That's a little too much to type, we need something called a Makefile.
Make
This is where we make it all easier, as I said. You need to create a new file just named "Makefile". You'll want to open it in your text editor. You can type all the commands you need to complete various tasks, such as compile and run here. Makefiles use a language on its own, but it's simple enough for us to be able to write one now. They follow this pattern:
Yes, you need to have ONE TAB before every command. The "targets" are just various tasks you want to do (compile, run, etc.). Then you can just type the commands like you just did, and end them with semicolons. I'll write the first Makefile for you:
I've seperated the different tasks out. The same commands we did to build the program are listed under build: and running under run:. Under clean: we have a couple of rm commands that delete files with the - before them. If these files AREN'T there, the commands fail. If the commands fail, then the rest of the target doesn't run! To prevent this, putting - means that if the command fails, keep going. Now, in your terminal, just type make (you left it in the directory with the .c file, right?) and you should see this:
A little noisy, but it ran the program just fine. make will run the first target in Makefile, you can simply put the target after make to run that target:
And it will only compile our program. You can put anything you can do on the command line as a command in a target making Makefiles a great way to automate almost anything . You can just edit your program and hit make to see the results- ..at least if you put a test target first in your Makefile like this one.
Package Manager
I did make the assumption you're using some form of Debian (Ubuntu, etc.), so there's this handy Package Manager . Somewhere under System/Administration is a tool called the Synaptic Package Manager . You can use this to install and uninstall the various tools you'll hear about. Sometimes you'll have to download the .deb file, but you can usually just double-click on it for it to install. Be sure it's a .deb file in that case and not a .rpm. Anything you install through the Package Manager may even be updated every time you check for updates, unlike what you would be used to on Windows. We just need to get SDL since that's the basic layer we're working with- open the Synaptic Package Manager . It looks like this:
You're going to search for a package named "libsdl1.2-dev":
You can make this package and apply changes. This will install SDL development files. You may see that the runtime files for SDL have already been installed- this means many people also using similar distributions of Linux can simply run your program without having to install anything.
Now that it's installed, here's a short program (sdltest.c) that can test to see if it's working:
Just do this command:
AAAAaand- woah:
I'm highlighting the most common problem people starting out writing their own game engines have- they didn't include or link the libraries correctly! OK.. so where.. where are they? Here's how to find out using the Synaptic Package Manager . Right click on the entry for libsdl1.2-dev and click "properties", and then there's the "Installed Files" tab as shown here:
So it's /usr/include/SDL, right.. OK, how do I include this? You'll have to compile like this:
And then we get the sdltest.o file we expected. Now it's just:
And:
Well of course! That was just compiling header files with names to things, the actual code is in what's called a "static library" with the binary code. This is much simpler, the modified command is just:
And the program binary gets linked and made. Running it should produce this output:
The program was written to tell you if SDL itself has an error trying to start up. If everything is fine at this point, you've successfuly added a library using the Package Manager to your environment and are using it.
Main Screen Turn On
A game engine, from the ordinary user's point of view, is just a rectangle window with stuff moving inside. It's much more complicated than that, but it doesn't hurt to start with the rectangle with stuff moving inside :3 We know how to start and stop SDL, and from the previous example we know it's initializing the video. Getting the actual screen to appear is a bit different. In SDL, we can make a call to SDL_SetVideoMode and the window should appear. The function returns something called an SDL_Surface which will point to this window's screen. Since it's the main screen, we can declare a global variable (write this outside of every function before the functions):
And then we can use it to store the screen as the result of creating the main window:
The SDL_HWSURFACE means to store the surface in Video Memory (hopefully, in your video card) and the SDL_DOUBLEBUF means to have a back buffer we draw on and show. I'll explain what the whole Double Buffering thing means later, once we have the video open we need to enter.. The Main Loop . You see, a video game starts up and then enters a loop where it constantly creates the next frame until the user quits. We need a main loop, and something to handle events so the game can actually be shut off when closed. Here is the main loop:
The SDL_PollEvent command takes the next event (in Linux, sometimes from the window manager, whatever it is) and lets you handle it. Note that the function returns zero once there are no events left, so this while loop continues to take ALL events until they are handled for the current iteration of the main loop, or frame. Instead of leaping out of the main loop, I use a flag so the game logic can be completly handled before it shuts down, you can do it another way if you'd like. At the end, SDL_Quit and we have the first SDL program (demo.c):
And since we have to compile all those libraries, we can use the handy Makefile:
Once you type make , a black, blank window should pop open.
Nothing special quite yet. We need to draw a picture every frame for there to be something happening. This is where the Double Buffering term comes back, what we do is draw on a screen hidden from view. Once we're finished drawing it, we show the screen as the next frame. Yes, if we just had it visible the whole time and no buffer, you would see us drawing on the screen and it could cause some weird visual artifacts and flickering. We can just write our drawing code inside the main loop, first we need to make the screen blue (or some non-black color):
SDL_FillRect will fill a rectangle on a surface with a solid color. Notice the second parameter is NULL, if you do this it fills the entire surface. SDL_MapRGBA lets you define a pixel with the familiar Red, Green, Blue and Alpha (Alpha?) channels. We can just give it the pixel format from the surface, don't worry about what that is for now- this will clear the screen using a blue color. Then we need to show it once we are done drawing:
Now all we are missing are the moving objects.
Action
The action, the most important part of the game. This is the game itself, we're going to handle and represent all of the moving objects in our engine.. Which right now is going to be just a bunch of dots-! Don't worry, we'll do more impressive things as I continue this tutorial series.
One very important thing about video games is the time. The common thing is to find out how long it took for the system to complete the previous frame, and then use that time to predict how long the next frame will take. You know the classic physics formula for velocity: v = d*s where d is distance and s is seconds. But wait, time is not constant in video games! So we need to measure the time it takes for each frame to pass. You may be thinking about some sort of system milliseconds or something, but what would be better is a High-Resolution System Timer . The high resolution timer in many CPUs have some odd quirks that make it impractical for some real timing applications, but for video games we don't care about real, steady time! We want high resolution smooth time. Unfortunatly Windows may not implement it in the same way we are about to. Let's take a look at the function we need:
We had to define a variable of type struct timespec and let this function put values into it. The CLOCK_PROCESS_CPUTIME_ID flag means to take the time from the CPU for this process or otherwise known as your game engine. We need to measure the distance between two points in our code over time, so wouldn't we just remember the time at the first point, and then subtract the ending time with our start time to get the distance? The clock_gettime function is a little complicated so we can convert it into a floating point number representing milliseconds. We're going to be using a floating point number at the end anyway. Let's write the start and end functions for time:
The part of the code to really focus on is this since you know "delta" is the distance in time, or the length of time the last frame took in milliseconds:
So, remembering your mathmatics, if the engine takes longer than 16 frames per second (1000/16 milliseconds), then the time step value becomes greater than 1.0f. Otherwise, the faster the engine runs, the smaller this value gets. The 16 frames per second is a magic number, if your speed is 1, then you will move 16 units a second. You can see where this is going, we can multiply our speeds with this value to get smooth, frame-independant movement that video games usually have. Let's add it to our main loop:
Note that we didn't surround the event handler. This prevents the underlying system from causing the engine to lag and jerk. Now for the moving objects. This right here is the simplest definition of what a video game is , which is important to know since we're making game engines. Video games are full of self-contained objects in an environment with behaviors, also known as an agent processing engine. This can also be extended to its own model of computation, and differs from your usual Input->Computation->Output workflow you might have been used to. Instead, we have a neverending environment of self-contained objects interacting with the environment and one another. This is the most elementary definition of a video game engine I can think of.
We're using dots since we don't know how to use real images yet. Let's make each dot a different shade of color and have a random speed, and each dot will bounce off the edges of the screen. Since we made our window 320 by 240, we can just use (0,0,320,240) as the containment rectangle. We can represent the objects as an array of state variables in C. Remember your struct s? I hope so~ Let's define our dot:
Simple enough, let's make an array of them:
And that's it. Let's make a function to initialize them:
demo_roll is just a handy function I made to return a random floating point number. This function should initialize all the variable space for demo_dots, and the program is good to go. We will need a function to handle all the dots, moving them by their velocity and bouncing them off the walls of the application window:
Note carefully how I multiply my speeds by demo_time_step, the floating point factor from before. This should produce the smooth, frame-independant movement we're looking for. This function should be called every frame before it's actually drawn.. and speaking of drawing:
What's going on here? We used SDL_LockSurface to lock the surface.. which means there's now a way for us to write to the pixels on it. Remember the thing I said about video memory? Once you unlock the surface, the data may return to the video card and no longer be directly accessable to you. The think I named "rank" is the bytes per scanline (one horizontal line of pixels). You should always calculate using this "pitch" value of the surface and never by the assumed or specified width. The basic formula is then x+y*rank (rank, as you can see, is the pitch divided by the bytes per pixel). Once you're done, just use SDL_UnlockSurface and, depending on the implementation, the data returns to your video card. Here, we aren't really getting much video acceleration since we're drawing the pixels ourselves in software- but normally the surface/texture can be used by the video card directly.
You probably won't be writing something like this again, but it's a great way to understand framebuffers. Since I assume your machine was made sometime within the past decade, the screen should be in 32-bit color. Remember the SDL_SetVideoMode call from earlier? We put 0 in the bit depth so the surface returned is in whatever color model your system is in. You wouldn't normally assume each pixel fits in something as big as a Uint32, but this is one (if not the only) time we'll be modifying a framebuffer directly in this way. Usually, we'll be doing this to offscreen surfaces and textures to load bitmap data, and the surface would be in any format we see fit- so we can make assumptions like the one I wrote into the code. Other than the warning about the bit depth not always being 32, this should simply put all the dots in the screen in their current state. Let's combine the functions into our main loop and arrive at what we wanted.
Here is the final (demo.c) program:
Compiling it all will give this result:
And there we have it! A snow-globe-esque swarm of moving dots.
Conclusion
..That wasn't so bad, was it :3 This might be a little much already to take in, or you may be one of those kinds of people to go wild and try getting the bitmap functions in SDL to run. We won't be using those in the next tutorials, we'll be going directly to OpenGL since even modern 2D engines are just drawing textured quads. The old model of blitter is obsolete as far as video games go, it's actually easier for the machine to handle basic polygons- even on my netbook. It will be much easier on you too: rotating, scaling, and advanced pixel effects (even in 2D) are trivial to implement.
You should sharpen your C programming skills, play with the code created here, and try using new functions from SDL or some other library. The next tutorial will involve using OpenGL and textured quads to make a basic sprite engine.
If you need to, you can download all the source code and Makefiles involved in this tutorial here:
1: Introduction
written by WolfCoder (2012)
Related source code and files are attached to this tutorial: tutorial.1.tar (60K)
Number of downloads: 41
I'm back~! Lots have changed since my old series of tutorials on Linux.. with Linux itself included. If you're here, there's a good chance it had something to do with Microsoft deciding every home and business needs a giant oversized smart phone, or Valve's recent plans to migrate their engine and Steam to Linux, ..-or perhaps you just wanted to learn how to write video games from scratch and I happen to be targeting Linux- any reason is a good reason to get started!
Introduction
The Games for Linux movement hasn't actually been invented yet after these years, it all starts with you~! I've stumbled around this confusing wasteland of Linux MAN pages trying to assemble a familiar ground for game development so you won't have to. I'll write about all the fundamental building blocks you'll need to create whatever it is you'll need to make your ideas. As far as this being your first experience writing game engines, it's actually going to be easier than if you started with Windows- OpenGL, SDL, and the basic Linux OS system functions are very simple in comparison. It actually makes the experience of game engineering kind of fun~
The major difference between this new series of tutorials and the old ones is the focus on being entirely inside the Linux environment. The techniques inside will also be written without much knowledge of the particular distribution of Linux or desktop environment you're using. If you don't know what either of those quite mean, don't worry! If you're migrating from a long experience writing games for Windows, you'll be OK as you share my aversion to using the command line "way too much". I'll try my best to avoid these common problems found in many Linux-related tutorials.
Finally, and importantly, here's what I assume you already know . You know how to use your computer, you've already installed a version of Debian Linux (Ubuntu, etc.), you know what a video game is, and have some knowledge of how to program in C. You don't need to be too experienced in C, but it will help. You'll quickly find out that I expect you to know how to use whatever desktop environment you chose (different distros have different desktop environments as default) since I can't be specific when it comes to finding particular things.
No more delay, let's begin with some basic Linux things and our first video program~!
Compile a C Program
Before you can write the game, you need to get a grasp on C. Let's get around to compiling and testing a C program using the basic tools that come with most Linux distros. Here is the basic Hello World program:
/* Hello World the basic test program */ /* Includes */ #include <stdio.h> /* Main */ int main(int argn,char **argv) { printf("Hello, world!\n"); return 0; }
You can just save this somewhere (remember where you put it!) as a .c file (I named mine hello.c). There, you've written a program, but you probably knew that.. What's probably different from your usual environment is how to build and run it. You'll need to open a Terminal window. If you're using the default Ubuntu (Gnome 3) or something similar, I have no idea how to help you- you'll need it if you followed the directions for installing MATE anyways. You're looking for the black box with >_ inside it, probably under all programs or Accessories. You'll need to navigate to wherever you saved your .c file.. Navigate.. Yes! If you're new, you can use ls to show all the files and folders in your current directory and cd to change your current directory. cd <folder> will ENTER that directory, cd .. will go "up" out of wherever you are, and you can ls after each step to see the effect. You'll be cd 'ing directly into where you want like a pro once you get the hang of it. On some systems, you can type the first part of what you want and hit TAB until the actual name appears to auto-complete. Handy for way-to-long file and folder names. Anyway, navigate until you see your .c file appear when you enter ls . We'll make this all easier later.
So now.. We compile the file! We're going to use the GNU C Compiler which should already be installed, but do know there's others out there. To compile, enter:
gcc -c hello.c
And if everything works, you should have these files now:
hello.c hello.o
Great so we.. can't run it yet-! You need to "link" them together even though there's only one .o file. The .o files are compiled, but not yet "linked" together into your actual ELF binary, which is the program we can run. To do that:
gcc hello.o -o hello
And then the program will appear:
hello hello.c hello.o
There's no .exe at the end, programs in linux are defined instead by permissions. Don't worry about that now. If you type:
./hello
Then the familiar message should appear:
Hello, world!
And there you go! That's how programs are written. That's a little too much to type, we need something called a Makefile.
Make
This is where we make it all easier, as I said. You need to create a new file just named "Makefile". You'll want to open it in your text editor. You can type all the commands you need to complete various tasks, such as compile and run here. Makefiles use a language on its own, but it's simple enough for us to be able to write one now. They follow this pattern:
<target1>: <TAB! Important to TAB!>command1 <TAB>command2 <TAB>command3 ... <target2>: <TAB>command1 <TAB>command2 <TAB>command3 ...
Yes, you need to have ONE TAB before every command. The "targets" are just various tasks you want to do (compile, run, etc.). Then you can just type the commands like you just did, and end them with semicolons. I'll write the first Makefile for you:
test: make build; make run; build: gcc -c hello.c; gcc hello.o -o hello; run: ./hello; clean: -rm hello; -rm hello.o;
I've seperated the different tasks out. The same commands we did to build the program are listed under build: and running under run:. Under clean: we have a couple of rm commands that delete files with the - before them. If these files AREN'T there, the commands fail. If the commands fail, then the rest of the target doesn't run! To prevent this, putting - means that if the command fails, keep going. Now, in your terminal, just type make (you left it in the directory with the .c file, right?) and you should see this:
make build; make[1]: Entering directory `/home/sayuri/tutorial/1/src' gcc -c hello.c; gcc hello.o -o hello; make[1]: Leaving directory `/home/sayuri/tutorial/1/src' make run; make[1]: Entering directory `/home/sayuri/tutorial/1/src' ./hello; Hello, world! make[1]: Leaving directory `/home/sayuri/tutorial/1/src'
A little noisy, but it ran the program just fine. make will run the first target in Makefile, you can simply put the target after make to run that target:
make build
gcc -c hello.c; gcc hello.o -o hello;
And it will only compile our program. You can put anything you can do on the command line as a command in a target making Makefiles a great way to automate almost anything . You can just edit your program and hit make to see the results- ..at least if you put a test target first in your Makefile like this one.
Package Manager
I did make the assumption you're using some form of Debian (Ubuntu, etc.), so there's this handy Package Manager . Somewhere under System/Administration is a tool called the Synaptic Package Manager . You can use this to install and uninstall the various tools you'll hear about. Sometimes you'll have to download the .deb file, but you can usually just double-click on it for it to install. Be sure it's a .deb file in that case and not a .rpm. Anything you install through the Package Manager may even be updated every time you check for updates, unlike what you would be used to on Windows. We just need to get SDL since that's the basic layer we're working with- open the Synaptic Package Manager . It looks like this:
You're going to search for a package named "libsdl1.2-dev":
You can make this package and apply changes. This will install SDL development files. You may see that the runtime files for SDL have already been installed- this means many people also using similar distributions of Linux can simply run your program without having to install anything.
Now that it's installed, here's a short program (sdltest.c) that can test to see if it's working:
/* SDL Test Demonstrates SDL being initialized and shut down. written by Sayuri L. Kitsune (2012) */ /* Includes */ #include <SDL.h> #include <stdio.h> /* Main */ int main(int argn,char **argv) { /* Init */ if(SDL_Init(SDL_INIT_VIDEO) != 0) fprintf(stderr,"Could not initialize SDL: %s\n",SDL_GetError()); /* Error? */ printf("SDL Initialized\n"); /* Exit */ SDL_Quit(); printf("SDL Shutdown\n"); /* Done */ return 0; }
Just do this command:
gcc -c sdltest.c
AAAAaand- woah:
sdltest.c:8:17: fatal error: SDL.h: No such file or directory compilation terminated.
I'm highlighting the most common problem people starting out writing their own game engines have- they didn't include or link the libraries correctly! OK.. so where.. where are they? Here's how to find out using the Synaptic Package Manager . Right click on the entry for libsdl1.2-dev and click "properties", and then there's the "Installed Files" tab as shown here:
So it's /usr/include/SDL, right.. OK, how do I include this? You'll have to compile like this:
gcc -I"/usr/include/SDL" -c sdltest.c
And then we get the sdltest.o file we expected. Now it's just:
gcc sdltest.o -o sdltest
And:
sdltest.o: In function `main': sdltest.c:(.text+0x15): undefined reference to `SDL_Init' sdltest.c:(.text+0x1e): undefined reference to `SDL_GetError' sdltest.c:(.text+0x4c): undefined reference to `SDL_Quit' collect2: ld returned 1 exit status
Well of course! That was just compiling header files with names to things, the actual code is in what's called a "static library" with the binary code. This is much simpler, the modified command is just:
gcc sdltest.o -lSDL -o sdltest
And the program binary gets linked and made. Running it should produce this output:
SDL Initialized SDL Shutdown
The program was written to tell you if SDL itself has an error trying to start up. If everything is fine at this point, you've successfuly added a library using the Package Manager to your environment and are using it.
Main Screen Turn On
A game engine, from the ordinary user's point of view, is just a rectangle window with stuff moving inside. It's much more complicated than that, but it doesn't hurt to start with the rectangle with stuff moving inside :3 We know how to start and stop SDL, and from the previous example we know it's initializing the video. Getting the actual screen to appear is a bit different. In SDL, we can make a call to SDL_SetVideoMode and the window should appear. The function returns something called an SDL_Surface which will point to this window's screen. Since it's the main screen, we can declare a global variable (write this outside of every function before the functions):
SDL_Surface *demo_screen;
And then we can use it to store the screen as the result of creating the main window:
/* Open main window */ demo_screen = SDL_SetVideoMode(320,240,0,SDL_HWSURFACE|SDL_DOUBLEBUF); if(!demo_screen) fprintf(stderr,"Could not set video mode: %s\n",SDL_GetError());
The SDL_HWSURFACE means to store the surface in Video Memory (hopefully, in your video card) and the SDL_DOUBLEBUF means to have a back buffer we draw on and show. I'll explain what the whole Double Buffering thing means later, once we have the video open we need to enter.. The Main Loop . You see, a video game starts up and then enters a loop where it constantly creates the next frame until the user quits. We need a main loop, and something to handle events so the game can actually be shut off when closed. Here is the main loop:
/* Main loop */ active = 1; while(active) { /* Handle events */ while(SDL_PollEvent(&ev)) { if(ev.type == SDL_QUIT) active = 0; /* End */ } }
The SDL_PollEvent command takes the next event (in Linux, sometimes from the window manager, whatever it is) and lets you handle it. Note that the function returns zero once there are no events left, so this while loop continues to take ALL events until they are handled for the current iteration of the main loop, or frame. Instead of leaping out of the main loop, I use a flag so the game logic can be completly handled before it shuts down, you can do it another way if you'd like. At the end, SDL_Quit and we have the first SDL program (demo.c):
/* SDL Video Demo demonstrates SDL's video working by moving dots around a screen written by Sayuri L. Kitsune (2012) */ /* Includes */ #include <SDL.h> /* Globals */ SDL_Surface *demo_screen; /* Main */ int main(int argn,char **argv) { SDL_Event ev; int active; /* Initialize SDL */ if(SDL_Init(SDL_INIT_VIDEO) != 0) fprintf(stderr,"Could not initialize SDL: %s\n",SDL_GetError()); /* Open main window */ demo_screen = SDL_SetVideoMode(320,240,0,SDL_HWSURFACE|SDL_DOUBLEBUF); if(!demo_screen) fprintf(stderr,"Could not set video mode: %s\n",SDL_GetError()); /* Main loop */ active = 1; while(active) { /* Handle events */ while(SDL_PollEvent(&ev)) { if(ev.type == SDL_QUIT) active = 0; /* End */ } } /* Exit */ SDL_Quit(); return 0; }
And since we have to compile all those libraries, we can use the handy Makefile:
test: make build; ./demo; build: gcc -I"/usr/include/SDL" -c demo.c; gcc demo.o -lSDL -o demo; clean: -rm demo.o; -rm demo;
Once you type make , a black, blank window should pop open.
Nothing special quite yet. We need to draw a picture every frame for there to be something happening. This is where the Double Buffering term comes back, what we do is draw on a screen hidden from view. Once we're finished drawing it, we show the screen as the next frame. Yes, if we just had it visible the whole time and no buffer, you would see us drawing on the screen and it could cause some weird visual artifacts and flickering. We can just write our drawing code inside the main loop, first we need to make the screen blue (or some non-black color):
/* Clear screen */ SDL_FillRect(demo_screen,NULL,SDL_MapRGBA(demo_screen->format,0,0,255,255));
SDL_FillRect will fill a rectangle on a surface with a solid color. Notice the second parameter is NULL, if you do this it fills the entire surface. SDL_MapRGBA lets you define a pixel with the familiar Red, Green, Blue and Alpha (Alpha?) channels. We can just give it the pixel format from the surface, don't worry about what that is for now- this will clear the screen using a blue color. Then we need to show it once we are done drawing:
Now all we are missing are the moving objects.
Action
The action, the most important part of the game. This is the game itself, we're going to handle and represent all of the moving objects in our engine.. Which right now is going to be just a bunch of dots-! Don't worry, we'll do more impressive things as I continue this tutorial series.
One very important thing about video games is the time. The common thing is to find out how long it took for the system to complete the previous frame, and then use that time to predict how long the next frame will take. You know the classic physics formula for velocity: v = d*s where d is distance and s is seconds. But wait, time is not constant in video games! So we need to measure the time it takes for each frame to pass. You may be thinking about some sort of system milliseconds or something, but what would be better is a High-Resolution System Timer . The high resolution timer in many CPUs have some odd quirks that make it impractical for some real timing applications, but for video games we don't care about real, steady time! We want high resolution smooth time. Unfortunatly Windows may not implement it in the same way we are about to. Let's take a look at the function we need:
struct timespec ts; clock_gettime(CLOCK_PROCESS_CPUTIME_ID,&ts);
We had to define a variable of type struct timespec and let this function put values into it. The CLOCK_PROCESS_CPUTIME_ID flag means to take the time from the CPU for this process or otherwise known as your game engine. We need to measure the distance between two points in our code over time, so wouldn't we just remember the time at the first point, and then subtract the ending time with our start time to get the distance? The clock_gettime function is a little complicated so we can convert it into a floating point number representing milliseconds. We're going to be using a floating point number at the end anyway. Let's write the start and end functions for time:
float demo_time_measure = 0.0f; float demo_time_step = 0.0f; /* Convert from timespec to float */ float demo_convert_time(struct timespec *ts) { float accu; /* Combine the value into floating number */ accu = (float)ts->tv_sec; /* Seconds that have gone by */ accu *= 1000000000.0f; /* One second is 1000x1000x1000 nanoseconds, s -> ms, us, ns */ accu += (float)ts->tv_nsec; /* Nanoseconds that have gone by */ /* Bring it back into the millisecond range but keep the nanosecond resolution */ accu /= 1000000.0f; return accu; } /* Start time */ void demo_start_time() { struct timespec ts; clock_gettime(CLOCK_PROCESS_CPUTIME_ID,&ts); demo_time_measure = demo_convert_time(&ts); } /* End time */ void demo_end_time() { struct timespec ts; float delta; clock_gettime(CLOCK_PROCESS_CPUTIME_ID,&ts); delta = demo_convert_time(&ts)-demo_time_measure; /* Find the distance in time */ demo_time_step = delta/(1000.0f/16.0f); /* Weird formula, equals 1.0f at 16 frames a second */ }
The part of the code to really focus on is this since you know "delta" is the distance in time, or the length of time the last frame took in milliseconds:
demo_time_step = delta/(1000.0f/16.0f); /* Weird formula, equals 1.0f at 16 frames a second */
So, remembering your mathmatics, if the engine takes longer than 16 frames per second (1000/16 milliseconds), then the time step value becomes greater than 1.0f. Otherwise, the faster the engine runs, the smaller this value gets. The 16 frames per second is a magic number, if your speed is 1, then you will move 16 units a second. You can see where this is going, we can multiply our speeds with this value to get smooth, frame-independant movement that video games usually have. Let's add it to our main loop:
/* Main loop */ active = 1; while(active) { /* Handle events */ while(SDL_PollEvent(&ev)) { if(ev.type == SDL_QUIT) active = 0; /* End */ } /* Start time */ demo_start_time(); /* Clear screen */ SDL_FillRect(demo_screen,NULL,SDL_MapRGBA(demo_screen->format,0,0,255,255)); /* Show screen */ SDL_Flip(demo_screen); /* End time */ demo_end_time(); }
Note that we didn't surround the event handler. This prevents the underlying system from causing the engine to lag and jerk. Now for the moving objects. This right here is the simplest definition of what a video game is , which is important to know since we're making game engines. Video games are full of self-contained objects in an environment with behaviors, also known as an agent processing engine. This can also be extended to its own model of computation, and differs from your usual Input->Computation->Output workflow you might have been used to. Instead, we have a neverending environment of self-contained objects interacting with the environment and one another. This is the most elementary definition of a video game engine I can think of.
We're using dots since we don't know how to use real images yet. Let's make each dot a different shade of color and have a random speed, and each dot will bounce off the edges of the screen. Since we made our window 320 by 240, we can just use (0,0,320,240) as the containment rectangle. We can represent the objects as an array of state variables in C. Remember your struct s? I hope so~ Let's define our dot:
/* Types */ typedef struct { int red,green; /* The shade of color in red,green. blue is always 0 since we're using it as the background */ float vx,vy; /* Speed the particle is moving */ float x,y; /* Current position of particle */ }dot;
Simple enough, let's make an array of them:
/* Defines */ #define NUM_DOTS 1024
dot demo_dots[NUM_DOTS];
And that's it. Let's make a function to initialize them:
/* Returns a random floating point number between 0.0f and 1.0f */ float demo_roll() { float r; r = (float)(rand()%RAND_MAX); /* 0 ~ whatever RAND_MAX is */ r /= (float)(RAND_MAX-1); /* one less than RAND_MAX makes it possible for 1.0f to happen */ return r; } /* Initialize dots */ void demo_init() { int i; for(i = 0;i < NUM_DOTS;i++) { demo_dots[i].red = rand()%255; demo_dots[i].green = rand()%255; demo_dots[i].vx = demo_roll()*16.0f-8.0f; demo_dots[i].vy = demo_roll()*16.0f-8.0f; demo_dots[i].x = demo_roll()*320.0f; demo_dots[i].y = demo_roll()*240.0f; } }
demo_roll is just a handy function I made to return a random floating point number. This function should initialize all the variable space for demo_dots, and the program is good to go. We will need a function to handle all the dots, moving them by their velocity and bouncing them off the walls of the application window:
/* Handle dots */ void demo_handle() { int i; for(i = 0;i < NUM_DOTS;i++) { /* Move */ demo_dots[i].x += demo_dots[i].vx*demo_time_step; demo_dots[i].y += demo_dots[i].vy*demo_time_step; /* Hit walls? */ if(demo_dots[i].x < 0.0f || demo_dots[i].x >= 320.0f) { /* Undo move (demo_time_step is still the same value it was before and is valid for the current frame) */ demo_dots[i].x -= demo_dots[i].vx*demo_time_step; /* Reverse */ demo_dots[i].vx = -demo_dots[i].vx; } if(demo_dots[i].y < 0.0f || demo_dots[i].y >= 240.0f) { /* Undo move (demo_time_step is still the same value it was before and is valid for the current frame) */ demo_dots[i].y -= demo_dots[i].vy*demo_time_step; /* Reverse */ demo_dots[i].vy = -demo_dots[i].vy; } } }
Note carefully how I multiply my speeds by demo_time_step, the floating point factor from before. This should produce the smooth, frame-independant movement we're looking for. This function should be called every frame before it's actually drawn.. and speaking of drawing:
/* Draw dots */ void demo_draw() { int i,bpp,rank,x,y; Uint32 *pixel; /* Lock surface */ SDL_LockSurface(demo_screen); rank = demo_screen->pitch/sizeof(Uint32); pixel = (Uint32*)demo_screen->pixels; /* Draw all dots */ for(i = 0;i < NUM_DOTS;i++) { /* Rasterize position as integer */ x = (int)demo_dots[i].x; y = (int)demo_dots[i].y; /* Set pixel */ pixel[x+y*rank] = SDL_MapRGBA(demo_screen->format,demo_dots[i].red,demo_dots[i].green,0,255); } /* Unlock surface */ SDL_UnlockSurface(demo_screen); }
What's going on here? We used SDL_LockSurface to lock the surface.. which means there's now a way for us to write to the pixels on it. Remember the thing I said about video memory? Once you unlock the surface, the data may return to the video card and no longer be directly accessable to you. The think I named "rank" is the bytes per scanline (one horizontal line of pixels). You should always calculate using this "pitch" value of the surface and never by the assumed or specified width. The basic formula is then x+y*rank (rank, as you can see, is the pitch divided by the bytes per pixel). Once you're done, just use SDL_UnlockSurface and, depending on the implementation, the data returns to your video card. Here, we aren't really getting much video acceleration since we're drawing the pixels ourselves in software- but normally the surface/texture can be used by the video card directly.
You probably won't be writing something like this again, but it's a great way to understand framebuffers. Since I assume your machine was made sometime within the past decade, the screen should be in 32-bit color. Remember the SDL_SetVideoMode call from earlier? We put 0 in the bit depth so the surface returned is in whatever color model your system is in. You wouldn't normally assume each pixel fits in something as big as a Uint32, but this is one (if not the only) time we'll be modifying a framebuffer directly in this way. Usually, we'll be doing this to offscreen surfaces and textures to load bitmap data, and the surface would be in any format we see fit- so we can make assumptions like the one I wrote into the code. Other than the warning about the bit depth not always being 32, this should simply put all the dots in the screen in their current state. Let's combine the functions into our main loop and arrive at what we wanted.
Here is the final (demo.c) program:
/* SDL Video Demo demonstrates SDL's video working by moving dots around a screen written by Sayuri L. Kitsune (2012) */ /* Defines */ #define NUM_DOTS 1024 /* Includes */ #include <time.h> #include <SDL.h> #include <stdlib.h> /* Types */ typedef struct { int red,green; /* The shade of color in red,green. blue is always 0 since we're using it as the background */ float vx,vy; /* Speed the particle is moving */ float x,y; /* Current position of particle */ }dot; /* Globals */ SDL_Surface *demo_screen; float demo_time_measure = 0.0f; float demo_time_step = 0.0f; dot demo_dots[NUM_DOTS]; /* Returns a random floating point number between 0.0f and 1.0f */ float demo_roll() { float r; r = (float)(rand()%RAND_MAX); /* 0 ~ whatever RAND_MAX is */ r /= (float)(RAND_MAX-1); /* one less than RAND_MAX makes it possible for 1.0f to happen */ return r; } /* Initialize dots */ void demo_init() { int i; for(i = 0;i < NUM_DOTS;i++) { demo_dots[i].red = rand()%255; demo_dots[i].green = rand()%255; demo_dots[i].vx = demo_roll()*16.0f-8.0f; demo_dots[i].vy = demo_roll()*16.0f-8.0f; demo_dots[i].x = demo_roll()*320.0f; demo_dots[i].y = demo_roll()*240.0f; } } /* Handle dots */ void demo_handle() { int i; for(i = 0;i < NUM_DOTS;i++) { /* Move */ demo_dots[i].x += demo_dots[i].vx*demo_time_step; demo_dots[i].y += demo_dots[i].vy*demo_time_step; /* Hit walls? */ if(demo_dots[i].x < 0.0f || demo_dots[i].x >= 320.0f) { /* Undo move (demo_time_step is still the same value it was before and is valid for the current frame) */ demo_dots[i].x -= demo_dots[i].vx*demo_time_step; /* Reverse */ demo_dots[i].vx = -demo_dots[i].vx; } if(demo_dots[i].y < 0.0f || demo_dots[i].y >= 240.0f) { /* Undo move (demo_time_step is still the same value it was before and is valid for the current frame) */ demo_dots[i].y -= demo_dots[i].vy*demo_time_step; /* Reverse */ demo_dots[i].vy = -demo_dots[i].vy; } } } /* Draw dots */ void demo_draw() { int i,bpp,rank,x,y; Uint32 *pixel; /* Lock surface */ SDL_LockSurface(demo_screen); rank = demo_screen->pitch/sizeof(Uint32); pixel = (Uint32*)demo_screen->pixels; /* Draw all dots */ for(i = 0;i < NUM_DOTS;i++) { /* Rasterize position as integer */ x = (int)demo_dots[i].x; y = (int)demo_dots[i].y; /* Set pixel */ pixel[x+y*rank] = SDL_MapRGBA(demo_screen->format,demo_dots[i].red,demo_dots[i].green,0,255); } /* Unlock surface */ SDL_UnlockSurface(demo_screen); } /* Convert from timespec to float */ float demo_convert_time(struct timespec *ts) { float accu; /* Combine the value into floating number */ accu = (float)ts->tv_sec; /* Seconds that have gone by */ accu *= 1000000000.0f; /* One second is 1000x1000x1000 nanoseconds, s -> ms, us, ns */ accu += (float)ts->tv_nsec; /* Nanoseconds that have gone by */ /* Bring it back into the millisecond range but keep the nanosecond resolution */ accu /= 1000000.0f; return accu; } /* Start time */ void demo_start_time() { struct timespec ts; clock_gettime(CLOCK_PROCESS_CPUTIME_ID,&ts); demo_time_measure = demo_convert_time(&ts); } /* End time */ void demo_end_time() { struct timespec ts; float delta; clock_gettime(CLOCK_PROCESS_CPUTIME_ID,&ts); delta = demo_convert_time(&ts)-demo_time_measure; /* Find the distance in time */ demo_time_step = delta/(1000.0f/16.0f); /* Weird formula, equals 1.0f at 16 frames a second */ } /* Main */ int main(int argn,char **argv) { SDL_Event ev; int active; /* Initialize SDL */ if(SDL_Init(SDL_INIT_VIDEO) != 0) fprintf(stderr,"Could not initialize SDL: %s\n",SDL_GetError()); /* Open main window */ demo_screen = SDL_SetVideoMode(320,240,0,SDL_HWSURFACE|SDL_DOUBLEBUF); if(!demo_screen) fprintf(stderr,"Could not set video mode: %s\n",SDL_GetError()); /* Initialize game */ demo_init(); /* Main loop */ active = 1; while(active) { /* Handle events */ while(SDL_PollEvent(&ev)) { if(ev.type == SDL_QUIT) active = 0; /* End */ } /* Start time */ demo_start_time(); /* Handle game */ demo_handle(); /* Clear screen */ SDL_FillRect(demo_screen,NULL,SDL_MapRGBA(demo_screen->format,0,0,255,255)); /* Draw game */ demo_draw(); /* Show screen */ SDL_Flip(demo_screen); /* End time */ demo_end_time(); } /* Exit */ SDL_Quit(); return 0; }
Compiling it all will give this result:
And there we have it! A snow-globe-esque swarm of moving dots.
Conclusion
..That wasn't so bad, was it :3 This might be a little much already to take in, or you may be one of those kinds of people to go wild and try getting the bitmap functions in SDL to run. We won't be using those in the next tutorials, we'll be going directly to OpenGL since even modern 2D engines are just drawing textured quads. The old model of blitter is obsolete as far as video games go, it's actually easier for the machine to handle basic polygons- even on my netbook. It will be much easier on you too: rotating, scaling, and advanced pixel effects (even in 2D) are trivial to implement.
You should sharpen your C programming skills, play with the code created here, and try using new functions from SDL or some other library. The next tutorial will involve using OpenGL and textured quads to make a basic sprite engine.
If you need to, you can download all the source code and Makefiles involved in this tutorial here: