Code Review - Pong clone..

Started by
4 comments, last by rip-off 10 years, 10 months ago

Hello

For the past few days I have been working on the Pong clone. I have made it before, but I wasn't satisfied with the quality of the code, so I decided to rewrite it. I guess you all are bored, because of the Pong Clones attacking you from every side, but here it is:

https://gist.github.com/Shayelxx/5691085

Well.. I think that it may be too big for such a simple game, but I am just starting to learn :)

Advertisement

I can't do a full code review, I don't think I am qualified to do that. ^^

But as a few suggestions.

1. SFML's rectangle(as do most graphics API's) have functions 'contains' or 'intersects.' I'd always use one of those functions rather than reinventing a bounding box collision.

2. Personally I like to create my own Vector class so I can remove coupling between graphics and physics as much possible. Since your graphics api, SFML, provides you with sf::vector I'd make your own for the physics. This way you can override basic operations like addition, subtraction and equals with C++. Also you can also write the normalize, mag, and other vector operations in that class rather than putting them in CBall.

3. Totally preference. But when I create my headers I go by this order. Public functions/variables -> private functions/variables. You have it the other way around. I like the most general, class defining functions to be at the top and the details(private variables/functions) to be at the bottom. Simply makes it more readable and maintainable in larger projects, for me.

Other than that, it seems well engineered to me for a simple game ^^. Easily readable and logical. GJ!

Thanks for your feedback

As for the SFML advices: I am new to sfml, I have just moved from SDL.. well.. previous bad and ugly pong was written using SDL. But I find SFML much more friendly. Anyway, thanks for advices. In the classes, I prefer to describe the private fields first. It is the core of a class, when I look at it I can see what it really is :) But as you said, it is a matter of preference :)

First of all, just to note that completing any game, regardless of how small or common, is an achievement. Congratulations!

These are my comments as I read your code:
  • You should not need to use "friend" here. The CBall class can expose sufficient accessors for the AI logic instead.
  • CBall::SetVelocity() and CBall::SetDirection() are ambiguous, what is the velocity or direction being set to? Consider "ResetVelocity" and "ResetDirection", or possibly just a single function Reset()!
  • CBall::Nomalize() should probably be a free function.
  • CGame::Start() actually runs the game, it doesn't just start it.
  • Do you need to call screen.close() in CGame::Start()? Would it not be better to do it in a destructor, in the case that SFML doesn't already implicitly handle this?
  • CGame::SetNextState() and CGame::ChangeState() appear to be helper functions, they should be "private".
  • You should not need to use public members here, CGame::screen should be private.
  • The default argument for CMenu::Logic is suspicious to me. Would you not simply avoid calling the function if you wanted to pass 0 seconds of elapsed time?
  • Prefer explicit functions like CPaddle::GetBounds() instead of providing operator sf::FloatRect().
  • CPaddle::HandleEvent() is ambiguous - what event is being handled? Prefer to be more explicit - e.g. HandleInput() or HandleKeyboard() even.
  • An alternative design for the paddle is to separate the AI and player controls elsewhere, and for the paddle to provide member functions such as StartMovingUp() and StartMovingDown(), etc, that the AI or input logic can call.
  • extern const int SCREEN_W = 800? Why not move these constants into a separate header, given that they are coming up again and again.
  • Consider separating the paddle and ball implementations into their own files.
  • You appear to be using stale values in CPaddle::Logic. If you move the sprite first, then check if the position needs to be fixed.
  • Call srand() at the start of main, or somewhere similar. This ensures the random number generator is seeded once and only once, which is exactly what you want for most games.
  • int CBall::Logic(float deltaT, sf::FloatRect rect1, sf::FloatRect rect2). Prefer "leftPaddle" and "rightPaddle" rather than "rect1" and "rect2". Another idea is to separate the collision checking from the ball's internal logic. Let the calling code update the paddle and the ball, and then determine if a paddle/ball collision occurred and notify the affected objects.
  • The return value of CBall::Logic is obscure. Don't use a comment, use an enum!
  • Normalize is badly named, as it doesn't just normalise the vector. If you make Normalize a free function, move the "desired speed" logic into the calling code. You can also create a second free function, SetLength(), which calls Normalize internally. The calling code might be clearer again.
  • Consider "left" or "leftPaddle" rather than "paddle1". Another option is a two element array. Variable names should generally not be numbered.
  • I think this line is doing a little too much.
    if(score.AddPoint(ball.Logic(deltaT.asSeconds(), paddle1, paddle2))) Reset();

    The return value of CScore::AddPoint is surprising.
  • Consider moving the reset logic into the objects being reset. Right now you're reaching into them and modifying their members, which is a poor design.
  • There are lots of magic numbers in the reset logic, consider naming them, e.g. 10 pixels might become "PADDLE_WALL_OFFSET" and 790 would be SCREEN_W - PADDLE_WALL_OFFSET. The middle of the screen seems to be a recurring constant, consider making it a named one. It will make the code more legible.
  • Consider moving CScore into a separate file.
  • Again, score1 and score2 are poor. Consider an array that can be indexed with the "Player number", e.g. the index of the player's paddle in CPlaying. Note that the presence of duplicate arrays can indicate that two concepts are related, i.e. you could create a Player type that contains a Paddle and a score, and pass the data to the relevant classes.
  • The CScore implementation is also using quite a few magic numbers.
  • CScore::DrawScore appears to have duplicate logic. Write a helper function to render a number at a given location, and then have DrawScore call it twice, once for each score at the appropriate locations.
  • CGameState lacks a virtual destructor, which means that your derived classes are leaking resources.
On a stylistic note, consider what value the "C" prefix on your classes actually gains you. I will quote an old post by JohnBolton (if he was quoting someone else, he didn't say it!):

Putting a 'C' on the front of every class name is about as useful as putting a 'V' on the front of every variable name and an 'F' on the front of every function name.

Wow.. thats.. quite a lot :) I will definitely keep your advices in mind when creating next project. There were some problems, that I couldn't solve other way than doing things such as


if(score.AddPoint(ball.Logic(deltaT.asSeconds(), paddle1, paddle2))) Reset();

I admit that it is not the best way, but at this point I couldn't make up any other, working solution. You can probably see that I am not the best in creating object oriented code :) I'm still learning what to make public/private, how to change the private fields etc.. As for putting C in the class names. Well.. it helps me :) I don't think it confuses other people, and if it does just tell me :) Thanks for advices! :)

Wow.. thats.. quite a lot smile.png I will definitely keep your advices in mind when creating next project.... Thanks for advices! smile.png

No problem.

I admit that it is not the best way, but at this point I couldn't make up any other, working solution.

Here is an example of how I might rewrite that part of the code:

float deltaSeconds = deltaT.asSeconds();
 
ball.Logic(deltaSeconds);
leftPaddle.Logic(deltaSeconds);
rightPaddle.Logic(deltaSeconds);
 
if(Collision(ball, leftPaddle)) {
     ball.BouceHorizontal(leftPaddle);
}
 
if(Collision(ball, rightPaddle)) {
    ball.BouceHorizontal(rightPaddle);
}
 
Vec ballPosition = ball.GetPosition();
 
if(ballPosition.y < 0 || ballPosition.y > SCREEN_H) {
    ball.BounceVertical();
}
 
 
bool resetPoint = false;
if(ballPosition.x < 0) {
    score.IncreaseRightPlayerPoints();
    resetPoint = true;
} else if(ballPosition.x > SCREEN_W) {
 
    score.IncreaseLeftPlayerPoints();
    resetPoint = true;
 
}
 
if(score.isGameOver()) {
    ResetGame();
} else if(resetPoint) {
    ResetForPoint();
}
 

The main change is pulling the code that deals with object interactions (ball vs paddle, ball vs wall, ...) outside the objects themselves. We no longer have the ball needing to know about the paddles and then trying to communicate collisions back to the playing class, which needs to forward them into the score class for further processing, which finally tries to return a decision about resetting back into the playing class.

A smaller, more focused change would be:

int pointScorer = ball.Logic(deltaT.asSeconds(), paddle1, paddle2);
if(pointScorer != 0) {
    score.AddPoint(pointScorer)
    if(score.isGameOver()) {
        Reset();
    }
}

You can probably see that I am not the best in creating object oriented code smile.png

That is OK, it is a skill that you refine over time. There are other people who might disagree with some of my comments. Leaving aside any outright errors I may have made, the likely cause of such disagreements is because there is no one true way to solve programming problems. You can get a sense of this even from the two approaches I outlined above.

Each person has their own "style", not merely at the presentation level but rather in how they fundamentally approach, understand, de-construct and solve problems. There are other aspects to style too, for example because I work with a team, I make an effort to write my programs in a more straight-forward manner, rather than the more idiosyncratic style I had before I started collaborating with other programmers. Another influence on my style is unit testing - the code where the ball doesn't need to know about the paddles is easier to unit test.

The way to improve your knowledge of object oriented code is to keep writing programs.

This topic is closed to new replies.

Advertisement