Critique My Implementation Of Pong?

Started by
17 comments, last by DishSoap 9 years, 4 months ago

So, I realize the bullet points above me are a little trolly :), but to be clear - I asked on this forum so I could hear people who design games better tell me why my designs were wrong. At work, I become a better coder through code reviews; In the same manner, I would get better at building games if people told my why what I do is crap. I don't mind if the advice is too much for such a simple game implementation, in fact that's kind of what I want to hear, so in my next implementation I can start out making better choices.


- It looks weird that the ball has BoundClose and BounceNear parameters. I'd let the ball move anywhere and check outside the Ball's Update method if it's a point for any of the players.

- It also looks weird that in the Ball's Update method you send both Paddles, it's like you put all the collision detection inside the Ball class. I'd take that out into something that checks for collisions, and add methods in Paddle and Ball classes to react to collisions.

So I like this advice, when shoving the logic into a ball Update function I thought about how that's probably pretty crappy. So to sum up what a good change would be is to have a module check for collisions and react by calling a function to Ball or changing its velocity? While all ball update should really do is it update its own position with its velocity or something? This is good just saying it out loud makes it feel better.


- I can't find anything about timing, it looks like you update and draw everything as soon as you can. Implementing a way to limit the the frame rate is usually a must for games, so the game fells the same in any computer you play. You basically check how much time has passed since your last iteration of the gameloop, send that value into all the "Update" methods (and do everything considering that time) and wait some milliseconds before executing the next iteration. Later you can add fixed updates for physics too.

I have actually implemented this before. I haven't done it here yet, I have no idea why, but this is something I should be doing from square one not after implementing game logic. Thank you.


- Speaking about Update methods, if you send the same thing every time consider setting those things as attributes once instead. It's also uncommon that your WorldObject doesn't have an Update method and the derived classes have different Update methods. It's not bad, but in almost every engine and game I worked in the base game object has an Update(float deltaTime) method. This also makes it easier to update the game objects in games that the object count is dynamic. Now you have the paddles and the ball and update each one in a line, but imagine a Breakout game, where you also have a different number of blocks in each level.

This is way better than what I'm doing. It actually enforces better design decisions. With an Update in WorldObject with a single parameter of DeltaTime I can't do stupid stuff like sending paddle parameters or parameters for input detection. I will be implementing this bit very soon.


- In WorldObject you have some empty constructors, is that OK?

- The "Movable" class has a name that sounds like an Interface, but it has some code. Couldn't that code be part of WorldObject? Or some WorldObjects don't move? I only see that Paddle and Ball are.

No, that's not ok. Good catch! Yeah that was the idea was that some objects may not move, but I've shot my own self in the foot because WorldObject's have Moveable protected member in them. This was a stupid choice of mine and I actually want to rethink how this works in general.

Awesome points. All of them. Thank you for looking at it. Actually means a lot that you guys are willing to blow your own time looking at my stinky code, so I do appreciate it.

Advertisement


So I like this advice, when shoving the logic into a ball Update function I thought about how that's probably pretty crappy. So to sum up what a good change would be is to have a module check for collisions and react by calling a function to Ball or changing its velocity? While all ball update should really do is it update its own position with its velocity or something? This is good just saying it out loud makes it feel better.

Changing the ball direction should be done inside the Ball class, if you add this specific stuff in a collision module you'll end up with a lot of unrelated code if the game is more complex. The best way I can think of implementing this is checking for collision in one module, and adding a method in WorldObject like onCollisionWith(WorldObject* obj). When you detect that the ball and the paddle collide, call onCollisionWith in both objects, sending the other object as a parameter.

In Ball and Paddle you implement or override the method, in the Ball's onCollisionWith you change the direction if the other object is a Paddle, and in the Paddle's onCollisionWith you do whatever you want to the paddle, or nothing at all.

You can go further adding 3 different states of collision (collision starts, collision hold and collision ends) and 3 methods. With a Pong game might be a little to much, but would be usefull in most games.


Awesome points. All of them. Thank you for looking at it. Actually means a lot that you guys are willing to blow your own time looking at my stinky code, so I do appreciate it.

Great! Glad it helped.

So I got some of CRYP7IK's points done. The repo looks a lot cleaner now. There exists a Pong3D and Engine subfolder at the root. Each has Include and Src directories with its associated files.

Going to move onto some of the other points made here! This page just got bookmarked. I am hoping to address every point here.

Over engineering? It's simply making it easier for a reader to see what is actually game code and what isn't, seeing as he isn't the only person reading the code it isn't a complete waste of time. Good structure is rarely a waste of time, even in small projects.

I completely disagree, its been my experience to see people have projects they could have completed in 6 months and are working on two years later because they kept constantly changing their architecture to something they thought was "more reusable, more pleasing to the eyes, more expandable." Structure becomes much more important the larger a project gets, or if the project is -planned- to be reused for multiple games, and also if the developer is working with others. Not saying it's always a complete waste of time but when you just throw a bunch of bullet points at someone "do this and that" it makes it sound like a golden rule.

There are no golden rules in coding, golden rules produce more bad code than good.

For the same reason above, so people reading it can directly go to src or inc for whatever they want to look at. I didn't say he was writing a library. I don't know why you wouldn't do this 'for a game'.

That's personal preference then, I personally don't see the point of that. If i'm browsing for a header file I'd see it just as easily in one folder as having to have two open, header files are usually paired with source files anyway. I wouldn't understand why you think having them in a seperate folder makes them easy to locate but to each their own.

Well, AI is just an example what if he wants to add a networked controller so another application could control a paddle via some port? It's all about SRP and makes it well designed, it is also maybe 10 minutes of work.

It's not the time to implement thats the thing, its that you're simply throwing suggestions at him he may not even use for anything. 10 minutes for who, you? Does he even know how he would implement it as a controller instead? It likely requires more code changes than just the class itself anyway.

I was just following his other design patterns (i.e movable which things inherit from) and I think it fits.

If you're going to throw a laundry list of things at him that you seem to think must be implemented, at least explain why.

Sure, constants would work, but require a recompilation every time, why aren't his shaders, materials etc constants as well? Again following what he already had in his game.

A recompilation of a game his size is negligible at best, as would be a game of a much larger size(even having to recompile the entire game via having naive constants.) Again I'm not necessarily saying its wrong, simply that you don't even explain why. I find far too many people on these forums who seem to think there is one true way to do anything, even if it massively overcomplicates a project you could finish in a few days into a 6 month horror.

Constants are useful and can be mixed with settings loaded from files, the trick is knowing when to use either.

  • Factories for everything, remove public constructors
  • Separate rendering and logic
  • Normalise all namespaces and add missing namespaces, make sure they have proper and long names.
  • Use premake or cmake (Preferably both) for agnostic compiler \ IDE project setup
  • Separate engine and game into different git repositories making use of sub-module
  • Add an asset database for handling resources (config files, materials, mesh etc)
  • Engine should definitely be it's own library(rather than just some code) and be seperated into core, math, render, physics, input, network, UI and game (WorldObject and Game class)
  • Input should be more generic and be loaded through a config file for ease of changing actions
  • Write a C interface for everything
  • Add a plugin system for mods (Allow use of hot-loaded dlls and scripts for at least 4 languages, make use of SWIG)
  • Capture metrics of players and send back to database using pig, hadoop and map reduce.
(Please continue this list)

Yes yes I get the sarcasm.
  • Why would factories for everything be overengineering? To me that sounds like a rather curious design choice in general.
  • Actually.. seperate rendering and logic is something you should do in general typically, its rather trivial to seperate them and they never should have really been together in the first place.
  • Namespaces wouldn't take too long to add, but I agree, overengineering.
  • I actually saw someone with a github that had a cmake setup for.. what, three files? Ugh.
  • My main thing about seperating game and engine is that it usually complicates design more, and people do it for the sake of engine reusability. Early on in particular you really shouldn't be trying to reuse your engine.
  • Any kind of resource manager is a pain to make, especially one that works well, definitely agree there.
  • Library engines.. ugh.. to be honest I find that silly just in general, The only time that would be sensible is if you had completely different teams working on either one and needed to swap constantly, or the Engine needed to be very reusable. Unless you work at a large company this seems extremely unlikely.
  • Honestly implementing mappable hotkeys is probably one of the earlier upgrades he'll do if he makes future games, so that wouldn't be a huge waste of time to learn. But for simple games I agree, waste of time.
  • A C interface? What? Why?
  • Clearly we need user made extensibility for our huge userbase, this pong game must be modified to death. Although adding official plugins would already make it more advanced than Minecraft(hurr hurr.)
  • Player metrics.. that one caught me offguard. Touche.

I completely disagree, its been my experience to see people have projects they could have completed in 6 months and are working on two years later because they kept constantly changing their architecture to something they thought was "more reusable, more pleasing to the eyes, more expandable." Structure becomes much more important the larger a project gets, or if the project is -planned- to be reused for multiple games, and also if the developer is working with others. Not saying it's always a complete waste of time but when you just throw a bunch of bullet points at someone "do this and that" it makes it sound like a golden rule.

There are no golden rules in coding, golden rules produce more bad code than good.


He wanted my critique, he got it. If he wanted reasoning he can ask for that as well. I never represented my bullet points as any golden rule.

That's personal preference then, I personally don't see the point of that. If i'm browsing for a header file I'd see it just as easily in one folder as having to have two open, header files are usually paired with source files anyway. I wouldn't understand why you think having them in a seperate folder makes them easy to locate but to each their own.


It's because one can see how things are interacting and working just by looking at header files, then you can look at implementation details later.

It's not the time to implement thats the thing, its that you're simply throwing suggestions at him he may not even use for anything. 10 minutes for who, you? Does he even know how he would implement it as a controller instead? It likely requires more code changes than just the class itself anyway.


If he doesn't know how he can ask, you know DishSoap can act on his own behalf right? Pong is incomplete without AI and it's safe for me to assume he is making pong.

A recompilation of a game his size is negligible at best, as would be a game of a much larger size(even having to recompile the entire game via having naive constants.) Again I'm not necessarily saying its wrong, simply that you don't even explain why. I find far too many people on these forums who seem to think there is one true way to do anything, even if it massively overcomplicates a project you could finish in a few days into a 6 month horror.

Constants are useful and can be mixed with settings loaded from files, the trick is knowing when to use either.


A config file is easier and faster to change than constants. You should add your suggestion about what he should make constants and what he should put in config files instead of complaining about my suggestions.

Yes yes I get the sarcasm.

  • Why would factories for everything be overengineering? To me that sounds like a rather curious design choice in general.
  • Actually.. seperate rendering and logic is something you should do in general typically, its rather trivial to seperate them and they never should have really been together in the first place.
  • Namespaces wouldn't take too long to add, but I agree, overengineering.
  • I actually saw someone with a github that had a cmake setup for.. what, three files? Ugh.
  • My main thing about seperating game and engine is that it usually complicates design more, and people do it for the sake of engine reusability. Early on in particular you really shouldn't be trying to reuse your engine.
  • Any kind of resource manager is a pain to make, especially one that works well, definitely agree there.
  • Library engines.. ugh.. to be honest I find that silly just in general, The only time that would be sensible is if you had completely different teams working on either one and needed to swap constantly, or the Engine needed to be very reusable. Unless you work at a large company this seems extremely unlikely.
  • Honestly implementing mappable hotkeys is probably one of the earlier upgrades he'll do if he makes future games, so that wouldn't be a huge waste of time to learn. But for simple games I agree, waste of time.
  • A C interface? What? Why?
  • Clearly we need user made extensibility for our huge userbase, this pong game must be modified to death. Although adding official plugins would already make it more advanced than Minecraft(hurr hurr.)
  • Player metrics.. that one caught me offguard. Touche.

  • Paddle -> PaddleFactory : Factory<Paddle>-> PaddleFactoryFactory<PaddleFactory>. Why? Because we want an agile solution with tightly packed memory allocations for EVERYTHING.
  • Changing world object to separate that would require fixing ball and paddle for no raisins.
  • Yep
  • CMake is faster to create a project than a visual studio wizard IMO, but maintaining premake and cmake is crazy, something wxWidgets almost did!
  • He already made re-usable parts, it's only logical to put them into their own module or library
  • Yep
  • Yep
  • Yep
  • We need maximum portability for major scalability and project redundancy. (Ogre3D, lol)
  • The mod API is for professional level scalability, we need to allow any and all changes to be made at any point in time by anyone. This increases synergy while promoting code re-usability, not to mention that we get Ultimate Synergy.
  • Don't forget it's scalable for enterprise level usage!

We should probably stop derailing this topic though.

Engineering Manager at Deloitte Australia

Here is some feedback on the source itself. Sorry if it jumps about a bit, just happened to be the order I visited the files. Overall, it is quite clear and readable, so bear that in mind that what I'm mentioning is the areas that are surprising or otherwise notable.


WorldObjects.push_back(dynamic_cast<WorldObject*>(&PlayerPaddle));
WorldObjects.push_back(dynamic_cast<WorldObject*>(&EnemyPaddle));
WorldObjects.push_back(dynamic_cast<WorldObject*>(&PongBall));
These dynamic casts are unnecessary.

do
{
    // ...
}
while (glfwGetKey(MainWindow.GetWindow(), GLFW_KEY_ESCAPE) != GLFW_PRESS &&
       glfwWindowShouldClose(MainWindow.GetWindow()) == 0);
The do...while loop is uncommon, and generally is reserved for cases where checking the condition last is relevent. Here, the loop is essentially the same either way, so I would find this slightly surprising and confusing.

PlayerPaddle.Update(MainWindow.GetWindow());
EnemyPaddle.Update(MainWindow.GetWindow());
It is stylistically nicer to separate game logic from rendering, even to the point of not sharing types.

    m_UpKey(GLFW_KEY_W),
    m_RightKey(GLFW_KEY_D),
    m_LeftKey(GLFW_KEY_A),
    m_DownKey(GLFW_KEY_S),
    m_MoveUp(new PaddleUpCommand()),
    m_MoveDown(new PaddleDownCommand()),
    m_MoveRight(new PaddleRightCommand()),
    m_MoveLeft(new PaddleLeftCommand())
 
// ...
 
void Paddle::Update(GLFWwindow*& Window)
{
    if (glfwGetKey(Window, m_UpKey) == GLFW_PRESS) m_MoveUp->Execute(*this);
    if (glfwGetKey(Window, m_DownKey) == GLFW_PRESS) m_MoveDown->Execute(*this);
    if (glfwGetKey(Window, m_RightKey) == GLFW_PRESS) m_MoveRight->Execute(*this);
    if (glfwGetKey(Window, m_LeftKey) == GLFW_PRESS) m_MoveLeft->Execute(*this);
}
 
// ...
 
void PaddleUpCommand::Execute(Paddle& P)
{
    P.MoveUp();
}
 
// ...
 
void Paddle::MoveUp()
{
    m_Moveable.Velocity.y = m_Speed;
    m_Moveable.ApplyFriction(true, false, false, 0.1f);
    m_Moveable.ApplyVelocity();
    Bounds.SetPosition(GetPosition());
}
The code to move paddles is very convoluted. I think it would be clearer to directly call MoveUp() rather than use the intermediate command objects.
The dynamic allocations are unnecessary. Note also that if an exception (like std::bad_alloc) were to be thrown midway during object creation, you may leak memory.

void Paddle::SetControlScheme(GLbyte Scheme)
{
    m_ControlScheme = Scheme;
    switch (m_ControlScheme)
    {
    case 0:
        m_UpKey = GLFW_KEY_W;
        m_RightKey = GLFW_KEY_D;
        m_LeftKey = GLFW_KEY_A;
        m_DownKey = GLFW_KEY_S;
        break;
    case 1:
        m_UpKey = GLFW_KEY_UP;
        m_RightKey = GLFW_KEY_RIGHT;
        m_LeftKey = GLFW_KEY_LEFT;
        m_DownKey = GLFW_KEY_DOWN;
        break;
    }
}
This is confusing. First of all, the type here indicates that something related to OpenGL is going on, which there isn't. Second, you have magic numbers here, it isn't clear what SetControlScheme(1) will do, and it isn't clear that SetControlScheme(3) is not valid. Using something like "unsigned controlSchemeIndex" might be clearer, or actually making the control scheme an explicit object.

BallCollider::BallCollider(Ball& Ball,
    Paddle& PaddleNear,
    Paddle& PaddleFar,
    /* ... */)
I would say these names are poor as the ball could actually be nearer the "far" paddle at times. Better names would be "left" and "right".

if (m_Ball->GetPosition().x < -10.0f)
    m_Ball->BounceHorizontal();
else if (m_Ball->GetPosition().x > 10.0f)
    m_Ball->BounceHorizontal();
if (m_Ball->GetPosition().y < 0.0f)
    m_Ball->BounceVertical();
else if (m_Ball->GetPosition().y > 10.0f)
    m_Ball->BounceVertical();
The third condition here doesn't match the others. This kind of mistake can be avoided by extracted the magic number into a named constant.
Looking at your engine project, there are still "ball" resource files located there.

FPSCamera(GLFWwindow*& Window);
Passing a reference to a pointer indicates the function could modify the caller's function. It would be very surprising for a constructor to do this. Just pass the pointer by value. Alternatively, pass the Window (or it's interesting attributes) as a parameter to Update().

// Include GLEW
#include <GL/glew.h>
 
// Include GLFW
#include <GLFW/glfw3.h>
These comments don't aid the reader's understanding, they are redundant.

if (Vertices.size() > 0)
{
    // ...
}
Some people prefer to test for Vertices.empty(), as it closer resembles the intent.
For classes like AABoundingBox, unless you are enforcing constraints, consider just having public members. For "dumb" data types, this can simplify use.
The constructor of FPSCamera that assumes the width and height of the window is surprising. Does GLFW not support reading the size of the window dynamically?

void FPSCamera::Update()
{
    static double LastTime = glfwGetTime();
    // ...
}
Prefer class members to static values. This static value is shared between cameras, e.g. if you were to implement a split screen game. Consider computing a game wide delta time and passing it to all Update() functions as a parameter.

void RenderWindow::Run(void *(GameLoop)(void)) const
{
    do
    {
        GameLoop();
    } // Check if the ESC key was pressed or the window was closed
    while (glfwGetKey(m_Window, GLFW_KEY_ESCAPE) != GLFW_PRESS &&
        glfwWindowShouldClose(m_Window) == 0);
}
This duplicates code you have elsewhere. I'd also say it is not the window's responsibility to run the game loop.

// Dark blue background
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
This comment is out of date.

void LoadOBJ(const char* Filename, std::vector<glm::vec4> &Vertices, std::vector<glm::vec3> &Normals, std::vector<GLushort> &Elements)
{
    std::ifstream in(Filename, std::ios::in);
    if (!in)
    {
        std::cerr << "Cannot Open" << Filename << std::endl;
        exit(1);
    }
    // ...
}
Ideally, low level code will not terminate the program. This function could return a boolean, or throw an exception instead. This would allow you to recover, e.g. load a generic replacement. This is especially important if you allow users to customise assets.

std::string line;
while (std::getline(in, line))
{
    if (line.substr(0, 2) == "v ")
    {
        // ...
    }
    else if (line.substr(0, 2) == "f ")
    {
        // ...
    }
    else if (line[0] == '#'){/* is a comment*/ }
    else {/* is a blank line */ }
}
Minor point, but the last comment is incorrect, it assumes that unrecognised lines are blank. The file could be malformed and contain unexpected lines too, however.
Your LoadShaders appears to have poor error handling. It leaks OpenGL objects if an error occurs loading the vertex shader, but it ignores errors loading the fragment shader. If an error occurs compiling or linking, they appear to be printed and ignored.
Also, there is duplicate code to slurp a text file into a string.

Thank you rip-off. I have actually pushed a commit with about half the changes you recommended. I will address the ones that I think will take me a little more time in the future. You have given me by far the most in-depth review on actual source code. I will probably read through your post a few more times as I visit each item one by one. I actually have a ton of really good improvements in this thread I want to address.

There is nothing you said that I have any disagreement with at all, they are all great points and fixing them would make my project much much cleaner.

It's really amazing how quickly I can learn through reading the feedback I've gotten here. This community is pretty awesome; I am really just amazed that you guys are willing to spend time looking at my code and helping me become a better programmer. Thanks!

If you are still interested in relocating magic numbers ...

A simple config file parser: Source Settings Parser from SFML wiki.

...or...

A simple JSON parser: JsonCPP. (I go with building my project with the resulting "amalgamated" source files).

Cheers!


If you are still interested in relocating magic numbers ...

A simple config file parser: Source Settings Parser from SFML wiki.

...or...

A simple JSON parser: JsonCPP. (I go with building my project with the resulting "amalgamated" source files).

Cheers!

This is good stuff! I was actually thinking of doing the key, value pair config files. I was kind of drawing out how I would implement that with a map or something similar. This is almost exactly what I had in mind, it would be a great source to compare my resultant work to. Thanks a lot!

This topic is closed to new replies.

Advertisement