1
\$\begingroup\$

Recently I decided to start optimizing code in the game development field.

I found this pacman project on github a good starting point: https://github.com/LucaFeggi/PacMan_SDL/tree/main

I found a lot of things that deserve an optimization. One of the most things that caught my attention is that a lot of classes have their Draw (render) method inside them, such as Pac, Ghost, Fruit classes...

So I decided to make a Renderer class, and move to it the Draw method and the appropriate fields that the latter works on.

Let us take the Pac class inside the PacRenderer.hpp as an example, to clarify things:

class Pac : public Entity{
    public:
        Pac();
        ~Pac();
        void UpdatePos(std::vector<unsigned char> &mover, unsigned char ActualMap[]);
        unsigned char FoodCollision(unsigned char ActualMap[]);
        bool IsEnergized();
        void ChangeEnergyStatus(bool NewEnergyStatus);
        void SetFacing(unsigned char mover);
        bool IsDeadAnimationEnded();
        void ModDeadAnimationStatement(bool NewDeadAnimationStatement);
        void UpdateCurrentLivingPacFrame();
        void ResetCurrentLivingFrame();
        void WallCollisionFrame();
        void Draw();
    private:
        LTexture LivingPac;
        LTexture DeathPac;
        SDL_Rect LivingPacSpriteClips[LivingPacFrames];
        SDL_Rect DeathPacSpriteClips[DeathPacFrames];
        unsigned char CurrLivingPacFrame;
        unsigned char CurrDeathPacFrame;
        bool EnergyStatus;
        bool DeadAnimationStatement;
};

So according to what I described above, the PacRenderer class will be:

class PacRenderer {
public:
        PacRenderer(std::shared_ptr<Pac> pac);
        void Draw();
private:
        LTexture LivingPac;
        LTexture DeathPac;
        SDL_Rect LivingPacSpriteClips[LivingPacFrames];
        SDL_Rect DeathPacSpriteClips[DeathPacFrames];
}

PacRenderer::PacRenderer(std::shared_ptr<Pac> pac)
{
    this->pac = pac;

    //loading textures here instead of in the Fruit class
    LivingPac.loadFromFile("Textures/PacMan32.png");
    DeathPac.loadFromFile("Textures/GameOver32.png");
    InitFrames(LivingPacFrames, LivingPacSpriteClips);
    InitFrames(DeathPacFrames, DeathPacSpriteClips);
}

After moving Draw() and 4 fields from the Pac class.

At the beginning, I thought that it is a very good optimization, and separation between game logic and graphics rendering.

Until I read this question and its right answer: How should renderer class relate to the class it renders?

I found that what the right answer describes as

you're just taking a baby step away from "objects render themselves" rather than moving all the way to an approach where something else renders the objects

applies exactly to my solution.

So I want to verify if this description really apply to my solution.

Is my solution good, did I actually separate game logic from graphics rendering?

If not, what is the best thing to do in this case?

\$\endgroup\$
3
  • 1
    \$\begingroup\$ This looks like it might be worth taking to the Code Review StackExchange instead. They handle cases where you have working code and want advice on how it can be improved. \$\endgroup\$
    – DMGregory
    Commented Aug 20 at 0:16
  • 1
    \$\begingroup\$ You can either "give" an object a particular ability; or have another class act as a "proxy" / adapter. The only example that might be familiar is the .NET "table adapter"; it's main purpose is to "fill" datatables; when one might say tables should "fill themselves". The "adapters" (IMO) allow you to isolate common functionality in the context of "acting on" versus "acting"; which in certain case, is just easier to get one's head around. A totally new behaviour doesn't mean having to go back and modify the object ... use an adapter / component / plug-in / extension instead. \$\endgroup\$ Commented Aug 20 at 19:12
  • \$\begingroup\$ Let's continue this discussion in chat. We try to avoid extended back-and-forth conversations in comment threads here. 😉 \$\endgroup\$
    – DMGregory
    Commented Aug 22 at 11:42

2 Answers 2

1
\$\begingroup\$

did I actually separate game logic from graphics rendering?

No, at some point the character must be drawn, adding an additional class per object does nothing to improve separation.

SDL is the abstraction layer.

As far as code size, CPU performance, or memory optimization is concerned, the proposed split will perform slightly worse than the original code.


Adding a DrawPac() method, along with the other objects' draw calls to a single Render class would separate render calls from objects.

This solution is equivalent to the original code, draw call in object, in most aspects including performance. Convention(tradition) alone groups the code in the object being drawn.


Remember that huge monolithic code generates the fastest and smallest code, but it is also a pain to maintain. This attribute in code should not be feared or corrected without cause.

Note, I am not advocating for writing new code this way. There are many dangers to over optimization.

Do what works best for you and optimize when needed.


My version of Pac-man kept the Ghosts, Pac-man, Transporters(screen wrap) and Fruit as objects, and everything else(dot, power-dot, walls) was done in a single "world" object from one array drawn in a single loop.


Just glancing at the Board code, the Ghost checks have no purpose here. So uncoupling objects would be a better place to start refactoring the code.

\$\endgroup\$
3
  • \$\begingroup\$ "huge monolithic code generates the fastest and smallest code" - so very not true xD \$\endgroup\$
    – Kromster
    Commented Aug 21 at 19:57
  • 1
    \$\begingroup\$ @Kromster, It is true. Just hard to write correctly. Static linking, minimizing/eliminating context switches, minimal runtime address resolutions + good programming(GIGO) cannot be beat in terms of performance and code size. OOP makes life easier for the programmer, and I gladly use it everyday. The original Pac-Man code was 192 Kb of monolithic Z80 code. \$\endgroup\$
    – agone
    Commented Aug 21 at 21:40
  • \$\begingroup\$ @agone you are right, nothing is free, and everything has its tax. OOP increase the code organization at the expense of the performance, and vice versa for other paradigms... \$\endgroup\$
    – X Y
    Commented Aug 21 at 23:43
0
\$\begingroup\$

Strictly speaking you did not optimize the code, you restructured it. You have performed a refactoring (but probably without tests).

There is a principle of locality inherent in object oriented programming. Meaning, functionality that belongs to an object should be kept at the object. Having a render method in each class is nothing to be condemned per se. There is even a design pattern for it, the command pattern.

In the presented case you collected all render stuff in one extra layer or package/namespace, but the control flow dependency is still there between each Pac and PacRenderer, Fruit and FruitRenderer etc. Note the reference you have to pipe to each renderer to make this approach work. If you truly had abstracted/separated things, such a dependency would not be present anymore.

The underlying idea is that changes on the object probably make changes of the rendering necessary. So the best/simplest case is, object logic and render code are held together. Similar parts could be moved to a common parent class (or static helper classes), if this makes things easier to handle.

Don't worry tho, these are all best practices coming with more experience. Just keep going. You did the best thing here: Trying to change code and learn something from it is the best teacher you can have!

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .