Skip to content

Commit

Permalink
WIP: ObjectPathRemapper
Browse files Browse the repository at this point in the history
  • Loading branch information
bdunderscore committed Nov 10, 2024
1 parent f9e3480 commit b3b21c3
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 7 deletions.
138 changes: 138 additions & 0 deletions Editor/API/AnimatorServices/ObjectPathRemapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
using System.Collections.Generic;
using nadena.dev.ndmf.runtime;
using UnityEngine;

namespace nadena.dev.ndmf.animator
{
/// <summary>
/// The ObjectPathRemapper is used to track GameObject movement in the hierarchy, and to update animation paths
/// accordingly.
/// While the ObjectPathRemapper is active, there are a few important rules around hierarchy and animation
/// maniuplation that must be observed.
/// 1. The ObjectPathRemapper takes a snapshot of the object paths that were present at time of activation;
/// as such, any new animations added while it is active must use those paths, not those of the current
/// hierarchy. To help with this, you can use the `GetVirtualPathForObject` method to get the path that
/// should be used for newly generated animations.
/// 2. Objects can be moved freely within the hierarchy; however, if you want to _remove_ an object, you must call
/// `ReplaceObject` on it first.
/// 3. Objects can be freely added; if you want to use those objects in animations, use `GetVirtualPathForObject`
/// to get the path that should be used. This will automatically register that object, if necessary. If you'd
/// like to use animation clips with pre-existing paths on, for example, a newly instantiated prefab hierarchy,
/// use `RecordObjectTree` to ensure that those objects have their current paths recorded first. This ensures
/// that if those objects are moved in later stages, the paths will be updated appropriately.
/// Note that it's possible that these paths may collide with paths that _previously_ existed, so it's still
/// recommended to use `GetVirtualPathForObject` to ensure that the path is unique.
/// </summary>
public class ObjectPathRemapper
{
private readonly Transform _root;
private readonly Dictionary<Transform, List<string>> _objectToOriginalPaths = new();
private readonly Dictionary<string, Transform> _pathToObject = new();

private bool _cacheValid;
private readonly Dictionary<string, string> _originalToMappedPath = new();

internal ObjectPathRemapper(Transform root)
{
_root = root;
RecordObjectTree(root);
}

public void ApplyChanges(AnimationIndex index)
{
UpdateCache();

index.RewritePaths(_originalToMappedPath);
}

private void UpdateCache()
{
if (_cacheValid) return;

_originalToMappedPath.Clear();

foreach (var kvp in _objectToOriginalPaths)
{
foreach (var path in kvp.Value)
{
_originalToMappedPath[path] = GetVirtualPathForObject(kvp.Key);
}
}
}

public void RecordObjectTree(Transform subtree)
{
GetVirtualPathForObject(subtree);

foreach (Transform child in subtree)
{
RecordObjectTree(child);
}
}

public GameObject GetObjectForPath(string path)
{
var xform = _pathToObject.GetValueOrDefault(path);
return xform ? xform.gameObject : null;
}

public string GetVirtualPathForObject(GameObject obj)
{
return GetVirtualPathForObject(obj.transform);
}

public string GetVirtualPathForObject(Transform t)
{
if (_objectToOriginalPaths.TryGetValue(t, out var paths))
{
return paths[0];
}

var path = RuntimeUtil.RelativePath(_root, t);
if (path == null) return null;

if (_pathToObject.ContainsKey(path))
{
path += "###PENDING_" + t.GetInstanceID();
}

_objectToOriginalPaths[t] = new List<string> { path };
_pathToObject[path] = t;
_cacheValid = false;

return path;
}

public void ReplaceObject(GameObject old, GameObject newObject)
{
ReplaceObject(old.transform, newObject.transform);
}

public void ReplaceObject(Transform old, Transform newObject)
{
if (!_objectToOriginalPaths.TryGetValue(old, out var paths)) return;

if (_objectToOriginalPaths.ContainsKey(newObject))
{
_objectToOriginalPaths[newObject].AddRange(paths);
}
else
{
_objectToOriginalPaths[newObject] = paths;
}

_objectToOriginalPaths.Remove(old);
foreach (var path in paths)
{
_pathToObject[path] = newObject;
}
}

public string MapPath(string originalPath)
{
UpdateCache();

return _originalToMappedPath.GetValueOrDefault(originalPath, originalPath);
}
}
}
3 changes: 3 additions & 0 deletions Editor/API/AnimatorServices/ObjectPathRemapper.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 19 additions & 7 deletions Runtime/RuntimeUtil.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;

#if NDMF_VRCSDK3_AVATARS
using VRC.SDK3.Avatars.Components;
#endif
Expand Down Expand Up @@ -43,7 +43,7 @@ internal static T GetOrAddComponent<T>(this Component obj) where T : Component
/// Returns whether the editor is in play mode.
/// </summary>
#if UNITY_EDITOR
public static bool IsPlaying => UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode;
public static bool IsPlaying => EditorApplication.isPlayingOrWillChangePlaymode;
#else
public static bool IsPlaying => true;
#endif
Expand All @@ -56,22 +56,34 @@ internal static T GetOrAddComponent<T>(this Component obj) where T : Component
/// <returns></returns>
[CanBeNull]
public static string RelativePath(GameObject root, GameObject child)
{
return RelativePath(root.transform, child.transform);
}

/// <summary>
/// Returns the relative path from root to child, or null is child is not a descendant of root.
/// </summary>
/// <param name="root"></param>
/// <param name="child"></param>
/// <returns></returns>
[CanBeNull]
public static string RelativePath(Transform root, Transform child)
{
if (root == child) return "";

List<string> pathSegments = new List<string>();
var pathSegments = new List<string>();
while (child != root && child != null)
{
pathSegments.Add(child.name);
child = child.transform.parent?.gameObject;
pathSegments.Add(child.gameObject.name);
child = child.parent;
}

if (child == null && root != null) return null;

pathSegments.Reverse();
return String.Join("/", pathSegments);
return string.Join("/", pathSegments);
}

/// <summary>
/// Returns the path of a game object relative to the avatar root, or null if the avatar root could not be
/// located.
Expand Down

0 comments on commit b3b21c3

Please sign in to comment.