#Decoupling game logic from my component logic

1 messages · Page 1 of 1 (latest)

devout tinsel
#

Alright sat down to write a bit of a story:
cc: @sick hill

Objectives:

  • Reduce/Eliminate Singletons
  • Increase Modularity
  • Components independent of systems of the game

Objective: Reduce/Eliminate Singletons

" I want to reduce reliance on singletons because they create hidden dependencies, making it hard to reuse or test components. When a script depends on a singleton like GameManager.Instance, it implicitly requires a specific runtime environment, which reduces flexibility and makes unit testing difficult without mocking or replicating the full game context."

Objective: Modularity

"One of my main priorities is modularity. I aim to design components and game objects that are self-contained and can function independently of the scene or game context they’re in. Ideally, any object should be reusable in other scenes or projects without needing global state, specific scene setups, or singletons to function properly."

Objective: GameObject & Component Independence

Specifically, I'm running into an issue where I want objects to be independent of each other, but they end up implicitly depending on shared systems like the score system.

For example, collecting a power-up might increase a score multiplier. This creates a direct dependency between the power-up object and something that manages score, whether it's a ScoreManager, singleton, or another global system. That means the power-up can no longer function in isolation—it assumes a score system must exist.

This breaks modularity. I want the power-up to work even in contexts where no score system is present.

For example, in a prototype scene, a tutorial level, or a completely different game mode. Its behavior (e.g. applying a multiplier) should be decoupled from the specific implementation or even the existence of a scoring system

slim roost
#

Establish a clear hierarchy of data access/source of truth. If you have managers and singletons, the should only relay the actual objects data to whoever asks, not have a copy of the state themselves, unless they are the only source of truth for that state.

But it's really hard to provide a good advice outside of the context of your game, or at least a specific feature. So maybe provide an example/real life scenario that you're worried about.

devout tinsel
#

My original idea was a global game state machine but I changed my mind today because I think that would basically lock me into whatever flow I decide, also it wouldn't really help with dependencies

devout tinsel
sick hill
# devout tinsel I think one of the main issues of my current design is that dependencies are hid...

If you have examples to show, it'd be miles easier to suggest stuff. Otherwise we just list the same old theory concepts that might not mean anything to you. We can only point out problems here that you are telling us exist.
Like yea a bunch of singletons fighting each other should never exist. One big class or having a manager per scene doesnt mean much to us. What are these managers for? What is this big class?

#

I dont expect you to paste your full code but seeing anything, like what one manager does and why you need one per scene, would at least let us suggest if you should continue like this.

devout tinsel
devout tinsel
#

right now, everything is pretty dependent on these managers

#

Game Manager just has methods to end the game, start it, or quit. it doesn't really do anything

sick hill
devout tinsel
#

And those dependencies are invisible in the editor, which is why im thinking to requiring them to be dragged and dropped into the scripts in the scene and have a game object with all required components

#

I was thinking of making a component called Attibute which holds some value, and then inheriting from it

#

A common pattern in this code is a manager which essentially holds a state object, and provides an API to manipulate that state.

devout tinsel
#

I'm kind of thinking of each scene as a state in the game-flow.

Main Menu -> Game -> Game Over

devout tinsel
slim roost
#

At this point, it's not clear to me what problem you're trying to address. Is the issue not being able to see/control dependencies inside the editor/inspector? Or something else? You keep on jumping between different topics.

sick hill
#

Yea i dont fully see what the topic here is either. The title says decoupling game logic and it seemed like you wanted to get away from singletons.

sick hill
#

I think you should really move away from the high level abstracted descriptions. The descriptions you've given arent possible to respond to imo. Just show what your code actually does and what problem you're trying to avoid

devout tinsel
devout tinsel
# slim roost At this point, it's not clear to me what problem you're trying to address. Is th...

Alright sat down to write a bit of a story:
cc: @sick hill

Objectives:

  • Reduce/Eliminate Singletons
  • Increase Modularity
  • Components independent of systems of the game

Objective: Reduce/Eliminate Singletons

" I want to reduce reliance on singletons because they create hidden dependencies, making it hard to reuse or test components. When a script depends on a singleton like GameManager.Instance, it implicitly requires a specific runtime environment, which reduces flexibility and makes unit testing difficult without mocking or replicating the full game context."

Objective: Modularity

"One of my main priorities is modularity. I aim to design components and game objects that are self-contained and can function independently of the scene or game context they’re in. Ideally, any object should be reusable in other scenes or projects without needing global state, specific scene setups, or singletons to function properly."

Objective: GameObject & Component Independence

Specifically, I'm running into an issue where I want objects to be independent of each other, but they end up implicitly depending on shared systems like the score system.

For example, collecting a power-up might increase a score multiplier. This creates a direct dependency between the power-up object and something that manages score, whether it's a ScoreManager, singleton, or another global system. That means the power-up can no longer function in isolation—it assumes a score system must exist.

This breaks modularity. I want the power-up to work even in contexts where no score system is present.

For example, in a prototype scene, a tutorial level, or a completely different game mode. Its behavior (e.g. applying a multiplier) should be decoupled from the specific implementation or even the existence of a scoring system

#

Hopefully this clears up my goals.

#

My Idea:

  • Use observer components which are scene-specific and event-driven so that the observed component is not coupled to it.

These components would not be reusable, but I can remove them safely in a test environment without the game exploding.

devout tinsel
blazing fog
#

Sounds like injecting dependencies and data would help with most of your goals. Splitting things too much brings its own complexity. Care more about the systems you actually have reused.

devout tinsel
devout tinsel
# blazing fog Sounds like injecting dependencies and data would help with most of your goals. ...

https://youtu.be/raQ3iHhE_Kk?t=968
This is pretty much the exact situation I'm in. (Timestamped)

Scriptable Objects are an immensely powerful yet often underutilized feature of Unity. Learn how to get the most out of this versatile data structure and build more extensible systems and data patterns. In this talk, Schell Games shares specific examples of how they have used the Scriptable Object for everything from a hierarchical state machine...

▶ Play video
#

I literally have to rebuild almost the entire game scene every time I want to test something.

#

I did try watching this specific talk a bit more to understand how exactly this architecture fixes the issue, and I did try implementing it. However I don't think I really understand how ScriptableObjects work enough to replicate his success with them.

He has an example of a scriptableobject variable, "Health"
On his player, he has a health component which references that SO. He has another UI element which references it.

But I don't understand how that would work for more than one instance of that Variable SO. Obviously, I don't want to create a new SO asset in the file browser for every instance of the same enemy. Maybe I'm just missing something with this specific implementation.

blazing fog
#

Try to make those dependencies optional. You can add separate initialization paths with data/state only for testing behaviour or separate the behaviour completely into something that purely works on data and have different components wrapping the behaviour based on which context it's being used in.

sick hill
#

I see a lot of people link to that same talk and ive always held the same opinion that its just an overuse of scriptable objects. I see no valid reason for why people try to use it as "event channels"

devout tinsel
#

I like the idea but I don't understand the execution at all. I've only seen praise for this architecture, though. It also sounds like exactly what I want, at least from the problems he described that this fixes.

#

Also, aren't SO's basically static? I again don't see how you would be able to have multiple instances of these variables exist

sick hill
# devout tinsel Alright sat down to write a bit of a story: cc: <@119616167894712320> Objectiv...

As for this, really seems AI written more than you writing it. Are you really unit testing/mocking in the first place where this is an issue?
And this does fall into the exact same "high level abstraction" i was telling you to avoid in the first place. Like when you (or ai) says "I have a power up that relies on a singleton, and I dont want that" well obviously the solution is to not rely on the singleton. There isnt anything more we can say about how to provide the reference otherwise since we dont know what this score tracker is.

sick hill
exotic folio
#

Also, you need to consider the following:

  • When you have a dependency on something, even if you hide behind an interface you still are dependent on it.
  • Reducing direct dependency increase complexity.
  • Events Spaghetti is a thing.
lofty flower
# devout tinsel I like the idea but I don't understand the execution at all. I've only seen prai...

FWIW, my impression is that he developed that pattern in a very specific context and it doesn't make sense for most teams, and especially not a one person project. I've heard only bad stories from folks who actually tried it at scale.

More generally, programming is all about tradeoffs and your goals will sometimes conflict with each other. Simplicity and clear dependencies tend to be directly at odds with modularity. There are patterns like Dependency Injection which try to 'have everything' but IMO the cost is very high and only worth paying if you are working in a context where people need to work within a scope they don't have time to actually understand.

For example, collecting a power-up might increase a score multiplier. This creates a direct dependency between the power-up object and something that manages score, whether it's a ScoreManager, singleton, or another global system. That means the power-up can no longer function in isolation—it assumes a score system must exist.

This breaks modularity. I want the power-up to work even in contexts where no score system is present.
You should build your game so that it functions as the game that it is, not every possible permutation of every possible piece of it. If what the powerup object does is related to score, then it does have a dependency on the score system and that's ok, because dependencies are not inherently bad. I would instead focus on making it easy to add/remove your systems so that a test environment would just be 'a world with the correct systems' in it.

If you haven't looked into ECS much, you might find some of those ideas helpful.

sick hill
#

now AI suddenly is saying they want to unit test/mock and have components work entirely without dependency. It was already tough to suggest anything because they were only using high level terminology, which I suggested against so we could have actual examples to critique. Which in response they used AI to only say more high level terminology they probably dont understand 🤷‍♂️

slim roost
# devout tinsel Alright sat down to write a bit of a story: cc: <@119616167894712320> Objectiv...

Basically, what most people said already:

  • There's nothing bad in singletons, as long as they are not used as a shortcut for every single reference in the game. They're an important and useful pattern in games.
  • Modularity can be achieved to a degree within a context of certain systems or features. Generic Modularity for the sake of Modularity is meaningless and you're gonna spend all your time on designing, implementing and debugging it instead of your game.
  • to make gameobjects/components independent, write them in such a way that they can function without relying on other objects/components. That might mean crippling their original function and design. Provide callbacks for code paths that rely heavily on dependencies.
#

Instead of trying to design an overly complicated architecture, just be mindful of your use case when writing specific code.
Also, as has been said many times before, it's hard to give a general good solution outside the context of an existing problem, so it would be much better if you share some code, explain a problem with it and ask how to handle. The same solution might be applicable to other parts of your project and then you basically have solved all you issues with just one example case.

fickle pewter
# devout tinsel I literally have to rebuild almost the entire game scene every time I want to te...

The main thing you should be asking yourself then IMO is why your undesirable dependencies exist in the first place. Singletons aren't inherently bad, but they can enable other bad practices. If there is no guarantee for your Singletons to exist and be initialized how you need them to be, then you need to rethink whether your dependent code really needs to be dependent -- 99% of the time it really doesn't. If you use singletons you should just be more strict about what is and isn't allowed to be done with them.

#

E.g. an enemy script should not allowed to just increment the score on the score manager when it dies. It should instead just raise an event and if the ScoreManager exists and has bound to that event it will react accordingly and increment the score on its own.

#

That way even if you completely remove the ScoreManager script, the enemy doesn't care.

devout tinsel
#

Also been working on splitting up responsibilities much more via different components

#

Trying to make behavior more compositional

devout tinsel
#

the amount of things i have that directly/indirectly reference a singleton is definitely too much

#

I've been learning the mediator pattern and refactoring

#

Ive also had the idea of creating individual scenes for features which forces me to make them independent

#

since I can't just mindlessly drag dependencies or make things into singletons

#

so right now I'm working on a pickup that gives you a score multiplier, all thats in the scene is the pickup, the score manager and a camera

fickle pewter
#

But it sounds like you are still dependent on the scoremanager

#

or, does it gracefully handle the scoremanager not existing?

slim roost
#

Depends on how close to the core features of the game the scoring system is, but usually you'd want to avoid making such a hard dependency.
Maybe instead make the power up invoke events that the score system can subscribe to. You could path the type of powerup/or even just the power up itself as the event parameter and let the scoring system decide what to do with it. This way the power up doesn't need to know anything about the scoring system.

devout tinsel
#

its a "multiplier pickup" which changes your score multiplier

#

though i have all of those things split out into components

fickle pewter
#

You’re still dependent on the score manager though so you haven’t solved your core problem

#

What happens if the score manager doesn’t exist?

devout tinsel
#

it crashes

devout tinsel
#

I dont want the score manager to know about pickups

#
public class IncreaseScore : MonoBehaviour
{
  [SerializeField] AutoComponent<ScoreManager> _scoreManager;
  [SerializeField] int _scoreAmount = 10;
  [UsedImplicitly, Button("Test"), DisableInEditorMode]
  public void Trigger()
  {
    if (_scoreManager.Component.Mediator is null)
    {
      throw new InvalidOperationException("ScoreManager Mediator is not initialized.");
    }

    _scoreManager.Component.Mediator.Notify(new ScoreEvent(ScoreEventType.IncreaseScore,_scoreAmount));
  }
}
#

this is what the Increase Score component looks like

fickle pewter
#

Why throw an exception?

devout tinsel
#

because that should never happen

fickle pewter
#

Don’t you want it to work such that things can exist without each other?

fickle pewter
#

Which is counter to what you originally said the goals was

devout tinsel
#

thats kind of my issue, I don't know how to communicate with the scoremanager without either of them knowing about eachother

#

other than a scriptableobject event

#

which I could do

fickle pewter
#

Or just C# events

#

Which is what we suggested earlier

devout tinsel
#

If I do a C# event wont that require them to know about eachother?

fickle pewter
#

Either on the objects themselves or through an event bus (which is basically what the SO approach is)

devout tinsel
#

because someone has to subscribe

devout tinsel
fickle pewter
fickle pewter
#

It’s an easily anticipated problem

devout tinsel
#

So I already have a ScoreEvent

fickle pewter
#

I guess I’m more used to only throwing exceptions when I’m like “technically this COULD happen, but I never would expect it to given how the code is set up”

devout tinsel
#

That is how im using it, because I considered that there would never be a situation where there would be a score powerup but no score system

#

but I didn't think about the fact that I dont have an interface for the score system so its tied to this specific class

devout tinsel
#

distinction

#

not extinction

#

lol

#

Mediator should never be null as long as there is a score manager in the scene

#

but there is the technicality that if you called the method in Awake for some reason it would not be initialized yet

fickle pewter
#

How do you guarantee the ScoreManager exists in the scene?

#

Lazy instantiating?

devout tinsel
#

I dont .-.

#

I have a class for lazy instantiating but I am trying to not do that

#

How would I go about an event bus as you said?

#
  public struct ScoreEvent
  {
    public ScoreEvent(ScoreEventType type, int basePoints = 0)
    {
      Type = type;
      BasePoints = basePoints;
    }

    public readonly int BasePoints;
    public readonly ScoreEventType Type;
  }
  public enum ScoreEventType
  {
    IncreaseMultiplier,
    IncreaseScore
  }

I have these two things already for the mediator

#

so I think I can use them for some kind of event system

#

Appreciate the continued help, btw, @fickle pewter

#

IncreaseScore.cs

  public void Trigger() => EventBus<ScoreEvent>.Publish(new ScoreEvent(ScoreEventType.IncreaseScore, _scoreAmount));

ScoreManager.cs

void Awake()
{
  Mediator = new ScoreMediator(this, _multiplier);
  EventBus<ScoreEvent>.Subscribe(OnScoreEvent);
}

void OnScoreEvent(ScoreEvent e) => Mediator!.Notify(e);
#

So I sent a ScoreEvent to the event bus, the ScoreManager gets that message and then forwards it to the mediator and the mediator figures out what to do with it

#

I could make it so the mediator subscribes to the event when it is initialized but that just seems like more code with no real benefit

#

since I would need to make it disposable

fickle pewter
#

What is the mediator in all this even?

#

Typically the mediator and the manager are the same thing

devout tinsel
#
    public void Notify(ScoreEvent e)
    {
      _multiplierManager.OnScoreEvent(e);
      var multiplier = _multiplierManager.GetMultiplier();

      var finalPoints = e.BasePoints * multiplier;
      if (finalPoints != 0) _scoreManager.AddPoints(finalPoints);
    }
#

What's wrong with that? lol

fickle pewter
#

So the score manager has a mediator and the mediator calls on a multiplier manager?

#

It just feels convoluted

devout tinsel
#

multiplier manager is owned by the score mediator, it's not another component

devout tinsel
#

I'm still figuring out how to use this pattern

fickle pewter
#

Which pattern?

devout tinsel
#

Mediator pattern

fickle pewter
#

The basic idea of mediator pattern is that everything goes through the mediator instead of directly communicating

devout tinsel
#

I wanted to move the logic that calculates the new score outside of the scoremanager itself

fickle pewter
#

But then you’re kinda not any different than when you started out

#

Mediator pattern and singleton pattern usually go together

devout tinsel
#

but now there's an event bus

#

so now the mediator is kind of pointless

#

it was a singleton before

#
    void OnScoreEvent(ScoreEvent e)
    {
      switch (e.Type)
      {
        case ScoreEventType.IncreaseScore:
          AddPoints(e.Value);
          break;
        case ScoreEventType.ResetScore:
          _score = 0;
          break;
        case ScoreEventType.IncreaseMultiplier:
          _multiplier.Increment();
          break;
        case ScoreEventType.ResetMultiplier:
          _multiplier.Reset();
          break;
        default:
          Debug.LogWarning($"Unknown score event type: {e.Type}");
          break;
      }
    }
#

there, now the score manager directly owns the multiplier and does the logic

#

Alright. No longer crashes when there is no score manager lol

#

I might refactor it so that it gives me a warning if nobody hears the event though

#

I don't love the behavior of it just failing silently since I do expect something to hear the event

sick hill
#

And even the message from prakkus explains a similar concept

#

Truthfully this is all just a big layer of nothing now

fickle pewter
#

Decoupling allows the pickup behavior to be tested without the score manager

#

So you don’t need to add a score manager to test it and not crash the app

#

That’s the whole point, no? And it’s fine to log a warning to say that one is expected

#

But then in that case you could just have it do so without raising the event and just log that the singleton was expected but was not found, so skip the dependent behavior and do the rest

#

Either way the point is you shouldn’t need to add a bunch of systems to a scene just to test simple isolated features

sick hill
#

If you suddenly need to raise a warning that it doesnt exist, you've gone through hoops for absolutely nothing

devout tinsel
#

That is the default behavior of unity's event bus

#

SendMessage does exactly that

fickle pewter
#

That’s almost certainly not the only thing they do though

sick hill
fickle pewter
#

They probably provide feedback of some sort

#

Which cannot be tested in isolation with tight coupling

devout tinsel
#

yes i have a multiplier number text effect

fickle pewter
#

Yes one example

devout tinsel
#

i can't test this if there's no score manager

#

but i want to test the animation code

#

but now i can just raise a request and ask for the multiplier

#

and i dont care who tells me what the multiplier is

#

that also means i can make another system to mock the score manager

sick hill
#

And in what case would you ever actually need to mock the score manager?

sick hill
#

You can go all enterprise hell on this, you'll very quickly realize that youre fighting the core of how game design works

fickle pewter
#

I don’t at all agree

devout tinsel
#

I wouldn't unit test in this situation

sick hill
sick hill
devout tinsel
#

I can, I like having that option if I need to

fickle pewter
#

In any case the point should be to make it easier to develop

devout tinsel
#

in the future if I need to I would need to rewrite the entire game

fickle pewter
#

I can agree that you can easily get into a problem of too much future planning

devout tinsel
#

The refactor with the event bus overall reduced the lines of code

fickle pewter
#

So try not to do that. Refactor things as much as you need to to solve your current problem

devout tinsel
#

because I could strip all of the getting and null checking etc

#

It feels like an API now

#

Which is nice

sick hill
#

And yet your game is going to use this strictly with a scoremanager at the end of the day.
I feel like you skipped over the actual advice above where multiple people said to be careful proceeding down this path

devout tinsel
#

Well actually I do use this game as the base for other games

#

this game uses the same base game

#

but it completely changes the logic of the scoremanager

#

and it was hell to debug

#

I like the workflow, I just need to code for it

#

So I can actually re-use my code

sick hill
devout tinsel
#

I guess I will see

#

To me, this seems much easier and way cleaner to work with

#

Everything is very modular

sick hill
# devout tinsel Everything is very modular

The main point here is that its modular for nothing. By chance, do you do web dev? Because people love to throw around the words modular, scalable, etc when there's no purpose to it.
For this point, I'm not* specifically referring to your score manager but everything. Your game WILL be designed to work a certain way. Things rely on other things, you cannot avoid this. You can hide it but at the end, this doesnt change the fact that the code eventually did the same logic, the end user sees the exact same result. You arent building an API that needs to handle every type of use case

devout tinsel
#

Everything seems to be working fine too

#

which is great

devout tinsel
#

I've already benefitted from it once making the multiplier text

#

I could test how it looks in the game scene without it breaking

#

I know I can't make it so there are no dependencies, but I can make it easier to distinguish when I'm making a dependency so I'm aware of it

#

I do think the scoremanager API thing is a bit overkill but it taught me how to do that kind of thing with an event bus in the future

#

I wouldn't do that with every system just because its way too much for something very simple

sick hill
#

I'm not specifically talking about this scenario of swapping from a singleton to events, these concepts apply to everything. Be careful just overengineering everything with the hope it'll be reusable or solve cases that dont exist

devout tinsel
#

Be careful just overengineering everything with the hope it'll be reusable or solve cases that dont exist
Yeah I totally get that, I'm keeping in mind what systems I know I'm going to use in the future

#

A score system is kind of a bad example for this pattern since theyre usually very specific to the game

#

but I can see how I can apply this pattern later

sick hill
#

Yea i think you understand what i mean. though just curious, what do you plan to do if the powerup is collected but there is no score manager?

devout tinsel
sick hill
#

No like any powerup that affects this score manager, if there were a bug at runtime for example where the powerup was collected but the score manager doesnt exist or isnt listening to the events yet, what happens?

devout tinsel
#

Nothing

#

It would just noop

#

The bigger concern is what happens when I request the score or multiplier and there's no score manager

#

I actually don't know what happens then

#

Oh actually yeah i do

sick hill
#

Then thats something you'd want to consider testing for, if its possible to ever collect a resource before the score manager gets setup

devout tinsel
#

It just defaults to 0

sick hill
#

Because now instead of an error, its possible you wouldnt see if this happens. so the end user would have an actual bug where the score isnt calculated properly

devout tinsel
#

Hm. Well, I don't know how to test that because I don't know how it would happen

#

The score manager is initialized on awake, and its expected to already be in the scene before anything happens

sick hill
#

If its initialized before the powerups ever exist then yea thats fine.
This is also something to consider though if making future systems. Sometimes you as the developer should want the error instead of it silently doing the wrong thing

devout tinsel
#

Wouldn't be a concern with a powerup because the collision callback wouldn't happen until after awake anyway

devout tinsel
#

I guess I could copy how SendMessage works, with its enum

#

I think it's a little silly that this is an enum because it looks like a boolean but I guess they wanted to futureproof it

#

since the parameter is just called "SendMessageOptions"

sick hill
#

SendMessage is different given it uses reflection
Anyways its not really a concern since the score manager would exist before powerups do, and before the player can be on a powerup. I'm mainly just bringing it up to show how it might be awkward now to debug

#

You cant really check if there's a receiver, multiple things could be listening to the event

devout tinsel
#

I was just thinking of logging something if there are 0 listeners (which would mean the event is null(?))

#

Hm. Actually don't think I can do that with the current implementation

#

I think I'll wait until it bites me in the ass

devout tinsel
#

afaik sendmessage is pretty slow

#

I dont know if theyve optimized it at all

sick hill
#

I would expect they do but i dont think we can see that side of the implementation. unity does cache something when you use reflection so probably

devout tinsel
#

Honestly not really sure what the use of Send/Broadcast message is other than very quick and dirty events

#

or like a very dirty "event bus"

sick hill
#

they use reflection for a lot, like every unity message. you type in an animation event via string, so that uses reflection too

devout tinsel
#

Can't you cache animation events?

#

via hashes

#

or is that just parameters

sick hill
#

you're probably thinking of the animator state hash value

devout tinsel
#

ah yeah

sick hill
#

the animation event is just something you type in by string and itll call that method on any component on the same object

devout tinsel
#

Do serialized unityevents use reflection?

#

because I'm using those quite a bit

#

like I could technically just do SendMessage(Trigger) here and it would do the same thing

#

technically..

#

I guess it would have to use reflection, wouldn't it.

fickle pewter
#

I can definitely say one thing about decoupling (up to a certain point, because sometimes dependencies are actually useful) is that when working with a group it’s really nice when you can just subscribe to events to get the hook you need rather than needing to dig into someone else’s code and insert something

#

But maybe I am biased on that since I primarily do audio implementation, so I appreciate having decoupled code where I just get the entry point I need and then handle the rest independently

sick hill