Skip to content

Instantly share code, notes, and snippets.

@AmbientLion
Last active February 19, 2024 22:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AmbientLion/d4dec00161a971bb9cf6adffa6cfd766 to your computer and use it in GitHub Desktop.
Save AmbientLion/d4dec00161a971bb9cf6adffa6cfd766 to your computer and use it in GitHub Desktop.
Unity EditorWindow - Copying Parameters Between AnimatorController Assets.
/// Author: AmbientLion@github
/// Purpose: This is an Odin-based (https://odininspector.com) editor tool window that simplifies
/// the work for copying AnimatorController parameters from one controller to another.
using System;
using System.Collections.Generic;
using System.Linq;
using Sirenix.OdinInspector;
using Sirenix.OdinInspector.Editor;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
namespace Utility.Editor {
public class AnimatorHelperWindow : OdinEditorWindow {
public enum HandleExisting {
[Tooltip("Deletes all parameters before copying from the source.")]
DeleteAll = 1,
[Tooltip("Deletes only the selected (same-named) parameters before copying from the source.")]
DeleteSelected = 2,
[Tooltip("Skips over existing (same-named) parameters, copying only ones that don't exist.")]
SkipExisting = 3,
[Tooltip("Overwrites only already existing (same-named) parameters in target, updating their types.")]
OnlyExisting = 4,
[Tooltip("Fails the copy operation (without affecting the target) if any parameters already exist.")]
FailIfAnyExist = 999,
}
[MenuItem("Mythica/Utils/AnimatorHelper")]
private static void OpenWindow() {
GetWindow<AnimatorHelperWindow>().Show();
}
[DetailedInfoBox("Usage Instructions", "This helper will copy parameters from a source animator" +
" controller to the target controller. If the 'Clear Existing' option is set, any existing parameter will be removed" +
" before copying. Existing parameters of the same name will be replaced in the target. Use this help with caution as" +
" the changes cannot be undone. It is recommended to use a copy of the target controller (or have a backup in VCS).")]
[VerticalGroup(GroupName = "Animators")]
[LabelText("Source", SdfIconType.Clipboard)]
[PropertyTooltip("The source animation controller from which to copy parameters.")]
[OnValueChanged("ExtractParameters")]
public AnimatorController sourceController;
[VerticalGroup]
[LabelText("Target", SdfIconType.Command)]
[PropertyTooltip("The destination animator controller to which to copy parameters to.")]
public AnimatorController targetController;
[VerticalGroup]
[LabelText("Handle Existing")]
[Tooltip("Defines how existing/mismatched parameters in the source/target are handled.")]
public HandleExisting existingParameterHandling = HandleExisting.SkipExisting;
[VerticalGroup(GroupName = "Parameters")]
[TableList(AlwaysExpanded = true, IsReadOnly = true)]
[ShowInInspector]
public List<ParameterSelection> SourceParameters {
get => m_ExtractedParameters;
set => m_ExtractedParameters = value;
}
// used by the DeleteParameters() method below to control delete button enabled state
private bool CanDelete => targetController != null || sourceController != null;
// used by the CopyParameters() method below to control Copy button enabled state
private bool CanCopy => targetController != null && sourceController != null;
private List<ParameterSelection> m_ExtractedParameters = new();
private int SelectedCount => m_ExtractedParameters.Count(x => x.Selected);
private IEnumerable<ParameterSelection> SelectedParameters => m_ExtractedParameters.Where(x => x.Selected);
[HorizontalGroup]
[Button("$DeleteButtonName", ButtonSizes.Large), GUIColor(1, 0, 0)]
[EnableIf("CanDelete")]
[ShowIf("CanDelete")]
[Tooltip("Delete the parameters that are selected; or all parameters if there is no selection.")]
public void DeleteParameters() {
if (!targetController && !sourceController) {
Debug.LogWarning("No target or source AnimatorController selected.");
return;
}
var fromController = targetController ? targetController : sourceController;
if (fromController != null) {
var countToDelete = MaxIfZero(SelectedCount, sourceController.parameters.Length);
var confirmDelete = EditorUtility.DisplayDialog(
"Confirm Irreversible Action",
$"Are you sure you want to PERMANENTLY delete {countToDelete} parameters from '{fromController.name}'?",
"Yes, Delete!", "No, Stop!");
if (!confirmDelete) {
Debug.LogWarning("Aborting parameter deletion at user request!");
return;
}
}
if (SelectedCount > 0) {
DeleteSelectedParameters(fromController);
} else {
DeleteAllParameters(fromController);
}
ExtractParameters();
}
[HorizontalGroup]
[EnableIf("$CanCopy")]
[ShowIf("CanCopy")]
[Button("$CopyButtonName", ButtonSizes.Large), GUIColor(0, 1, 0)]
[Tooltip("Copy the parameters that are selected; or all parameters if there is no selection.")]
public void CopyParameters() {
if (!sourceController) {
Debug.LogError("No source AnimatorController specified.");
EditorApplication.Beep();
return;
}
if (!targetController) {
Debug.LogError("No target AnimatorController specified.");
EditorApplication.Beep();
return;
}
switch (existingParameterHandling) {
case HandleExisting.DeleteAll:
CopyAndDeleteAllParameters();
break;
case HandleExisting.DeleteSelected:
CopyAndDeleteSelectedParameters();
break;
case HandleExisting.SkipExisting:
CopyAndSkipExistingParameters();
break;
case HandleExisting.OnlyExisting:
CopyAndSyncOnlyExistingParameters();
break;
case HandleExisting.FailIfAnyExist:
CopyAndFailWhenExistingParameters();
break;
default:
Debug.LogError($"Unrecognized parameter handling option: {existingParameterHandling}");
EditorApplication.Beep();
break;
}
ExtractParameters();
}
public string CopyButtonName => DynamicName("Copy");
public string DeleteButtonName => DynamicName("Delete");
private string DynamicName(string root) {
var selectedCount = m_ExtractedParameters?.Count(x => x.Selected) ?? 0;
return selectedCount switch {
0 => $"{root} All Parameters",
1 => $"{root} 1 Parameter",
_ => $"{root} {selectedCount} Parameters"
};
}
protected override void Initialize() {
base.Initialize();
ExtractParameters();
}
private void DeleteAllParameters(AnimatorController controller) {
if (controller) {
while (controller.parameters.Length > 0) {
controller.RemoveParameter(0);
}
}
}
private void DeleteSelectedParameters(AnimatorController controller) {
if (controller) {
int countDeleted = 0;
foreach (var parameter in m_ExtractedParameters.Where(parameter => parameter.Selected)) {
countDeleted += RemoveExistingParameter(controller, parameter.Name) ? 1 : 0;
}
Debug.Log($"Deleted {countDeleted} parameters from {controller.name}");
}
}
private void CopyAndDeleteAllParameters() {
DeleteAllParameters(targetController);
CopySelectedParameters();
}
private void CopyAndDeleteSelectedParameters() {
DeleteSelectedParameters(targetController);
CopySelectedParameters();
}
private void CopyAndSkipExistingParameters() {
var existingParams = FindExistingParameters();
CopySelectedParameters(exclude: existingParams);
}
private void CopyAndSyncOnlyExistingParameters() {
var existingParams = FindExistingParameters();
CopySelectedParameters(include: existingParams);
}
private void CopyAndFailWhenExistingParameters() {
var existingParams = FindExistingParameters();
if (existingParams.Count == 0) {
CopySelectedParameters();
} else {
Debug.LogError($"Copy operation aborted: {existingParams.Count} selected parameter(s) already exist.");
EditorApplication.Beep();
}
}
/// <summary>
/// Returns a list of the parameters on the target that already exist in the selection (if any).
/// When there is no selection of parameters, it returns all of the parameters that exist in both source and target.
/// </summary>
/// <returns></returns>
private HashSet<string> FindExistingParameters() {
if (sourceController && targetController) {
var sourceParams = SelectedParameters.Any()
? SelectedParameters.Select(x => x.Name)
: sourceController.parameters.Select(x => x.name);
var targetParams = targetController.parameters
.Select(x => x.name)
.ToHashSet();
targetParams.IntersectWith(sourceParams);
return targetParams;
}
return new();
}
private void CopySelectedParameters(
[CanBeNull] HashSet<string> include = null,
[CanBeNull] HashSet<string> exclude = null) {
if (!sourceController || !targetController) return;
// copy the included (but not excluded) parameters
int countCopied = 0;
var includedSet = include ?? SelectedParametersSet();
foreach (var param in sourceController.parameters) {
var shouldCopy = includedSet?.Contains(param.name) ?? true; // default to true if include == null
var shouldSkip = exclude?.Contains(param.name) ?? false; // default to false if exclude == null
if (shouldCopy && !shouldSkip) {
targetController.AddParameter(param);
countCopied++;
}
}
Debug.Log($"Copied {countCopied} parameters from '{sourceController.name}' to '{targetController.name}'");
}
private bool RemoveExistingParameter(AnimatorController controller, string paramName) {
if (controller && controller.parameters.Length > 0) {
var index = Array.FindIndex(controller.parameters, x => x.name == paramName);
if(index >= 0) {
controller.RemoveParameter(index);
return true;
}
}
return false;
}
private void ExtractParameters() {
m_ExtractedParameters.Clear();
if (sourceController) {
int index = 0;
foreach (var param in sourceController.parameters) {
m_ExtractedParameters.Add( new(param.name, index++) );
}
}
}
private int MaxIfZero(int value, int valueIfZero) => value > 0 ? value : valueIfZero;
private HashSet<string> SelectedParametersSet() {
return SelectedParameters.Any() ? SelectedParameters.Select(x => x.Name).ToHashSet() : null;
}
}
public class ParameterSelection {
private readonly string m_ParameterName;
private readonly int m_ParameterIndex;
public ParameterSelection(string paramName, int paramIndex) {
m_ParameterName = paramName;
m_ParameterIndex = paramIndex;
}
[TableColumnWidth(25)]
[ShowInInspector]
public bool Selected;
[TableColumnWidth(275)] [ShowInInspector] public string Name => m_ParameterName;
public int Index => m_ParameterIndex;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment