This is my first time writing a game in C++/SDL and I decided to post the code here so you can tell me what I am doing wrong.
main.cpp
#include <SDL.h>
#include "Game.h"
int screenWidth = 640;
int screenHeight = 480;
const char* title = "Pong Clone";
Game game;
int main(int argc, char* args[])
{
game.init(title, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, screenWidth, screenHeight, false);
while (game.running())
{
game.eventHandler();
game.render();
}
return 0;
}
Game.h
#ifndef GAME_H
#define GAME_H
#include <SDL.h>
#include <iostream>
#include <string>
#include "GameObject.h"
class Game
{
public:
bool init(const char* title, int xPos, int yPos, int width, int height, bool flags);
void eventHandler();
void render();
void clean();
void collision();
void reset();
bool running() { return m_Running; }
private:
SDL_Window* window;
SDL_Renderer* renderer;
int screenWidth;
int screenHeight;
int xSpeed, ySpeed;
bool m_Running;
GameObject* paddle;
GameObject* ball;
};
#endif
Game.cpp
#include "Game.h"
#include <stdlib.h>
#include <time.h>
//player speed
int playerVelocity = 0;
int playerSpeed = 15;
bool Game::init(const char* title, int xPos, int yPos, int width, int height, bool flags)
{
// screen and renderer initialization
screenWidth = width;
screenHeight = height;
window = SDL_CreateWindow(title, xPos, yPos, screenWidth, screenHeight, flags);
if (window == nullptr)
{
std::cout << "SDL_CreateWindow Error: " << SDL_GetError() << std::endl;
}
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (renderer == nullptr)
{
std::cout << "SDL_CreateRenderer Error: " << SDL_GetError() << std::endl;
}
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
//game objects
ball = new GameObject();
paddle = new GameObject();
//image load
ball->load("Assets/ball.png", "ball", renderer);
paddle->load("Assets/paddle.png", "paddle", renderer);
//starting speed and directions
xSpeed = rand() % 8 + 5;
ySpeed = rand() % 8 + 5;
reset();
m_Running = true;
return true;
}
void Game::reset()
{
// center screen position
ball->setPosition((screenWidth - (ball->getW() / 2)) / 2, (screenHeight - (ball->getH() / 2)) / 2);
}
void Game::eventHandler()
{
// ball speed
ball->setX(ball->getX() + xSpeed);
ball->setY(ball->getY() - ySpeed);
// game loop
SDL_Event event;
if (SDL_PollEvent(&event))
{
switch(event.type)
{
case SDL_QUIT:
{
m_Running = false;
break;
}
case SDL_KEYDOWN:
{
switch( event.key.keysym.sym )
{
case SDLK_UP:
{
playerVelocity = -playerSpeed;
break;
}
case SDLK_DOWN:
{
playerVelocity = playerSpeed;
break;
}
default:
break;
}
break;
}
case SDL_KEYUP:
{
switch( event.key.keysym.sym )
{
case SDLK_UP:
{
if (playerVelocity < 0)
playerVelocity = 0;
break;
}
case SDLK_DOWN:
{
if (playerVelocity > 0)
playerVelocity = 0;
break;
}
default:
break;
}
break;
}
default:
break;
}
}
paddle->setY(paddle->getY() + playerVelocity);
collision();
}
void Game::render()
{
// render game objects
SDL_RenderClear(renderer);
ball->draw(renderer, "ball", ball->getX(), ball->getY());
paddle->draw(renderer, "paddle", paddle->getX(), paddle->getY());
SDL_RenderPresent(renderer);
}
void Game::collision()
{
// top collision
if (ball->getY() < 0)
{
ySpeed = -ySpeed;
}
// paddle collision
else if (ball->getX() <= paddle->getX() + paddle->getW() &&
(ball->getY() >= paddle->getY() && ball->getY() <= paddle->getY() + paddle->getH()))
{
xSpeed = -xSpeed;
}
// bottom collision
else if (ball->getY() >= (screenHeight - ball->getH()))
{
ySpeed = -ySpeed;
}
// player collision
else if (ball->getX() >= (screenWidth - ball->getW()))
{
xSpeed = -xSpeed;
}
// reset ball location
else if (ball->getX() < 0)
{
xSpeed = rand() % 8 + 5;
ySpeed = rand() % 8 + 5;
reset();
}
}
GameObject.h
#ifndef GAMEOBJECT_H
#define GAMEOBJECT_H
#include <SDL.h>
#include <SDL_image.h>
#include <string>
#include <iostream>
class GameObject
{
public:
bool load(std::string filename, std::string id, SDL_Renderer* renderer);
void draw(SDL_Renderer* renderer, std::string id, int x, int y);
void setX(int x);
void setY(int y);
int getX();
int getY();
int getW();
int getH();
void setPosition(int x, int y);
private:
SDL_Renderer* renderer;
SDL_Surface* tempImage;
SDL_Texture* texture;
SDL_Rect srcRect;
SDL_Rect dstRect;
};
#endif
GameObject.cpp
#include "GameObject.h"
bool GameObject::load(std::string filename, std::string id, SDL_Renderer* renderer)
{
// image load manager
tempImage = IMG_Load(filename.c_str());
texture = SDL_CreateTextureFromSurface(renderer, tempImage);
SDL_FreeSurface(tempImage);
SDL_QueryTexture(texture, NULL, NULL, &srcRect.w, &srcRect.h);
dstRect.w = srcRect.w;
dstRect.h = srcRect.h;
return true;
}
void GameObject::draw(SDL_Renderer* renderer, std::string id, int x, int y)
{
// image draw
dstRect.x = x;
dstRect.y = y;
SDL_RenderCopy(renderer, texture, &srcRect, &dstRect);
}
void GameObject::setX(int x)
{
dstRect.x = x;
}
void GameObject::setY(int y)
{
dstRect.y = y;
}
int GameObject::getX()
{
return dstRect.x;
}
int GameObject::getY()
{
return dstRect.y;
}
int GameObject::getW()
{
return dstRect.w;
}
int GameObject::getH()
{
return dstRect.h;
}
void GameObject::setPosition(int x, int y)
{
dstRect.x = x;
dstRect.y = y;
}
I'm still working on AI/2nd player and score.
My main problem right now is: how do I correctly randomize the starting direction of the ball? How can I increase the ball speed after each bounce?
-
\$\begingroup\$ I should note that the last two questions are off-topic as we don't assist in adding additional functionality. For such questions, consult Stack Overflow. \$\endgroup\$Jamal– Jamal2014年04月15日 19:54:40 +00:00Commented Apr 15, 2014 at 19:54
3 Answers 3
This is just a quick partial review. I can't comment on the SDL parts because I know very little about SDL.
main.cpp
Avoid global mutable state. All of the variables shown below could be made local.
int screenWidth = 640;
int screenHeight = 480;
const char* title = "Pong Clone";
Game game;
If for some reason, you want them in a global area, at least make them constants. Since these are in a .cpp file and not a .h, you can mark them to be static
. This gives them internal linkage, which basically keeps them 'private' to the file. Of course, if you want to extern
them, then do not do this.
static const int SCREEN_WIDTH = 640;
static const int SCREEN_HEIGHT = 480;
static const char* TITLE = "Pong Clone";
An alternative to static global variables, is putting them into an unnamed namespace.
namespace {
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;
const char* TITLE = "Pong Clone";
}
If you ever need to put these values into a header file, I would place them into a class or named-namespace. If you do the former (put them in a class), it's okay to make them static. If you do the latter (put them in a named-namespace), do not make them static or else every translation unit will have its own private copy of the variable.
Game.h
Remove #include <iostream>
and #include <string>
. This particular header file doesn't use them at all.
Two-step initialization is old-fashioned (though admittingly still used). Your init()
function could be turned into a constructor instead.
Game (const char* title, int xPos, int yPos, int width, int height, bool flags);
The const char* title
should also be changed to const std::string &title
, but I'll talk about that later.
I'm not seeing any reason why the objects below are pointers.
GameObject* paddle;
GameObject* ball;
Save yourself the headache of pointless memory management in this case and just make them objects.
GameObject paddle;
GameObject ball;
Game.cpp
Let's go back to my previous point of using std::string
instead of char*
.
bool Game::init(const char* title,//...
// ... More code
window = SDL_CreateWindow(title,//...
Since you are not checking to see if title
is nullptr
, it would be better to use const std::string &title
instead.
If you choose to keep init()
instead of creating a constructor, then I'm guessing these two if-statements should return false
.
window = SDL_CreateWindow(title, xPos, yPos, screenWidth, screenHeight, flags);
if (window == nullptr)
{
std::cout << "SDL_CreateWindow Error: " << SDL_GetError() << std::endl;
return false; // probably?
}
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (renderer == nullptr)
{
std::cout << "SDL_CreateRenderer Error: " << SDL_GetError() << std::endl;
return false; // probably?
}
Checking error codes is also somewhat old fashioned. I would recommend throwing exceptions instead.
I see that you are using rand()
right here.
//starting speed and directions
xSpeed = rand() % 8 + 5;
ySpeed = rand() % 8 + 5;
I do not see you using srand() anywhere though. Without srand()
, your random number generator seed will always be the same and you will always get the same results.
Even better, since you are using C++11, I would recommend using functions from the new <random
> header. Since you are looking for a random number in the range [5,12], here is a rough example:
// Be sure to #include <chrono>
auto seed = std::chrono::system_clock::now ().time_since_epoch ().count () ;
// Be sure to #include <random>
typedef std::default_random_engine::result_type seed_type ;
std::default_random_engine generator (static_cast <seed_type> (seed)) ;
std::uniform_int_distribution <int> distribution (5, 12) ;
xSpeed = distribution (generator) ;
ySpeed = distribution (generator) ;
You would probably want to create the std::default_random_engine
only once though, or else you'll be constantly reseeding it.
-
2\$\begingroup\$
Avoid global variables.
. The real saying isAvoid global mutable state.
The important part is mutable. If the values are constant for the duration of the application then it is not an issue. \$\endgroup\$Loki Astari– Loki Astari2014年04月15日 18:08:32 +00:00Commented Apr 15, 2014 at 18:08 -
\$\begingroup\$
old-fashioned
is being generous. \$\endgroup\$Loki Astari– Loki Astari2014年04月15日 18:13:41 +00:00Commented Apr 15, 2014 at 18:13 -
1\$\begingroup\$ Since you're going the C++11 route, you should mention
<random>
and discontinued use ofrand()
. \$\endgroup\$Jamal– Jamal2014年04月15日 19:57:54 +00:00Commented Apr 15, 2014 at 19:57 -
\$\begingroup\$ Here is my Pong clone in SDL: github.com/Loki-Astari/ThorsSDL/tree/main/src/example/pong \$\endgroup\$Loki Astari– Loki Astari2023年01月11日 18:24:03 +00:00Commented Jan 11, 2023 at 18:24
General comments:
- You have inconsistent indentation. The statements in the outermost block scope in the functions in GameObject.cpp are indented four spaces, while everywhere else they're not indented at all. The declarations in the
private
section of GameObject.h are indented, but thepublic
section isn't.
File main.cpp:
You're not using the arguments to
main()
, so you could use the alternative form that doesn't declare them:int main()
The function
Game::init()
returns abool
(it always returnstrue
, but presumably it's intended to indicate success or failure), but you're not checking the return value here. However, I'd go even more C++ and use a constructor for Game and use exceptions to indicate an error during construction.I don't see a reason for having the
Game
object defined outside ofmain()
; I'd make it a local variable within your main function.
File Game.h:
The member function
running()
doesn't modify the instance in any way, so you should add aconst
specifier to it:bool running() const { return m_Running; }
File Game.cpp:
You have two global variables,
playerSpeed
andplayerVelocity
. Since they're used exclusively within theGame
class' methods, they should be members of the class.playerSpeed
is never changed, so it should be declared as aconst int
. Also consider different names, since the words "speed" and "velocity" are very close to being synonyms; perhapsmaximumSpeed
andactualSpeed
respectively?Consider adding a constructor for the Game class, instead of having a separate
init()
method that initializes all the class-level variables. The compiler will generate a constructor for you if you don't provide one, but that means that you get default initialization of all the member variables, followed by re-initialization when you call theinit()
method. If you switch to using a constructor, you should probably also look into using exceptions to report errors to the constructor.In
Game::init()
you have this code:window = SDL_CreateWindow(title, xPos, yPos, screenWidth, screenHeight, flags); if (window == nullptr) { std::cout << "SDL_CreateWindow Error: " << SDL_GetError() << std::endl; } renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); if (renderer == nullptr) { std::cout << "SDL_CreateRenderer Error: " << SDL_GetError() << std::endl; } SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
- By convention, errors should go to
std::cerr
; normal output should go tostd::cout
. - Even though you check if the
SDL_CreateWindow
call fails, you still go on to use the returned value in therenderer = ...
line. Same for the call toSDL_CreateRenderer
, where you use the value in theSDL_SetRenderDrawColor
color. I'm not familiar with SDL so I don't know if it will handlenullptr
argument values gracefully, but this looks suspicious.
- By convention, errors should go to
You're using
nullptr
, so you must be using C++11. You're usingnew
to allocate a couple of instances of yourGameObject
class; in C++11, the recommendation is to usestd::unique_ptr
to manage instances of raw pointers. So you would have:// In Game.h: std::unique_ptr<GameObject> ball; // In Game.cpp ball = std::unique_ptr<GameObject>(new GameObject());
The
unique_ptr
will automatically calldelete
the objects they manage when the containing class is destroyed. If you don't want to useunique_ptr
, you need to define a destructor for the Game class and delete the GameObject objects there:Game::~Game() { delete ball; delete paddle; }
In
Game::event_handler()
, you have code like:// ball speed ball->setX(ball->getX() + xSpeed); ball->setY(ball->getY() - ySpeed);
Consider changing
GameObject
's interface toupdateX()
andupdateY()
that takes just the offset, and let theGameObject
class worry about the details of changing its X- or Y-coordinates.
File GameObject.h:
Member functions
GameObject::SetX()
,SetY()
,GetX()
,GetY()
,GetW()
,GetH()
are all one-liner functions. For simple setters and getters like these, it's often better to define them in the header file so that (a) you don't have to look for them in two places, and (b) the compiler can inline them wherever they're used, meaning that instead of making a function call, it can simply insert the function's code directly at the point of use.The getters should be declared
const
(likeGame::running()
above):class GameObject { // ...code elided... int GetX() const { return dstRect.x; } // ...remaining getters... };
File GameObject.cpp:
Consider adding a constructor for GameObject instead of having a separate
load()
method; see the equivalent comment for Game.cpp above for the reason why.In function
GameObject::load()
, the variabletempImage
is used. It is declared at class scope, but it doesn't seem to be used outside of this function. If that's true, you could move the declaration into this function, keeping it closer to its use. It also means that you don't have a class-level variable that isn't valid outside of the one function where it's used.The same applies for the class-level
srcRect
which is only used in this function. You also initializesrcRect
in the call toSDL_QueryTexture()
, then copy the values intodstRect
immediately afterwards; could you usedstRect
in the call, thus eliminating the need forsrcRect
?Functions
GameObject::load()
andGameObject::draw()
take anid
argument that they never use.Move all the setters and getters into the header file, as mentioned above.
Please keep your indentation consistent. You are correctly indenting within many curly brace blocks, but not all of them. You should especially indent all code within functions, class
es, and struct
s, so that any indented code blocks within the function can be distinguished. It also makes it harder to line up the curly braces. One example of this inconsistency is in main()
. If two or more closing braces are lined up vertically, that's a good indication that something is not correctly indented.