XDracam / unity-corelibrary

Collection of classes and extension methods that make life with Unity3D more comfortable
MIT License
15 stars 3 forks source link

AutoComponent #1

Open Eregerog opened 5 years ago

Eregerog commented 5 years ago

Consider the following code:

private Component _myComp;
public Component {
  get{
    if(!_myComp) _myComp = GetComponent<Component>();
    if(!_myComp) _myComp = AddComponent<Component>();
    return _myComp;
  }
  set{
    _myComp = value;
  }
}

It would be nice to have that as a one-liner in the declarartion, something like:

public AutoComponent<Component> MyComp; It would implicitly convert to and from Component, is Serializable! and does the same as the code above. I'm currently not sure if it is even possible without Reflection.

XDracam commented 5 years ago

Setting a public AutoComponent from somewhere else does not sound like a use case you'd want. Then again, setting a wrapper without additional boilerplate like a property is not possible in plain C#.

What we could do is something like this:

class LazyComponent<T> where T : Component 
{
    [NotNull] private readonly GameObject _referenceObject;
    public LazyComponent(GameObject referenceObject) 
    { 
        this._referenceObject = referenceObject;
    }

    private T _underlying;

    public T Value { get {
        if (_underlying != null) return _underlying;
        _underlying = _referenceObject.GetComponent<T>();
        if (_underlying != null) return _underlying;
        _underlying = _referenceObject.AddComponent<T>();
        return _underlying; 
    }}

    public static implicit operator T(LazyComponent<T> lazyComponent) 
    { 
        return lazyComponent.Value; 
    }
}

What I don't know is how well generics perform with implicit conversions. I know this can work in Scala. In the worst case we would have to live with the .Value call.

There is no setter because setting a public Component field has two use cases:

  1. caching a component present on the host gameobject for faster access <- this
  2. Providing a reference to a component on another object usually via inspector.

Things to consider for this:

Naming:

Performance:

GetComponent<T> is already a really fast call. People only cache it's result for a very minor increase in performance, mostly for repeated usage in Update or similar. Using the LazyComponent or whatever adds an additional property access, a potential function call (conversion) as well as a custom null check (function call) to a simple pointer dereference. So this whole mechanism might eliminate the main pro of caching a component reference. Benchmarks are required for further evaluation.

One could optimize the code in expense for readability: introducing an additional boolean flag to save further null checks at the cost of one additional byte (or word?) per instance, and force inlining of a new function backing the Value getter. Or copy-pasting the initialization code. Anyways, benchmarks.

Usability

The whole lazyness mechanism requires a reference to the game object the component is on. This might result in a massive penalty for usability when using this, potentially adding more boilerplate code than this is saving:

// does this even work in C# or do we need a constructor specifically for this? -> even worse
public readonly LazyComponent<Renderer> Renderer = new LazyComponent<Renderer>(this); 

Implementing use case 2

So far, the only use case for this is to be a little more lazy in a rare case, thereby potentially sacrificing performance. Bjarne Stroustrup would cry when seeing this non-zero-cost abstraction.

We might add to the usefulness of this when providing a more abstract version:

class ComponentWrapper<T> where T : Component 
{
    [NotNull] private readonly GameObject _referenceObject;
    private readonly Func<T> _getter; 
    private readonly Func<T> _provider;
    public ComponentWrapper(GameObject referenceObject, Func<T> getter, Func<T> provider) 
    { 
        this._referenceObject = referenceObject;
        this._getter = getter; 
        this._provider = provider;
    }

    private T _underlying;

    public T Value { get {
        if (_underlying != null) return _underlying;
        _underlying = _getter();
        if (_underlying != null) return _underlying;
        _underlying = _provider();
        return _underlying; 
    }}

    public static implicit operator T(ComponentWrapper<T> wrapper) 
    { 
        return wrapper.Value; 
    }
}

Implementing this could lead to more in-place definitions for use case number 2, by providing a custom inspector for setting the value from the inspector and whatnot. But that is a lot of trouble to save a little bit of boilerplate, and I don't know if that's worth it. Expecially when my fears about the constructor call bonus boilerplate come true.

XDracam commented 5 years ago

Leaving this issue open as reference - It might come in handy for a future feature. At least some further discussion is required.