0
\$\begingroup\$

I have a class called 'GraphicalHandler' that all my graphical handlers inherit from the main game loop. The class manages a static vector of pointers to all instances of itself. I can then call the member fucntion GraphicalHandler::drawAllInstances() to draw everything to the screen.

But since my graphical handlers need to update their graphical objects, as well as rendering them, I figured I would change the function to updateAndDrawAllInstances(). I know that game logic and rendering should be as separated as possible, but since updating the graphical handlers doesn't seem like game logic I figured it would be valid.

So my question is if this is 'accepted' in game development, i.e. to update and render the graphical objects in the same function.

\$\endgroup\$
1
\$\begingroup\$

I'd advise not worrying about what's "accepted" or "OK". These things do not matter.

Very successful games have shipped with absolute nightmares lurking in their code. Nobody decompiles the game and docks your Metacritic score or claws back your sales because you didn't follow the "best practice" (whatever that might be interpreted to mean).

Instead, if you know common advice often points against a particular practice, examine what are the consequences of that practice that lead folks to advise against it. Could those consequences be a problem for your game, now or in the future? That would be the reason to avoid the practice: concrete impacts it has on your game and development, not because it's "unaccepted".

So, let's look at some consequences that come from bundling together your update and rendering into one call:

  • This locks your simulation time step to your rendering frame rate.

    • If you render with a variable frame rate, then this variability in your update timing can make your game behave inconsistently, depending on the hardware it's running on or the load on the system. This can change your game feel or even impact the game's fairness.

    • If you lock your framerate, then you've forced even players with super-powerful gaming rigs to play at the same framerate as your minimum recommended hardware. And if ever under load you miss the next frame deadline, you'll get a very visible stall/hitch in the gameplay.

    For reasons like these, games often prefer to decouple simulation from rendering, stepping the simulation at a fixed time step while the rendering is free to run as fast as the hardware allows. I go into more detail about these reasons, and benefits we get from this strategy, in this answer.

    Even if you're not planning on using this strategy at present, you might want to switch over to it in future. The more tightly bound-together your update and rendering are in your base version, the more refactoring work you'll need to put in down the line if you ever want to change that.

  • This mixes-together very dissimilar work, which may impact performance due to memory access patterns.

    • Each time you call a function or touch data you haven't touched in a while, the processor needs to pull the memory containing those instructions or data into its cache, and that's very slow.

      If you're doing something consistent, like always fetching the next sprite from an array to add to a draw batch, the processor can often predict the next chunk of memory you'll need and pre-fetch it to minimize this wait time.

      But if you mix together lots of different operations - like running an AI script (that needs to read memory about its patrol path or targets), updating object positions (that needs to store transformations to memory), processing collisions (using memory about colliders), updating health, spawning particles, etc... then drawing the next sprites, ...then this memory access looks super random, and the processor will have a hard time guessing what you'll need next.

      And this compounds: by the time you get to updating the positions of the next batch of objects, you might have done so many other things in between that the instructions for how to update object positions got kicked out of the cache, so you need to wait to haul them back in again.

    • Compare this versus doing all your random-accessing gameplay logic first, then doing all your drawing at the end. A lot of the drawing routines will be working with similar code - calling the same graphics API methods to submit draw calls and such. So they'll often find the instructions they need are still warm in the cache from the previous drawing action, and be able to proceed that much faster.

  • This clusters multiple different responsibilities in one place.

    This doesn't have as immediate an impact on your game, but it can affect the experience of development as you try to grow and evolve it.

    • Let's say one member of your team is working on optimizing the updates of game objects, to skip over static/dormant objects that don't need an update this frame and save some processor cycles.

    • Another team member is working on changing the sorting of draw calls for rendering, so you get less overdraw and better performance on low-end graphics cards.

    • They go to submit their changes, but they have a merge conflict, because they're both modifying the same files in mutually-contradictory ways. The grouping of multiple responsibilities in one code path creates extra friction your team needs to work around to accomplish their goals.

    • You also get a bug, because the first coder changed the update logic of the first 3 GraphicHandler classes, but missed updating the fourth GraphicHandler to match. Because the update logic is duplicated or called from multiple different classes not obviously related to game update order, it's easy to make a change in one place and miss the corresponding changes needed in the other place.

    • Together, these aspects increase the cognitive load of working with the system, or learning about it when ramping up a new team member. If the update and drawing sections were completely separate, then you could ramp up a new graphics programmer by showing them just the drawing code, and you can better isolate where a bug affecting one particular system might be coming from.

    Because of scenarios like the above, we have principles like "Don't Repeat Yourself (DRY)" and "The Single Responsibility Principle (SRY)" to remind us that while this code might work, now, it's often setting us up for extra complexity and avoidable problems down the line.

So, these types of effects are the consequences that lead game devs to recommend separating game simulation updates from rendering.

You might find, after examining the issue in detail, that these consequences are not a concern for your game, and choose to proceed as you initially proposed. That's fine. The trick is to always make your decisions based on the real effects of a choice, not on whether or not some strangers on the Internet would "accept" or "approve" of it. 😉

\$\endgroup\$
1
  • \$\begingroup\$ Thank you so much! Warms my heart that people like you want to help people and expect nothing in return. \$\endgroup\$
    – JensB
    Dec 20 '20 at 17:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.