Last active
November 9, 2024 09:47
-
-
Save AmbientLion/d4dec00161a971bb9cf6adffa6cfd766 to your computer and use it in GitHub Desktop.
Unity EditorWindow - Copying Parameters Between AnimatorController Assets.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// 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