1
\$\begingroup\$

I'm working on a relatively simple game, but even simple games have a lot of moving parts, and I'm running into some architecture issues. Just to be clear, everything works fine, but it feels dirty, and that's usually a sign of a problem.

So, I'm following SOLID principles, which, by my understanding means that I should have many scripts that each do a single thing versus large scripts that do many things. It makes things easier to maintain, but linking things that require references turns into a mess, for me at least.

To get more specific, my game uses a grid. Great, I've got a script that represents a grid and contains methods related to the grid (ex: converting from world to grid position).

I've got another script that instantiates the grid and manages things with the grid in a level, such as populating it. Then I've got a script that handles the merging logic for the items on the grid. (matching game logic)

Then, I've got a score manager that manages the game's score, and then I've got a UI manager that manages the UI.

The thing is that the merge script requires a hard reference to the grid to work, and the score manager requires a reference to the merge script, and the UI manager requires a reference to the score manager, and by the end of it, everything is referencing everything.

For the record, I'm trying to avoid inspector references, and instead referenced the main components using the Singleton pattern. But I don't like it.

Is there a better way?

The only thing I can think of is having a game manager script that references everything and then handles communication between all the other managers.

\$\endgroup\$
3
  • \$\begingroup\$ Try the KISS (Keep It Super Simple) design principle first. \$\endgroup\$ Commented Mar 14 at 5:32
  • \$\begingroup\$ You can as well use an event based approach. While that still requires you to know where the other managers or functions are located, it will allow you to actually implement the functionality later or exchange it. Your score manager would invoke an event newScore and does not care what else happens. Your UI manager would add a listener to that and each time it gets invoked, make changes to the UI. But maybe your autosave will trigger as well on score change. Or a sound manager. Or Some effect once you reach a highscore. \$\endgroup\$
    – Zibelas
    Commented Mar 14 at 15:44
  • \$\begingroup\$ It is always like that to some degree and only gets somewhat better with practice. I don't remember where I read it but the saying goes: You want to reuse a banana but you see that it is held by a gorilla. You decide to take the gorilla because you really need that banana. And only then you realize that the gorilla is holding the banana with one hand and the whole jungle with the other. \$\endgroup\$
    – Nikaas
    Commented Mar 17 at 6:15

3 Answers 3

0
\$\begingroup\$

It seems that you're generally following SOLID principles and utilizing the Singleton pattern, which is a step in the right direction. However, relying heavily on the Singleton pattern can often result in tangled code where components are overly interconnected, as in your case.

As with many architectural challenges, the appropriate solution depends on the specific context of your problem. In this scenario, employing a service locator pattern could offer a more elegant approach to managing dependencies.

Here's how a service locator pattern might be applied:

Registration: Essential services, such as the grid or score manager, would register themselves with the service locator, associating each with a unique key or identifier. This registration typically occurs during application startup or initialization.

Lookup: Clients of these services can then request an instance by specifying its corresponding key or type. The service locator responds by querying its registry and returning the requested service instance to the client.

Instantiation: In some cases, the service locator may need to handle the creation and lifecycle management of services. However, in your scenario, since essential services like the grid are likely already present in the scene, you may be able to skip this instantiation step.

Using the service locator, clients are effectively decoupled from the specific implementations of services. They can simply request services from the locator without needing to be aware of how those services are created or where they originate. This promotes cleaner, more modular code and simplifies the process of integrating and maintaining various components within your game architecture.

\$\endgroup\$
0
\$\begingroup\$

There are a couple ways you could approach this depending on your needs and viewpoints.

The first method would indeed be to use singleton patterns for each manager. This is the approach I personally use in my game with about 80+ scripts and I've never had a huge problem with it.

Benefits:

  • Easy to implement
  • Accessible by any script (no reference needed)

Drawbacks:

  • Can be confusing

The other method is to use some sort of "level asset", I also use these in my project however its slightly more complicated.

The idea is you have a Game Object, I usually call mine "Level", tagged with some specific tag like "LevelAsset". Then you create some kind of LevelAssetLoader script that can take all the references and place them into reference slots within the specific managers.

Benefits:

  • Gives a clear place to drag and drop scene references
  • Easy to swap/remove/change values

Drawbacks:

  • You would have duplicates (for example all the references in GameManager would also need to be in LevelAsset which may become confusing and unneccessary)
  • More references
  • Can be messy

Both have their pros and cons, so me and my team tend to use both, but it's up to how you like it. I think singletons are much easier, but its personal preference.

\$\endgroup\$
0
\$\begingroup\$

Ultimately, I find that giving classes meaningful names, and using "as many namespaces" as necessary to make sense of it all, is what keeps things sane. The harder a class is to name, the "busier" it probably is; that's where the (static) "one method per class" starts making more sense. I realized I had a "Geometry" namespace, if I just gathered all my "calculators" in one place. That was a reward in itself. (Now I can create a "geometry dll" if I need to).

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .