I made a simple Pong game using SDL2, with a twist: You don't get to play :), only the 2 AIs get to. ;)
Because Pong is a very small game, I put everything in a single file (main.cpp). If that was the wrong choice, please tell me :).
I know that global variables are bad, but in my defense they are all constexpr (if that's not ok, please tell me).
I didn't put many comments, because I think the code is fairly self-documenting (I could be wrong though).
As far as I know, the 2 AIs are unbeatable, and won't ever let the ball pass (except if the paddle speed is so low and the ball speed is so high). The score in the middle shows how many times the AIs failed (should always be 0).
I am fairly advanced (maybe even only low intermediate) in C++, so if I did something wrong in terms of idioms, etc in C++11/C++14, please tell me :).
As a bonus, if you could maybe review the AI, that would be great!
Here's the code in question:
#include <SDL.h>
#include <SDL_ttf.h>
#include <string>
#include <memory>
#include <random>
constexpr unsigned short gWidth = 640U;
constexpr unsigned short gHeight = 480U;
constexpr unsigned short gDistance = 25U;
constexpr unsigned short gStartY = 50U;
constexpr unsigned short gPaddleWidth = 10U;
constexpr unsigned short gPaddleHeight = 45U;
constexpr unsigned short gPaddleVelocity = 8U;
constexpr unsigned short gBallWidth = 10U;
constexpr unsigned short gBallHeight = 10U;
constexpr unsigned short gBallVelocity = 8U;
constexpr short gPaddleDefaultDest = -999;
//Constructs object with supplied parameters (note that object **must** have variables 'x', 'y', 'width' and 'height')
template<typename T>
void loadObject(T& pad, short x, short y, unsigned short w, unsigned short h);
//Checks if variable is in a valid state, cleanups resources if not
template<typename T>
inline bool check(const T& t)
{
if (!t)
{
TTF_Quit();
SDL_Quit();
return false;
}
return true;
}
enum class DirectionX
{
Right = +1,
Left = -1
};
enum class DirectionY
{
Up = -1,
Down = 1
};
struct Paddle
{
short x;
short y;
unsigned short width;
unsigned short height;
short destination = gPaddleDefaultDest;
bool tryMoveY(const short value)
{
//Move only if inside window bounds
if (y + value >= 0 && y + height + value <= gHeight)
{
y += value;
return true;
}
return false;
}
};
struct Ball
{
short x;
short y;
unsigned short width;
unsigned short height;
void move(const Paddle& pad1, const Paddle& pad2, const short value, unsigned short& score, bool disable = false)
{
//Flip direction if applicable
if (x + value <= 0 || x + value + width >= gWidth)
{
flipX *= -1;
++score;
}
if (y + value <= 0 || y + value + height >= gHeight)
flipY *= -1;
if (x > pad1.x && x < pad1.x + pad1.width && ((y > pad1.y && y < pad1.y + pad1.height) || (y + height > pad1.y && y + height < pad1.y + pad1.height)) && !disable)
flipX *= -1;
if (x + width > pad2.x && x + width < pad2.x + pad2.width && ((y > pad2.y && y < pad2.y + pad2.height) || (y + height > pad2.y && y + height < pad2.y + pad2.height)) && !disable)
flipX *= -1;
x += value * flipX;
y += value * flipY;
}
DirectionX getDirectionX() const { return flipX == +1 ? DirectionX::Right : DirectionX::Left; }
DirectionY getDirectionY() const { return flipY == +1 ? DirectionY::Up : DirectionY::Down; }
private:
char flipX = 1;
char flipY = 1;
};
//Moving AIs
void moveAIs(Paddle& pad1, Paddle& pad2, const Ball& ball);
//Helper function
void movePaddle(Paddle& pad1, Paddle& pad2, const Ball& ball);
int main(int, char**)
{
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) == -1)
return -1;
if (TTF_Init() == -1)
return -1;
std::unique_ptr<SDL_Window, decltype(&SDL_DestroyWindow)> window{ nullptr, &SDL_DestroyWindow };
window.reset(SDL_CreateWindow("Pong", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, gWidth, gHeight, SDL_WINDOW_SHOWN));
if (!check(window)) return -1;
std::unique_ptr<SDL_Renderer, decltype(&SDL_DestroyRenderer)> render{ nullptr, &SDL_DestroyRenderer };
render.reset(SDL_CreateRenderer(window.get(), -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC));
if (!check(render)) return -1;
Paddle pad1, pad2;
loadObject(pad1, gDistance, gStartY, gPaddleWidth, gPaddleHeight);
loadObject(pad2, gWidth - gDistance - gPaddleWidth, gStartY, gPaddleWidth, gPaddleHeight);
//Random start position
std::random_device re;
std::mt19937 mt{ re() };
std::uniform_int_distribution<int> uid{ 100, 300 };
Ball ball;
loadObject(ball, uid(mt), uid(mt), gBallWidth, gBallWidth);
//Load textures
std::unique_ptr<SDL_Surface, decltype(&SDL_FreeSurface)> surface{ SDL_LoadBMP("paddle.bmp"), &SDL_FreeSurface };
std::unique_ptr<SDL_Texture, decltype(&SDL_DestroyTexture)> tex{ SDL_CreateTextureFromSurface(render.get(), surface.get()), &SDL_DestroyTexture };
//Load font for text rendering
std::unique_ptr<TTF_Font, decltype(&TTF_CloseFont)> cornerstone{ TTF_OpenFont("Cornerstone.ttf", 32), [](TTF_Font*) {} };
SDL_Color White = { 255, 255, 255 };
//Main loop
SDL_Event event;
bool run = true;
unsigned short score = 0;
while (run)
{
//Event loop
while (SDL_PollEvent(&event))
{
if (event.type == SDL_QUIT)
run = false;
}
//Move ball
ball.move(pad1, pad2, +gBallVelocity, score);
//Update AI positions
moveAIs(pad1, pad2, ball);
//Clear screen
SDL_RenderClear(render.get());
//Copy paddles
SDL_Rect rect{ pad1.x, pad1.y, pad1.width, pad1.height };
SDL_RenderCopy(render.get(), tex.get(), nullptr, &rect);
rect = { pad2.x, pad2.y, pad2.width, pad2.height };
SDL_RenderCopy(render.get(), tex.get(), nullptr, &rect);
//Copy ball
rect = { ball.x, ball.y, ball.width, ball.height };
SDL_RenderCopy(render.get(), tex.get(), nullptr, &rect);
//Copy text
std::unique_ptr<SDL_Surface, decltype(&SDL_FreeSurface)> fontSurface{ TTF_RenderText_Solid(cornerstone.get(), std::to_string(score).c_str(), White), &SDL_FreeSurface };
std::unique_ptr<SDL_Texture, decltype(&SDL_DestroyTexture)> fontTexture{ SDL_CreateTextureFromSurface(render.get(), fontSurface.get()), &SDL_DestroyTexture };
rect = { gWidth / 2 - 30, 60, 80, 80 };
SDL_RenderCopy(render.get(), fontTexture.get(), nullptr, &rect);
//Present
SDL_RenderPresent(render.get());
}
TTF_Quit();
SDL_Quit();
return 0;
}
template<typename T>
void loadObject(T& pad, short x, short y, unsigned short w, unsigned short h)
{
pad.x = x;
pad.y = y;
pad.width = w;
pad.height = h;
}
//Paddle should move to the ball
void moveAIs(Paddle& pad1, Paddle& pad2, const Ball& ball)
{
if (ball.getDirectionX() == DirectionX::Left)
{
movePaddle(pad1, pad2, ball);
pad2.destination = gPaddleDefaultDest;
}
else
{
movePaddle(pad2, pad1, ball);
pad1.destination = gPaddleDefaultDest;
}
}
void movePaddle(Paddle& pad1, Paddle& pad2, const Ball& ball)
{
//Stop if already in correct position
if (pad1.destination > pad1.y && pad1.destination < pad1.y + pad1.height
&& pad1.destination + ball.height > pad1.y && pad1.destination + ball.height < pad1.y + pad1.height)
return;
if (pad1.destination != gPaddleDefaultDest)
{
pad1.tryMoveY(pad1.destination - pad1.y > 0 ? +gPaddleVelocity : -gPaddleVelocity);
return;
}
//Move virtual ball until final position is known
Ball copy = ball;
unsigned short fakeScore = 0;
if (copy.getDirectionX() == DirectionX::Right)
{
while (copy.x < pad1.x)
copy.move(pad2, pad1, +gPaddleVelocity, fakeScore, true);
}
else
{
while (copy.x > pad1.x)
copy.move(pad1, pad2, +gPaddleVelocity, fakeScore, true);
}
pad1.destination = copy.y;
}