Go to Top

There are times when you really need to associate data with an object you have no control over. This article explores how we can make an easy to use extension that does just that. The article includes an example of the technique to send animations and mixing transforms via RPC.

Motivation

You should read this article if:

  • You'd like to send AnimationStates over the network (including mixing transforms)
  • You'd like to know how to imbue any object with new properties without leaking memory
  • You'd like to learn more about garbage collection and weak references

Introduction

One of the perennial problems with Unity is that the back end is C++ and our coding interface is .NET.  The guys have done a great job but there are places where you just can't get to the values you need.  One classic example is Mixing Transforms that you add to animations to make them only play on part of the character.  You can add them and you can remove them, but only if you know what they are. In many circumstances you can find yourself adding mixing transforms in one piece of code and then needing to know what they are, perhaps to send them via an RPC call or to remove them, in a different function.

You could jump through hoops trying to solve that problem - having to store and manage a lot of new variables and classes to keep track of the information - and who knows when that object will be destroyed?  How will you free your memory? Will you lock the object forever and leak?  Quite possibly.

This article covers the creation of a method of associating a class with any other object and to have that class only live while the object itself is alive.  It's very easy to use:

public class Extra
{
    public int anyVariablesYouLike;
}

...

anyObject.Get<Extra>().anyVariablesYouLike = 1;

 

A First Association - Animation Mixing

Ok so lets have a look at a basic way we could associate data with the AnimationState we want to mix - we could build a little helper class that allowed us to key objects off other objects:

public static class Extensions
{
    //Dictionary to hold our extensions
    static Dictionary<object, Dictionary<Type, object>> _extensions = new Dictionary<object, Dictionary<Type, object>>();

    //Get an associated extension class
    public static T Get<T>(this object o) where T : class, new()
    {
         //Do we have this object yet?
         if(!_extensions.ContainsKey(o))
         {
             //If not then add it
             _extensions[o] = new Dictionary<Type, object>();
         }
         //Do we have this associate type yet?
         if(!_extensions[o].ContainsKey(typeof(T)))
         {
             //If not create a new one
             _extensions[o][typeof(T)] = new T();
         }
         //Return the associate
         return _extensions[o][typeof(T)];

    }
}

Ok so with that class we can do anyObject.Get<AnyClass>() and it will give us an instance of that class!  The Dictionaries are giving us O(1) lookup so it should be very fast.  So we want to associate transforms with animations states, let's make up a class to do that.  We will use the names of the transforms so that we can find them on another computer if we send them via RPC.

	public class Mixing
	{
		public List<string> transforms = new List<string>();
	}

 Now lets add something to that class, using our Get<T> helper function.

Transform spine2;

public void SwingSword()
{
    var state = animation["swingSword"];
    //We'd probably only do this if we were 
    //walking - but for ease I'm leaving out
    //any testing for that
    state.AddMixingTransform(spine2);
    //Get a Mixing instance for this animation state and add the
    //name of our new transform
    state.Get<Mixing>().transforms.Add(spine2.name);
    state.enabled = true;
    state.weight = 1;
}

Ok so that will associate our Mixing class with this particular animation  - let's look at how we might send AnimationStates by RPC to another player.

We need a class suitable for RPC transmission that represents the AnimationState:

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

/// <summary>
/// Stores the animation state for a character
/// </summary>
public class StoredAnimationState
{
    /// <summary>
    /// The name of the animatoin
    /// </summary>
    public string name;
    /// <summary>
    /// Is the animation enabled
    /// </summary>
    public bool enabled;
    /// <summary>
    /// The current speed of the animation
    /// </summary>
    public float speed;
    /// <summary>
    /// The current progress through the animation
    /// </summary>
    public float time;
    /// <summary>
    /// The current weight of the animation
    /// </summary>
    public float weight;
    /// <summary>
    /// The current blend mode of the animation
    /// </summary>
    public AnimationBlendMode blendMode;
    /// <summary>
    /// The current layer of the animation
    /// </summary>
    public int layer;
    /// <summary>
    /// The current wrap mode of the animation
    /// </summary>
    public WrapMode wrapMode;
    /// <summary>
    /// The list of current mixing transforms for the animation
    /// </summary>
    public List<string> mixingTransforms = new List<string>();

    public override int GetHashCode()
    {
        return name.GetHashCode() ^ weight.GetHashCode();
    }

    public override bool Equals (object obj)
    {
        if(!(obj is StoredAnimationState))
            return false;
        var other = (StoredAnimationState) obj;
        return name == other.name && enabled == other.enabled &&
               Math.Abs(speed - other.speed) < Single.Epsilon && Math.Abs(weight - other.weight) < Single.Epsilon &&
               blendMode == other.blendMode && 
               layer == other.layer &&
               wrapMode == other.wrapMode &&
               mixingTransforms.Count == other.mixingTransforms.Count;

    }

    public StoredAnimationState()
    {
    }

    //Configure from an animation	
    public StoredAnimationState(AnimationState state)
    {
        name = state.name;
        enabled = state.enabled;
        speed = state.speed;
        time = state.time;
        weight = state.weight;
        blendMode = state.blendMode;
        layer = state.layer;
        wrapMode = state.wrapMode;
        mixingTransforms = state.Get<Mixing>().transforms;
    }

    /// <summary>
    /// Configure an existing animation state from the information in this transfer class
    /// </summary>
    /// <param name="animation"></param>
    public void Set(Animation animation)
    {
        var anim = animation[name];
        //Do our mixing transform match?
        //First check if we have the same number, if they match then check
        //the names
        if(anim.Get<Mixing>().transforms.Count != mixingTransforms.Count || !anim.Get<Mixing>().transforms.All(mixingTransforms.Contains))
        {
            //Remove all of the existing mixings
            foreach(var m in anim.Get<Mixing>().transforms)
            {
                anim.RemoveMixingTransform(animation.transform.Find(m));
            }
            //Update the list
            anim.Get<Mixing>().transforms = mixingTransforms;
            //Apply the new mixings
            foreach(var m in mixingTransforms)
            {
                anim.AddMixingTransform(animation.transform.Find(m), true);
            }

        }
        //Apply other variables
        anim.enabled = enabled;
        anim.weight = weight;
        anim.speed = speed;
        if(Mathf.Abs(anim.time - time) > 1)
            anim.time = time;
        anim.blendMode = blendMode;
        anim.layer = layer;
        anim.wrapMode = wrapMode;
    }

    /// <summary>
    /// Class to transfer a list of animation states
    /// </summary>
    public class AnimationTransfer
    {
        /// <summary>
        /// The animation state to be transferred
        /// </summary>
        public List<StoredAnimation> StoredAnimations;
        /// <summary>
        /// The representation of an animation state
        /// </summary>
        public class StoredAnimation
        {
            /// <summary>
            /// The name of the animation
            /// </summary>
            public string name;
            /// <summary>
            /// The data for the stored state of the animation
            /// </summary>
            public StoredAnimationState data;
        }
    }
}

Ok, so that's quite a lot of code - we've basically made a class that can represent an animation state, suitable for serialization.  It also knows how to apply itself back onto an animation.

Let's look at how we could send that to the other players:

public void SendAnimations()
{
      //Create a transfer structure
      var transfer = new StoredAnimationState.AnimationTransfer();
      //Create the list of stored animations
      transfer.StoredAnimations = 
         //All of the states in the current list
         animation.Cast<AnimationState>()
            //Only when enabled
            .Where(state=>state.enabled)
            //Create a stored animation
            .Select(state=>new StoredAnimationState.AnimationTransfer.StoredAnimation
                   { 
                        //Animation name
                        name = state.name, 
                        //Animation data
                        data = new StoredAnimationState(state) 
                   })
            .ToList();   
       //Serialize the data
       var m = new MemoryStream();
       var b = new BinaryFormatter();
       b.Serialize(transfer, m);
       //Send it
       networkView.RPC("ReceiveAnimations", RPCMode.Others, m.ToArray());

}

[RPC]
void ReceiveAnimations(byte[] data)
{
     //Get the transfer structure
     var m = new MemoryStream(data);
     var b = new BinaryFormatter();
     var transfer = (StoredAnimationState.AnimationTransfer)b.Deserialize(m);
     //Disable all animations (they will be re-enabled
     //if required)
     foreach(var state in animation.Cast<AnimationState>())
        state.enabled = false;
     //Set up all of the animations from the
     //transfer
     foreach(var state in transfer.StoredAnimations)
         state.data.Set(animation);
}

It Leaks Like a Sieve!!!

Yep, sorry to tell you - but without forcibly removing things from our _extensions Dictionary this code is sinking fast.  When you destroy the game objects, the memory won't be freed because we're holding on to those AnimationStates as keys in our Dictionary - if it happens a lot there will be vast memory usage.

Ye of Little Faith

I'm hoping that you don't think I've lead you down the garden path for no reason?  There is a way around this problem - but it's going to get techy.

What we need is to free our Mixing (or other extensions) when the AnimationState (or whatever the key is) is destroyed.  Of course they aren't getting destroyed because we're holding on to them.

Weak References

What we need to do first is to hold a weak reference to the AnimationState rather than a strong one.  .NET has the WeakReference type built in - so we can manage that.  Now two weak references to the same object are not going to have the same hash code - so we can't use them as the key in a Dictionary!  That's ok - all objects in .NET have a GetHashCode call that we can use to find out what it would be for our real key and use that instead.  It's an int by the way.

Genericising everthing we come up with a WeakTable<T> class that associates our objects.  We'll see this in a minute, for the moment we also need to remove our associates when the original key is garbage collected.

Its kind of hard to figure out when the Garbage Collector has run - but we can actually find out by creating some objects that have no references to them, and see when they are collected - using their finalization methods!  We borrow this from Jeff Richter:

//The following class is from:
//Jeff Richter - http://www.wintellect.com/CS/blogs/jeffreyr/archive/2009/12/22/receiving-notifications-garbage-collections-occur.aspx
public static class GCNotification {
   private static Action<Int32> s_gcDone = null; // The event’s field
   public static event Action<Int32> GCDone {
      add {
         // If there were no registered delegates before, start reporting notifications now 
         if (s_gcDone == null) { new GenObject(0); new GenObject(2); }
         s_gcDone += value;
      }
      remove { s_gcDone -= value; }
   }
   private sealed class GenObject {
      private Int32 m_generation;
      public GenObject(Int32 generation) { m_generation = generation; }
      ~GenObject() { // This is the Finalize method
         // If this object is in the generation we want (or higher), 
         // notify the delegates that a GC just completed
         if (GC.GetGeneration(this) >= m_generation) {
	        //Thread safe get of the s_gcDone delegate - will not be interrupted
            Action<Int32> temp = Interlocked.CompareExchange(ref s_gcDone, null, null);
			//Fire the event
            if (temp != null) temp(m_generation);
         }
         // Keep reporting notifications if there is at least one delegate
         // registered, the AppDomain isn't unloading, and the process 
         // isn’t shutting down
         if ((s_gcDone != null) && 
            !AppDomain.CurrentDomain.IsFinalizingForUnload() && 
            !Environment.HasShutdownStarted) {
            // For Gen 0, create a new object; for Gen 2, resurrect the
            // object & let the GC call Finalize again the next time Gen 2 is GC'd
            if (m_generation == 0) new GenObject(0);
            else GC.ReRegisterForFinalize(this);
         } else { /* Let the objects go away */ }
      }
   }
}

This class creates a couple of objects with no references, waits for them to be finalized by the Garbage Collector and when that happens it fires a GCDone event.  We can use this event as a cue to decide to check whether our weak references are still valid, the ones that aren't need to have their associated objects removed...

The final WeakTable<T> and static extension method Get<T> look like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Collections;
using System.Threading;

/// <summary>
/// Weak entry for the table
/// </summary>
class WeakEntry 
{
	/// <summary>
	/// The weak reference to the collectable object
	/// </summary>
	public WeakReference weakReference;
	/// <summary>
	/// The associated object with the reference
	/// </summary>
	public object associate;
}

/// <summary>
/// A weak table for a particular type of associated class
/// </summary>
public class WeakTable<T> : IDisposable where T : class, new()
{

	//When finished we can dispose of the notification
	public void Dispose()
	{
		GCNotification.GCDone -= Collected;
	}

	/// <summary>
	/// The references held by the table
	/// </summary>
	Dictionary<int, List<WeakEntry>> references = new Dictionary<int, List<WeakEntry>>();

	/// <summary>
	/// Gets an associated object give an index of another object
	/// </summary>
	/// <param name='index'>
	/// The object to use as an index
	/// </param>
	public T this[object index]
	{
		get
		{
			//Get the hash code of the indexed object
			var hash = index.GetHashCode();
			List<WeakEntry> entries;
			//Try to get a reference to it
			if(!references.TryGetValue(hash, out entries))
			{
				//If we failed then create a new entry
				references[hash] = entries = new List<WeakEntry>();
			}
			//Try to get an associated object of the right type for this
			//indexer/make sure it is still alive
			var item = entries.FirstOrDefault(e=>e.weakReference.IsAlive && e.weakReference.Target == index);
			//Check if we got one
			if(item == null) 
			{
				//If we didn't then create a new one
				entries.Add(item = new WeakEntry { weakReference = new WeakReference(index), associate = new T() });
			}
			//Return the associated object
			return (T)item.associate;
		}

	}

	/// <summary>
	/// Get an associate given an indexing object
	/// </summary>
	/// <param name='index'>
	/// The object to find the associate for
	/// </param>
	/// <typeparam name='T2'>
	/// The type of associate to find
	/// </typeparam>
	public T2 Get<T2>(object index) where T2 : T, new()
	{
		//Get the hash code of the indexing object
		var hash = index.GetHashCode();
		List<WeakEntry> entries;
		//See if we have a reference already
		if(!references.TryGetValue(hash, out entries))
		{	
			//If not the create the reference list
			references[hash] = entries = new List<WeakEntry>();
		}
		//See if we have an object of the correct type and that the
		//reference is still alive
		var item = entries.FirstOrDefault(e=>e.weakReference.IsAlive && e.weakReference.Target == index && e.associate is T2);
		if(item == null) 
		{
			//If not create one
			entries.Add(item = new WeakEntry { weakReference = new WeakReference(index), associate = new T2() });
		}
		//Return the associate
		return (T2)item.associate;
	}

	public WeakTable() 
	{
		//Setup garbage collection notification
		GCNotification.GCDone += Collected;
	}

	/// <summary>
	/// Called when the garbage has been collected
	/// </summary>
	/// <param name='generation'>
	/// The generation that was collected
	/// </param>
	void Collected(int generation)
	{
		//Remove the references which are no longer alive

		//Scan each reference list
		foreach(var p in references)
		{
			//Scan each item in the references and remove
			//items that are missing
			removeEntries.Clear();
			foreach(var r in p.Value.Where(r=>!r.weakReference.IsAlive))
				removeEntries.Add(r);
			foreach(var entry in removeEntries)
			{
				if(entry.associate is IDisposable)
					(entry.associate as IDisposable).Dispose();                      
				p.Value.Remove(entry);
			}
		}
	}

	List<WeakEntry> removeEntries = new List<WeakEntry>();

}

/// <summary>
/// Extension class to support getting weak tables easily
/// </summary>
public static class Extension
{
	static Dictionary<Type, WeakTable<object>> extensions = new Dictionary<Type, WeakTable<object>>();

	/// <summary>
	/// Get an associate for a particular object
	/// </summary>
	/// <param name='reference'>
	/// The object whose associate should be found
	/// </param>
	/// <param name='create'>
	/// Whether the associate should be created (defaults true)
	/// </param>
	/// <typeparam name='T'>
	/// The type of associate
	/// </typeparam>
	public static T Get<T>(this object reference, bool create = true) where T : class, new()
	{
		WeakTable<object> references;
		//Try to get a weaktable for the reference object
		if(!extensions.TryGetValue(reference.GetType(), out references))
		{
			//Verify that we should be creating it if missing
			if(!create)
				return null;
			//Create a new table
			extensions[reference.GetType()] = references = new WeakTable<object>();
		}
		//Get the associate from the table
		return (T)references.Get<T>(reference);
	}

}

//The following class is from:
//Jeff Richter - http://www.wintellect.com/CS/blogs/jeffreyr/archive/2009/12/22/receiving-notifications-garbage-collections-occur.aspx
public static class GCNotification {
   private static Action<Int32> s_gcDone = null; // The event’s field
   public static event Action<Int32> GCDone {
      add {
         // If there were no registered delegates before, start reporting notifications now 
         if (s_gcDone == null) { new GenObject(0); new GenObject(2); }
         s_gcDone += value;
      }
      remove { s_gcDone -= value; }
   }
   private sealed class GenObject {
      private Int32 m_generation;
      public GenObject(Int32 generation) { m_generation = generation; }
      ~GenObject() { // This is the Finalize method
         // If this object is in the generation we want (or higher), 
         // notify the delegates that a GC just completed
         if (GC.GetGeneration(this) >= m_generation) {
			//Thread safe get of the s_gcDone delegate - will not be interrupted
            Action<Int32> temp = Interlocked.CompareExchange(ref s_gcDone, null, null);
			//Fire the event
            if (temp != null) temp(m_generation);
         }
         // Keep reporting notifications if there is at least one delegate
         // registered, the AppDomain isn't unloading, and the process 
         // isn’t shutting down
         if ((s_gcDone != null) && 
            !AppDomain.CurrentDomain.IsFinalizingForUnload() && 
            !Environment.HasShutdownStarted) {
            // For Gen 0, create a new object; for Gen 2, resurrect the
            // object & let the GC call Finalize again the next time Gen 2 is GC'd
            if (m_generation == 0) new GenObject(0);
            else GC.ReRegisterForFinalize(this);
         } else { /* Let the objects go away */ }
      }
   }
}

Just note that we have had to switch around our Dictionary definition to use the type of the reference first, that's so we can get a WeakTable that knows about those references.

When we are finished with the associated classes we call IDisposable.Dispose on them in case they want to know that they are finished with.

Conclusion

This has been a pretty techy article, hopefully it makes some sense!  You should be able to see how associating a class of our own devising with something we don't control the lifetime of (like an AnimationState) is a powerful technique. Enjoy!

, , , , , ,

5 Responses to "Weak Associations – Leak Free Extension Properties"

  • cbm010
    February 11, 2013 - 5:45 pm Reply

    b.Serialize(m, transfer); // author swapped these for some reason

  • whydoidoit
    March 21, 2013 - 8:17 am Reply

    Yep, that’s not available in the version of .NET in Unity

    • whydoidoit
      April 22, 2014 - 1:13 pm Reply

      Yes it uses a List of the referenced items so if the hash codes did collide there would be more than one entry. This entry list is then tested to see which, if any, of the matching weak references actually matches the selected target.

Leave a Reply

%d bloggers like this: