How to modify an ExternalBehaviorTree via editor code

rhys_vdw

New member
I'm trying to migrate my BT's to use a new node, but the code misbehaves. They claim to have no root node. I have tried to force the assets to load by assigning them to a GameObject and calling `EnableBehavior`, but that doesn't work either.

In every case `tree.BehaviorSource.RootTask` is null. 😐

C#:
[MenuItem("Effort Star/Migrate/Migrate use inventory item BT action")]
public static void MigrateUseInventory() {
  var go = new GameObject { hideFlags = HideFlags.HideAndDontSave };

  try {
    var btComponent = go.AddComponent<BehaviorTree>();
    var seen = new HashSet<ExternalBehaviorTree>();
    foreach (var actor in AssetUtility.LoadAssetsOfType<ActorConfig>()) {
      var tree = actor.ExternalBehaviorTree;
      if (tree == null) {
        continue;
      }
      if (!seen.Add(tree)) {
        Debug.LogWarning($"Skipped '{tree.name}' as it was already processed", tree);
        continue;
      }

      // Init tree.
      btComponent.ExternalBehavior = tree;
      btComponent.EnableBehavior();

      if (tree.BehaviorSource.RootTask == null) {
        Debug.LogWarning($"'{tree.name}' has no root task!", tree); // <-- point of failure
        continue;
      }
      var changed = ReplaceChildTasks(
        tree.BehaviorSource.RootTask,
        (UseInventoryItemAction from) => {
          var index = from.Index;
          var item = actor.InitialItems[index];
          return new UseItemAction(item);
        },
        tree.name
      );
      if (changed) EditorUtility.SetDirty(tree);
    }
  } finally {
    UnityEngine.Object.DestroyImmediate(go);
  }
}

static FieldInfo _btChildrenField = typeof(ParentTask).GetField(
  "_children",
  BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic
);

static bool ReplaceChildTasks<TFrom, TTo>(
  Task root, Func<TFrom, TTo> cb, string graphName
) where TFrom : Task where TTo : Task {
  if (root.GetType().Equals(typeof(TFrom))) {
    throw new InvalidOperationException("Can't replace root task");
  }

  if (root is not ParentTask { Children: not null } parent) {
    return false;
  }

  var anyChanged = false;
  for (int i = 0; i < parent.Children.Count; ++i) {
    var child = parent.Children[i];
    if (child is TFrom from) {
      var fromChildren = from is ParentTask { Children: not null } fp
        ? fp.Children
        : null;

      var nodeData = from.NodeData;

      var to = cb(from);
      parent.Children[i] = to;
      Debug.Log($"{graphName} Replaced {typeof(TFrom).Name} with {typeof(TTo).Name}");

      to.NodeData = nodeData;

      if (fromChildren != null) {
        if (to is not ParentTask toParent) {
          throw new InvalidOperationException($"Expected {typeof(TTo)} to be a parent task");
        }
        _btChildrenField.SetValue(toParent, fromChildren);
      }
    }
    anyChanged |= ReplaceChildTasks(child, cb, graphName);
  }

  return anyChanged;
}
 
It doesn't look like you are deserializing the tree. You can do so by calling ExternalBehavior.Init.
 
Hey @Justin. Cool, that has helped, thanks!

So my general problem here is that I'm trying to replace one old node type (`UseInventoryItemAction`) with another (`UseItemAction`). The problem is that when I do the replacement (by replacing `ParentNode.Children` with a new node type.

I know it's changing the data structure, because when I run the migration again it doesn't find any `UseInventoryItemAction` instances. However, even though I'm marking the tree dirty and saving project, I get no changes on disk. And when I open the graph the old nodes are still there.

I'm very confused.

Here's the code if it helps:


(I'm sharing a gist because there's something wrong with this website on my browser. It's showing the code as light yellow on white)
 
It doesn't look like you are serializing the tree. When you save the project make sure you first serialize the new tree. Here's a good example:

 
Back
Top