- Home /
Custom [Require*] attribute
I'm trying to do something custom RequireTrait
attribute that should mark components so that they are attached only if some other component of that gameObject has publicly available field or property of given type. I'm trying to determine where should that checking function run.
Say I have a custom attribute:
[AttributeUsage(AttributeTargets.Class)]
[BaseTypeRequired(typeof(Component))]
public class RequireTraitAttribute : Attribute {
public readonly Type traitType;
public RequireTraitAttribute(Type traitType) {
this.traitType = traitType;
}
}
And then use it like this:
[RequireTrait(typeof(HitPoints))]
public class HealthManager : MonoBehaviour {
private HitPoints hitPoints;
private void Awake() {
hitPoints = gameObject.GetTrait<HitPoints>(); // custom extension function
}
...
}
Now HealthManager
should require some provider of HitPoints, or otherwise should be detached from game object:
[???] // Appropriate unity attribute?
public static void CheckRequiredTraits() {
... // Detach components that do not fit criteria
}
When and where should this check be done?
How does RequireComponent
deal with that?
Reset()
seems inaproptiate and cluttering. As well as creating custom editor every time.
Answer by Dawdlebird · Aug 04, 2019 at 10:13 AM
Interesting approach.. but have you considered using OnValidate()? Doesn't require a custom editor, but allows you to run any code or method in the editor and can remove components based whatever criteria you manage to check gameobjects on.
OnValidate()
would unnecessarily run every time some property would change, so I don't think that's correct.
Reset()
seems better, because it executes when I add my annotated component, so I can validate it and, if needed, WaitForEndOfFrame()
and DestroyImmediate()
.
But then I have to write that logic in every class, and it just doesn't look right conceptually. I really need similar behaviour to [RequireComponent(...)]
attribute, but cannot find information on how to achieve that. :(
Answer by vElumi · Aug 04, 2019 at 06:54 PM
I wrote this as a workaround. Maybe this can clarify what I'm after.
I used [InitializeOnLoad]
class and subscribe to EditorApplication.update
to check for all components and then remove component if no public provider was found.
Dounsides are that it:
Loops through all components
Executes every frame
using System; using System.Linq; using System.Reflection; using UnityEditor; using UnityEngine; using Object = UnityEngine.Object;
namespace Scenes.FightScene.Character.Trait { [InitializeOnLoad] public class TraitChecker { private class TypeData { public readonly Type markedType; public readonly Type requiredType;
public TypeData(Type markedType, Type requiredType) { this.markedType = markedType; this.requiredType = requiredType; } } private static TypeData[] typeData; private const BindingFlags Bindings = BindingFlags.Instance | BindingFlags.Public; static TraitChecker() { UpdateMarkedTypes(); EditorApplication.update += CheckRequiredTraits; } [UnityEditor.Callbacks.DidReloadScripts] private static void UpdateMarkedTypes() { typeData = AppDomain.CurrentDomain.GetAssemblies() .SelectMany(asm => asm.GetTypes()) .Select(type => { var attribute = type.GetCustomAttribute<RequireTraitAttribute>(true); return attribute == null ? null : new TypeData(type, attribute.traitType); }) .Where(td => td != null) .ToArray(); } private static void CheckRequiredTraits() { // Get all components var sceneComponents = Object.FindObjectsOfType<Component>(); foreach (var td in typeData) { // Filter all instances of types, that are marked with [RequireTrait] var markedInstances = sceneComponents.Where(component => component.GetType() == td.markedType); foreach (var instance in markedInstances) { // Get all components on gameObject, that are not component itself var components = instance.GetComponents<Component>() .Where(c => c != instance); // Select distinct types of those components var componentsTypes = components .Select(component => component.GetType()) .Distinct().ToArray(); // If no components provide required trait via fields or properties if (componentsTypes.All(type => type.GetFields(Bindings) .All(fieldInfo => fieldInfo.FieldType != td.requiredType) && type.GetProperties(Bindings) .All(propertyInfo => propertyInfo.PropertyType != td.requiredType))) // Remove instance Object.DestroyImmediate(instance); } } } } }