Skip to main content
gamedev.net
🔒 Locked

Object-oriented vs loop-based programming?

Started by suliman Aug 21, 2017 at 8:25 AM 28 replies 7.8k views
Original Post
suliman
suliman

Hi!

I've been doing games as a hobby in c++ for many years, but have little formal programming education. As a result i might have adopted a rather arcane way of structuring my games. I typically have:

1. A master-class called "world" that is huuge. Keeps track of game state as well as lists for units, buildings, projectiles etc. Some things are the same in many games, but i have a unique world class in every game.
2. Typically: cPlayer player[10]; where player[0] is the player itself in a single-player game and the rest is ai/opponents.
3. A main loop where i do stuff like


if(world.running){
  if(world.editorOn)
    runEditor();
  else
    runWorld();
}
else
  runLobby();

4. Typically entities like "units" have their own functions like unit::update, unit::move etc, but all of this is held together by the "world"-class.

I guess my thinking is very "loop-based" ei sequencial. Is this problematic? I don't think it's a very modern way of coding.

Thankful for any feedback!
Erik

Kylotan
Kylotan

If you have a class that is too large, split it up. Ideally each class has only one responsibility. That is something to aspire towards, even if meeting that criteria exactly is unrealistic. I'd recommend that you don't try and plan this all out, but instead get used to improving code as you go along. If you see that a class has several functions that are related to each other, but not related much to the other functions, move them out to a new class. And just keep doing that whenever applicable.

Get into the habit of naming things accurately. e.g. Don't make instances of cPlayer when you're creating an opponent. Think of a better name, such as 'Character' or 'Actor' or 'GameEntity' or whatever most closely resembles the content of that class. And again, if it then turns out that your class is doing 2 things - e.g. player-related things and npc-related things - that's a good candidate for refactoring into more than one class.

Don't use 'magic numbers' to mean things. In this case, that means don't use the zero index to tell the difference between a player and an enemy. What if you ever want to make a 2 player game? Instead, use some property of the object to tell you whether it's a player or not. Or maybe consider having 2 classes, one for players, one for other characters.

Main loops are fine. Everyone has something like one. However you might not want to hard-code your game states (editor/world/lobby) like that. A simple state machine using a switch/case statement is a better alternative. A more advanced alternative might have different objects for each state.

Guy Thomas
Guy Thomas

So,

In order for a program to run for any real duration there must be a loop of some kind, so it's not so much an argument between having many loops or using good OO practice as it is about using good OO practice while having loops.

Most game engines have a relatively simple main loop, that calls a handful of member methods that do some very complex things, in a set order. For example the small game I'm working on at the moment calls a physics step every 10 milliseconds (often not exactly, due to the time it takes the draw frames to complete) but calls a logic and draw step every frame computed.

Within these loops it's common to see something like a std::set> of current game entities being looped over and having their 'update()', 'draw()', or 'onCollision()' methods being called, and here is where the good OO practice comes in. Entity is a base class that almost every other entity derives from, be that characters, projectiles, pickups etc.

Because I store pointers to the base class, I can store any derived class within that set and access the virtual methods within the base class. This means I don't need many collections within the world for every different class type.


tl;dl : Looping over collections of OO objects is perfectly fine.

suliman
suliman

Well that's another issue: I rarely ever use inheritance. I have specific classes for each game, typically stuff like unit, projectile, map, player, building. I don't find common properties that would benefit from inheritance.

I guess that might be because I'm used to thinking this way i guess.

But what do you gain by inheriting from "entity"? Update and draw function for each one of these must look totally different anyway?

Kylotan
Kylotan

There are usually lots of commonalities across game entities when you think about it. For example, in Unreal, pretty much everything in the world is derived from AActor - so that contains information such as where it is located in the world, where it's facing, where it's moving, etc. Players might be derived from that object; but so could projectiles, or non-player characters, because they all have positions in the world and can potentially move.

You don't necessarily need to use inheritance. Many games use composition to express the fact that entities are roughly interchangeable but with some differing properties. For example, 2 entities could contain different SpriteComponents which dictates how each of them gets drawn. The most important thing about improving your coding is to spot when you can replace special-case solutions with generic and extensible solutions, reducing the amount of code necessary and therefore reducing bugs. That can include replacing explicit if-statements with a state machine, or making the difference between players and non-players explicit rather than dependent on their position within an array, etc. (And this isn't necessarily modernising, because these principles are 40 years old.)

guccang
guccang

I have some suggestions:

1: The most importent is finish your work as soon as simple coder.

2: Not use Complex skills

3: ''code complete'' read this book,and Reading and Writing code .

alvaro
alvaro

First of all, if your general code structure is working for you, I wouldn't go out of my way to change it, although there are alternatives you could experiment with.

One class with massive state, of which there is a single instance that everyone has a reference or pointer to is indistinguishable from a large collection of global variables, and has exactly the same problems (e.g., it's hard to determine correctness because any part of the code could be changing the global state).

If you don't use inheritance much, I consider that a good sign. There are places where inheritance could make your life easier, but it's easy to overdo, and then the resulting code is very confusing and hard to follow.


Guy Thomas
Guy Thomas

I use composition a lot too and most of my classes are made up of a few interfaces.

Lets look at it this way, consider this: you have many objects that need to be drawn to the screen via a 'draw()' method. You also want to update them every frame to compute their collisions, etc.

We will call the classes Car, Boat, Plane, Man, Box.

Now, without composition or inheritance you would need to have the following arrays:



std::set<Car*> mCars;
std::set<Boat*> mBoats;
std::set<Plane*> mPlanes;
std::set<Man*> mMen;
std::set<Box*> mBoxes;
  
// Then to update and draw...
  for (auto c : mCars)
  {
  	c->update();
  	c->draw();
  }
    for (auto c : mBoats)
  {
  	c->update();
  	c->draw();
  }
  
 // Etc.. etc...

This is really messy and breaks the DRY principle of 'Don't Repeat Yourself'.

Lets look at this problem another way.

Although cars and boats and men etc. may have different members and different methods, they all share two common methods, draw() and update(). Each class will implement draw and update differently - for example the car might calculate it's fuel remaining in update() and draw a picture of a car on screen in draw(). Whereas a person might calculate their age in update() and draw a picture of a person in draw();

Because all the classes share these same methods, we can implement an interface that declares these methods, but allows each class to implement it differently.

consider this code (I'll just use Car and Man to save space):


class iGameEntity 
{

	virtual void draw() = 0;
	virtual void update() = 0;
};

class Car : public iGameEntity
{
	virtual void draw() override { mRenderWindowPtr->draw("PictureOfCar.png"); }
	virtual void update() override { mFuelRemaining -= 1; }
};

class Man : public iGameEntity
{
	virtual void draw() override { mRenderWindowPtr->draw("PictureOfMan.png"); }
	virtual void update() override { mAge += 1; }
};

// Then we can store all our entities like this
std::set<iGameEntity*> mAllEntities;
  
// Add some cars and men
mAllEntities.insert(new Car());
mAllEntities.insert(new Car());
mAllEntities.insert(new Man());
mAllEntities.insert(new Man());
mAllEntities.insert(new Man());
  
// Then to call the methods we simply do this
for (auto e : mAllEntities)
{
  e->update();
  e->draw();
}

Now, we only need one container, and we only need to loop over it once to call the draw() and update() methods of every game object.

But the real beauty is that every subClass of iGameEntity does something different when draw() or update() is called.

Hope this helps get your head around OO coding.

suliman
suliman

Cool, that was a good and clear example :)

dmatter
dmatter
On 8/21/2017 at 9:25 AM, suliman said:

but have little formal programming education.

That probably works in your favour, there's a lot of people out there just itching to teach you bad OO in exchange for money!

On 8/21/2017 at 9:25 AM, suliman said:

1. A master-class called "world" that is huuge. Keeps track of game state as well as lists for units, buildings, projectiles etc. Some things are the same in many games, but i have a unique world class in every game.

The only problem here may be the size of your World class so you might just want to carve it apart somewhat. For example If you find you have a bunch of code in there just for managing projectiles (spawning them, recycling them, detecting when they've hit something, etc) then factor all of that out into a ProjectileSystem and have World hold an instance of that.

In principle though, having something that represents the 'active state of your game' along with update() methods that transition from state(t) to state(t+1) is a good design choice.

Just don't overload it with other responsibilities such as rendering, audio, etc.

On 8/21/2017 at 9:25 AM, suliman said:

2. Typically: cPlayer player[10]; where player[0] is the player itself in a single-player game and the rest is ai/opponents.

If you treat or process player[0] differently then better to move it out of that array:


Player player;
Player ai[10];


On 8/21/2017 at 9:25 AM, suliman said:

3. A main loop

No issues there!

On 8/21/2017 at 9:25 AM, suliman said:

4. Typically entities like "units" have their own functions like unit::update, unit::move etc, but all of this is held together by the "world"-class.

This is also fine! I do the same thing myself.

On 8/21/2017 at 9:25 AM, suliman said:

I guess my thinking is very "loop-based" ei sequencial. Is this problematic? I don't think it's a very modern way of coding.

There is no problem with this thinking. In fact there is a recent trend towards that kind of thing because uniformly processing an array of same-kinded objects in sequence is good for cache coherency and branch-prediction.

When you start hitting up objects that are allocated all over the heap and invoking virtual methods on them all, then performance starts to take a hit. (Although Moore's Law is always hard at work to mitigate this!).

0r0d
0r0d
On 8/21/2017 at 8:29 AM, suliman said:

Well that's another issue: I rarely ever use inheritance. I have specific classes for each game, typically stuff like unit, projectile, map, player, building. I don't find common properties that would benefit from inheritance.

I guess that might be because I'm used to thinking this way i guess.

But what do you gain by inheriting from "entity"? Update and draw function for each one of these must look totally different anyway?

Depending on your object types, there's usually quite some commonality in attributes and behaviors for all or at least most world objects. Things like ID, name, position, rotation, scaling, bounding volume, ability to render, update, load, save, etc. Without inheritance you're duplicating a lot of code, which means more bugs and a maintenance nightmare.

What do you gain from inheriting from a base class? Well, for starters it means that you can have a single list of all your objects, which can be a really useful thing. Imagine you have a character in the world and you want to see if its current bounding volume intersects with anything else. But, you have 10 object types in the world. Do you write 10 loops to check for intersections? Do you replicate that code with 10x loops everywhere you want to do anything will all the objects? With a single list you just write 1 loop and you're done. When you add a new object type, you dont have to go everywhere you have your 10x loops and turn them into 11x loops. Also, you can now decouple your game code from your engine code. The engine can provide functions like update_all() or render_all() that goe through all objects, and when you add game-specific objects you can keep those separate from the engine code, but the engine will still be able to access them through your virtual interface.

dmatter
dmatter

My default setting is anti-inheritance as a rule-of-thumb. It's fine to inherit pure interfaces, no real problems there, but once you start inheriting implemented methods, data-members, any multiple-inheritance whatsoever, etc. Then it's a slippery slope towards brittle code and poor reusability.

There are times when it makes sense of course, but in my experience most people will tend to abuse it. In fact where I work we monitor any and all use of inheritance closely and we treat it with suspicion in code-reviews of newly written code.

So with that in mind...


1 hour ago, 0r0d said:

Without inheritance you're duplicating a lot of code

Are you though? Inheritance is not the only way to share or reuse code.

If that was the case then languages without any inheritance would be seriously screwed! ;)


1 hour ago, 0r0d said:

Imagine you have a character in the world and you want to see if its current bounding volume intersects with anything else. But, you have 10 object types in the world. Do you write 10 loops to check for intersections?

One option is if you have a list of bounding volumes then you can just write 1 loop and check against all those volumes. That will work regardless of how many object types there are in the world (no inheritance needed either). This is similar to how a physics engine would do it.

Alternatively, maybe for each of those 10 different object types you want 10 different behaviours for when there is a collision. That means 10 different implementations anyway, so why not just have 10 different loops!

Hodgman
Hodgman
2 minutes ago, dmatter said:

My default setting is anti-inheritance as a rule-of-thumb.

That's OO's default setting too. A core rule of OO is to default to using Composition, and only use Inheritance when you have to (which is very rarely).

If you use inheritance all the time as your main tool for achieving code-reuse, you're using OOP tools, but are not following OO theory.

0r0d
0r0d
1 hour ago, dmatter said:

My default setting is anti-inheritance as a rule-of-thumb. It's fine to inherit pure interfaces, no real problems there, but once you start inheriting implemented methods, data-members, any multiple-inheritance whatsoever, etc. Then it's a slippery slope towards brittle code and poor reusability.

There are times when it makes sense of course, but in my experience most people will tend to abuse it. In fact where I work we monitor any and all use of inheritance closely and we treat it with suspicion in code-reviews of newly written code.

Inheritance is a tool like anything else. It's as good or bad as the use that it's being put towards. Almost anything in c++ can lead to problems if the programmer misuses or abuses it.


Are you though? Inheritance is not the only way to share or reuse code.

If that was the case then languages without any inheritance would be seriously screwed! ;)

Sure, there's absolutely many different ways to deal with setting up objects and their behaviors. I was speaking just towards what it looked like the OP was going for.


One option is if you have a list of bounding volumes then you can just write 1 loop and check against all those volumes. That will work regardless of how many object types there are in the world (no inheritance needed either). This is similar to how a physics engine would do it.

The OP could certainly do that, but that would be more of a component-object model which didnt seem to be what he's going for. The thing is that using a component/object model still leaves you with some of the same problems I talked about, namely that if you add new functionality to the game, you either have to alter the engine or you have to put it in the game code... which means now you've scattered your component handling calls all over the place. Inheritance would solve this, for example by having a component manager for each component type, and have new types derive from that. Then you create your new type in the game code, register it with the engine, and now the engine can have a single list of component managers to go through for common tasks like updating, rendering, and any queries that you might want to do.

So, for example, lets say I have some code that wants to query intersection with any components that have bounding volumes. Does that code need to know about all the component types in the game, and where to get them? Or does it just go to the engine and ask it to do intersections with any components that are registered for that?

Alternatively, maybe for each of those 10 different object types you want 10 different behaviours for when there is a collision. That means 10 different implementations anyway, so why not just have 10 different loops!

I suppose, but how often does that happen? A more likely scenario is that they all have some common attributes and code and specific differences on top of that.

Of course people can abuse inheritance, just like they can abuse anything else. But, inheritance is a valuable tool for organizing code. I dont approach solving a problem by first deciding which tools I wont use, I figure out how I want to solve the problem and then get the most effective tool for the job.

dmatter
dmatter
11 hours ago, 0r0d said:

Inheritance is a tool like anything else. It's as good or bad as the use that it's being put towards.

Yes I agree with that. But not all tools should be used equally.

If inheritance is your hammer then everything will start to look like a nail.

Really it should be that tool at the bottom of your toolbox that you get out begrudgingly sometimes while muttering noises about how C++ should just have better constructs to solve the problem at hand. (Even though you know that's never gonna happen for C++)

(Note, just to reiterate: I'm not talking about pure virtual base-classes or interfaces or protocols or traits or whatever your favourite language calls them. I've few gripes with those. I'm talking about any other uses of inheritance).


11 hours ago, 0r0d said:

that would be more of a component-object model

Sure! I mean call it whatever's fashionable, I just call it a list of bounding volumes :D

E.g. in C++:


std::vector<BoundingVolume>


11 hours ago, 0r0d said:

if you add new functionality to the game, you either have to alter the engine or you have to put it in the game code

(emphasis mine).

I honestly don't see the problem with that.

This isn't something I would be trying to 'solve' my way out of.


11 hours ago, 0r0d said:

I suppose, but how often does that happen? A more likely scenario is that they all have some common attributes and code and specific differences on top of that.

In my games that happens all the time!

Usually I'm not interested in just testing "if there was a collision", usually I want to know what was collided with so that there can be different behaviours in response to different kinds of collision.

For example I want different behaviours when my player hits a solid wall compared to if they hit water or lava or they get hit by a bullet or they get a health pickup. That's 5 different object types (Wall, Water, Lava, Bullet, Health) with 5 different reactive behaviours for my player upon collision.

One way (but by no means the only way) of handling this might actually be to loop over each type of thing separately (psuedo code):


func processCollisionsWith(things, interaction):
  foreach thing in things:
    if collided(thing, player):
      interaction(thing, player)
  

processCollisionsWith(walls, HitWallInteraction)
processCollisionsWith(water, BeginSwimmingInteraction)
processCollisionsWith(lava, BurntAliveInteraction)
processCollisionsWith(bullets, TakeBulletDamageInteraction)
processCollisionsWith(healthpickups, HealthBonusInteraction)

This also means that I get to decide the processing order, for example if I want all health-pickups to be handled before anything that might cause damage (like bullets) then I can make that happen easily and explicitly here.

TheComet
TheComet
On ‎22‎/‎08‎/‎2017 at 2:45 PM, Guy Thomas said:




// Then to call the methods we simply do this
for (auto e : mAllEntities)
{
  e->update();
  e->draw();
}

Now, we only need one container, and we only need to loop over it once to call the draw() and update() methods of every game object.

But the real beauty is that every subClass of iGameEntity does something different when draw() or update() is called.

It should be pointed out that doing it this way will introduce new, harder to solve design issues into your code that can potentially explode into a huge mess.

For instance: Draw order of different entities is no longer defined. Before, you could be certain that no matter how many Cars you added and removed and how many Boats you added and removed, all of your cars would always draw before boats. With the Entity base class, this is no longer the case. It could be that Cars and Boats and Boars and Goats are all interleaved in weird ways and thus rendered/updated as such.

Why is this an issue? Well, if you want to put cars on your boats and properly calculate collision for this (for example), while updating one of your car entities, the boat your car is on is in an unknown state. Was the boat already updated? Or is it still waiting to be updated? Half of the time you will be calculating collisions using the previous frame's boat position, the other half of the time you will be using the correctly updated boat position.

As a side-note: This is the fundamental flaw with ECS frameworks and why I've taken such a disliking towards them.


"I would try to find halo source code by bungie best fps engine ever created, u see why call of duty loses speed due to its detail." -- GettingNifty
Guy Thomas
Guy Thomas

I think it is important that we show an example of good inheritance vs bad inheritance after this comment:

Quote

My default setting is anti-inheritance as a rule-of-thumb

dmatter is absolutely right to say that inheritance is quite often misused. But it is also right to say that there are correct and perfectly acceptable ways to use it, indeed there are even times where it is the best thing to use.

So, here is an example of good inheritance:


class Soldier
{
private:

	int health;
	int ammo;
	WeaponType* weapon;
	Image* sprite;
	Team team;

public:
	Soldier(int h, int a, WeaponType *w, Image* img, Team t)
	:	health(h),
		ammo(a),
		weapon(w),
		sprite(img),
		team(t)
	{
	}

	public virtual ~Soldier() 
	{
		delete sprite;
		delete weapon;
	}
};

class BLU_Missile_Soldier : Soldier
{
public:
	BLU_Missile_Soldier()
	:	Soldier(100, 100, new Missile(), new Image("missileSoldier.png"), Team::Blufor)
	{
	}
};

class RED_Rifleman_Soldier : Soldier
{
public:
	RED_Rifleman_Soldier()
	:	Soldier(100, 100, new Rifle(), new Image("RiflemanSoldier.png"), Team::Redfor)
	{
	}
};

The example above is considered good inheritance because we are only making derivatives of the existing object, but we aren't taking ownership of any of the members of the base class.

The reason inheritance can be considered bad at times is because it is all too easy to put member variables (like health, ammo etc.) into the 'protected:' or heaven forbid even the 'public:' sections of your declaration and thus you break the rules of encapsulation.

If the class members are private, and you are only instantiating a variation of the object, then it is usually ok to use inheritance.

If however you are changing the core functionality of the object then you may want to consider whether you could get away with using interfaces and composition.

Finally, another note on inheritance, always make sure that your ~destructor() is virtual, else your derived classes ~destructor() will not be called when a pointer to the base class is used to destroy your object instance!

I think it is important that we show an example of good inheritance vs bad inheritance after this comment:

Quote

My default setting is anti-inheritance as a rule-of-thumb

dmatter is absolutely right to say that inheritance is quite often misused. But it is also right to say that there are correct and perfectly acceptable ways to use it, indeed there are even times where it is the best thing to use.

So, here is an example of good inheritance:


class Soldier
{
private:

	int health;
	int ammo;
	WeaponType* weapon;
	Image* sprite;
	Team team;

public:
	Soldier(int h, int a, WeaponType *w, Image* img, Team t)
	:	health(h),
		ammo(a),
		weapon(w),
		sprite(img),
		team(t)
	{
	}

	public virtual ~Soldier() 
	{
		delete sprite;
		delete weapon;
	}
};

class BLU_Missile_Soldier : Soldier
{
public:
	BLU_Missile_Soldier()
	:	Soldier(100, 100, new Missile(), new Image("missileSoldier.png"), Team::Blufor)
	{
	}
};

class RED_Rifleman_Soldier : Soldier
{
public:
	RED_Rifleman_Soldier()
	:	Soldier(100, 100, new Rifle(), new Image("RiflemanSoldier.png"), Team::Redfor)
	{
	}
};

The example above is considered good inheritance because we are only making derivatives of the existing object, but we aren't taking ownership of any of the members of the base class.

The reason inheritance can be considered bad at times is because it is all too easy to put member variables (like health, ammo etc.) into the 'protected:' or heaven forbid even the 'public:' sections of your declaration and thus you break the rules of encapsulation.

If the class members are private, and you are only instantiating a variation of the object, then it is usually ok to use inheritance.

If however you are changing the core functionality of the object then you may want to consider whether you could get away with using interfaces and composition.

Finally, another note on inheritance, always make sure that your ~destructor() is virtual, else your derived classes ~destructor() will not be called when a pointer to the base class is used to destroy your object instance!

Guy Thomas
Guy Thomas
11 minutes ago, TheComet said:

For instance: Draw order of different entities is no longer defined. Before, you could be certain that no matter how many Cars you added and removed and how many Boats you added and removed, all of your cars would always draw before boats. With the Entity base class, this is no longer the case. It could be that Cars and Boats and Boars and Goats are all interleaved in weird ways and thus rendered/updated as such.

Why is this an issue? Well, if you want to put cars on your boats and properly calculate collision for this (for example), while updating one of your car entities, the boat your car is on is in an unknown state. Was the boat already updated? Or is it still waiting to be updated? Half of the time you will be calculating collisions using the previous frame's boat position, the other half of the time you will be using the correctly updated boat position.

I totally agree and you were right to point it out, but that was just a simple example. There are a few solutions to the draw order issue you've highlighted and depend very much on the depth sorting of the scene.

Without digging into the details of octrees and Z-buffering in 3D scenes, it boils down to needing to sort your drawable objects before looping through them.

An example of a way of doing this in a 2D scene might be storing a 'draw layer' in an int (or byte, hardly likely to have more than 255 layers.). Then, when a drawable object registers itself with the engine, it's layer is checked, and it is stored in an appropriate array corresponding with it's layer; if one does not exist, it is created there and then.

Then, when the call to 'drawAllEntities()' is made, the renderer makes it's way through the layers, looping through each in turn and drawing the contents.

In fact this is exactly the method I used recently in an SFML application recently, and it worked very well.

Kylotan
Kylotan
20 minutes ago, Guy Thomas said:

The example above is considered good inheritance because we are only making derivatives of the existing object, but we aren't taking ownership of any of the members of the base class.

But on the other hand, if you have used inheritance to create a new derived class and literally the only difference is that it has changed some values in the base, there's no need for that class to exist at all. It's just a hard-coded way of doing something that could be entirely data-driven.

I think good inheritance is hard to define because it applies when you have objects that have different behaviour but respond to much the same interface. If behaviour is almost uniform then the objects should probably be the same type but differing only by data, and at the other extreme they should probably be completely different types, so it's understandable that people really struggle knowing where to pitch things. But if you know you have 2 object types that seem to fall broadly into that area - e.g., PlayerCharacter and NonPlayerCharacter - then inheritance from a common base is a reasonable first stab at solving the problem.


Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.