Try organizing your code around the data your components have to work with and let your code react to changes to that data. Using Scriptable-objects is a great way to facilitate this model, because with them your game components will reference data assets instead of other components, reducing dependencies, which gets harder and harder to manage as the complexity of your application grows.
I can really recommend watching Ryan Hipple's presentation titled "Game Architecture with Scriptable Objects". [0]
At our studio we've built a framework based on these ideas for a high-complexity game project and we're very happy with it so far. We have a clean and well-performing codebase that's easy to maintain and test, because no sub-system has any hard dependency on any other sub-system.
This was useful.
The tl;dr for anyone who can't spend an hour watching (why are so many Unity things done as videos?!?! so infuriating!) is that you use "Scriptable Objects" (new to me - this is a first-class thing in Unity) to store common state and so break statically defined dependencies between Game Objects by having them share the Scriptable Object rather than reference each other.
So you might have a Scriptable Object that contains the player's score/coins picked-up/zombies killed etc. On its own this is conceptually basically just a number/several values/etc wrapped in a small singleton class. You then reference that Scriptable Object in your player game object/script that will mutate that value. Anything else that needs to know the score/coins picked-up/zombies killed (e.g. UI code) references the same Scriptable Object.
So now two separate parts of the game share the state without being statically bound to each other (they are only bound to the Scriptable Object). Plug in as many or as few new components all using the same state stored in the same Scriptable Object and nothing else needs to change as they only share the Scriptable Object. No references to other game objects required.
You can do the same for events - create a Scriptable Object for an event (e.g. PlayerHurt or PlayerShoot etc). Since Scriptable Objects can contain code like any other class, you can have GameObjects register their interest in this shared singleton event by calling "PlayerHurt.RegisterListener(this)" etc, and then have another GameObject trigger/raise/activate the event ScriptableObject ("PlayerHurt.Trigger()" etc). Neither side know about each other, so you can plug in as many or as few listeners as you want. The talk uses some extra cleverness to use a UnityEvent to allow for a function call in reaction to the event being triggered - this means a lot can be done directly in the unity editor, but that seems separate from the main secret-sauce of the scriptable object as the mechanism for shared state.
Site here that basically repeats the video nearly verbatim: https://unity.com/how-to/architect-game-code-scriptable-obje...
Cheers!
Don't be too worried about mediators though. Games are a little bit different in that you typically want to act on a shared world. If you have systems that act globally on the world then you're just matching the data that exists. Even the physics system itself is basically a singleton that's mediating interaction.
This can get kind of tricky if you use a lot of procedural generation or prefabs where for some reason you can’t immediately set the reference to that other GameObject (for example, the player has no gun yet). In those cases you should try to set the reference programatically, such as when the Player picks up the gun. You might have a reference from the Player to the HUD, and when the player picks up the gun you set the Gun’s UI reference via some SetUiObject(GameObject ui)
I end up relying on asset-ized SOs to do a lot of this. When communication between any two nodes is mostly one directional, events and triggers work great. (Ryan Hipple has a great talk on this, just search for "Ryan Hipple ScriptableObject".
One thing that I am struggling with though is settling on good ideas for general architecture for structuring the logic, and then doing things like communicating between entities/objects in a scalable way. Not everything that needs to communicate exists in a nice parent-child relationship - occasionally you need to communicate "across branches" in a hierarchical tree when there is no other interactions such as collisions etc.
You can get a long way with just cobbling stuff together, but eventually you get into growing pains where it becomes a mess.
The "best" solution I have so far is awkward "god"/"manager" objects that act as glue to let everything access everything. There has to be a better way?
The nice with with Unity though is that the logic is in .NET IL so it is trivial to see how people are doing things and learn.