/ Game Programming

Unity Behavior Trees pt. 3: Better Serialization

Previous Article

Last we left off we had a behavior tree implementation that could be serialized into assets, but the way we were converting them into assets left a lot to be desired. Since then I've figured out how to improve upon this by better using Unity's asset serialization system to our advantage.

As I mentioned last time, the ideal way of creating behavior tree assets would be to make the nodes derive from ScriptableObject and serialize them out as normal Unity assets. This would allow us to handle references between objects and polymorphism without the need for the hackery we previously employed. The problem that was preventing us from doing this was that it would have meant that each node would have to be stored in a separate file in order to be its own asset, which would have quickly gotten out of hand as the tree got larger. As it turns out, Unity allows you to add multiple objects to our asset files, which means we can vastly simplify our serialization process.

Implementation

This is the method I was originally using to create the behavior tree assets:

[MenuItem( "Assets/Create/Behavior Tree" )]
public static void CreateBehaviorTreeAsset()
{
	BehaviorTree behaviorTreeAsset = ScriptableObject.CreateInstance<BehaviorTree>();
	AssetDatabase.CreateAsset( behaviorTreeAsset, "Assets/NewBehaviorTree.asset" );
	
	AssetDatabase.SaveAssets();
}

We create a new BehaviorTree object, and then use AssetDatabase.CreateAsset() to create an asset file from that object. To store multiple nodes within that file only a minor change is needed:

[MenuItem( "Assets/Create/Behavior Tree" )]
public static void CreateBehaviorTreeAsset()
{
	// Create behavior tree asset.
	BehaviorTree behaviorTreeAsset = ScriptableObject.CreateInstance<BehaviorTree>();
	AssetDatabase.CreateAsset( behaviorTreeAsset, "Assets/NewBehaviorTree.asset" );

	// Add root node to behavior tree.
	TreeNode newNode = ScriptableObject.CreateInstance<NullNode>();
	AssetDatabase.AddObjectToAsset( newNode, behaviorTreeAsset );

	// Save the new asset.
	AssetDatabase.SaveAssets();
}

Using AssetDatabase.AddObjectToAsset() we can have multiple objects living in one asset file. While this is a major improvement over the old serialization method there are a few changes that need to be made to avoid problems.

The first is when we create new nodes. Before this TreeNode was just a normal C# class, so we created nodes with new and we didn't have to handle destroying them since the garbage collector did that for us. Now that TreeNode inherits from ScriptableObject we have to use ScriptableObject.CreateInstance() to create nodes and we have to be sure that each node is added to the asset when its created. To handle this I put together a simple helper method:

TreeNode CreateNode( Type nodeType )
{
	TreeNode newNode = (TreeNode)ScriptableObject.CreateInstance( nodeType );
	AssetDatabase.AddObjectToAsset( newNode, _behaviorTree );
	return newNode;
}

One thing to note is that this method takes a Type parameter -- there is no generic version. This is because in almost all cases the type is determined at runtime (usually from the node type dropdown in the editor -- more on that in a future post), so a generic option wouldn't be useful.

Node destruction also becomes more complicated. Even if we remove all references to the node it won't automatically be deleted because it's still part of the asset file. If we just ignored this nothing would break per se, but nodes would never be deleted and eventually the asset file would be full of dead nodes. To solve this problem we add another helper method:

void DeleteNode( TreeNode node )
{
	// Delete all children.
	if ( node is Decorator )
	{
		DeleteNode( ( (Decorator)node )._child );
	}
	else if ( node is Compositor )
	{
		foreach ( TreeNode child in ( (Compositor)node )._children )
		{
			DeleteNode( child );
		}
	}

	// Remove node from asset file.
	DestroyImmediate( node, true );
}

Removing the node from the asset file is easy enough: DestroyImmediate() handles both destroying the node and removing it from the file. Note, though, that we have to pass true in as the second parameter. This tells Unity that we want it to also remove the node from the asset file, otherwise it'll throw an exception.

We also have to manually handle destroying the node's children. This was handled automatically before by the GC, but now that we aren't using the GC to track the objects its up to us. Fortunately it's as simple as recursively calling DeleteNode() on all of node's children.

Other Considerations

There's one other problem, though I don't yet have a solution for this one. This is how a custom asset looks when it only has a single object in it:

Single object assets.

The assets as they appear in the explorer have the type of the object used to create them, meaning if you have an inspector slot that's expecting a behavior tree, you can drag one of the custom behavior tree objects over to it and it will work. However, this is how assets look when you put multiple objects in them:

Multi-object asset

Instead of having a single object, all the objects show up under a single header file. In this case, the only object with the behavior tree type is the last one under each asset. The root asset doesn't have a type that I can tell, which means you can't just drag the root object to a variable slot in the inspector, you have to expand it and grab the last one. This is an unfortunate side effect of the change, and it makes using the behavior trees less ergonomic than I would like, but it's still substantially better than the alternative.

David LeGare

David is a game developer and systems programmer. He likes cats, he tolerates dogs, and he often remembers to dress himself before leaving the house.

Read More