2
\$\begingroup\$

I'm trying to determine the best architectural approach for handling communication in Unity, specifically when the interaction is strictly one-to-one.

The Core Design Conflict

I have five crucial, scene-persistent services (e.g., AudioManager, SaveManager) that need to be accessed by nearly every other object in the scene.

My current approach is to implement these services as Singletons to allow for direct access (e.g., AudioManager.Instance.PlaySfx()).

Moreover, I have a (not singleton) gameobject (let's call it X) which has just a 1-to-1 communication with another (not singleton) gameobject (let's call it Y). In this case, I just assign a reference from the inspector. Y would be the only receiver of the event, so why not just doing something like yObject.foo(); inside X class?

However, some developers heavily advocate for using a decoupled Event System even for single-listener scenarios (e.g., raising an OnPlaySfx event that only the AudioManager subscribes to).

My position is that using events for one-to-one communication is unnecessary architectural overkill. Why introduce the complexity of an event system when a simple, direct method call on a Singleton (or gameobject in general) is cleaner and more explicit?

Here's the ScriptableObject I use to create events (this was given me by my professor, saying that he found it from a Unity book):

[CreateAssetMenu(menuName = "Events/Void Event Channel")] public class VoidEventChannelSO : ScriptableObject { public UnityAction OnEventRaised; public void RaiseEvent() { if (OnEventRaised != null) OnEventRaised.Invoke(); else { Debug.LogWarning("Void event has been raised but there is no UnityAction associated."); } } } 

If X has to raise an event E to Y and Z, then X, Y, Z must have a reference to E. For instance, X must do myEvent.RaiseEvent();.

Or if you don't want to assign the event from the inspector, you must use Resources.Load(...); to load the event (because it's an asset).

The Inevitable Exception

I recognize that events become necessary when direct referencing is impossible. For example:

  • Prefab Communication: A script on a reusable Prefab X needs to send data to a static, scene-based GameObject Y (which isn't a prefab).
  • The Problem: Since Prefab X cannot hold a hard reference to GameObject Y in the Inspector (as Y may not exist in every scene), events or global messaging become the only reliable way to connect them, short of using expensive methods like GameObject.FindObjectOfType().

The Question

Is my thinking correct that direct access (via Singletons or Inspector references) is superior for high-frequency, one-to-one communication with stable services, and that events should primarily be reserved for scenarios where:

  1. Multiple Listeners are required (one-to-many).
  2. Decoupling is mandatory because a direct reference cannot be established (e.g., between an instantiable Prefab and a scene-only object).
\$\endgroup\$
3
  • 1
    \$\begingroup\$ It sounds like you have a solution that makes sense to you and works for your use case. Why would it matter whether other developers agree? (Minor note: another way to solve the prefab problem is dependency injection, where the thing that spawns the prefab configures it with dependencies it needs to know about but couldn't prior to spawning). (Also: FindAnyObjectByType() is a faster alternative to FindObjectOfType() for cases where you need it) \$\endgroup\$ Commented 2 days ago
  • 1
    \$\begingroup\$ Looking up once for your direct referencing problem is not expensive. You should not do a lookup for the same thing on each update, but nothing prevents you to do so once and keep that reference. \$\endgroup\$ Commented 2 days ago
  • \$\begingroup\$ @DMGregory I'm in no way stating that this is the case for OP here, but "it works for your use case" is very relative to developer skill level - I would not trust a junior developer to assess the big picture (performance, readability, maintainability, whether there's a better alternative available), I wouldn't even trust every senior developer to make those decisions in isolation - the pull request system exists for a reason. I agree with your underlying spirit of "try it and learn" but that doesn't make it wrong for OP to pre-emptively be interested in having their approach assessed. \$\endgroup\$ Commented yesterday

2 Answers 2

10
\$\begingroup\$

This question is on the border of "primarily opinion-based". There are no right or wrong solutions to software architecture problems. Only solutions that work for you or solutions that don't work for you.

However, I would like to point out two advantages of events over direct references:

  1. Single-listener scenarios might not stay single-listener scenarios forever. Already having an event allows you to add additional listeners with very little effort.
  2. Having events allows you to have two objects communicate with each other without having a direct dependency on each other on the sourcecode level. This is particularly useful when you use assembly definitions to speed up compilation times. The fewer dependencies you have between classes, the more you can benefit from having lots of small asmdef's.
\$\endgroup\$
2
  • \$\begingroup\$ i don't agree with your intro: there are objectively bad arhitectures. writing the whole game as a single script might work, but it would be a nightmare to implement and maintain \$\endgroup\$ Commented 16 hours ago
  • 1
    \$\begingroup\$ @ShivanDragon Depends on the game. \$\endgroup\$ Commented 5 hours ago
2
\$\begingroup\$

I've found this is more or less the entire point of design principles.

When you learn design principles in schools or otherwise serious courses, you get taught to always use them no matter what. This is a useful lesson for someone learning these principles exist in the first place, and I'm pretty sure the reason they are taught like this is to avoid having half the codebase using them and the other half not, which would definitely be a no-no (and is a thing that i've seen happen multiple times!).

A more pragmatic programmer that knows what they're doing should be able to say "this is unnecessary complexity" like you did and forgo applying a design principle where it makes sense. This is a valid opinion to have, but it's still possible it's not correct, because being a pragmatic programmer and being a skilled programmer are not the same thing. For example, it's entirely possible your next update to your project involves doing something that would be better served by event-driven development where you previously decided this was unnecessary, and you'll be paying for that in refactoring time. It's also possible this will never be the case.

There's also a point to be made about "Why introduce the complexity of an event system?". The fact of the matter is, you already have done that when you used this event system where you knew it would be useful, and so the complexity is already increased. By choosing not to unify the communication model across all objects in your project, you're actually introducing more complexity because there are now 2 ways objects can communicate and it's not necessarly instantly clear which one an object uses. In the scenario you're describing, I'd personally choose to use the event system even in the trivial cases you describe. I think you'll find that this is really not that more complex, and you'll build the habit that "this is how communication happens" across your project.

ultimately, this is a subjective decision and there is no right answer in general. Considering you're doing this in the context of a course with a professor, the professor will probably expect you to use the event system regardless of if it's a good idea or not.

P.S. the point about "high-frequency" communication doesn't really hold water, because the compiler will be able to optimize away a lot of the event infrastructure where it makes sense and raising an event will be just as fast as calling a method on a singleton. As you learn more about programming in general, you'll learn that basically nothing you write ends up being what actually happens physically on the CPU. Most of the performance optimization tips you will learn are actually just ways of structuring your code in a way the compiler understands better, and where better programmers than both of us have written really efficient code paths.

New contributor
Themoonisacheese is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
\$\endgroup\$
3
  • 1
    \$\begingroup\$ The point about compiler optimization is interesting, but I'm curious whether it applies in the case of the C# scripts we write in Unity (since that's the context for this question). I haven't seen anything about the C# compiler being able to optimize-away listener list traversal in cases where it detects there will be only one listener, but I'd love to be enlightened if that's the case! Do you (or does anyone) have a link where I could read more about that? \$\endgroup\$ Commented 10 hours ago
  • \$\begingroup\$ @DMGregory The c# compiler (and by extension the whole .NET virtual machine stack, what exact part performs this type of optimization is unclear) should (in most cases at least) be able to reduce lists of callbacks to tail calls that are permitted to clobber the stack and have no return value, including in cases where there is only one such callback, but even moreso when there are multiple. this is a standard optimization in all .NET languages so i'd wager this is performed by the VM. \$\endgroup\$ Commented 9 hours ago
  • \$\begingroup\$ @DMGregory effectively, since listeners are systematically unconditional branches, the low level .NET asm of firing the event looks like list of calls to each callback, which may be in a special memory region or may even be inlined if the number of callbacks is small enough. \$\endgroup\$ Commented 9 hours ago

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.