I’m having trouble deciding where within a couple different scripts I should place certain functions and could use some input about this specific example but also the principles of these designs. I don’t have the right language to make this an abstract question, so I’m going to make it as concrete as a I can.
In my game I want to be able to load a level, load NPCs into that level, and load the player character into that level. I am planning on having game_manager, level_manager, npc_manager, and player_manager singletons. The plan is that upon some level load trigger the game_manager would manage the process of fully loading the level with characters spawned at certain locations by sending instructions to the level_manager, npc_manager, and player_manager singletons.
My question is where among these various scripts I would want to put something like a “load_player_character” function. This function would rely on information about the player character but also information about the level (like where to load the character). Two competing ideas are having the player_manager script control loading the character (with a spawn location argument coming from some other node) or having the level_manager load the player character (possibly needing to interact with the player_manager).
More generally I’m trying to understand how you’re supposed to design the layout of systems like this. Maybe this difficulty is actually a symptom of the overall design being suboptimal.
If I get your question correctly you want to generalize the spawning function. From design perspective I usually ask myself about the usefulness of a class (incl. singletons) before I create one. For instance, why have multiple managers for each “entity” in the game, can’t you just have a general spawning call that can handle different prefabs?
Regardless of this, the logic won’t change much to generalize the spawning action.
The next step I try to always apply is to see if there is somewhere my function might come in handy again and if in that case there is anything hardcoded in the function that might be an issue. If so, then I see if it’s reasonable to inject the dependency or pass a parameter (i.e. pass the nodepath for your spawning location).
To give a concrete example, this is a signature I use in my project to spawn a node on the server:
In this case I generalize a prefab object, so I can just pass any UID and it can be loaded on the server. I also pass the location, being the spawning node path, and I pass some extra values I want to map on my replicated instance on the server (i.e. scale or any other property I want to map) since when instantiating on the server I am creating a new copy that is then replicated to the peers.
I hope this can help you and feel free to ask more - although there’re most likely better devs out there than me!
Thanks, I think that’s useful to see and what you wrote makes sense to me.
I guess another way to formulate the question is if I’m not going to have a monolithic game manager that stores all the information and functions needed to run the game, what are some general principles about where/how to put functions that require inputs from multiple different singletons? It’s less about making a function generalized and I think maybe more about minimizing spaghetti code. Once you have multiple singletons with important information in them you’re stuck grabbing info from multiple places and what’s the most principled way to do that? Should all my important functions live in a game_manager script that goes out to other singletons to get inputs or should each singleton have a couple functions that the game_manager script calls?
I rewrote this reply 4 times and realized i was just quoting SOLID principles one by one.
So basically godot uses a Node-based architecture combined with Class-based inheritance.
Singletons are… well, singletons, and the nodes are instances of classes, thus objects in the OOP sense.
Just as a starter, your singletons talking a lot to each other in an “A.f() calls B.f() that calls C.f()” fashion violates the Single Responsibility Principle, making it so calling A.f() also triggers stuff from B and from C. You can instead call B.f() and C.f() directly from the node that would otherwise use A, or if you do that a lot you can create a B_and_C class that aggregates stuff that uses a lot of both B and C, then it becomes a single point of entry.
Check out SOLID design principles for a very simplified intro
The design patterns catalog or the big old design patterns book have details on stuff you can do on your code to make it more neat and tidy, or more reusable, or more extensible, or just less painful to read through. There’s a lot of those code organization techniques out there so don’t worry about grasping them all, just pick a few that make sense to you and try to apply them on your codebase.
Here’s some artistic mind-fodder on the topic.
Hammers bash things, they do not fly. Birds fly, they do not bash. You want bashing birds? tie a hammer to a bunch of birds.Turns out, they are flying hammers as well! they can bash() AND fly().
I want my coffee machine to brew my coffee, not to order 6 tons of fertilizer for the company that runs the coffee farm! I can do that on my phone without the big red button on the side of the chassis.
When the knife slides on the tomato, does it ask for a knife manager to deliver tomato slices? no! does it carefully align itself and press downwards? No! It’s ME! i am the one slicing the tomato by hand_grab(left)-ing the Knife and zigzag(hand)-ing on the Z axis and push(hand)-ing on the Y axis!
Also, your player only needs coordinates, and absolutely nothing else to spawn in an already loaded, ready to render/rendering world.
Apply that philosophy to all your actors and assets that you thought needed a whole bunch of data. It’s tangential to SOLID principles, but I’m known for not following any development approach to the detriment of others, except, KISS (Keep It Stupidly Simple) and for games, does it run, is it fun.
Also, hi @dragonforge-dev , some would say naming classes with manager in the name is an anti pattern. Someone might be on shortly to say more on that topic
This is a classic Future-proofing, XY Problem using the Manager Anti-Pattern. You have decided that you may have some future problem that will be solved with manager singletons galore. You have then come here to ask us how to best implement this vision.
Props on being specific, because a generic question is rarely helpful to getting an answer. However, you have not described your problem. You have described the problem with the solution you have come up with for your original problem. It would be helpful if you described that problem, or more likely - anticipated problem. Then we can help you with that problem.
This is the Factory Pattern, and if you were to follow it @gyoll then you would want to be making factories, not managers.
@colelli if the first line of that function is not checking to see if you are the server, a player can hack it by passing packages to anyone in the game - including the server - and spawning whatever they want.
Also, just for fun, here’s what my Main scene looks like. No managers in sight.
I have a StateMachine that tracks what part of the game we are in. The whole level is loaded by Loading state. The Player (named Rick in this example) is passed by reference - and if nothing is passed, the default Player is created and loaded with the level.
There are also a ton of tiny (most less than 100 lines of code) singletons to handle all sorts of things that need to be tracked at a higher level. (None of them are named manager.)
@dragonforge-dev I am indeed checking that, I’ve omitted it for the sake of simplicity and also because I believe OP is not making an mp game so it was just an example taken from my project.
Personally I believe OP just likes the naming - I also personally use the name “Manager” for whatever is managing something in my game. For instance if I have a singleton that is taking care of my network then for me it’s a NetworkManager - simply because I like to call it that way.
I can agree that it is a bit counter intuitive if my “manager” is also a factory, but since I’m the only one who’s ever going to see this code, I’d rather group this kind of logic by “domain” than by “responsibility” (if it makes sense ahah)
Fair enough. That point was also for anyone coming along and reading this thread later and not knowing that.
Calling something a Manager causes you to think about the object as if it should have more responsibility than it needs. The Manager Anti-Pattern encourages tight coupling. The fact that you have an object managing anything in your game is the problem.
Just by changing the name from NetworkManager to Network can expose things that perhaps don’t belong in that class. Without code, it’s impossible to give concrete examples. Often, just changing the name can show that a lot of functionality in the Manager class should actually be delegated to the class it is “managing”.
For a detailed and concrete example of this, take a look at this thread here:
In which I explain at great length, with examples, the same thing and how one can refactor code to eliminate the need for the Manager Anti-Pattern.
It’s your code, so you do you. Remember though, that 95% of the time the person frustrated by your code will be future you. I am always making coding choices to make it easier for myself when I come back to a project after months or years. Because I got tired of trying to figure out what past me was thinking.
Thanks for all the replies. It looks like I have a lot to learn with regards to design patterns, composition vs inheritance, etc. I’ve been coding things in a way that is intuitive which isn’t necessarily a good strategy when less experienced. I’m going to try to come back to this problem after learning more.
The specific technical challenge isn’t unique at all, it’s just loading a level and then loading a character and placing it in the level after some kind of trigger to do so. My original question was probably messy because I had already worked myself into a corner with bad design.