Rate my engine!

Started by
56 comments, last by thre3dee 16 years, 10 months ago
My viewpoint on this is:

Striving for a good design is fantastic, but sometimes brute force nasty methods are simple necessary to get the damn thing out of the door.

The last company I worked at used global variables which were intended to have only a single instance, littered all over the code, not even singletons. Very successful company, jobs were completed on time, games were shipped multi-platform, you've probably played some of them.

Practice & theory, often go hand in hand, sometimes do not. Finding the right balance is the real problem.
- Teach a programmer an answer, he can code for a day. Show a programmer the documentation, he can code for a lifetime.
Advertisement
But wouldn't multiple instances of a class accessing a renderer cause a problem? Admittedly I've moved to DirectX not very long ago from OpenGL, and I'm a bit sketchy on the details. It seems to me like having multiple, different objects each generating a device wouldn't be such a good idea (like I said, I want to wrap up everything in this engine so I could swap it out for, say, an OpenGL version if I wanted - I can't set up Direct3D and keep all of its interfaces in my game as global variables.) Or maybe make these things static members of my engine could also work...

Either way, I'm more interested in the actual DirectX-related material and not the means by which it is designed - I can always alter the engine to be a bona-fide class instead of a singleton if it causes issues before I put the finishing touches on it and move on to integrating it in my game.

I'm still hoping to hear more about that aspect - the actual engine, not singletons vs. classes (though there's some good stuff being tossed around) - though. :)
Quote:
My viewpoint on this is:

Striving for a good design is fantastic, but sometimes brute force nasty methods are simple necessary to get the damn thing out of the door.

The last company I worked at used global variables which were intended to have only a single instance, littered all over the code, not even singletons. Very successful company, jobs were completed on time, games were shipped multi-platform, you've probably played some of them.

Practice & theory, often go hand in hand, sometimes do not. Finding the right balance is the real problem.


The thing about singletons, though, is that they tend to come into play early on -- in the design phase or early implementation, before time crunch is a problem. They need to be killed off there, so that they don't propagate their design rot into the code for the lifetime of the project. Once that happens, refactoring them out can be unbearably painful and in almost all cases there will not be time or budget to do it properly, as you point out.

However the "balance" issue is not a valid argument against singletons, because at the beginning of a project you should be much more concerned with writing robust, maintainable and extensible code -- singletons are none of these and have no business being there. If you prevent their existence early on, you won't have as many problems with them later.

Quote:
But wouldn't multiple instances of a class accessing a renderer cause a problem?

Why would it? It's easy to build an implementation that does not have such issues, and its easy to see situations where it makes sense. Assume a renderer represents a fixed rendering destination; you might want multiple for multiple viewports. Assume a renderer represents an interface to a specific graphics card; some machines have multiple. Those are particularly naive examples, but they serve to illustrate the point.

Quote:
It seems to me like having multiple, different objects each generating a device wouldn't be such a good idea

Don't build a one-to-one correspondence between renderer and "device" then. It's easy enough to maintain a single device without restricting yourself to a single device, thus allowing you to create multiple devices (multimonitor support?) in the future.
Quote:
I'm still hoping to hear more about that aspect - the actual engine, not singletons vs. classes (though there's some good stuff being tossed around) - though. :)

This warrants its own post. Here's a quick off-the-top-of-my-head run-down of how the rendering system I'm currently building works, so far.

There's a collection of so-called "back end" objects, that implement compile-time-polymorphic interfaces to an underlying rendering API (this is so I can build versions of the rendering assembly -- I use C# for this -- for both D3D9 and D3D10; theoretically I could add OpenGL support, but there's little reason to). These objects, and what they map to in the D3D9 interface, are as follows:
GraphicsDevice  --> D3D9 deviceTextureResource --> D3D9 textureBufferResource  --> D3D9 vertex buffer, index bufferShaderProgram   --> D3D9 vertex shader, pixel shader


These are fairly thin wrappers. Also grouped into this logical collection of functionality are some enumerations for API-agnostic ways to specify primitive topology, shader program targets, buffer contents, and so on, and some routines to marshal between my API-agnostic values and the compile-time-selected underlying render API values. There's also some stuff for doing buffer and texture resource in-memory IO (for locking resources).

Abstracting the underlying rendering API began as a way to clean up the rest of my code and ensure I was always making use of underlying API objects in the same fashion (such as always passing the same creation flags to certain classes of vertex buffer, et cetera). It wasn't originally done with API portability in mind -- I don't really recommend that be something you consider as a major feature. So far, this seems to have worked out well as far as portability goes, but that's just a fluke. I didn't really design for it. When I get further along, I'll probably write up an article about how it worked out, since so many people seem infatuated with the albatross of API portability.

Anyway.

The rest of the renderer is built on top of that thin abstraction. The client interacts primarily with a Renderer object, which hold a GraphicsDevice internally. Multiple Renderer objects share the underlying device unless explicitly told to create their own (actually it's a bit more involved than that, but whatever). No requirement, restriction, or assumption of the existence of only one GraphicsDevice or Renderer at all.

Renderer aggregates a couple of other classes that the user tends to deal with. ResourcePool, through which the client requests the creation of new types of resources (geometry objects, 2D canvas objects, et cetera). RenderQueue, which is ultimately responsible for the bulk of the actual rendering work, and MethodMapper, which is used to construct an association between concrete renderable instances (RenderInstance) and objects that dictate the rendering process (RenderMethod) via a common interface (RenderDescription).

A given Renderer contains only one each of ResourcePool, RenderQueue and MethodMapper (and they are not creatable by client code), but they aren't (and can't be) singletons because each Renderer needs to have a unique instance of each.

Actual rendering works as follows. The RenderInstance object represents a specific instance of something that will be rendered. It contains a reference to the source object (for example, the chunk of geometry data, so you don't need to duplicate it), and some assorted per-instance data, like the world transformation matrix, and a RenderDescription. RenderDescription is a uniform means of specifying how the client would like an instance to be rendered (which effects to apply, do you want to render this instance with debug indicators, et cetera -- I'm still working on interesting ways to use this class as a means of controlling shader metageneration).

RenderInstance objects are inserted into the RenderQueue, which sorts them into appropriate buckets (solid, alpha, requires-shadow-pass, et cetera) based on the RenderDescription (which has a method of forcing specific and/or multiple buckets, if you like). To perform actual rendering, instances are associated with a particular RenderMethod (which implements both CPU-side processing -- setting shader parameters and maybe deforming geometry -- and GPU-side processing -- binding shaders and handing the geometry from the source object to the graphics card) via the MethodMapper, based on the contents of the RenderDescription.
When the queue is flushed, each method pushes its source object's geometry to the card to perform actual rendering.

There are two primary points of extensibility, so far. The actual sorting in the render queue into various render buckets can be customized by the client if they provide a class that subclasses the RenderBucket class. New means of rendering things can be implemented by provided subclasses of the RenderMethod class; for example a client might want to implement software skinning by implementing a class derived from RenderMethod that overloading the appropriate method that is called when the render method does its CPU-side geometry processing. The method has full access to the state of the geometry at that point, as well as at least read access to most of the other relevant portion of the render pipeline, so it can deform geometry according to whatever wave model it likes as a preprocess step prior to submitting the geometry to the graphics card.

So that's a brief (heh) overview. Some caveats:
- This is still a prototype, and might change quite a bit as I refactor it.
- This was designed primarily with extensibility of rendering methods in mind (in other words, its for prototyping graphics algorithms).
Quote:Original post by jpetrie
Quote:
Isn't the OGRE engine littered with singletons? If singletons are so deadly (which I completely disagree on) why does a large popular open source engine use them so much??

Yes, and that's one of it's huge weak points. Being popular and open-source says nothing about whether or not it is well designed.

Quote:
Most of the time its better to have global accessibility as well.

That's amusing. Wrong, but amusing. Having global accessibility is usually a crutch; if its not there because you already have an unstructured and tightly-bound design, it will allow you (without much thought; accidentally) to create an unstructured and tightly-bound mess.

Quote:
I use singletons in engine design when there should be only one. Where having two would screw things up, or you simply shouldn't need more than one.

Then you're misusing them, because the second point ("or you simply shouldn't need more than one") means you simply should not create more than one, and the first point is rarely if ever true in game development (if you think its true, that's a sign that your design and/or implementation is broken, as I touch upon in the thread I linked).

Quote:
* FileSystem (keeps a list of archive codecs, loaded archives etc)
* ThreadManager (handles creation of named Win32 threads)
* InputManager (handles DirectInput enumeration etc)
* PluginSystem (loads and unloads plugin dlls)

- Renderer and Engine will be singletons too.

None of those need to be singletons, consequently, none of them should be.

Lol its funny how theres all these tutorials on the net showing how u do singletons and why they're so great, and then theres these people who think that having a globally accessible, one-instance needed class instance made through object orientation is the devil. Isn't that entirely defeating the purpose of OO design? Yeh sure, its a 'global' but its a far cry from global variables like "extern Device * g_Device;"

I use the singleton design pattern because it allows a program with several DLL's linked in to access the SAME instance very easily and very straight forward.

Sure the Engine class will probably handle the actual creation and destruction of the sub-systems, but why does that mean they have to be annoying and indirectly accessible when they are used so much.

Like my above example: I'd rather code with "IO::GetInst ()" than "Engine::GetInst ().GetIOSystem ()".
Cool, so you didn't really understand anything I just said, then, did you?

Just because you build something with "object orientation" does not magically make it okay. You don't seem to understand much about what "OO design" actually is. It's not just about using "design patterns" and the "class" keyword. It's about building reusable, modular, extensible software components via a number of methodologies and design principals, all of which are pretty much completely violated by the use of singletons in just about every context.

Quote:
"IO::GetInst ()" than "Engine::GetInst ().GetIOSystem ()".

This, for example, is little more than syntactic sugar and illustrates rather well that you don't seem to have much of a clue what's really going on here.
Singletons came out of C, in my oppinion. Sometimes it is nice to not instantiate classes and use global functions. Sometimes it makes sense to use a single instance; for instance a single instance of a File System object, or a single instance of a Current User object. Sometimes, developers missuse them as static classes --which are good for global code containers.
Quote:Original post by thre3dee
Like my above example: I'd rather code with "IO::GetInst ()" than "Engine::GetInst ().GetIOSystem ()".


You are using a singleton in both of those, its just a matter of what you implemented as a singleton. So, basically, this example misses the point entirely.

[Edited by - Driv3MeFar on May 23, 2007 10:15:08 PM]
Seems the thread's turned into a singleton debate, but I think I got my answer so it's all good. :) Actually, it's fairly interesting.

One thing caught my interest though.

Quote:Then you're misusing them, because the second point ("or you simply shouldn't need more than one") means you simply should not create more than one

Originally I was going to say, isn't that the whole point of a singleton? But as I was writing that I realized how unlikely that situation would be in a well-designed system. If your code is going to access something willy-nilly and you have no control or no idea over what it does to the point where you can't ensure only one instance of your class exists, your code is broken. And if your code is logical and well-designed, you shouldn't have to rely on a singleton for something like this after all.

Seems to me, though, it could still have some uses in enforcing the accesibility of data. Maybe something involving threads, but I can't really think of any concrete examples at the moment where critical sections or mutexes wouldn't do the trick. It's 1 am anyway. Brain no function well coffee without. ;)



@jpetrie:

Very appreciated! That's exactly the kind of answer I was looking for, and way more. You've given me a lot of material to ponder over. :)

In terms of API portability, I have no real plans for using OpenGL any time soon. The project originally began in OpenGL (screenie) but for various reasons I decided to restart it. One of the main reasons being the lack of structure in my game engine - it was getting really messy and hard to manage, and despite the screenshots, it wasn't all that far along yet anyway.

I like having the option open to me just in case, but I'm aiming a bit more for something independant from my game so I can just pluck it out and reuse it for future projects. Having it integrated too tightly with the game will make that difficult. :)

If I understood your architecture right, your engine receives references to objects it needs to render, places them in a render queue, and various internal classes manage the how and when of the actual rendering based on what kind of object it is/treatment it requires. You also (mostly) abstract the API from the client and let your interfaces deal with that aspect. Assuming my (grossly oversimplified) understanding is correct, that sounds a lot like what I was hoping to achieve, but considerably more structured. I think you've put me on the right track - I'm going to work this out on paper and post about what I can come up with later on.

Again, thanks - that really helped. :)
Well actually I've been thinking and realised I hadn't been focusing on creating the engine classes a singular modular unit that is governed by an 'engine' super class. Rather I had been simply adding sub-system classes as their own thing that would eventually be managed by an 'engine' class, and not designing them as "sub" systems of the engine so to speak.

I guess you could say that I had been coding it more along the lines of a big library as opposed to an engine.

I've now come up with a new structure design for the engine:

class FileSystem;	// normal classesclass Renderer;class InputManager;class SubSystems{	SmartPtr <FileSystem> fileSystem;	SmartPtr <Renderer> renderer;	SmartPtr <InputManager> inputManager;	// etc etc};class Engine : Singleton <Engine>{private:	SubSystems m_Systems;public:	bool CreateSubsystems (void)	{		m_Systems.fileSystem = new FileSystem (/* ... */);		// etc etc	}	SmartPtr <FileSystem> GetFileSystem (void) { return m_Systems.fileSystem; }	SubSystems GetSubsystems (void) { return m_Systems; }		/* etc etc */};


So only the Engine class is a singleton and thus can be referenced anywhere, and the sub-systems can be gotten from that.

That being said, a class that uses a sub-system might have the Engine * passed in to its ctor so it can get any sub-system references it needs..

BTW, this is a very basic version of my proposed structure, and its pretty early on in terms of development so this change will be very easy since I haven't actually started coding the Engine super class yet.

Anything you can recommend?

By the way, the engine is currently split up into about 8 modules, each being its own DLL (with the exception of the Math (vector3, quaternion etc) module, with memory tracking and logging etc. This is to help build times and code changes/updates as well as keep the engine structured and easy to find code. It's being coded in VisualStudio 2005 so its all built in the one project.

I've currently got it as:
(SGE is the initials of the engine name...)

* SgeCore.dll (helper classes, thread management, memory allocation/tracking, logger, time and window classes)

* SgeData.dll (file system, plugin system, archives, resource/manager base classes etc)

* SgeInput.dll (controller/ms/kb device management and polling)

* SgeMath.lib (math functions, vector/quaternion/matrix etc classes)

* SgeNet.dll (networking and internet)

* SgeScript.dll (scripting [Squirrel proposed], xml, config-scripts)

* SgeSound.dll (sound management, playing etc [library to be decided, FMODEx or OpenAL])

* SgeVideo.dll (DirectX10 renderer, texture, vertex buffers, shaders etc etc)

This topic is closed to new replies.

Advertisement