Object-oriented vs loop-based programming?

Started by
28 comments, last by ApochPiQ 6 years, 7 months ago

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

Advertisement

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.

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<std::shared_ptr<Entity>> 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.

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?

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.)

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 .

 

 

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.

 

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.

Cool, that was a good and clear example :)

Glad it helped

This topic is closed to new replies.

Advertisement