- Home /
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
Answer by Quickz · Apr 12, 2020 at 12:04 AM
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: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/static
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: https://docs.unity3d.com/Manual/class-ScriptableObject.html
[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 by kirbygc00 · Apr 12, 2020 at 12:58 AM
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.