0
\$\begingroup\$

If I have a class called Enemy:

// Simplified version
class Enemy
{
    float xPosition_;
    float yPosition_;
    int hp_;
};

Should this class also contain the information needed to render an enemy? Like in these two examples:

class Enemy
{
    float xPosition_;
    float yPosition_;
    int hp_;
    Model renderModel_;
};
class Enemy
{
public:
    void drawSelf(Window& window);
private:
    float xPosition_;
    float yPosition_;
    int hp_;
};

Or should the rendering information be inside a special renderEnemy function?

void renderEnemy(const Enemy& enemy)

Which is the better alternative?

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

"Should" is a tricky question in games. Really, all that game code "should" do is play your game correctly and be comfortable enough to work with that your team can release the game on time. If you break every rule in the book and still release a good game, then the code's doing its job.

(It's not hard to find stories of absolutely monstrous code that managed to ship great games, even if it did everything the books say code "should" not do)

That said, there are some principles that dev teams have found tend to help make code easier to maintain and improve development speed and flexibility.

One is the Single Responsibility Principle (SRP).

It says that each type in your program should have just one job, which it owns entirely. This helps make it easier to know which file to edit when you want to make a change to a particular behaviour, because it lives in exactly one, clearly-defined place. It also makes it easier to track down bugs, because they're more often confined inside one clearly-identifiable file, rather than diffused across a messy pattern of interaction between many files.

A related idea is the DRY Principle, "Don't Repeat Yourself".

It says that you should write each bit of code once. If you find yourself copying and pasting code into multiple files, you're repeating yourself, and that opens up the potential for bugs. In the future, you'll often need to change this code, and you might make the change in one place, but miss updating one of the copies to match. That creates an inconsistency that can flare up into a bug you'd swear up and down you fixed, leading to a frustrating search for the case you missed.

In this situation, I'd argue your enemy's single responsibility is to be an enemy. It shouldn't contain any behaviours that are also needed by things that are not enemies, because then we'd be repeating ourselves.

  • A position is also needed by player/neutral/friendly characters and by inanimate objects like barrels. Not the enemy's job.

  • Health is also needed by other characters or destructible objects. Not the enemy's job.

  • Rendering is needed by other characters, objects, background tiles, UI sprites, etc. Not the enemy's job.

One way that games commonly break up these different (and often shared) responsibilities is to use a component-based architecture.

In this approach, instead of deriving a new subtype of game object for each different kind of thing in your game, your base game object or entity becomes a kind of container for a list of components.

Each component type has a very narrow responsibility:

  • A position or transform component stores and manipulates the object's position in the world, handling placement and movement

  • A health component stores and manages its HP, handling damage and firing events on death

  • A collider component stores the hitbox / collision representation for objects that should be solid or react to contact

  • A sprite renderer component stores the sprite used to draw the object

...etc. Components (or the systems that process them) can reference one another - so the collider or sprite renderer components/systems can reference the transform component to know where to offset the collider in the world / draw the sprite on screen. A sprite animation component can reference the sprite renderer component and swap its sprite to match the current frame of animation. And so on.

This frees up your enemy behaviour components to not have to re-invent the wheel by implementing their own positioning/rendering boilerplate, but also without locking them into a strict place in an inheritance hierarchy where that functionality is applied to all descendants (whether they need it or not).

In fact you could even break "enemy" itself into multiple components like...

  • A faction component that establishes that this entity is hostile to the player

  • A patrol behaviour component that handles its idle movement pattern

  • A vision/awareness component that detects players in a vision cone or radius

  • A pursuit behaviour component that chases a target once spotted (disabling the patrol behaviour component)

Now each individual component type is very simple and easy to understand in isolation. Bugs are less common because we're working with smaller, simpler files, and it's easier to track them down because they're usually restricted to a single component file. It's also easier to collaborate with a larger team, because you'll less often be fighting over the same files and sorting out merge conflicts when making unrelated changes.

You don't have to re-implement things like HP again and again for players and enemies and destructibles, they can all re-use the same health component - even if they sit in vastly different parts of your game's taxonomy of objects (which can be a challenge to accomplish with inheritance).

And better yet, you can often create new entity types in your game without writing new code, just kitbashing together different combinations of the components you've already built, and populating them with different parameters. This is an especially big advantage if you're working in a bigger team that might include non-programmers, like level designers and game designers. Giving them these reusable building blocks of behaviour they can snap-together in new combinations frees them to build unique gameplay scenarios independently, without needing to request new work from the programming team for each and every variant or experiment.

You might find those principles and the component-based approach help you, or you might find they don't, so I hesitate to call them a "should" obligation. But these are the reasons why I wouldn't recommend tying up rendering in an enemy class.

\$\endgroup\$
0
\$\begingroup\$

It really depends on what type of architecture you are trying to mimic.

In ECS, you would have a component for model information that you would associate with any entity that needs it and a system that handles rendering those models.

For a more manager style system, which is what I used in my game engine, I have a model manager class which loads, stores, and unloads model class objects using function calls. Anything that needs a model associated with it gets a pointer to the model class which can be acquired with a GetModel function in the model manager class object. I hadn't optimized my rendering setup yet, but my objects were grouped into 1kmx1km cells which contained all the instances of objects, and the cells had their own render function.

\$\endgroup\$

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.