Saturday, December 27, 2008
Putting SDL Calls into Classes
welcome to infomix.blogspot.com
Note: This tutorial assumes that you have read all of the previous tutorials in this series. Although the purpose of this tutorial is to show you how to put the code into classes, you'll still need to know the basic of OOP in C++. At the very least, be sure to have a C++ book on hand.
One of the most common questions beginners ask me is how I know how to organize the classes in my projects. If you ever find yourself wondering how people come up with their class designs, this tutorial is for you. I'll first show you a complete project that has all of its code in one file: global variables, functions, constants, and all. I'll then take you step by step through pulling this code out and organizing it into classes.
I think that one of the reasons why people have so much trouble understanding OOP, is that they're only shown the code when it's already organized into classes. My hope is that if you see the code working first without classes, you'll have an easier time seeing how it works once it's separated into classes.
If you're already comfortable with OOP, I recommend that you at least skim over this tutorial so you can see how I've organized everything. I'll be using these classes throughout the rest of my tutorials, so you'll want to understand how they work.
Before we continue, I have to warn you that this tutorial will require a little more effort on your part than the previous tutorials in this series. In the previous tutorials, I was able to explain each line individually. This tutorial is covering code design, so explaining the code line by line would be irrelevant. I'll explain how the classes are structured, why I structured them the way I did, and how they interact together. It'll be up to you to read through the code until you understand it.
The Procedural Way
Alright, hold your breath cuz something code dumpy this way comes.
The following code draws some text and lets you move a sprite with the left and right arrow keys. All of the code is based on previous tutorials, but I've organized it into related parts. The bitmap that contains the sprite can be downloaded here.
If you're feeling brave, go ahead and read through the code now. Make sure that you understand everything that's going on though.
Before you can understand object oriented code, you have to learn the golden rule of programming: reduce complexity! Your main focus when programming should be to make your code as quick and easy to understand as possible. The people who ramble on about efficiency over readability have either never worked in the programming industry, or were absolutely horrible to work when they did work in the industry.
There'll be times when you have to sacrifice the readability or your code in order to optimize it, but when you do, you'll hide it away. For example, if you have to write super fast code to write a line you'll make a function called writeLine() and hide you optimized code within it. This means that when you need to work on the code that actually draws the line, you'll go into the writeLine() function and mess with the code. When people need to use your function though, they won't need to know how you implemented it, they'll just call writeLine() and trust that your code works. No one but you will ever need to see your messy, optimized code.
This brings me to a very important part of reducing complexity: reducing the number of things you need to keep in your head at one time. If you put all of your code in one big function, you'll have to keep track of that state of every variable in your code, which is impossible. If you break into it up into smaller functions though, you'll just have to be aware of what each function does. If your functions have names that properly identify what they do, you'll have no problem keeping track of what each function does.
Another important part of reducing complexity is to make sure that each function in your code does only one thing. A function that draws a line shouldn't do anything but draw a line. You should never have a function called drawLine() that takes two points, draws a line between the two points, then deletes the two points. You could change the function name to be called drawLineAndDeletePoints(), but that's increasing complexity. A much better approach would be to have drawLine() draw a line, and let the caller delete the points. You might think this is obvious, but I've seen many functions in production code that do very similar things and cause endless trouble.
The thing I want you to take away from this the most is that you should learn to accept the fact that you don't always need to understand how everything works. If you see a function called drawText(), don't worry about looking at what the drawText() function does unless you want to know how to draw text. If you just want to draw the text, just call drawText() and trust that it works.
When you're looking at the code below, start by reading through the main() function. When you come across things like initGraphics(), updateDoomGuy(), and closeBitmap(), don't worry about how they work. Just understand how the main() function works. Once you understand the flow of the game, go ahead and look at how each individual function works. When I wrote the code, I started by writing functions for the functionality I knew I'd need. I then started putting the functions together in main(). Once I had written the functions though, I let myself forget how they worked inside. That way, I reduced the complexity of my code to something I could manage.
Spend some time with the code below. Make sure you get how it all works. Like I said, main() is a good place to start. I'll provide a summary of the code below. Before you start reading the code though, be sure to compile and run it to see exactly what it does.
#include "SDL.h"
#include "SDL_TTF.h"
/****************************
* Window related constants *
****************************/
const int WINDOW_WIDTH = 640;
const int WINDOW_HEIGHT = 480;
const char* WINDOW_TITLE = "SDL Start";
/***********************************
* Window related global variables *
***********************************/
bool g_gameIsRunning = true;
/*******************************
* Character related constants *
*******************************/
const char* DOOMGUY_BITMAP_FILE_NAME = "doomguy.bmp";
const int DOOMGUY_START_X = 250;
const int DOOMGUY_START_Y = 150;
const int DOOMGUY_IMAGE_X = 6;
const int DOOMGUY_IMAGE_Y = 6;
const int DOOMGUY_IMAGE_WIDTH = 34;
const int DOOMGUY_IMAGE_HEIGHT = 40;
// Distance the doom guy moves in pixels per second
const float DOOMGUY_SPEED = 100.0;
/**************************************
* Character related global variables *
**************************************/
SDL_Surface* g_doomguyBitmap = NULL;
// These variables store the current location of the doom guy.
// To move the doom guy, we just have to change these.
float g_doomguyX = DOOMGUY_START_X;
float g_doomguyY = DOOMGUY_START_Y;
// This variables store the doom guy's current speed. Note that he can only move
// from left to right. This variable must be added to the doom guy's location
// each frame.
float g_doomguySpeedX = 0.0;
/*******************************
* Character related functions *
*******************************/
void updateDoomGuy(float deltaTime);
/*****************************
* Drawing related constants *
*****************************/
// Black
const int BACKGROUND_RED = 0;
const int BACKGROUND_GREEN = 0;
const int BACKGROUND_BLUE = 0;
// Magenta
const int TRANSPARENT_RED = 255;
const int TRANSPARENT_GREEN = 0;
const int TRANSPARENT_BLUE = 255;
/************************************
* Drawing related global variables *
************************************/
SDL_Surface* g_screen = NULL;
/*****************************
* Drawing related functions *
*****************************/
// This creates a window, initializes SDL's video component, and initializes
// the SDL_ttf library. It returns a pointer to the screen surface.
SDL_Surface* initGraphics(int windowWidth, int windowHeight,
const char* windowTitle);
void shutdownGraphics();
SDL_Surface* loadBitmap(const char* bitmapFileName,
int transparentRed, int transparentGreen, int transparentBlue);
void closeBitmap(SDL_Surface* bitmap);
// These calls must surround all drawing routines.
void beginScene();
void endScene();
void drawSprite(SDL_Surface* imageSurface,
SDL_Surface* screenSurface,
int srcX, int srcY,
int dstX, int dstY,
int width, int height);
void drawText(SDL_Surface* screen,
const char* string,
int size,
int x, int y,
int fR, int fG, int fB,
int bR, int bG, int bB);
/**********************************
* Input related global variables *
**********************************/
SDL_Event g_event;
bool g_keysHeld[323] = {false}; // everything will be initialized to false
/***************************
* Input related functions *
***************************/
void readInput();
void handleKeyboardInput();
/****************************
* Entry point for our game *
****************************/
int main(int argc, char **argv)
{
g_screen = initGraphics(WINDOW_WIDTH, WINDOW_HEIGHT,
WINDOW_TITLE);
g_doomguyBitmap = loadBitmap(DOOMGUY_BITMAP_FILE_NAME,
TRANSPARENT_RED, TRANSPARENT_GREEN, TRANSPARENT_BLUE);
SDL_Init(SDL_INIT_TIMER);
float deltaTime = 0.0;
int thisTime = 0;
int lastTime = 0;
while (g_gameIsRunning)
{
thisTime = SDL_GetTicks();
deltaTime = (float)(thisTime - lastTime) / 1000;
lastTime = thisTime;
// Handle input
readInput();
handleKeyboardInput();
// Handle game logic
updateDoomGuy(deltaTime);
// Draw the scene
beginScene();
drawText(g_screen,
"This tutorial rules!",
12, 250, 100,
200, 0, 0,
0, 0, 0);
drawSprite(g_doomguyBitmap,
g_screen,
DOOMGUY_IMAGE_X, DOOMGUY_IMAGE_Y,
g_doomguyX, g_doomguyY,
DOOMGUY_IMAGE_WIDTH, DOOMGUY_IMAGE_HEIGHT);
endScene();
// Give the computer a break (optional)
SDL_Delay(1);
}
closeBitmap(g_doomguyBitmap);
shutdownGraphics();
return 0;
}
/*****************************
* Drawing related functions *
*****************************/
SDL_Surface* initGraphics(int windowWidth, int windowHeight,
const char* windowTitle)
{
SDL_Init(SDL_INIT_VIDEO);
TTF_Init();
SDL_Surface* screen = SDL_SetVideoMode(windowWidth, windowHeight,
0, SDL_HWSURFACE | SDL_DOUBLEBUF );
SDL_WM_SetCaption(windowTitle, 0);
return screen;
}
void shutdownGraphics()
{
TTF_Quit();
SDL_Quit();
}
SDL_Surface* loadBitmap(const char* bitmapFileName,
int transparentRed, int transparentGreen, int transparentBlue)
{
SDL_Surface* bitmap = SDL_LoadBMP(bitmapFileName);
SDL_SetColorKey(bitmap,
SDL_SRCCOLORKEY,
SDL_MapRGB(bitmap->format, transparentRed, transparentGreen, transparentBlue));
return bitmap;
}
void closeBitmap(SDL_Surface* bitmap)
{
SDL_FreeSurface(bitmap);
}
// This just clears the screen.
void beginScene()
{
SDL_FillRect(g_screen,
NULL,
SDL_MapRGB(g_screen->format, BACKGROUND_RED, BACKGROUND_GREEN, BACKGROUND_BLUE));
}
// This just displays the scene.
void endScene()
{
SDL_Flip(g_screen);
}
void drawSprite(SDL_Surface* imageSurface,
SDL_Surface* screenSurface,
int srcX, int srcY,
int dstX, int dstY,
int width, int height)
{
SDL_Rect srcRect;
srcRect.x = srcX;
srcRect.y = srcY;
srcRect.w = width;
srcRect.h = height;
SDL_Rect dstRect;
dstRect.x = dstX;
dstRect.y = dstY;
dstRect.w = width;
dstRect.h = height;
SDL_BlitSurface(imageSurface, &srcRect, screenSurface, &dstRect);
}
void drawText(SDL_Surface* screen,
const char* string,
int size,
int x, int y,
int fR, int fG, int fB,
int bR, int bG, int bB)
{
TTF_Font* font = TTF_OpenFont("ARIAL.TTF", size);
SDL_Color foregroundColor = { fR, fG, fB };
SDL_Color backgroundColor = { bR, bG, bB };
SDL_Surface* textSurface = TTF_RenderText_Shaded(font, string,
foregroundColor, backgroundColor);
SDL_Rect textLocation = { x, y, 0, 0 };
SDL_BlitSurface(textSurface, NULL, screen, &textLocation);
SDL_FreeSurface(textSurface);
TTF_CloseFont(font);
}
/***************************
* Input related functions *
***************************/
void readInput()
{
if (SDL_PollEvent(&g_event))
{
if (g_event.type == SDL_QUIT)
{
g_gameIsRunning = false;
}
if (g_event.type == SDL_KEYDOWN)
{
g_keysHeld[g_event.key.keysym.sym] = true;
}
if (g_event.type == SDL_KEYUP)
{
g_keysHeld[g_event.key.keysym.sym] = false;
}
}
}
void handleKeyboardInput()
{
if (g_keysHeld[SDLK_ESCAPE])
{
g_gameIsRunning = false;
}
if (g_keysHeld[SDLK_LEFT])
{
g_doomguySpeedX = -DOOMGUY_SPEED;
}
else if (g_keysHeld[SDLK_RIGHT])
{
g_doomguySpeedX = DOOMGUY_SPEED;
}
else
{
// If neither arrow key is being held, the doom guy isn't moving.
g_doomguySpeedX = 0.0f;
}
}
/*******************************
* Character related functions *
*******************************/
void updateDoomGuy(float deltaTime)
{
g_doomguyX += g_doomguySpeedX * deltaTime;
}
The code above is divided into four sections relating to the window, character, graphics, and input. Each section is further broken down into constants, global variables, and functions. I broke the code up like this because it makes it more manageable. If I need to deal with the character, I just go to the character section. Code can be extremely unreadable when it's all clumped together, but very readable when it's divided properly. Each function takes care of one task. When I wrote the code, I only had to concentrate on one thing at a time because I only needed to worry about the purpose about the one function I was writing.
The only time I needed to think about how things worked together was in the main() function. Because I named each function after what it does, the task of putting everything together was quite simple. For example, when I got to the drawing code all I needed to say was "start drawing, draw the text, draw the doom guy, stop drawing". I didn't need to worry about how each individual task was accomplished; I just had to trust that each function would work.
I hope I'm not annoying you by going on about reducing complexity. I really think that it's the most important part of programming and that it's absolutely key to understanding object oriented programming.
The Object Oriented Way
Now that you understand how the code works, it's time to break it up into classes. If you have trouble understanding this stuff, try to keep in mind that all we're doing is taking the same code and grouping it into related parts. Instead of having a bunch of global variables and functions that handle drawing stuff to the screen, we put them into classes.
The other thing to keep in mind is how we want to interact with these classes. The interface of a class is basically all the public methods on the class. These public methods define the operations that objects from this class can perform. This reduces complexity by allowing users of the class to forget about how the class is implemented and only worry about what operations the class allows them to do. No matter how clean or messy the code is within the class, a good interface will make it easy to use your class.
We are essentially moving up a level of abstraction. Before, we separated a bunch of operations into functions. When we needed those operations performed, we didn't need to worry about how to perform them. We just called the necessary functions. Because we properly named the functions, it was clear what each one did.
We'll now be doing the same thing with classes. We'll take related operations and data, and put them into a classes. When we need to deal with something like graphics, we'll just create a graphics object and tell it to do graphics related operations for us. We won't care what the graphics object is doing to make these things happen. Because we will be cleanly organizing our classes, it will be clear what each one does.
With functions, we went from a bunch of data and code instructions to a bunch of data and functions that perform instructions. Now we're going to go from a bunch of data and functions to a bunch of classes that represent a set of closely related data and instructions.
Classes can represent abstract concepts, or they can represent actual physical objects. For example, we'll have a class that represents the concept of drawing graphics. We'll also have a class that represents the physical object of the doom guy. Let's start with the graphics.
The SDLGraphics Class
In our current code, we have a global variable that stores our screen surface, and a bunch of functions that do things to the screen surface like initializing and drawing to it. These functions are all closely related so they can easily be put into a class together. This will come with the benefit that when we draw graphics from now on, we'll be able to think of it as using a graphics object instead of doing lower level things with a screen surface. In fact, we won't even need to know that the screen surface exists. All we'll care about is that the graphics class is working.
Putting the graphics code into a class is going to be very easy because we'll be able to pretty much copy and paste our drawing functions into the drawing class. Take a look at the following code and see how well you understand it. I'll provide an explanation below.
// SDLGraphics.h
#ifndef SDLGRAPHICS_H
#define SDLGRAPHICS_H
#include "SDL.h"
#include "SDL_TTF.h"
class SDLGraphics
{
// Methods
public:
// This creates a window and initializes the graphics object.
// The last three parameters are the desired background color.
// Call setBackgroundColor() to change this later.
SDLGraphics(int windowWidth, int windowHeight,
const char* windowTitle,
int bgR, int bgG, int bgB);
~SDLGraphics();
// Each call to loadBitmap() must have a corresponding call to closeBitmap().
SDL_Surface* loadBitmap(const char* bitmapFileName,
int transparentRed, int transparentGreen, int transparentBlue);
void closeBitmap(SDL_Surface* bitmap);
// These calls must surround all drawing routines.
void beginScene();
void endScene();
void drawSprite(SDL_Surface* imageSurface,
int srcX, int srcY,
int dstX, int dstY,
int width, int height);
void drawText(const char* string,
int size,
int x, int y,
int fR, int fG, int fB,
int bR, int bG, int bB);
void setBackgroundColor(int r, int g, int b);
// Data
private:
SDL_Surface* m_screen;
int m_backgroundColorRed;
int m_backgroundColorGreen;
int m_backgroundColorBlue;
};
#endif
Once thing you may notice here is that I didn't include the init or shutdown functions. I've seen a lot of people use init and shutdown functions in classes, but it really doesn't make sense when you can just use the constructor and destructor.
Another thing you probably noticed is the addition of the background color data members and the setBackgroundColor() method. This will allow users of the class to change the background color whenever they want to.
You'll also see that the drawing methods no longer take a pointer to the screen surface. Since we store this internally in the class, we don't need to take it as a parameter. This is great, because it saves users of our class from having to worry about how the screen surface works.
The implementation code should be pretty straightforward since I just copied it from my previous code. One thing to note though is that I call setBackgroundColor() from the constructor instead of setting the background color members manually. If you ever have a method in a class that performs an operation you need, let the method do it instead of doing it yourself.
// SDLGraphics.cpp
#include "SDLGraphics.h"
SDLGraphics::SDLGraphics(int windowWidth, int windowHeight,
const char* windowTitle,
int bgR, int bgG, int bgB)
{
SDL_Init(SDL_INIT_VIDEO);
TTF_Init();
m_screen = SDL_SetVideoMode(windowWidth, windowHeight,
0, SDL_HWSURFACE | SDL_DOUBLEBUF);
SDL_WM_SetCaption(windowTitle, 0);
setBackgroundColor(bgR, bgG, bgB);
}
SDLGraphics::~SDLGraphics()
{
TTF_Quit();
SDL_Quit();
}
SDL_Surface* SDLGraphics::loadBitmap(const char* bitmapFileName,
int transparentRed, int transparentGreen, int transparentBlue)
{
SDL_Surface* bitmap = SDL_LoadBMP(bitmapFileName);
SDL_SetColorKey(bitmap,
SDL_SRCCOLORKEY,
SDL_MapRGB(bitmap->format, transparentRed, transparentGreen, transparentBlue));
return bitmap;
}
void SDLGraphics::closeBitmap(SDL_Surface* bitmap)
{
SDL_FreeSurface(bitmap);
}
void SDLGraphics::beginScene()
{
// Clear the screen
SDL_FillRect(m_screen,
NULL,
SDL_MapRGB(m_screen->format, m_backgroundColorRed,
m_backgroundColorGreen,
m_backgroundColorBlue));
}
// This just displays the scene.
void SDLGraphics::endScene()
{
SDL_Flip(m_screen);
}
void SDLGraphics::drawSprite(SDL_Surface* imageSurface,
int srcX, int srcY,
int dstX, int dstY,
int width, int height)
{
SDL_Rect srcRect;
srcRect.x = srcX;
srcRect.y = srcY;
srcRect.w = width;
srcRect.h = height;
SDL_Rect dstRect;
dstRect.x = dstX;
dstRect.y = dstY;
dstRect.w = width;
dstRect.h = height;
SDL_BlitSurface(imageSurface, &srcRect, m_screen, &dstRect);
}
void SDLGraphics::drawText(const char* string,
int size,
int x, int y,
int fR, int fG, int fB,
int bR, int bG, int bB)
{
TTF_Font* font = TTF_OpenFont("ARIAL.TTF", size);
SDL_Color foregroundColor = { fR, fG, fB };
SDL_Color backgroundColor = { bR, bG, bB };
SDL_Surface* textSurface = TTF_RenderText_Shaded(font, string,
foregroundColor, backgroundColor);
SDL_Rect textLocation = { x, y, 0, 0 };
SDL_BlitSurface(textSurface, NULL, m_screen, &textLocation);
SDL_FreeSurface(textSurface);
TTF_CloseFont(font);
}
void SDLGraphics::setBackgroundColor(int r, int g, int b)
{
m_backgroundColorRed = r;
m_backgroundColorGreen = g;
m_backgroundColorBlue = b;
}
To keep the size of this tutorial reasonable, I'm not going to show the changes to main.cpp after each of these classes are added. I do encourage you to try to change the code in main.cpp to use the new graphics class though. If you have any trouble with this, just head on over to the forum and I'll do my best to help.
The Input Class
The input class is pretty simple but it's a bit different from the graphics class. With the graphics class, we were able to strip out all of the graphics code and put it into one class. At first, it seems like we can do the same thing for the input class by moving the readInput() and handleKeyboardInput() functions into a class.
This works fine for readInput(), but handleKeyboardInput() changes things about our game by moving the character around. If we move handleKeyboardInput() into the input class, we'll have to make the input class aware of the character.
There are two problems with doing this. One is that the input class won't be reusable any more. Ideally, we'd like to be able to take the input class files and put them into another project without changing it. We wouldn't be able to do this if the class was aware of the character, because we'd have to go in and strip out all of the character related code.
The other problem is that it makes our code more complex. There's nothing about a class named "Input" that suggests it's aware of the controllable character in the game. All that "Input" suggests is that the class deals with input.
This means that we'll have to leave handleKeyboardInput() in main.cpp. The global keys array and the readInput() method will be going to the input class though. We'll have to add a method that returns the keys array so handleKeyboardInput() can use it.
Another issue is that readInput() did a check to see if the user manually closed the window. Since the Input class doesn't have access to the g_isGameRunning variable, readInput() won't be able to tell the program to stop running anymore. We'll fix this by giving the Input class a boolean that stores whether or not the window has been closed. We'll also provide a windowClosed() method that users of the Input class will have to call to see if the window has been closed.
This is actually better because having readInput() close the window was a design flaw. I broke the reduce complexity rule by having a function do two things, one of which wasn't implied by the name of the function.
Here is the code for the Input class. As before, study it until you understand it, then try to change main.cpp to use it. Also, come to the forums if you're having trouble.
// Input.h
#ifndef INPUT_H
#define INPUT_H
#include "SDL.h"
class Input
{
// Methods
public:
Input();
~Input();
// Call this before any other methods
void readInput();
bool* getInput();
// Check this each frame
bool windowClosed();
// Data
private:
SDL_Event m_event;
bool m_keysHeld[323];
bool m_windowClosed;
};
#endif
------------------------------------------------------
// Input.cpp
#include "Input.h"
Input::Input()
{
m_windowClosed = false;
// Make sure all the keys are set to false.
for (int i = 0; i < 323; i++)
{
m_keysHeld[i] = false;
}
}
Input::~Input()
{
}
void Input::readInput()
{
if (SDL_PollEvent(&m_event))
{
if (m_event.type == SDL_QUIT)
{
m_windowClosed = true;
}
if (m_event.type == SDL_KEYDOWN)
{
m_keysHeld[m_event.key.keysym.sym] = true;
}
if (m_event.type == SDL_KEYUP)
{
m_keysHeld[m_event.key.keysym.sym] = false;
}
}
}
bool* Input::getInput()
{
return m_keysHeld;
}
bool Input::windowClosed()
{
return m_windowClosed;
}
The Character Class
A common example used to illustrate OOP is a ball. A ball has a number of properties, like size, altitude, and fuzziness. Instead of having a bunch of integers floating around to represent the ball, we can bundle them into a class so we can deal with a ball instead of some numbers.
We're going to do the same thing with the character. Right now, we store the character's physical appearance as an SDL surface, his location as two numbers, and his speed as another number. Instead of thinking of the character as these variables, we'll put the variables into a class and think of the character as a character.
One thing to note about the code below is that the character class stores a pointer to the graphics object. This is so we can load the character's bitmap and draw it to the screen. There are actually two problems with this. One is that the character has to know about the existence of the graphics object. The other is that we new have two pointers that point to the same graphics object (the one in main.cpp and the one the character class has). This can cause problems if we delete the object in one place but assume it's still alive in another.
I've stuck to this design anyways because it's so widely used that anyone using the code should be aware of it. If you don't like it, an alternative is to just pass the graphics pointer to any method in the character class that needs a graphics object. This way, you can still use it, but you don't have to worry about it being pointed to by multiple pointers.
The rest of the code should be easy enough to follow. Again, try to make this class work in main.cpp and ask questions on the forums if you get stuck.
// Character.h
#ifndef CHARACTER_H
#define CHARACTER_H
#include "SDL.h"
#include "SDLGraphics.h"
class Character
{
// Methods
public:
// This class holds a pointer to the graphics object but will not destroy it.
Character(SDLGraphics* graphics,
int imageX, int imageY,
int width, int height,
int transparentR, int transparentG, int transparentB,
const char* bitmapFileName,
float x, float y,
float maxSpeed);
~Character();
// This must be called each frame
void update(float deltaTime);
void draw();
void moveLeft();
void moveRight();
void stopMoving();
// Data
private:
SDLGraphics* m_graphics;
SDL_Surface* m_bitmap;
float m_x;
float m_y;
float m_width;
float m_height;
float m_imageX;
float m_imageY;
float m_maxSpeed;
float m_currentSpeedX;
};
#endif
--------------------------------------------------------
// Character.cpp
#include "Character.h"
Character::Character(SDLGraphics* graphics,
int imageX, int imageY,
int width, int height,
int transparentR, int transparentG, int transparentB,
const char* bitmapFileName,
float x, float y,
float maxSpeed) : m_graphics(graphics),
m_imageX(imageX), m_imageY(imageY),
m_width(width), m_height(height),
m_x(x), m_y(y),
m_maxSpeed(maxSpeed),
m_currentSpeedX(0.0f)
{
m_bitmap = m_graphics->loadBitmap(bitmapFileName,
transparentR, transparentG, transparentB);
}
Character::~Character()
{
m_graphics->closeBitmap(m_bitmap);
}
void Character::update(float deltaTime)
{
m_x += m_currentSpeedX * deltaTime;
}
void Character::draw()
{
m_graphics->drawSprite(m_bitmap,
m_imageX, m_imageY,
m_x, m_y,
m_width, m_height);
}
void Character::moveLeft()
{
m_currentSpeedX = -m_maxSpeed;
}
void Character::moveRight()
{
m_currentSpeedX = m_maxSpeed;
}
void Character::stopMoving()
{
m_currentSpeedX = 0.0f;
}
The Timer Class
The timer class is very small and works exactly as the timer code from main.cpp. The only thing that might be confusing is the timeSinceLastFrame() call. This method actually returns the time since it was last called. The first time you call this method, it will return an invalid value. This can either be ignored since it should cause problems, or you can always call timeSinceLastFrame() once right before the game loop to get rid of the invalid value.
When timeSinceLastFrame() gets called, it stores the current time. This means that every time it gets called, except for the first time, it will be able to determine the time that has passed since it was last called.
The next section will show my version of main.cpp after these classes have been added. If you've had trouble up until now, take a look at that when you're done with the timer class.
// Timer.h
#ifndef TIMER_H
#define TIMER_H
class Timer
{
// Methods
public:
Timer();
~Timer();
// This returns the number of milliseconds since this object was created.
float timeSinceCreation();
// This method returns the number of milliseconds that have passed since it was last called.
float timeSinceLastFrame();
// Data
private:
// This stores the time of that last call to timeSinceLastFrame().
float m_timeOfLastCall;
};
#endif
------------------------------------------------------------------
// Timer.cpp
#include "Timer.h"
#include "SDL.h"
Timer::Timer() : m_timeOfLastCall(0.0f)
{
SDL_Init(SDL_INIT_TIMER);
}
Timer::~Timer()
{
}
float Timer::timeSinceCreation()
{
// SDL_GetTicks() returns in seconds. We want to return in milliseconds.
return SDL_GetTicks() / 1000.0f;
}
float Timer::timeSinceLastFrame()
{
float thisTime = timeSinceCreation();
float deltaTime = thisTime - m_timeOfLastCall;
m_timeOfLastCall = thisTime;
return deltaTime;
}
main.cpp
Without further ado, here's the new main.cpp.
#include "SDLGraphics.h"
#include "Input.h"
#include "Character.h"
#include "Timer.h"
// Constants
const int WINDOW_WIDTH = 640;
const int WINDOW_HEIGHT = 480;
const char* WINDOW_TITLE = "SDL Start";
const char* DOOMGUY_BITMAP_FILE_NAME = "doomguy.bmp";
const float DOOMGUY_START_X = 250.0f;
const float DOOMGUY_START_Y = 150.0f;
const int DOOMGUY_IMAGE_X = 6;
const int DOOMGUY_IMAGE_Y = 6;
const int DOOMGUY_IMAGE_WIDTH = 34;
const int DOOMGUY_IMAGE_HEIGHT = 40;
const float DOOMGUY_SPEED = 100.0;
const int BACKGROUND_RED = 0;
const int BACKGROUND_GREEN = 0;
const int BACKGROUND_BLUE = 0;
const int TRANSPARENT_RED = 255;
const int TRANSPARENT_GREEN = 0;
const int TRANSPARENT_BLUE = 255;
// Global variables
bool g_gameIsRunning = true;
Character* g_doomguy = NULL;
SDLGraphics* g_graphics = NULL;
Input* g_input = NULL;
Timer* g_timer = NULL;
// Function prototypes
void handleKeyboardInput();
int main(int argc, char **argv)
{
g_graphics = new SDLGraphics(WINDOW_WIDTH, WINDOW_HEIGHT,
WINDOW_TITLE,
BACKGROUND_RED, BACKGROUND_GREEN, BACKGROUND_BLUE);
g_input = new Input();
g_doomguy = new Character(g_graphics,
DOOMGUY_IMAGE_X, DOOMGUY_IMAGE_Y,
DOOMGUY_IMAGE_WIDTH, DOOMGUY_IMAGE_HEIGHT,
TRANSPARENT_RED, TRANSPARENT_GREEN, TRANSPARENT_BLUE,
DOOMGUY_BITMAP_FILE_NAME,
DOOMGUY_START_X, DOOMGUY_START_Y,
DOOMGUY_SPEED);
g_timer = new Timer();
while (g_gameIsRunning)
{
float deltaTime = g_timer->timeSinceLastFrame();
// Handle input
g_input->readInput();
if (g_input->windowClosed())
{
g_gameIsRunning = false;
}
handleKeyboardInput();
// Handle game logic
g_doomguy->update(deltaTime);
// Draw the scene
g_graphics->beginScene();
g_graphics->drawText("This tutorial rules!",
12, 250, 100,
200, 0, 0,
0, 0, 0);
g_doomguy->draw();
g_graphics->endScene();
// Give the computer a break (optional)
SDL_Delay(1);
}
delete g_timer;
delete g_doomguy;
delete g_input;
delete g_graphics;
return 0;
}
void handleKeyboardInput()
{
bool* keysHeld = g_input->getInput();
if (keysHeld[SDLK_ESCAPE])
{
g_gameIsRunning = false;
}
if (keysHeld[SDLK_LEFT])
{
g_doomguy->moveLeft();
}
else if (keysHeld[SDLK_RIGHT])
{
g_doomguy->moveRight();
}
else
{
// If neither arrow key is being held, the doom guy isn't moving.
g_doomguy->stopMoving();
}
}
Conclusion
A lot of beginners get worked up when they see object oriented code because the code size and file count increase. This is a failure to recognize that writing clean, reusable code isn't about having fewer lines of code or fewer files. It's about breaking your code into manageable, recognizable parts. If in another project, we need to draw bitmaps to the screen, we can just grab the graphics class and start using it. We won't need to rewrite the code or even worry about how it works.
I hope that this tutorial has convinced you of the importance of OOP and that it has given you more confidence in using OOP for your own projects. I also hope that you understand how these classes are used, because I'll be using them for a number of tutorials in the future.
0 Responses to "Putting SDL Calls into Classes"
Post a Comment