In part 1 of this series I covered the arhcitecture of our little Pong game. We’ve got a few classes that we need to build and a little bit of glue-code to keep it all together. For this second article I’m going to go through each of those classes and the glue and I’m going to give you some code that will compile into Pong and you can play it, and have fun.
I’ll be using C++ as my language but I’ll keep the C++ only features to a minimum and will explain every use of the STL (Standard Template Library). It’ll be on you to find the equivalents in your language of choice. For the library that I mentioned in the previous article, I’ll be using SFML. If you use SFML too (there’s bindings to many languages) it should make translating my code to your language a bit easier.
A note on compilers and IDEs
It shouldn’t matter, but I am using CodeBlocks IDE and the Mingw-w64 compier toolchain. I develop on Windows 10 on a Surface Pro laptop/tablet thing.
Now, you might wonder a few things here. Firstly, why aren’t I using Visual Studio? Well, I don’t like it. I’m also prone to develop on Linux every now and then, and having my project files translate from one OS to the other helps with that. Secondly, when I was starting out, Visual Studio didn’t have a free version (we all pirated Visual C++ 6). Anyway, that’s just continued to carry into modern times.
You might also wonder why I’m developing on such a terrible machine. Well it’s convienient for one, I’m writing this on the train ride into work (yes, I have a real job). But also because if I can run my game on this toaster, then everyone can run my game. As small developers we should think about this a lot. If we were to ever price our games competitively, we can’t charge whatever AAA or even AA games are charging. With our likely low low prices, you need to consider that someone who can only spend that much has a crappy machine to match.
Keep in mind that there are some minor changes from the original architecture in the first part to what we’ve got in here. Some of these changes are because I didn’t think of something, others are just in order to satisfy SFML. I’ve left the original article as is to highlight that programming games, really any development, has iterative changes that you cannot stop. Thankfully, ours are small.
#includes & #defines
We’ve got a few things to add in first. As I said, I’m using SFML, so in addition to linking to the SFML libraries, I need to include the correct header files. If you’re not using SFML or C++ you’ll need to work out how to include the right code, but that should be in the installation/usage files of your chosen lib. I’ve got some C++ includes to use as well.
#include <SFML/Audio.hpp>
#include <SFML/Graphics.hpp>
#include <cstdint>
#include <chrono>
#include <functional>
#include <iostream>
Code language: C++ (cpp)
I’ve also got a few global defines as well. I don’t like to use #define
and instead use const
values, but it’s really neither here or there. If you’re not using C++ you basically want these numbers to be available anywhere so you’re not using magic numbers.
What’s a Magic Number?
These are those numbers you end up with in your game code, like 0.123420
that are just there. They are magic. They are used as multipliers, or are added to positions here and there, but it’s not clear looking at the code what that number is for.
And then to make it worse, there’s a few places the same 0.123420
appears. Are those all linked or is it just a coincidence?
To avoid this confusion, don’t use magic numbers. Create constant named variables and use them instead.
const std::uint16_t WINDOW_WIDTH = 1600;
const std::uint16_t WINDOW_HEIGHT = 900;
const float UPDATE_MS = 33;
const float BALL_RADIUS = 10;
const float BALL_VELOCITY = 400;
const float BALL_VEL_INCR = 60;
const float PADDLE_WIDTH = 10;
const float PADDLE_LENGTH = 50;
const float PADDLE_PADDING = 20;
const float PADDLE_SPEED = 400;
const float COURT_MARGIN = 10;
const float COURT_OUTLINE_WIDTH = 5;
Code language: C++ (cpp)
I really hope these are self-explanatory. If they’re not now, they should be when you see them used in the code.
A note on std::uint16_t
I use the fixed types in C++. I do this because I like to know exactly how big my values can be, and my games tend to do a lot of Messaging which requires packed data to be sent. I’ve found it helps me to think about my code a little harder if I have to think about the size of my variables, not just the types.
You can safely get away with just using int
or unsigned
if you like.
WINDOW_WIDTH
and WINDOW_HEIGHT
are the size of our game window. In your language or library, this could be the client area in your browser or something similar.
UPDATE_MS
was referred to in the previous article as UPDATE_RATE
. It’s the number of milliseconds between game updates. Refer to this article for explanation.
BALL_RADIUS
, is how big our ball is. BALL_VELOCITY
is the initial speed of our ball when it’s served measured in pixels a second. This is increased every time a player returns the ball by BALL_VEL_INCR
which is short for velocity increment. It’s important to note that these are measured in pixels per second, which means they are directly affected by the size of the Window/Client Area.
In other words, if we increase the area, it will take the ball longer to get from one player to the other. So a bigger screen size would give you more time to react to the ball travelling because the speed isn’t measured as a percentage of the screen traversed per second.
This is not good. But, it’s good enough for now. In the next article we’ll talk about improvements.
PADDLE_WIDTH
and PADDLE_LENGTH
are the size of our paddles in pixels. Again, the size of the window is irrelevant which we’ll talk about next time. PADDLE_PADDING
is the amount of space between the paddle and the edge of the screen. PADDLE_SPEED
is how many pixels per second (that measurement issue again!) that the player can move their respective paddles.
COURT_MARGIN
is how much space is between the court and the edge of the window. COURT_OUTLINE_WIDTH
is the thickness of the line that draws the court.
GAME_STATE
First up is our GAME_STATE
enumeration. It’s possible that your programming language doesn’t have enumerations, in which case three separate const
variables will do.
enum class GAME_STATE : std::uint_fast8_t
{
MENU,
IN_GAME,
EXIT
};
Code language: C++ (cpp)
There really should be no surprises here.
PLAY_STATE
enum class PLAY_STATE : std::uint_fast8_t
{
SERVE_PLAYER_ONE,
SERVE_PLAYER_TWO,
TOWARD_PLAYER_ONE,
TOWARD_PLAYER_TWO
};
Code language: C++ (cpp)
MOUSE_STATE
enum class MOUSE_STATE : std::uint_fast8_t
{
UP,
DOWN
};
Code language: C++ (cpp)
Vector2D
In this particular case, I could have used the inbuilt SFML sf::Vector2f
, but I wanted to keep the code as I designed it in the original architecture on the chance that your library of choice doens’t have an inbuilt vector.
struct Vector2D
{
float x;
float y;
};
Code language: C++ (cpp)
I think it’s prudent to point our that our vector struct
is really just a Point. There’s no real… vectory stuff here. There’s no cross-product math or dot-product functions. These are all things you’d need to add later when you want to make more exciting games.
RectangleShape
struct RectangleShape
{
float x;
float y;
float width;
float height;
};
Code language: C++ (cpp)
Much like the Vector class, there’s no helpful functions or operations here. There’s nothing that will help us with collision detection or calculating overlapping rectangles etc, all things that you would definitely need for more complicated games.
Court
The court is as expected. I think it’s worth mentioning that this class, as discussed last article, isn’t needed right now. But when we get into the next article in the series, there will be a point to it (at least I think so :).
class Court
{
public:
Court(const RectangleShape dimensions)
:
m_dimensions(dimensions)
{
}
const RectangleShape& GetDimensions() const
{
return m_dimensions;
}
private:
RectangleShape m_dimensions;
};
Code language: C++ (cpp)
Paddle
Now we’re getting into some real class. There’s not a lot happening in this class, really just one interesting method.
class Paddle
{
public:
Paddle(const RectangleShape startingPosition)
:
m_rect(startingPosition)
{
}
const RectangleShape& GetPositionSize() const
{
return m_rect;
}
void SetPositionSize(const RectangleShape newPositionSize)
{
m_rect = newPositionSize;
}
void SetPosition(const Vector2D newPosition)
{
m_rect.x = newPosition.x;
m_rect.y = newPosition.y;
}
private:
RectangleShape m_rect;
};
Code language: C++ (cpp)
The SetPosition
method is moving the position of the Paddle
without changing it’s size, but everything is still stored in the same RectangleShape
.
Ball
This is another very straight-forward class; which like most of the others so far, is just acting as a container for grouped attributes. This isn’t the best way to go about designing your code-base, but for our educational purposes we’ll stick with it.
class Ball
{
public:
Ball(const Vector2D startPosition, const float radius)
:
m_position(startPosition),
m_radius(radius),
m_velocity({0,0})
{
}
const Vector2D& GetPosition() const
{
return m_position;
}
const float& GetRadius() const
{
return m_radius;
}
const Vector2D& GetVelocity() const
{
return m_velocity;
}
void SetPosition(const Vector2D newPosition)
{
m_position = newPosition;
}
void SetVelocity(const Vector2D newVelocity)
{
m_velocity = newVelocity;
}
private:
Vector2D m_position;
float m_radius;
Vector2D m_velocity;
};
Code language: C++ (cpp)
GameRenderer
This is the first class that actually does something. From the last article we know it’s a static
class because it doesn’t need any instance-specific state, and we would only ever need one of it. We wouldn’t have multiple renderers for our game.
A lot of people don’t like static
classes in C++ because they’re just glorified globals. But we’re just working on our little game, with our very small code-base, on our own. We don’t need to worry too much about this.
class GameRenderer
{
public:
static bool Init(sf::RenderTarget* target, sf::Font* font)
{
m_target = target;
m_font = font;
}
static void Render(const float& elapsedMilliseconds,
const Paddle& playerOne,
const Paddle& playerTwo,
const Ball& ball,
const Court& court,
const std::uint_fast8_t& p1Score,
const std::uint_fast8_t& p2Score)
{
sf::RectangleShape courtShape;
const RectangleShape& cShape = court.GetDimensions();
courtShape.setPosition({cShape.x,cShape.y});
courtShape.setSize({cShape.width,cShape.height});
courtShape.setFillColor(sf::Color::Transparent);
courtShape.setOutlineColor(sf::Color::White);
courtShape.setOutlineThickness(-COURT_OUTLINE_WIDTH);
m_target->draw(courtShape);
courtShape.setPosition({WINDOW_WIDTH/2-COURT_OUTLINE_WIDTH/2,COURT_MARGIN});
courtShape.setSize({COURT_OUTLINE_WIDTH,WINDOW_HEIGHT-COURT_MARGIN*2});
m_target->draw(courtShape);
sf::RectangleShape paddleShape;
const RectangleShape& p1Shape = playerOne.GetPositionSize();
paddleShape.setPosition({p1Shape.x,p1Shape.y});
paddleShape.setSize({p1Shape.width,p1Shape.height});
paddleShape.setFillColor(sf::Color::White);
m_target->draw(paddleShape);
const RectangleShape& p2Shape = playerTwo.GetPositionSize();
paddleShape.setPosition({p2Shape.x,p2Shape.y});
paddleShape.setSize({p2Shape.width,p2Shape.height});
paddleShape.setFillColor(sf::Color::White);
m_target->draw(paddleShape);
sf::CircleShape ballShape;
const Vector2D& ballPosition = ball.GetPosition();
const float& ballRadius = ball.GetRadius();
ballShape.setPosition({ballPosition.x-BALL_RADIUS,ballPosition.y-BALL_RADIUS});
ballShape.setRadius(ballRadius);
ballShape.setFillColor(sf::Color::White);
m_target->draw(ballShape);
sf::Text score(std::to_string(p1Score) + " " + std::to_string(p2Score), *m_font, 40);
sf::FloatRect bounds = score.getLocalBounds();
score.setPosition({WINDOW_WIDTH/2-bounds.width/2,COURT_MARGIN + COURT_OUTLINE_WIDTH + 5});
m_target->draw(score);
}
private:
static sf::RenderTarget* m_target;
static sf::Font* m_font;
};
sf::RenderTarget* GameRenderer::m_target = nullptr;
sf::Font* GameRenderer::m_font = nullptr;
Code language: C++ (cpp)
You would have instantly recognised that our code does not match our original UML diagram! That’s because I had to change some things to work for SFML. Depending on your library, you may have to as well.
The big change is the inclusion of a couple pointers to an sf::RenderTarget
and an sf::Font
. These will be needed to actually draw the game to the screen. These get populated via the new Init
method.
I’ve also added additional parameters to the Render
method. At this point, there’s just too many of them, and if I was sensible, I would have simply passed a constant reference to the PongGame
object that was calling it, and make it a friend class. But, these are problems we can solve in the next article.
Let’s step through that Render
method while we’re talking about it:
- Draw the court using the
COURT_OUTLINE_WIDTH
that we set before.
Depending on your library, you may need to make several calls here to draw 4 skinny rectangles / or 4 lines, for each side of the court. SFML allows me to do it in a single call by setting the fill colour of the rectangle to transparent. - Draw an extra line down the center
- Draw the first players paddle
- Draw the second players paddle
- Draw the ball
It’s important to note that the ball is drawn so that the center of the circle is at our ball position, and the circle extends around that point byBALL_RADIUS
(so the diameter of the ball is twice this radius). SFML renders from the top-left corner of the ball (if you imagine it in a box), and so I have to offset it by theBALL_RADIUS
, which is why you seeballShape.setPosition({ballPosition.x-BALL_RADIUS,ballPosition.y-BALL_RADIUS});
- Draw the score, and position it over the center-top of the screen
PongGame
Now we get to the workhorse of our game. This is the class that does all the work. Again, it’s not fantastic design to put everything in here like this, but we will keep it as is for this article. In the next article, we’re going to go through and improve our game and our code.
class PongGame
{
public:
PongGame(const std::uint_fast8_t scoreToWin, sf::RenderTarget& target, sf::Font& font)
:
m_playerOneScore(0),
m_playerTwoScore(0),
m_maxScore(scoreToWin),
m_court({
COURT_MARGIN,
COURT_MARGIN,
WINDOW_WIDTH-COURT_MARGIN*2,
WINDOW_HEIGHT-COURT_MARGIN*2
}),
m_ball({
WINDOW_WIDTH/2,
WINDOW_HEIGHT/2
},
BALL_RADIUS
),
m_playerOne({
COURT_MARGIN+PADDLE_PADDING,
WINDOW_HEIGHT/2-(PADDLE_LENGTH/2),
PADDLE_WIDTH,
PADDLE_LENGTH
}),
m_playerTwo({
WINDOW_WIDTH-COURT_MARGIN-PADDLE_PADDING-PADDLE_WIDTH,
WINDOW_HEIGHT/2-(PADDLE_LENGTH/2),
PADDLE_WIDTH,
PADDLE_LENGTH
}),
m_playState(PLAY_STATE::SERVE_PLAYER_ONE)
{
GameRenderer::Init(&target,&font);
}
GAME_STATE Update(const float elapsedMilliseconds)
{
float timeMultiplier = elapsedMilliseconds / 1000.0f;
const RectangleShape& paddle1 = m_playerOne.GetPositionSize();
const RectangleShape& paddle2 = m_playerTwo.GetPositionSize();
if(sf::Keyboard::isKeyPressed(sf::Keyboard::Q))
m_playerOne.SetPosition({paddle1.x,paddle1.y-PADDLE_SPEED*timeMultiplier});
if(sf::Keyboard::isKeyPressed(sf::Keyboard::Z))
m_playerOne.SetPosition({paddle1.x,paddle1.y+PADDLE_SPEED*timeMultiplier});
if(sf::Keyboard::isKeyPressed(sf::Keyboard::P))
m_playerTwo.SetPosition({paddle2.x,paddle2.y-PADDLE_SPEED*timeMultiplier});
if(sf::Keyboard::isKeyPressed(sf::Keyboard::Period))
m_playerTwo.SetPosition({paddle2.x,paddle2.y+PADDLE_SPEED*timeMultiplier});
switch(m_playState)
{
case PLAY_STATE::SERVE_PLAYER_ONE:
{
m_ball.SetVelocity({0,0});
const RectangleShape& paddle = m_playerOne.GetPositionSize();
m_ball.SetPosition({paddle.x + PADDLE_WIDTH,paddle.y + PADDLE_LENGTH/2});
if(sf::Keyboard::isKeyPressed(sf::Keyboard::Space))
{
m_ball.SetVelocity({BALL_VELOCITY,0});
m_playState = PLAY_STATE::TOWARD_PLAYER_TWO;
}
break;
}
case PLAY_STATE::SERVE_PLAYER_TWO:
{
m_ball.SetVelocity({0,0});
const RectangleShape& paddle = m_playerTwo.GetPositionSize();
m_ball.SetPosition({paddle.x - BALL_RADIUS,paddle.y + PADDLE_LENGTH/2});
if(sf::Keyboard::isKeyPressed(sf::Keyboard::Space))
{
m_ball.SetVelocity({-BALL_VELOCITY,0});
m_playState = PLAY_STATE::TOWARD_PLAYER_ONE;
}
break;
}
default:
break;
}
Vector2D ballPos = m_ball.GetPosition();
Vector2D ballVelocity = m_ball.GetVelocity();
const RectangleShape& courtShape = m_court.GetDimensions();
ballPos.x += ballVelocity.x * timeMultiplier;
ballPos.y += ballVelocity.y * timeMultiplier;
m_ball.SetPosition(ballPos);
switch(m_playState)
{
case PLAY_STATE::TOWARD_PLAYER_ONE:
{
if(ballPos.x - BALL_RADIUS > paddle1.x + PADDLE_WIDTH)
break; // ball hasn't reached player 1
if(ballPos.y + BALL_RADIUS >= paddle1.y && ballPos.y - BALL_RADIUS <= paddle1.y + PADDLE_LENGTH)
{
m_ball.SetPosition({paddle1.x + PADDLE_WIDTH + BALL_RADIUS + 1, ballPos.y});
ballVelocity.x = -ballVelocity.x;
if(ballPos.y + BALL_RADIUS <= paddle1.y + PADDLE_LENGTH/3)
ballVelocity.y -= BALL_VELOCITY/2;
else if(ballPos.y - BALL_RADIUS >= paddle1.y + PADDLE_LENGTH/3*2)
ballVelocity.y += BALL_VELOCITY/2;
if(ballVelocity.x > 0)
ballVelocity.x += BALL_VEL_INCR;
else
ballVelocity.x -= BALL_VEL_INCR;
if(ballVelocity.y > 0)
ballVelocity.y += BALL_VEL_INCR;
else
ballVelocity.y -= BALL_VEL_INCR;
m_ball.SetVelocity(ballVelocity);
m_playState = PLAY_STATE::TOWARD_PLAYER_TWO;
break;
}
if(ballPos.x + BALL_RADIUS < paddle1.x)
{
++m_playerTwoScore;
m_playState = PLAY_STATE::SERVE_PLAYER_ONE;
}
break;
}
case PLAY_STATE::TOWARD_PLAYER_TWO:
{
if(ballPos.x + BALL_RADIUS < paddle2.x)
break;
if(ballPos.y + BALL_RADIUS >= paddle2.y && ballPos.y - BALL_RADIUS <= paddle2.y + PADDLE_LENGTH)
{
m_ball.SetPosition({paddle2.x - BALL_RADIUS - 1, ballPos.y});
ballVelocity.x = -ballVelocity.x;
if(ballPos.y + BALL_RADIUS <= paddle2.y + PADDLE_LENGTH/3)
ballVelocity.y -= BALL_VELOCITY/2;
else if(ballPos.y - BALL_RADIUS >= paddle2.y + PADDLE_LENGTH/3*2)
ballVelocity.y += BALL_VELOCITY/2;
if(ballVelocity.x > 0)
ballVelocity.x += BALL_VEL_INCR;
else
ballVelocity.x -= BALL_VEL_INCR;
if(ballVelocity.y > 0)
ballVelocity.y += BALL_VEL_INCR;
else
ballVelocity.y -= BALL_VEL_INCR;
m_ball.SetVelocity(ballVelocity);
m_playState = PLAY_STATE::TOWARD_PLAYER_ONE;
break;
}
if(ballPos.x - BALL_RADIUS > paddle2.x + PADDLE_WIDTH)
{
++m_playerOneScore;
m_playState = PLAY_STATE::SERVE_PLAYER_TWO;
}
break;
}
default:
break;
}
if(ballPos.y <= courtShape.y)
{
m_ball.SetPosition({ballPos.x,courtShape.y});
m_ball.SetVelocity({ballVelocity.x,-ballVelocity.y});
}
else if(ballPos.y >= courtShape.y + courtShape.height)
{
m_ball.SetPosition({ballPos.x,courtShape.y + courtShape.height});
m_ball.SetVelocity({ballVelocity.x,-ballVelocity.y});
}
if(m_playerOneScore >= m_maxScore || m_playerTwoScore >= m_maxScore)
return GAME_STATE::MENU;
return GAME_STATE::IN_GAME;
}
void Render(const float elapsedMilliseconds) const
{
GameRenderer::Render(elapsedMilliseconds, m_playerOne, m_playerTwo, m_ball, m_court, m_playerOneScore, m_playerTwoScore);
}
private:
std::uint_fast8_t m_playerOneScore;
std::uint_fast8_t m_playerTwoScore;
std::uint_fast8_t m_maxScore;
const Court m_court;
Ball m_ball;
Paddle m_playerOne;
Paddle m_playerTwo;
PLAY_STATE m_playState;
};
Code language: C++ (cpp)
Man, there’s a lot in there. Let’s step through each method.
The constructor is setting the initial values for everything, none of that should be too weird if you understand the magic number variables that I explained at the top of the page. It’s also setting the initial state of the game to be SERVE_PLAYER_ONE
, which seems a little unfair, but this is something we can revisit later.
Because the PongGame
is also sort of responsible for drawing itself, it initiates the GameRenderer
class, which means it needs access to some members that it didn’t have in our original design. Again, a side effect of SFML.
Update
is where all the magic happens. Let’s step through each little block of code:
- We set a multiplier. This basically says, that because we’ve moved
elapsedMilliseconds
into the future, and if you remember, everything was measured in “pixels per second”, this tells you how many of those pixels-per-second have to be moved. Think about it, we never would take 1000ms to get to the next frame, so we can’t just move the ball by 400 pixels, instead we’ll move it the right amount for the number of milliseconds that have passed.
So, if 5ms passed the math would be:float timeMultiplier = 5 / 1000.f
which comes out at0.005
. Then, if we need to move the ball at a rate of400
pixels per second:ballPosition += 400 * 0.005
because a movement of2
pixels. - We grab the position and size of the paddles and keep them for later
- We check if either of the players are pressing keys to move up or down. In this case, I’m using
Q
andZ
for Player One, andP
and.
for Player 2. Notice that the movement amount uses ourtimeMultiplier
from above. - If we’re in a serving state, we attach the ball to the center of the paddle, facing toward the center of the court. We change it’s velocity to zero so it doesn’t move anywhere.
We allowed the paddles to move before setting the ball to the paddle position, otherwise we’d just need to move the ball again.
If the space bar is pressed, the ball is launched away from the paddle at our initialBALL_VELOCITY
and the state is changed to the appropriateTOWARD_PLAYER_X
value. - The ball now has it’s position updated based on it’s current velocity (which could be
0
if serving and the space bar hasn’t been hit), and again, using thetimeMultiplier
. - We then have a bit of tricky code to either:
- Do nothing if the ball is nowhere near the paddle it’s heading toward
- Bounce off the paddle if it’s gone past it’s position
- If it hit the top 3rd, we bounce at an upward angle
- If it hit the center, we bounce in the complete opposite direction
- If it hit bottom 3rd, we bounce it at a downward angle
- Register a score if the player missed the ball
If the player hit the ball, we also increase the velocity of the ball and change the state to return to the other player.
- We check if the ball needs to bounce off the edges of the court.
- If a player has score enough to win, we return a
MENU
state so that the game exits.
That’s everything in a game of Pong!
Button
But we still need a menu screen. First is the buttons that you can click:
class Button
{
public:
typedef std::function<void(void)> CallbackFunc;
enum class STATE : std::uint_fast8_t
{
UP,
DOWN,
HOVER
};
Button(const std::string text, const RectangleShape positionAndSize)
:
m_text(text),
m_positionAndSize(positionAndSize),
m_colorUp(sf::Color::Black),
m_colorDown(sf::Color::Red),
m_colorHover(sf::Color::Yellow),
m_callback([](){}),
m_state(STATE::UP)
{
}
const RectangleShape& GetPositionAndSize() const
{
return m_positionAndSize;
}
void SetPositionAndSize(const RectangleShape newPositionAndSize)
{
m_positionAndSize = newPositionAndSize;
}
void SetPosition(const Vector2D newPosition)
{
m_positionAndSize.x = newPosition.x;
m_positionAndSize.y = newPosition.y;
}
void SetSize(const Vector2D newSize)
{
m_positionAndSize.width = newSize.x;
m_positionAndSize.height = newSize.y;
}
bool HandleInput(const Vector2D& mousePosition, const MOUSE_STATE& mouseState)
{
if(mousePosition.x >= m_positionAndSize.x &&
mousePosition.x <= m_positionAndSize.x + m_positionAndSize.width &&
mousePosition.y >= m_positionAndSize.y &&
mousePosition.y <= m_positionAndSize.y + m_positionAndSize.height)
{
m_state = STATE::HOVER;
}
else
m_state = STATE::UP;
if(mouseState == MOUSE_STATE::DOWN && m_state == STATE::HOVER)
{
m_state = STATE::DOWN;
m_callback();
return true;
}
return false;
}
void SetColors(const sf::Color upColor, const sf::Color downColor, const sf::Color hoverColor)
{
m_colorUp = upColor;
m_colorHover = downColor;
m_colorHover = hoverColor;
}
void SetCallback(const CallbackFunc callback)
{
m_callback = callback;
}
const Button::STATE& GetState() const
{
return m_state;
}
void SetState(const Button::STATE newState)
{
m_state = newState;
}
void Render(sf::RenderTarget& target, const sf::Font& font) const
{
sf::Text buttonText(m_text,font,60);
buttonText.setColor(m_colorUp);
if(m_state == STATE::DOWN)
buttonText.setColor(m_colorDown);
else if(m_state == STATE::HOVER)
buttonText.setColor(m_colorHover);
buttonText.setPosition({m_positionAndSize.x,m_positionAndSize.y});
sf::RectangleShape bg;
bg.setPosition({m_positionAndSize.x,m_positionAndSize.y});
bg.setSize({m_positionAndSize.width,m_positionAndSize.height});
bg.setFillColor(sf::Color::White);
target.draw(bg);
target.draw(buttonText);
}
private:
std::string m_text;
RectangleShape m_positionAndSize;
sf::Color m_colorUp;
sf::Color m_colorDown;
sf::Color m_colorHover;
CallbackFunc m_callback;
STATE m_state;
};
Code language: C++ (cpp)
Like I said in the previous article, the UI takes so much effort! If you’re lucky (or smart) the library you chose already features UI elements. There’s really only two interesting methods here:
HandleInput
checks if the provided mouse position happens to be on top of our button. If it is, we go into HOVER
state. If not, we change to UP
state.
Then, if the mouse is currently DOWN
and we’re in a HOVER
state (as determined in the code just prior), then we can safely say that the button is actually DOWN
. (we can’t be DOWN
if the mouse isn’t over the top of us). If we are down, then we need to hit the callback.
Callback?
In C++11, a new feature was introduced called lambdas, which is what I’m using here. Javascript passes functions around as variables all the time, but that didn’t use to be super popular in C/C++ because it required function pointers. Now it’s a lot easier.
If your language doesn’t support something like this, you’ll need to be a little bit smarter in how it works. What I would suggest is that outside of the button, in the PongMenu
class, you check if the button is in a down state, and execute the required code then.
The Render
function is the other interesting one. Based on the state of the button, we’re going to draw the text in one of three different colours.
PongMenu
The final class is the menu.
class PongMenu
{
public:
PongMenu(sf::RenderTarget& target, sf::Font& font)
:
m_target(target),
m_font(font),
m_playButton("PLAY",{WINDOW_WIDTH/2,WINDOW_HEIGHT/2,140,65}),
m_exitButton("EXIT",{WINDOW_WIDTH/2,WINDOW_HEIGHT/2+100,130,65}),
m_shouldExit(false),
m_shouldStart(false)
{
m_playButton.SetCallback([this](){m_shouldStart = true;});
m_exitButton.SetCallback([this](){m_shouldExit = true;});
}
GAME_STATE Update(const float elapsedMilliseconds, const Vector2D& mousePos)
{
MOUSE_STATE state = MOUSE_STATE::UP;
if(sf::Mouse::isButtonPressed(sf::Mouse::Left) ||
sf::Mouse::isButtonPressed(sf::Mouse::Middle) ||
sf::Mouse::isButtonPressed(sf::Mouse::Right))
state = MOUSE_STATE::DOWN;
m_playButton.HandleInput(mousePos, state);
m_exitButton.HandleInput(mousePos, state);
if(m_shouldExit)
return GAME_STATE::EXIT;
if(m_shouldStart)
return GAME_STATE::IN_GAME;
return GAME_STATE::MENU;
}
void Render(const float elapsedMilliseconds) const
{
m_playButton.Render(m_target,m_font);
m_exitButton.Render(m_target,m_font);
}
void Reset()
{
m_shouldExit = false;
m_shouldStart = false;
}
private:
sf::RenderTarget& m_target;
sf::Font& m_font;
Button m_playButton;
Button m_exitButton;
bool m_shouldExit;
bool m_shouldStart;
};
Code language: C++ (cpp)
There’s not a lot happening here, it just looks big. Pay attention to the constructor where the callbacks are provided to the two buttons. The Play button is going to set the m_shouldStart
variable, and the Exit button is going to set the m_shouldExit
variable. These are used in the Update
method to determine if the game should quit or play.
There’s also a new Reset
method that we use in the Glue Code (coming next) that resets these variables. There’s a better way to handle this, but we’ll talk about it next time.
Main Loop
And now the final part of our game code is the main function, which contains the initialisation and the main loop.
int main()
{
sf::RenderWindow window(sf::VideoMode(WINDOW_WIDTH, WINDOW_HEIGHT), "Pong");
sf::Font font;
if(!font.loadFromFile("SourceSansPro-Regular.otf"))
{
std::cerr << "could not load font " << std::endl;
return 0;
}
GAME_STATE gameState = GAME_STATE::MENU;
PongGame pong(3, window, font);
PongMenu menu(window, font);
std::chrono::system_clock::time_point lastTime = std::chrono::system_clock::now();
float frameLag = 0;
while(window.isOpen() && gameState != GAME_STATE::EXIT)
{
sf::Event event;
while (window.pollEvent(event))
{
if (event.type == sf::Event::Closed)
window.close();
}
std::chrono::system_clock::time_point currentTime = std::chrono::system_clock::now();
std::chrono::milliseconds elapsedTime = std::chrono::duration_cast<std::chrono::milliseconds>(currentTime - lastTime);
lastTime = currentTime;
frameLag += elapsedTime.count();
sf::Vector2i mousePos = sf::Mouse::getPosition(window);
while(frameLag >= UPDATE_MS)
{
frameLag -= UPDATE_MS;
if(gameState == GAME_STATE::MENU)
gameState = menu.Update(UPDATE_MS, {mousePos.x,mousePos.y});
else
{
gameState = pong.Update(UPDATE_MS);
if(gameState == GAME_STATE::MENU)
menu.Reset();
}
}
window.clear();
if(gameState == GAME_STATE::MENU)
menu.Render(elapsedTime.count());
else
pong.Render(elapsedTime.count());
window.display();
}
window.close();
return 0;
}
Code language: C++ (cpp)
This is heavy on the C++ and SFML code, because it has to be. If you’re not using this combination, then your main function will look very different. The key parts are:
- Initialise everything we need. Create a window, load a font, create our
PongMenu
andPongGame
instances - Go through our loop
- Check if there are any system-messages that need to be processed
- Find out how much time has passed since the last frame
- If we’ve accumulated enough time to update the game-world, do it
- Render the game
#3 and #4 are different depending on whether we’re inMENU
orIN_GAME
- Close the window and exit
Is that it?
Yes! Copy and paste all that code if you want, in order, into one big file a compile. You can now play Pong!
What’s Next?
Well, quite a lot:
- There’s a few questionable architecture decisions that need to be thought out better
- There’s improvements to the rendering to make the game look smoother, right now you’ll notice that it’s “stuttery” (but totally playable)
- We can also improve the menu a good amount
- There’s no sound!
- We can spice up the standard game of pong a little bit I’m sure.
You can play this now, with your friends, family, neighbour, or by yourself if you have two hands. We’ll go through these improvements in the third and final article.
I sincerely hope that you found this helpful and that you are super excited about having made a fucking game. Right there, you just made Pong. Sure, you needed a little bit of help, but that doesn’t matter. From the next article, you’re going to make this game your own.