Category Archives: unity3d

Unity3d Extension methods example

Extension methods are awesome for extending the functionality of existing classes. What is really cool is that if you use an IDE that supports autocompletion, your extension methods will show up when using the classes you extend.

For this example, let’s suppose that you want to extend Unity’s AnimationClip to support the idea of adding an “OnAnimationEnd” event. When the animation clip ends, a callback will be called.

using UnityEngine;

public static class AnimationClipExtensions
{
    public static void AddOnAnimationEndEvent (this AnimationClip animationClip, string onAnimationEndCallbackName)
    {
        AddOnAnimationEndEvent (animationClip, onAnimationEndCallbackName, 0, 0.0f, "", null);
    }

    public static void AddOnAnimationEndEvent (this AnimationClip animationClip, string onAnimationEndCallbackName, int intParameter)
    {
        AddOnAnimationEndEvent (animationClip, onAnimationEndCallbackName, intParameter, 0.0f, "", null);
    }

    public static void AddOnAnimationEndEvent (this AnimationClip animationClip, string onAnimationEndCallbackName, float floatParameter)
    {
        AddOnAnimationEndEvent (animationClip, onAnimationEndCallbackName, 0, floatParameter, "", null);
    }

    public static void AddOnAnimationEndEvent (this AnimationClip animationClip, string onAnimationEndCallbackName, string stringParameter)
    {
        AddOnAnimationEndEvent (animationClip, onAnimationEndCallbackName, 0, 0.0f, stringParameter, null);
    }

    public static void AddOnAnimationEndEvent (this AnimationClip animationClip, string onAnimationEndCallbackName, Object objectReferenceParameter)
    {
        AddOnAnimationEndEvent (animationClip, onAnimationEndCallbackName, 0, 0.0f, "", objectReferenceParameter);
    }

    private static void AddOnAnimationEndEvent (this AnimationClip animationClip, string onAnimationEndCallbackName, int intParameter, float floatParameter, string stringParameter, Object objectReferenceParameter)
    {
        AnimationEvent animEvent = new AnimationEvent ();
        animEvent.time = animationClip.length;
        animEvent.functionName = onAnimationEndCallbackName;
        animEvent.intParameter = intParameter;
        animEvent.floatParameter = floatParameter;
        animEvent.stringParameter = stringParameter;
        animEvent.objectReferenceParameter = objectReferenceParameter;
        animEvent.messageOptions = SendMessageOptions.DontRequireReceiver;
        animationClip.AddEvent (animEvent);
    }
}

Now that we have a few extension methods, let’s test it out. Create a new component and fill in the animation clip in the inspector

using UnityEngine;
using System.Collections;

public class ExtensionMethodTest : MonoBehaviour
{
    public AnimationClip testClip;

    void Start ()
    {
        testClip.AddOnAnimationEndEvent("TestOnAnimationEndEvent");
        testClip.name= "TestClip";
        animation.Play("TestClip");
    }

    void Update ()
    {

    }

    void TestOnAnimationEndEvent()
    {
        Debug.Log("TestOnAnimationEndEvent() was called.");
    }
}

Unity3D AssetPostprocessor Example

In Unity3D, you’ll often find yourself repeating a lot of simple tasks on prefabs if you make changes to the source content after you’ve placed instances of the prefab in the editor; stuff like turning off the renderer for some meshes, or adding colliders and scripted components. Unity provides a mechanism to automate much of this work with the AssetPostProcessor class.

In your Maya scenes, every mesh, bone, etc are all GameObjects in Unity. When you save your Maya scenes, it triggers them to be re-imported in Unity and when that happens, the AssetPostProcessor’s OnPostprocessGameObjectWithUserProperties() method will get called once for every GameObject in the Maya scene. If any of those GameObjects have custom attributes added to them, each attribute will have its attribute name and attribute value in the arrays passed into OnPostprocessGameObjectWithUserProperties.

Ok. So what is all this code? The examples I have seen usually have something like this inside the OnPostprocessGameObjectWithUserProperties method:

if (userPropertyName == "AddMeshCollider")
{
    ...
}
else if (userPropertyName == "AddSphereCollider")
{
    ...
}

There is nothing wrong with that code, but my preference is to separate out the logic that handles what to do for each user property into its own class, and never touch the OnPostprocessGameObjectWithUserProperties method again.

To do that, we can use the Attribute class and C# reflection. We’ll create an ImportTaskAttribute that takes a name in its constructor. That name will correspond to the user property name (the custom attribute name in Maya.)

All classes that are marked up with that attribute will get collected by an ImportTaskCollector class which we’ll use to fetch them and then call their Execute method to do the work.

using System.Linq;
using UnityEngine;
using UnityEditor;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;

This is the ImportTaskAttributeClass. The TaskName property will hold the name matching the user property.

[AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
public class ImportTaskAttribute : Attribute
{
    public string TaskName;
    
    public ImportTaskAttribute(string taskName)
    {
        TaskName = taskName;
    }
}

This interface is the contract for all of the import tasks. And the concrete class below is what we’ll inherit from.

public interface IPostProcessGameObjectWithUserPropertiesTask
{
    GameObject ImportedGameObject {get; set;}
    string UserPropertyName {get; set;}
    object UserPropertyValue {get; set;}
    
    bool ExecuteTask();
    void PostExecuteTask();
}

public class PostProcessGameObjectWithUserPropertiesTask : IPostProcessGameObjectWithUserPropertiesTask
{
    public GameObject ImportedGameObject {get; set;}
    public string UserPropertyName {get; set;}
    public object UserPropertyValue {get; set;}
    public object UserData {get; set;}
    
    public virtual bool ExecuteTask() { return false; }

    public virtual void PostExecuteTask() {}
}

This is the ImportTaskCollector. Its job is to find all classes marked up with the ImportTaskAttribute.

public sealed class ImportTaskCollector
{
    private Dictionary<string, object> _importTasks = new Dictionary<string, object>();
    
    public ImportTaskCollector()
    {
        Assembly assembly = typeof(IPostProcessGameObjectWithUserPropertiesTask).Assembly;
        Type target = typeof(IPostProcessGameObjectWithUserPropertiesTask);        
        var importTaskTypes = assembly.GetTypes()
                            .Where(type => target.IsAssignableFrom(type));

        foreach (Type importTaskType in importTaskTypes)
        {   
            if (importTaskType.IsAbstract || importTaskType.IsGenericType)
            {
                continue;
            }
 
            var importTaskInstance = Activator.CreateInstance(importTaskType);
            var importTaskTypeAttributes = Attribute.GetCustomAttributes(importTaskType);

            var importTaskTypeAttribute = importTaskTypeAttributes
                .SingleOrDefault(attr => attr as ImportTaskAttribute != null) as ImportTaskAttribute;
            
            if (importTaskTypeAttribute != null)
            {
                _importTasks.Add(importTaskTypeAttribute.TaskName, importTaskInstance); 	
            }
        }
    }
    
    public IPostProcessGameObjectWithUserPropertiesTask GetTask(string taskName)
    {
        if (_importTasks.ContainsKey(taskName))
        {
            return _importTasks[taskName] as IPostProcessGameObjectWithUserPropertiesTask; 
        }
        else
        {
            return null;
        }
    }
}

Finally, this is our custom AssetPostprocessor.

public class AssetImport : AssetPostprocessor
{
    private readonly ImportTaskCollector _importTaskCollector;
    
    public AssetImport()
    {
        _importTaskCollector = new ImportTaskCollector();
    }
    
    void OnPostprocessGameObjectWithUserProperties(GameObject g, String[] userPropertyNames, System.Object[] userPropertyValues)
    {
        for (int propertyPairIndex = 0; propertyPairIndex < userPropertyNames.Length; propertyPairIndex++)
        {
            var userPropertyName = userPropertyNames[propertyPairIndex];
            var userPropertyValue = userPropertyValues[propertyPairIndex];
            
            IPostProcessGameObjectWithUserPropertiesTask importTask = _importTaskCollector.GetTask(userPropertyName);
            
            if (importTask != null)
            {
                importTask.ImportedGameObject = g;
                importTask.UserPropertyName = userPropertyName;
                importTask.UserPropertyValue = userPropertyName;
                
                if (importTask.ExecuteTask())
                {
                    importTask.PostExecuteTask();
                }
            }
        }
    }
}

Ok, with all of that in place, here is an example of how you would use it:

[ImportTaskAttribute("AddMeshCollider")]
public class AddMeshColliderTask : PostProcessGameObjectWithUserPropertiesTask
{
    public bool ExecuteTask()
    {
        ImportedGameObject.AddComponent<MeshCollider>();
        return true;
    }
}

Then, if you add an attribute named “AddMeshCollider” to any mesh in your maya scene, your task will get run for that game object and a MeshCollider will be added.

But we can do better. We can make a generic AddComponentTask where the generic type parameter is the Component we want to add.

public class AddComponentTask<T> : PostProcessGameObjectWithUserPropertiesTask where T : Component
{
    public override bool ExecuteTask()
    {
        UserData = ImportedGameObject.AddComponent<T>();    

        return UserData != null;
    }
}

[ImportTask("AddMeshCollider")]
public class AddMeshColliderTask : AddComponentTask<MeshCollider>
{
	// Nothing to do, ExecuteTask() gets called on AddComponentTask<T>
	// and T is MeshCollider in this case.
}

Now, what if we wanted to add a CapsuleCollider and then set the height and radius from within Maya? The UserData property holds the component we just added, and now that code lives in the AddComponenTask‘s Execute method. We don’t want to keep duplicating the code for adding a component, so this is where the PostExecute method comes into play.

[ImportTask("AddCapsuleCollider")]
public class AddCapsuleColliderTask : AddComponentTask<CapsuleCollider>
{
	public override void PostExecuteTask()
	{
		var capsule = UserData as CapsuleCollider;
		var capsuleData = UserPropertyValue as Vector2?;
			
		if (capsule != null && capsuleData.HasValue)
		{
			capsule.height = capsuleData.Value.x;
			capsule.radius = capsuleData.Value.y;
		}
	}
}

As always, if you’ve got any questions, shoot me an email at jspataro@gmail.com