Singletons in game engine architecture

Started by
61 comments, last by LorenzoGatti 6 years, 1 month ago
2 hours ago, Hodgman said:

The context is a command-list writer for use by a single thread, and you can create multiple of them if you wish to have multi-threaded command-list creation.

But one immediate per display/rendering system, right?

🧙

Advertisement
11 hours ago, Mike2343 said:

Could the RenderManager not hold onto ID3D11Device/Context pointers and in a function provide access to them.  Then pass the RenderManager to those sub-systems, like RenderingOutputManager, RenderingStateManager, ForwardPass, etc.?  Create RenderManager as a unique_ptr and just pass the raw pointer from that down to the sub-systems.  Their responsibility to check it's still valid before using it obviously.  But should keep things fairly tidy.

But this feels like passing the service locator of the service locator pattern around. Though, passing the manager is indeed tide. I was thinking about passing only the required members around which can become numerous. Though, passing the manager can still result in some pointer chain.

 

 

My largest concern while getting rid off globals and passing them as context arguments, is what about scripting? My script components have an update method which could require mouse, keyboard, display configuration, model and texture creation. In order to use all of that, you'll need to provide lots of arguments (the manager/factory to the very least) as user, or the base script class needs all of that as member variables?

My largest concern while getting rid off globals and passing them as context arguments, is what about scripting? My script components have an update method which could require mouse, keyboard, display configuration, model and texture creation. In order to use all of that, you'll need to provide lots of arguments (the manager/factory to the very least) as user, or the base script class needs all of that as member variables?

🧙

5 hours ago, cyberpnk said:

It seemed to make sense that this would be a Singleton, since the GLFW library (I believe) only allows a single callback function (i.e. for the keyboard) per window.

So you're saying the sound system needs access to the input system?  Because if input is a singleton, than the sound system CAN access your input system and I would be FAR more concerned with bugs being introduced that way than by worrying about creating a second instance of the input system on the same window.  Document that only one should be created.  Or have a startup class that configures and gets the engine ready for rendering that returns reference/smart pointer to the input system, etc.

22 minutes ago, matt77hias said:

My largest concern while getting rid off globals and passing them as context arguments, is what about scripting? My script components have an update method which could require mouse, keyboard, display configuration, model and texture creation.

I have no idea what scripting language you're using, so I cannot really help; but that seems like it needs a lot of items, refactor?

 

58 minutes ago, matt77hias said:

How would ImGui even do this?

Pretty sure cyberpnk said GLFW not ImGui.  And @Hodgman was more making a point.  Singletons only make sense when something physically (hardware) says only one instance can exist.  Which these days is pretty much never.  The number of singleton horror stories FAR outweigh the success stories, especially when it comes to debugging and multithreading.

"Those who would give up essential liberty to purchase a little temporary safety deserve neither liberty nor safety." --Benjamin Franklin

3 hours ago, matt77hias said:

How would ImGui even do this?

In GLFW, call glfwCreateWindow twice? In ImGui, people are doing stuff like this already: Docking.gif

3 hours ago, matt77hias said:

But one immediate per display/rendering system, right?

In D3D12 you get a device, many command queues, and many deferred command lists.
In D3D11 you get a device, one immediate command list (with built in graphics queue) and zero/many deferred command lists.
In GL you get a device (with built-in immediate command list ).

3 hours ago, matt77hias said:

My largest concern while getting rid off globals and passing them as context arguments, is what about scripting?

There's no difference between "normal programming" and "scripting". Coding and scripting are both just slang for programming. The entire body of software engineering doesn't get thrown out because you're programming in a different language. What language are your  game scripts written in?

1 hour ago, Hodgman said:

There's no difference between "normal programming" and "scripting". Coding and scripting are both just slang for programming. The entire body of software engineering doesn't get thrown out because you're programming in a different language. What language are your  game scripts written in?

I am not referring to nor implying that scripting is different from normal programming. Scripting is for me the act of writing scripts, which are components that execute small programs (written by a user of an "engine", as expressive as the "engine" supports it, in a language that the "engine" supports). It does not imply a different language, it does not imply dynamic loading, etc. I literally referred to scripting components.

I am concretely talking about C++ itself, in fact a single method of a derived class MyDerivedScript::Update(double delta) overriden from a MyBaseScript::Update(double delta) contained in a child class of MyComponent. Lets assume you want to change a material texture with a fixed color equal to a given inputted RGB, how are you going to create the texture? Do you want to user to pass a ID3D11Device around or invoke a method on some TextureFactory the user first of all needs to have and second needs to find somewhere? Or are you going to wrap all of this in some methods of MyBaseScript or even MyComponent. Both cases require that the all the "context" needs to be accessible by some base class. So how are you going to query key presses?

1 hour ago, Hodgman said:

In ImGui, people are doing stuff like this already: 

First of all the gif image, you show, is just one window according to the Windows terminology.

Second, I was actually referring to this:


// Win32 data
static HWND g_hWnd = 0;
static INT64 g_Time = 0;
static INT64 g_TicksPerSecond = 0;
static ImGuiMouseCursor g_LastMouseCursor = ImGuiMouseCursor_Count_;

// DirectX data
static ID3D11Device* g_pd3dDevice = NULL;
static ID3D11DeviceContext* g_pd3dDeviceContext = NULL;
static ID3D11Buffer* g_pVB = NULL;
static ID3D11Buffer* g_pIB = NULL;
static ID3D10Blob * g_pVertexShaderBlob = NULL;
static ID3D11VertexShader* g_pVertexShader = NULL;
static ID3D11InputLayout* g_pInputLayout = NULL;
static ID3D11Buffer* g_pVertexConstantBuffer = NULL;
static ID3D10Blob * g_pPixelShaderBlob = NULL;
static ID3D11PixelShader* g_pPixelShader = NULL;
static ID3D11SamplerState* g_pFontSampler = NULL;
static ID3D11ShaderResourceView*g_pFontTextureView = NULL;
static ID3D11RasterizerState* g_pRasterizerState = NULL;
static ID3D11BlendState* g_pBlendState = NULL;
static ID3D11DepthStencilState* g_pDepthStencilState = NULL;
static int g_VertexBufferSize = 5000, g_IndexBufferSize = 10000;

How can you support multiple completely separate windows? There is no law carved in stone saying there should only be at most one window at the same time? How are you going to use the D3D11 capabilities of your dedicated and integrated graphics card at the same time? There is neither a law carved in stone saying you should only use one graphics card.

🧙

3 hours ago, Mike2343 said:

I have no idea what scripting language you're using, so I cannot really help; but that seems like it needs a lot of items, refactor?

Lets assume a massive script (I am not saying that is the way to go, but it should be possible to write): You want to rotate a camera transform based on the mouse state, translate the camera based on the keyboard state, change the aspect ratio of the camera based on the display resolution, and change a texture of a mesh attached to a camera (I was running out of inspiration :P).

Or even simpler, you want to just output the mouse, keyboard, display state, etc.

🧙

33 minutes ago, matt77hias said:

a single method of a derived class MyDerivedScript::Update(double delta) overriden from a MyBaseScript::Update(double delta) contained in a child class of MyComponent

One answer is: don't write code like that. You've deliberately taken some concrete game feature (whatever it is that MyDerivedScript does) and hidden it behind a very limited interface that restricts it's ability to communicate, and then asked how to solve that artificial communication restriction. Globals are the ultimate solution to any kind of restricted communication between components, so they look like the only answer once you've walled yourself off like this... but you can also just not wall yourself off like that in the first place.

Inheritance is designed for situations where you can apply the LSP rule and write algorithms that are applicable to any object that implements the interface. In your situation, I doubt that there are any algorithms that are applicable to all your different derived types (i.e. your scripts do not just consume deltaTime as their only input and have zero outputs) so this is a violation of OO's concept of inheritance. Again, like globals, this style of code is common, but technically wrong.

33 minutes ago, matt77hias said:

Lets assume you want to change a material texture with a fixed color equal to a given inputted RGB, how are you going to create the texture?

As above, the actual problem is the use of inheritance in the first place -- without the incorrect interface, you would just pass the appropriate inputs to the function and have it do the appropriate work... but if you do stick with that style, you'd have to work around the arbitrary interface restriction it by passing the GPU-device / texture-factory / etc into MyDerivedScript's constructor and have it keep it's own copy of the pointers to those systems that it interacts with, or use globals, or a context-object/god-object, etc. Without inheritance, it can just have it's own specific interface that's honest about its real inputs/outputs.

33 minutes ago, matt77hias said:

the gif image, you show, is just one window according to the Windows terminology

A few seconds in, they click the [Create] button and a second Windows window is created, which they they dock back into the original window (destroying the 2nd one). There are multiple windows for parts of it.

33 minutes ago, matt77hias said:

How can you support multiple completely separate windows?

Do you mean the specifics of the code, or the use-case for the user? Code-wise, you make one D3D swap-chain per window, or in GL you make one GL-context per window and use shared FBO's to copy data from your main context to your "secondary" contexts. Also, none of those variables should be globals.
Use-case wise, in an editor/tool, it's nice to be able to drag in-engine-windows out of the OS-window and spawn them into a new OS-window (like in the GIF). You can also use it to do multi-monitor support -- e.g. lots of flight-sim / racing fans will have three monitors and like to run games across all of them. Some drivers let the user set up their three monitors to appear as one and your game doesn't need to do anything, but to be nice, you can support creating a window on each monitor yourself and then rendering the game across all of them.

16 minutes ago, Hodgman said:

Inheritance is designed for situations where you can apply the LSP rule and write algorithms that are applicable to any object that implements the interface. In your situation, I doubt that there are any algorithms that are applicable to all your different derived types (i.e. your scripts do not just consume deltaTime as their only input and have zero outputs) so this is a violation of OO's concept of inheritance. Again, like globals, this style of code is common, but technically wrong.

This has nothing to do with Liskov, and definitely not with violating Liskov. Multiple languages have something like a Runnable (concretely referring to Java here) and again this has nothing to do with Liskov. Your derived class may add all the member variables it wants that are needed for the "Update" or "Run" method implementation.

And if you want to have the concrete application of Liskov:

void MyScript::Update(double)

Precondition: All systems (start enumerating) are in a valid state.

Input argument types; double

Exception specification: all possible exception since I do not declare noexcept, C++ isn't so strict as Java anyway

Output argument types: void

Postcondition: All systems (start enumerating) are in a valid state.

 

void MyDerivedScript::Update(double)

Precondition: All systems (start enumerating) are in a valid state. (the same or weaker, thus ok)

Input argument types; double (the same or weaker, thus ok)

Exception specification: all possible exception since I do not declare noexcept, C++ isn't so strict as Java anyway (the same or weaker, thus ok)

Output argument types: void (the same or stronger, thus ok)

Postcondition: All systems (start enumerating) are in a valid state. (the same or stronger, thus ok)

🧙

21 minutes ago, Hodgman said:

A few seconds in, they click the [Create] button and a second Windows window is created, which they they dock back into the original window (destroying the 2nd one). There are multiple windows for parts of it.

static HWND  g_hWnd = 0; as global and as singleton as can be

🧙

10 minutes ago, matt77hias said:

This has nothing to do with Liskov, and definitely not with violating Liskov. Multiple languages have something like a Runnable (concretely referring to Java here) and again this has nothing to do with Liskov. Your derived class may add all the member variables it wants that are needed for the "Update" or "Run" method implementation.

The LSP-violation for me is that the sub-types are changing what the actual task performed by the algorithm is. Your algorithm is "for each bit of code, execute the code"... which is a non-algorithm.

Regardless, sure, let's say it's a std::function or a callback or a runnable, etc, to avoid OO terminology. The problem with "for each bit of code, execute the code" is that you are throwing out most of the software engineering toolbox. Most bits of code do have different inputs and outputs, and if you deliberately choose to obfuscate their inputs/outputs by forcing them to all conform to the same interface, then you don't get to complain when their inputs/outputs are obfuscated...

I mean, why do this pattern just for parts of the gameplay code? Why not rewrite all of the code in the entire project so that the signature of every function has to be void(double) and all communication is done via global state?

4 minutes ago, matt77hias said:

static HWND  g_hWnd = 0; as global and as singleton as can be

I don't understand what you're trying to say. If you want to support multiple windows, you definitely shouldn't use a global/singleton there... Even if you only need one window, it still probably shouldn't be a global.

This topic is closed to new replies.

Advertisement