How to structure a game in OOP

Recommended Posts

Tispe    1468

Hi, I have rewritten my project several times to adapt to new game structures. I do this to conform to rules such as, no static or global variables, no singleton patterns and the single responsibility principle.

The application consist of class objects. I often find myself creating a "super" class to only make one instance of it. Such as a Renderer class. This class handles the window, resolution, fullscreen toggle and draws the scene.

Another class is the Asset Manager, one instance is created and it handles loading files and preparing them for later use. It keeps an array of all assets. Since the Asset Manager needs the device to create resources (VB, IB, Textures) in the managed pool, I keep a private pointer to the device in Asset Class and make a copy in the constructor. This cross object dependancy is to conform to the "rules", maybe there is a better solution?

The Entity class has one instance, it has a list of (pointers to) Player objects (struct). These player structs contain information such as what Skeleton to use, what Mesh to use, and character variables.

When rendering I pass the list of Entities to the renderer which looks up the Asset Manager for the resources and draws them as described in the Player object.

There are also other classes such as the animator and gui but I kept those out to keep it short. Is this a good game structure? What should I pay attention to, and are there any other game structures I should consider?

The pseudo-code:

main(){
InputOutputClass IO;
DeviceClass Renderer;
AssetClass Assets(Renderer.GetDevice());
EntityClass Entities;
GuiClass Gui(Renderer.GetDevice());

MainLoop(Renderer, Assets, Entities, IO, Gui);

}

Edited by Tispe

Share on other sites
Buster2000    4310

Sorry that has got to be the absolute worst reason to rewrite your code

Totally agree 100%.  The only reason you should rewrite this stuff is if you find it stops you from extending your game or is inflexible in some way.   Refactoring just to avoid other peoples opinions of what is bad software engineering is just a time suck when what you should be concentrating on is getting things done.

Sure some people don't like statics, globels or singletons but that doesn't mean they should be avoided entirely.  Each has its use cases.

Share on other sites
blutzeit    1650

I often find myself creating a "super" class to only make one instance of it.

Premature abstraction is the object-oriented root of all evil.

Use a good refactoring tool, go over the code and try to remove all unnecessary abstractions. Then, go over the code again and see if you find code duplications. Those are the candidate points for introducing new abstractions. Repeat this process a few times and you should end up with a much cleaner framework.

Share on other sites
Tispe    1468

But if I don't bundle things into an object, I would have to either keep them global or have massive amounts of arguments to pass around, creating a huge web of dependancies. Is it bad to make a class if only one instance will be made from it? Surely classes are not only for abstraction?

Share on other sites
Buster2000    4310

Is this class just a pod?
couldn't you just have a struct with a global instance?

Share on other sites
Tispe    1468

The render class holds everything from device to shaders. It's the glue between my assets and the Graphics API. And in this class I have functions like 'ChangeResolution(MyStruct Params)' and Render(MyClass *Entities).

I read that the only difference between struct and class is that members are public in struct by default and private in classes by default. So changing they key word 'class' to 'struct' has no meaning.

Bundling stuff in classes or structs is the same, but is it proper to use it in this case?

Share on other sites
blutzeit    1650

Yes, it's good practice to put your state inside of objects, even if there's only one instance of the class. The question is of course where to put each individual variable. If you're not satisfied with the architecture but you don't know what to do, one approach is to just shuffle the code around and see what happens. Experiment with merging and splitting classes and functions. Look for new names, and try to make them as concrete as possible. Poke around and see if you find any redundant duplications or code smells. It usually takes a few iterations before the architecture settles.

Share on other sites
metsfan    679

It sounds to me like you have a lot of questions but few answers.  If you want my opinion, before you start coding, take a step back and draw your application on paper.  Make sure you understand the interactions between the different subsystems, and how you want them to communicate with eachother.  Get a real understanding of how you WANT the system to work, then you can refine your design.  However, if you just jump in and start making a mess, and hope to figure it out as you go along, you're going to run into problems, and just make your code messier.

These are some things I like to avoid in my design:

• Global state.  There's just no need.  Any situation where a Singleton is a solution, there is often a better solution that doesn't use a Singleton.  This is an opinion I formed after using Singletons for a long time, and gradually realizing how evil they are the hard way.
• Avoid global event systems.  They are a nightmare to debug.  Prefer a callback/delegate architecture.  Another thing I had to learn the hard way.
• Make sure your subsystems are compartmentalized.  For instance, there is no reason for a sound object to contain a pointer to a physics object.  Keep your systems separate and join them using a higher level interface.
• Make your APIs easy to understand, and difficult to use incorrectly.

Good luck.

Share on other sites
Tispe    1468

• For instance, there is no reason for a sound object to contain a pointer to a physics object.  Keep your systems separate and join them using a higher level interface.

Well, this is where it gets tricky for me. The APIs for sound and graphics are different systems. I can encapsulate them in my own classes and simplify the interfaces for my use. But for example there has to be a place in the code where a sound is triggered when the animation passes a point. We are now far down a method in the animation class and we discover that we need to trigger a sound. Where is the pointer to the sound interface when we are in the animation system?

Globals?

Singletons?

Coupling by having a pointer to the sound system as a member in the animation class?

Having a higher level interface that calls the DoAnimation() method, I need return values in order to call DoSound() afterwards. But I just shuffle the burden up the food chain. Now I have to code a ton of different states that must be passed and returned and bloat the calling code, making it more complex.

Share on other sites
metsfan    679

• For instance, there is no reason for a sound object to contain a pointer to a physics object. Keep your systems separate and join them using a higher level interface.

Well, this is where it gets tricky for me. The APIs for sound and graphics are different systems. I can encapsulate them in my own classes and simplify the interfaces for my use. But for example there has to be a place in the code where a sound is triggered when the animation passes a point. We are now far down a method in the animation class and we discover that we need to trigger a sound. Where is the pointer to the sound interface when we are in the animation system?

Globals?
Singletons?
Coupling by having a pointer to the sound system as a member in the animation class?

Having a higher level interface that calls the DoAnimation() method, I need return values in order to call DoSound() afterwards. But I just shuffle the burden up the food chain. Now I have to code a ton of different states that must be passed and returned and bloat the calling code, making it more complex.

The way I handle this is with callbacks. For instance, let's take a Rock object as an example. A Rock object falls to the ground, and when it hits the ground, it should trigger a "thump" sound.

typedef std::function<void()> DelegateFunction;

class Rock {
Sound mSound;
Animation mFallAnimation;
Rock()
{
DelegateFunction delegate = std::bind(&Rock::FallAnimationEnded, *this);
mFallAnimation.Start();
}

void FallAnimationEnded()
{
mSound.Play();
}
};

class Animation
{
std::vector<DelegateFunction> mDelegates;
void AddDelegate(DelegateFunction func) { mDelegates.push_back(func); }

void Start(); // Begins a timer, or adds it to an animation manager, however you want to do this.

void Update() // Called every animation update
{
if(animationStillRunning)
{
this->UpdateAnimationStuff();
} else {
for(int i = 0; i < mDelegates.size(); i++) {
mDelegates[i]();
}
}
}
};


So as you can see, we have no coupling between our Sound system or Animation system. The Rock is created, it will attach its FallAnimationEnded member function to the animation's delegate list, and when the animation is done, it will call all registered delegates. Obviously this code is far from complete and I'm not even sure if all my syntax is correct, but the idea should be clear.

Edited by metsfan

Share on other sites
Tispe    1468

Gonna bump this, I would like some feedback on a structure I am working on.

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
Dx9Device DeviceInterface;							//Wrapper Interfaces to hardware APIs
Dx9Audio AudioInterface;
Network NetworkInterface;
WinInput InputInterface;

/*
Some code
*/

Assets AssetsManager;								//Game managers
Entities EntitiesManager;
Area AreaManager;
Audio AudioManager;

MainLoop(&DeviceInterface, &AudioInterface, &NetworkInterface, &InputInterface, &AssetsManager, &EntitiesManager, &AreaManager, &AudioManager);

return 0;
}

void MainLoop(Dx9Device* pDeviceInterface, Dx9Audio* pAudioInterface, Network* pNetworkInterface, WinInput* pInputInterface, Assets* pAssetsManager, Entities* pEntitiesManager, Area* pAreaManager, Audio* pAudioManager)
{
ToLogicData Data;
GameData Gamestate;

while(TickMessages())
{

GameInput(pNetworkInterface, pInputInterface, &Data);													//Get Input and output pData
GameLogic(pAudioManager, pAreaManager, pEntitiesManager, pAssetsManager, &Data, &Gamestate);			//Get pData, update and output game state
GameOutput(pDeviceInterface, pAudioInterface, pNetworkInterface, &Gamestate);							//Render game state, play audio from state and networking
}
}


Is this a good way for passing interfaces and data around?

Edited by Tispe

Share on other sites
L. Spiro    25622

Whenever you need to pass a ton of parameters it is better to make a structure with those parameters as its members and pass a reference to that structure.

L. Spiro

Share on other sites
Alpha_ProgDes    6921

Sorry that has got to be the absolute worst reason to rewrite your code

Totally agree 100%.  The only reason you should rewrite this stuff is if you find it stops you from extending your game or is inflexible in some way.   Refactoring just to avoid other peoples opinions of what is bad software engineering is just a time suck when what you should be concentrating on is getting things done.

Sure some people don't like statics, globels or singletons but that doesn't mean they should be avoided entirely.  Each has its use cases.

Let's back up. If he's already finished writing the game and wants to go back and refactor it, then I don't see the harm or the waste of time. Now, if he's hasn't finished writing the game yet, then yes it's more important to finish writing the game then worry about good software engineering. You'll get that with experience. But I wouldn't call the effort a complete waste of time.

Share on other sites
metsfan    679

Whenever you need to pass a ton of parameters it is better to make a structure with those parameters as its members and pass a reference to that structure.

L. Spiro

Another option for this particular situation is the Service Locator Pattern (http://en.wikipedia.org/wiki/Service_locator_pattern), though there are some who consider this an anti-pattern.

Share on other sites
Tispe    1468

Whenever you need to pass a ton of parameters it is better to make a structure with those parameters as its members and pass a reference to that structure.

L. Spiro

Why not pass by address, that way you can pass a NULL?

Share on other sites
Alpha_ProgDes    6921

Whenever you need to pass a ton of parameters it is better to make a structure with those parameters as its members and pass a reference to that structure.

L. Spiro

Why not pass by address, that way you can pass a NULL?

Well if you pass the structure by reference, then you only have to pass one NULL, as opposed to 10 or 15.

Share on other sites
Tispe    1468

I ment if I place everything in a single structure, why pass that structure as reference instead of address.

Share on other sites

Because the reference to the struct has replaced a load of required parameters, so not passing one would be pointless...

Share on other sites
markr    1692

Really, do the simplest thing that could possibly work.

I tend to end up with some big hairy object called "GameManager" or something, which I create as a local in main() or somewhere fairly close, pass by reference between a small number of high-level functions, which then call the more specific things.

This "GameManager" tends to be a composition of a number of more specific managers which handle specific aspects.

I often seem to have some "ObjectManager" or something which contains lists of various objects which it manages, and deals with calling the tick functions, rendering functions (possibly passing a renderer object by reference).

Share on other sites
L. Spiro    25622

Tispe, on 07 Jun 2013 - 20:04, said:

L. Spiro, on 07 Jun 2013 - 09:37, said:
Whenever you need to pass a ton of parameters it is better to make a structure with those parameters as its members and pass a reference to that structure.

L. Spiro

Why not pass by address, that way you can pass a NULL?

NULL is not a valid parameter or your engine would crash and burn.

Always use references when NULL is invalid. Use pointers only when NULL is a valid parameter.

L. Spiro Edited by L. Spiro