Trying to figure out how to make my Pong clone in C++ with classes

Started by
5 comments, last by vanka78bg 8 years, 1 month ago

I've decided to take on the task of recreating Pong in C++ using SDL. I feel like it's a very simple way of getting used to game development, as well as starting off with graphical programming. SDL for the most part seems easy enough, but I'm not entirely certain where to put things in terms of classes.

What I'm trying to figure out is: Should I have different classes for the graphics, input, etc? As of right now, I have a class for the Engine, which is responsible for creating the window, score and game loop, and Ball and Paddle classes for obvious reasons. I'm just not sure if I should make classes that handle input and graphics separately, or give my Ball and Paddle functions like draw() and move().

If what I've said sounds wrong, or could be better advised, I would greatly appreciate it.

Thanks, Lurid.

Advertisement

There are many ways to approach this and they have different pros and cons. The better/more flexible methods would probably end up being total overkill for Pong so with that in mind I make my suggestions. Have a something that deals with all your initialisation of things unrelated to the game (setting up a window, initialising graphics and so on), this could be just a few functions.

Have a 'Pong' class, this is effectively your game, it would just run the main loop, if there is any input it will send it to it's input function and in it's loop it will just call two methods, Update/Render. It will have a few objects, e.g. 2 paddles, a ball, whatever is needed to draw the board. Those will be separate classes. I suppose it should also have an AIPlayer object too (if you have AI).

I would put all rendering stuff for each object in the object itself (i.e. ball.Render()). The updating of each object I would probably keep a level higher (in 'Pong' class) since each needs to know about each other so I think it's better that the Pong class deals with moving things and checking the collision and so on.

There are plenty of ways to go about this but don't worry about making it too architecturally perfect, you know Pong and exactly what it is supposed to do, you don't need to worry about making anything future proof or expandable so keep it as simple as you can.

Interested in Fractals? Check out my App, Fractal Scout, free on the Google Play store.


I have a class for the Engine, which is responsible for creating the window, score and game loop

I would recommend each class having a single purpose, i.e. you can have separate Window and Game classes, so you do not have to mix window handling and game logic in one place.


I'm just not sure if I should make classes that handle input and graphics separately, or give my Ball and Paddle functions like draw() and move()

You can have a base class Entity with abstract draw() and move() methods and make Ball and Paddle inherit from it, so the Game class would simply iterate over all entities and call draw() or move() when necessary. Regarding the drawing and input handling, I would prefer having a separate Graphics class to handle the low-level drawing details and pass it to draw(). Something similar can be done for input, you can have a Controller class that returns high-level commands like: MoveUp/MoveDown etc. and use it in the move() method of the player's paddle Entity. Then you can implement more specific input handling in, say Keyboard and Mouse classes and use them in the Controller. This way you can configure Controller to respond to either the keyboard or the mouse, based on the player's choice.

I hope you like my suggestions. smile.png

I'm just not sure if I should make classes that handle input and graphics separately, or give my Ball and Paddle functions like draw() and move().


For pong it won't really matter which option you choose - they are both sensible options for a project like this, so I would suggest that you choose whichever one you feel most comfortable with.

But "in general" I would opt to extract graphics and input handling into separate classes, as that is a more scaleable design.

You would still want a move() function on a Paddle but the purpose of such a function is to tell the paddle where (or how far) to move e.g. It might have a signature like this: Paddle::move(int amount)

Some kind of InputHandler will process the keyboard/mouse/whatever events and decide which move() functions to invoke.

To extract the rendering you would go more in the direction of gfx.draw(paddle). Except that I usually pass in the whole game-state at once: gfx.draw(world) which, internally, will draw the paddle, ball, stage, etc (by calling into private methods such as draw(ball);).

Hope that helps.

I'm just not sure if I should make classes that handle input and graphics separately, or give my Ball and Paddle functions like draw() and move().


For pong it won't really matter which option you choose - they are both sensible options for a project like this, so I would suggest that you choose whichever one you feel most comfortable with.

But "in general" I would opt to extract graphics and input handling into separate classes, as that is a more scaleable design.

You would still want a move() function on a Paddle but the purpose of such a function is to tell the paddle where (or how far) to move e.g. It might have a signature like this: Paddle::move(int amount)

Some kind of InputHandler will process the keyboard/mouse/whatever events and decide which move() functions to invoke.

To extract the rendering you would go more in the direction of gfx.draw(paddle). Except that I usually pass in the whole game-state at once: gfx.draw(world) which, internally, will draw the paddle, ball, stage, etc (by calling into private methods such as draw(ball);).

Hope that helps.

I understand the concepts everyone is providing on paper, but when it comes to putting it into code, there's a hiccup in translation. I'm getting mixed up with once I have these separate classes, how do I get them to interact with one another? How would I apply attributes of the graphics and input classes into the Ball and Paddle classes that make everything work succinctly. I know that I can create instances of those classes like I were declaring a variable, but actually using them to bridge it all together isn't clicking for me.

I really do like the idea of having everything in their separate classes, as it allows me to compartmentalize every aspect as optimally as possible. If something goes wrong in input section, I'll know it isn't something in the graphics messing something up. I do understand and know that I can make these work, but I just don't understand how. I tend to look at everything I set to accomplish as "If I can figure this out, anyone can", and it'll be a stepping stone to further making more unique games and applications.

Don't try to do the entire picture at once in full details, you just go mad in overload of details. Instead, just start with something simple, and gradually extend.

For pong:

1. Display black window (SDL init, opening a window, setting title bar, setting background, detect 'quit', close down SDL)

2. Display black window with white ball (ball object, draw game objects in window)

3. Display black window with moving white ball (ball speed and direction, position updates, frame rate limiting)

4. Display black window with moving bouncing ball (detect edges, reverse direction)

5. Add center white line

6. Add score board

7. Detect ball leaving the window at the left or the right

8. Change score and update the displayed score.

This is just one option, there are many other ways to do this.

Obviously, gradually extending functionality has the problem that at some point you'll find it doesn't fit. Worse, you made the wrong turn several steps back. You'll have to fix this before you can extend further.

As for the interaction between objects, the main thing is to decide who is going to make the final call. Usually that follows from what object is responsible for it. The other objects then have to provide information to the decision maker. Also, the decision maker must have means to tell others about the result.

For example, ball and window, in context of a bounce at the top (bounce at the bottom is probably similar enough to also handle here, but I don't want to discuss that here):

1. Ball is responsible for staying inside the window. (Assumption: ball knows its own x/y position and x/y speed).

The ball thus needs to know what window it must stay inside, and it must know where the upper border of that window is.


class Ball {
public:
    int x, y; // position
    int vx, vy; // speed
};

class Window {
public:
    int top; // y position of top border
};

Ball gets the window, and can access its top. It doesn't need to notify anyone of its decision.


void Ball::StayBelowTop(Window &w) {
    if (y < w.top && vy < 0) { // Oops, I must correct my course!
        vy = -vy;
    }
}

2. Window must ensure ball stays inside.


void Window::KeepBallBelowTop(Ball &b) {
    if (b.y < top && b.vy < 0) b.vy = -b.vy;  // Changes ball vy directly without notifying the ball
}

void Window::KeepBallBelowTop(Ball &b) {
    if (b.y < top && b.vy < 0) b.RequestReverseVy(); // Ask ball to reverse it vertical speed.
}

void Window::KeepBallBelowTop(Ball &b) {
    if (b.y < top && b.vy < 0) b.ReverseVy(); // Tell ball to reverse it vertical speed.
}

Three different solutions. The first one directly interferes with the b.vy variable, which implies you have decided that vy of the ball may be changed by others.

In the second solution, the window gently asks "please change your vy" (and hopefully the ball complies). In the third solution, the window just tells "reverse your vy!!".

The point I am trying to make is that the code follows the decisions that you make at a higher level wrt object responsibilities, and ways of telling other objects about decisions. This is why names of objects and methods is so important. Both Ball::RequestReverseVy, and Ball::ReverseVy may just do "vy = -vy;", the code line doesn't say if there was room for negotiation. (Unlikely to be useful for pong, though.)

So decide about the object responsible, decide how the information it needs is made available to it, code the decision making process, and decide how to let other objects know of its decision.

I don't really agree with any of the above solutions though, it all feels like over-engineering to me. I'd do


const int TOP = 0;
void Ball::StayBelowTop() {
    if (y < TOP && vy < 0) vy = -vy;
}

Your ball is never going to be used in more than 1 window, and the top is never ever going to move (ie even the TOP constant is already overkill in a sense!). There is no need for a Window class, just directly code the boundary check.

While it looks like I am violating the OO ideas of object responsibilities I just explained here (Who is responsible for defining "inside"??? I have no Window to stay inside!), I actually still follow the rules above, I just made a different high level decision.

I decided at the highest level that TOP is the top y coordinate, and it is going to be 0 always. This is global constant information freely available to all objects. Furthermore I decided that the ball must make sure by itself to stay below the top. It doesn't need to notify anyone of its course change. This then leads to the implementation I just showed.

It's easy to get carried away by OO and object responsibilities, and how to notify others, leading to complicated patterns of object interactions. Sometimes you need it, but that doesn't imply you always have to do it in that way. Making a different decision at a higher level can cause dramatic simplifications further down the line.


Instead, just start with something simple, and gradually extend.

This is probably the most sensible suggestion so far. There is no single best approach to follow. Plus, it is almost impossible to get things done right on the first try, even if you have experience (although some experience can help you get things right faster). The best you can do is to get something working in the most straightforward way, then revise your implementation with every new feature you add, to cleanup your code and reuse existing functionality better. When you have something done (working or not), you can always share your code with us, so we can comment how to improve it, or suggest how to resolve problems you have encountered. Without showing some code, the best thing we can possibly do is shooting random suggestions, which won't get you too far with your game. smile.png

This topic is closed to new replies.

Advertisement