Trying to build system without hard coupling

I’m building a simple strategy game and stumble on multiple problems with communication between components. So i have “Unit” object, “Camera” object and UI. The idea is that when you click on Unit, camera moves to its position and UI displays some text. My approach:

public class Unit : MB
{
    public event Action OnSelected;
    public void Select()
    {
        OnSelected?.Invoke(this);
    }
}

Unit invokes delegate and passes self, so camera and UI can use its public fields

public class Camera : MB
{
    private void OnEnable()
    {
        foreach (Unit unit in FindObjectsOfType<Unit>())
            unit.OnSelected += MoveTo;
    }

    public void MoveTo(Unit unit) { // some code }
}

First problem comes from Camera. In order to subscribe i must search entire scene for all Units and then subscribe. But i can’t subscribe for new ones. In order to battle this i must implement some sort of spawner mechanics. But my code gets convoluted quickly and it still uses FindObjectsOfType<>. Something like this:

private void SubscribeToAllSpawners()
    foreach (Spawner spawner in FindObjectsOfType<Spawner>())
        spawner.OnSpawned += SubscribeToUnit;

private void SubscriptToUnit(Unit unit)
    unit.OnSelected += MoveTo;

And i need two more methods to unsubscribe. Feels like too much for problem like this, and i need to write almost exactly the same code for UI component. Is there some sort of pattern that i don’t know of?

Second problem is that i want to reuse “MoveTo” camera method and pass Vector3 as its parameter. I can subscribe using lambda expression:

unit.OnSelect += (unit) => MoveTo(unit.transform.position);

but a don’t know how to unsubscribe. Please help

Regarding the second problem, you need a reference to the method you want to remove from the event. You’re creating a new method using the lambda expression that wraps around your MoveTo method. It either needs to be stored in a variable for later use or you should just make an ordinary method that will contain a call to MoveTo. I would make a method called OnUnitSelected or something like that.

Regarding the first problem, I can offer two solutions.

Simple solution

Make a static version of OnSelected event for the units. That could be used to catch the moment when any of the active units are selected.

Reference: static modifier - C# Reference | Microsoft Learn

public class Unit : MonoBehaviour
{
    public static event Action<Unit> AnyInstanceSelected;

    public void Select()
    {
        AnyInstanceSelected?.Invoke(this);
    }
}

public class CameraController : MonoBehaviour
{
    private void OnEnable()
    {
        Unit.AnyInstanceSelected += OnUnitSelected;
    }

    private void OnDisable()
    {
        Unit.AnyInstanceSelected -= OnUnitSelected;
    }

    private void OnUnitSelected(Unit unit)
    {
        // ...
    }
}

Complex solution

Make a container that units will add themselves to upon instantiation. This container then could be passed around to whoever needs it.

Reference: Unity - Manual: ScriptableObject

[CreateAssetMenu(
    fileName = "New Unit Container",
    menuName = "Unit Container")]
public class UnitContainer : ScriptableObject
{
    public event Action<Unit> UnitAdded;
    public event Action<Unit> UnitRemoved;

    public ReadOnlyCollection<Unit> Units => units.AsReadOnly();
    private List<Unit> units = new List<Unit>();

    public void Add(Unit unit)
    {
        units.Add(unit);
        UnitAdded?.Invoke(unit);
    }

    public void Remove(Unit unit)
    {
        units.Remove(unit);
        UnitRemoved?.Invoke(unit);
    }
}

public class Unit : MonoBehaviour
{
    public event Action<Unit> Selected;
    public UnitContainer container;

    public void Select()
    {
        Selected?.Invoke(this);
    }

    private void OnEnable()
    {
        if (container != null)
        {
            container.Add(this);
        }
    }

    private void OnDisable()
    {
        if (container != null)
        {
            container.Remove(this);
        }
    }
}

public class CameraController : MonoBehaviour
{
    [SerializeField]
    private UnitContainer unitContainer = null;

    private void OnEnable()
    {
        foreach (Unit unit in unitContainer.Units)
        {
            unit.Selected += OnUnitSelected;
        }
        unitContainer.UnitAdded += OnUnitAdded;
        unitContainer.UnitRemoved += OnUnitRemoved;
    }

    private void OnDisable()
    {
        foreach (Unit unit in unitContainer.Units)
        {
            unit.Selected -= OnUnitSelected;
        }
        unitContainer.UnitAdded -= OnUnitAdded;
        unitContainer.UnitRemoved -= OnUnitRemoved;
    }

    private void OnUnitSelected(Unit unit)
    {
        // ...
    }

    private void OnUnitAdded(Unit unit)
    {
        unit.Selected += OnUnitSelected;
    }

    private void OnUnitRemoved(Unit unit)
    {
        unit.Selected -= OnUnitSelected;
    }
}

answer to problem 2 is: unit.OnSelect -= yourFunc;.

You may have to assign your lambda to some function i haven’t tred unassigning a lambda.