How to react properly to component field changes?

How to react properly to component field changes?

Let's say, that we have the following components:
Direction, which contains direction the object moves:
public class Direction : MonoBehaviour
{
    [SerializeField]
    private float angle = 0.0f;

    public float Angle
    {
        get
        {
            return angle;
        }
        set
        {
            angle = value;
        }
    }
}

DirectionVisualizer, which visualizes this direction in form of a sphere:
[RequireComponent(typeof(Direction))]
public class DirectionVisualization : MonoBehaviour
{
    private const float Radius = 2.0f;
    private GameObject sphere;

    private void PositionSphere()
    {
        Direction direction = GetComponent();
        float angle = direction.Angle;

        Vector3 position = transform.position + new Vector3(
            Radius * Mathf.Sin(angle * Mathf.PI / 180.0f),
            0.0f,
            Radius * Mathf.Cos(angle * Mathf.PI / 180.0f));
        sphere.transform.position = position;
    }

    // Use this for initialization
    void Start () {
        sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
        sphere.transform.localScale = new Vector3(1.0f, 1.0f, 1.0f);

        PositionSphere();
    }

    // Update is called once per frame
    void Update () {

    }
}

The relation above represents simplified model I'm having in my game - a component is used to visualize a set of parameters contained in other component(s).
Now I'd like the sphere to be automatically positioned basing on the angle field in Direction component without explicitly notifying DirectionVisualization, like:
var direction = someObj.GetComponent();
direction.Angle = 25; // DirectionVisualization reacts automatically here

The simplest solution is to position sphere frame by frame, but I know, that Angle will change very infrequently and such solution would have dramatic impact on performance (in my game this visualization is a lot more complex).
I might do that by merging Direction and DirectionVisualization classes, such that I can handle Angle property setter, but that's a very ugly solution directly against SRR principle. So it's a no-go.
Other solution would be to create an event in Direction component and subscribe to it from within the DirectionVisualization component. But that raises following concerns:

If DirectionVisualization is destroyed, the event handler will keep it alive (I should probably detach the event handler when DirectionVisualization is destroyed?)
When DirectionVisualization is added to object, it should subscribe to Direction's event
What should happen if DirectionVisualization is added to object, which does not (yet) have Direction component? Is there a way to detect adding Direction component, such that DirectionVisualization might subscribe to the event?
What happens if Direction component is destroyed? How DirectionVisualization should behave? Is there way to detect it?

Is there a better way to handle notifications between components than one I proposed?

Solutions/Answers:

Answer 1:

I personally would go with the event sending, because it’s more explicit and easier to follow in code. Something like:

public delegate void DirectionChangedHandler(float direction);

public class Direction {
    [SerializeField]
    private float angle = 0.0f;

    public event DirectionChangedHandler DirectionChanged;

    public float Angle
    {
        get
        {
            return angle;
        }
        set
        {
            angle = value;
            DirectionChanged?.Invoke(value);
        }
    }
}

[RequireComponent(typeof(Direction))]
public class DirectionVisualization : MonoBehaviour
{
    Direction directionComponent;

    void OnEnable() {
        directionComponent = GetComponent<Direction>();
        directionComponent.DirectionChanged += OnDirectionChanged;
    }

    void OnDisable() {
        directionComponent.DirectionChanged -= OnDirectionChanged;
        directionComponent = null;
    }

    void OnDirectionChanged(float direction) {
        ...
    }

    ...
}
  • OnDisable will be called when component is removed. So nothing will keep it alive.
  • OnEanable will be called when component is added to game object.
  • RequireComponent will make sure Direction component is added first.
  • You can’t remove Direction component if DirectionVisualization component is still present, because of RequireComponent attribute.

You can also send messages with GameObject.SendMessage(), this way you don’t need to worry about event subscription.

References