I like global events for this type of thing, especially in Unity. I often have a UI scene I load additively on top of other scenes and the buttons are all hooked up to global events. Things in other scenes are listeners in these events and it all works well without objects in the UI scene having to know about objects in the game scene or vice versa.
The first thing I do is define structs (in C# this is nice since they're value types, they don't make garbage) to define the events, I'll have structs like EVUIStartButtonPressed or EVGamePlayerDied. These structs not only define the presence of a type of event, but also all of the information about the event. So EVGamePlayerDied will have at the minimum a reference to the player so any code listening to the event can tell which player died, or query the player object for information or something.
Then I have a template class that just looks something like this. Again, this is C# and Unity, but it's a generic concept that can extend to any language or framework.
public static class Event<T> where T : struct
{
private static event Action<T> ev;
public static void Add(Action<T> action)
{
ev += action;
}
public static void Remove(Action<T> action)
{
ev -= action;
}
public static void Invoke(T evData)
{
ev?.Invoke(evData);
}
}
This is a templated static class, which means Event for each event struct type is a different type, and that any of those types can be referred to by any object in any scene. This allows your UI code to call Event<EVUIGameStart>.Invoke(x) and your GameManger to call Event<EVUIGameStart>.Add(GameStart). They don't have to know about each other, they just have to know about this common, generic event. The only downside to this is that you need tiny scripts everywhere that just kind of hook things up to events. It's not much of a problem, but it's annoying.