diff --git a/Packages/nadena.dev.modular-avatar/Editor/Menu/ActionProcessing/ActionGenerator.cs b/Packages/nadena.dev.modular-avatar/Editor/Menu/ActionProcessing/ActionGenerator.cs index c4b61927..c8d4a7a3 100644 --- a/Packages/nadena.dev.modular-avatar/Editor/Menu/ActionProcessing/ActionGenerator.cs +++ b/Packages/nadena.dev.modular-avatar/Editor/Menu/ActionProcessing/ActionGenerator.cs @@ -208,16 +208,48 @@ namespace nadena.dev.modular_avatar.core.editor List expParameters = expParams.parameters.ToList(); List blendTrees = new List(); - int index = 0; + Dictionary> groupedItems = + new Dictionary>(); + foreach (var item in items) { - var paramname = "_MA/A/" + item.gameObject.name + "/" + (index++); - // TODO toggle group handling + List group; + if (item.toggleGroup) + { + if (!groupedItems.TryGetValue(item.toggleGroup, out group)) + { + group = new List(); + groupedItems.Add(item.toggleGroup, group); + } + } + else + { + group = new List(); + groupedItems.Add(item, group); + } + + group.Add(item); + } + + int paramIndex = 0; + foreach (var kvp in groupedItems) + { + // sort default first + var group = kvp.Value; + group.Sort((a, b) => b.isDefault.CompareTo(a.isDefault)); + + // Generate parameter + var paramname = "_MA/A/" + kvp.Key.gameObject.name + "/" + (paramIndex++); + + var expParamType = group.Count > 1 + ? VRCExpressionParameters.ValueType.Int + : VRCExpressionParameters.ValueType.Bool; + expParameters.Add(new VRCExpressionParameters.Parameter() { name = paramname, defaultValue = 0, // TODO - valueType = VRCExpressionParameters.ValueType.Bool, + valueType = expParamType, saved = false, // TODO }); acParameters.Add(new AnimatorControllerParameter() @@ -227,31 +259,48 @@ namespace nadena.dev.modular_avatar.core.editor defaultFloat = 0, // TODO }); - item.Control.parameter = new VRCExpressionsMenu.Control.Parameter() {name = paramname}; - item.Control.value = 1; + var hasDefault = group[0].isDefault; + for (int i = 0; i < group.Count; i++) + { + var control = group[i].Control; + control.parameter = new VRCExpressionsMenu.Control.Parameter() {name = paramname}; + control.value = hasDefault ? i : i + 1; + } var blendTree = new BlendTree(); blendTree.name = paramname; blendTree.blendParameter = paramname; blendTree.blendType = BlendTreeType.Simple1D; blendTree.useAutomaticThresholds = false; - blendTree.children = new[] + + List children = new List(); + + List motions = GenerateMotions(group, out var inactiveMotion); + + if (!hasDefault) { - new ChildMotion() + motions.Insert(0, inactiveMotion); + } + + for (int i = 0; i < motions.Count; i++) + { + children.Add(new ChildMotion() { - motion = GenerateMotion(item, false), - position = new Vector2(0, 0), - threshold = 0.25f, + motion = motions[i], + position = new Vector2(i, 0), + threshold = i - 0.1f, timeScale = 1, - }, - new ChildMotion() + }); + children.Add(new ChildMotion() { - motion = GenerateMotion(item, true), - position = new Vector2(1, 0), - threshold = 0.75f, + motion = motions[i], + position = new Vector2(i, 0), + threshold = i + 0.1f, timeScale = 1, - }, - }; + }); + } + + blendTree.children = children.ToArray(); _context.SaveAsset(blendTree); blendTrees.Add(blendTree); @@ -263,18 +312,16 @@ namespace nadena.dev.modular_avatar.core.editor return blendTrees; } - private Motion GenerateMotion(ModularAvatarMenuItem item, bool active) + void MergeCurves( + IDictionary curves, + ModularAvatarMenuItem item, + Func> getCurves, + bool ignoreDuplicates + ) { - AnimationClip clip = new AnimationClip(); - _context.SaveAsset(clip); - clip.name = item.gameObject.name + (active ? " (On)" : " (Off)"); - - Dictionary curves = - new Dictionary(); - foreach (var action in item.GetComponents()) { - var newCurves = active ? action.GetCurves() : action.GetDefaultCurves(); + var newCurves = getCurves(action); foreach (var curvePair in newCurves) { @@ -283,7 +330,7 @@ namespace nadena.dev.modular_avatar.core.editor if (curves.TryGetValue(binding, out var existing)) { - if (active) + if (!ignoreDuplicates) { BuildReport.LogFatal("animation_gen.conflict", new object[] { @@ -301,7 +348,67 @@ namespace nadena.dev.modular_avatar.core.editor } } } + } + private List GenerateMotions(List items, out Motion inactiveMotion) + { + Dictionary inactiveCurves = + new Dictionary(); + + var defaultItems = items.Where(i => i.isDefault).ToList(); + if (defaultItems.Count > 1) + { + BuildReport.LogFatal("animation_gen.multiple_defaults", Array.Empty(), + defaultItems.ToArray()); + defaultItems.RemoveRange(1, defaultItems.Count - 1); + } + + MergeCurves(inactiveCurves, defaultItems[0], a => a.GetInactiveCurves(true), false); + + foreach (var item in items) + { + if (defaultItems.Count == 0 || defaultItems[0] != item) + { + MergeCurves(inactiveCurves, item, a => a.GetInactiveCurves(false), true); + } + } + + inactiveMotion = CurvesToMotion(inactiveCurves); + var groupName = (items[0].toggleGroup != null + ? items[0].toggleGroup.gameObject.name + : items[0].gameObject.name); + inactiveMotion.name = + groupName + + " (Inactive)"; + + List motions = new List(); + + foreach (var item in items) + { + Dictionary activeCurves = + new Dictionary(); + + MergeCurves(activeCurves, item, a => a.GetCurves(), false); + foreach (var kvp in inactiveCurves) + { + if (!activeCurves.ContainsKey(kvp.Key)) + { + activeCurves.Add(kvp.Key, kvp.Value); + } + } + + var clip = CurvesToMotion(activeCurves); + clip.name = groupName + " (" + item.gameObject.name + ")"; + motions.Add(clip); + } + + return motions; + } + + Motion CurvesToMotion(IDictionary curves) + { + var clip = new AnimationClip(); + _context.SaveAsset(clip); foreach (var entry in curves) { clip.SetCurve( diff --git a/Packages/nadena.dev.modular-avatar/Runtime/Menu/Actions/ActionToggleObject.cs b/Packages/nadena.dev.modular-avatar/Runtime/Menu/Actions/ActionToggleObject.cs index dcc9b567..7134a229 100644 --- a/Packages/nadena.dev.modular-avatar/Runtime/Menu/Actions/ActionToggleObject.cs +++ b/Packages/nadena.dev.modular-avatar/Runtime/Menu/Actions/ActionToggleObject.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using UnityEngine; +using Object = System.Object; namespace nadena.dev.modular_avatar.core { @@ -46,8 +47,18 @@ namespace nadena.dev.modular_avatar.core public interface MenuAction { + /// + /// Returns the curves applied when this action is active + /// + /// ImmutableDictionary GetCurves(); - ImmutableDictionary GetDefaultCurves(); + + /// + /// Returns the curves applied when this action is inactive (and no other actions override). + /// + /// True if this action is part of the default toggle option. + /// + ImmutableDictionary GetInactiveCurves(bool isDefault); } [RequireComponent(typeof(ModularAvatarMenuItem))] @@ -71,12 +82,24 @@ namespace nadena.dev.modular_avatar.core ).ToImmutableDictionary(); } - public ImmutableDictionary GetDefaultCurves() + public ImmutableDictionary GetInactiveCurves(bool isDefault) { return Objects.Select(obj => - new KeyValuePair( - new MenuCurveBinding(obj.target, typeof(GameObject), "m_IsActive"), - AnimationCurve.Constant(0, 1, obj.target.activeSelf ? 1 : 0)) + { + bool active; + if (isDefault) + { + active = !obj.Active; // inactive state is the opposite of the default state + } + else + { + active = obj.target.activeSelf; // inactive state is the current state + } + + return new KeyValuePair( + new MenuCurveBinding(obj.target, typeof(GameObject), "m_IsActive"), + AnimationCurve.Constant(0, 1, active ? 1 : 0)); + } ).ToImmutableDictionary(); } } diff --git a/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuItem.cs b/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuItem.cs index 79c35be5..272c98ee 100644 --- a/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuItem.cs +++ b/Packages/nadena.dev.modular-avatar/Runtime/ModularAvatarMenuItem.cs @@ -20,6 +20,9 @@ namespace nadena.dev.modular_avatar.core public GameObject menuSource_otherObjectChildren; + public ToggleGroup toggleGroup; + public bool isDefault; + public override void Visit(NodeContext context) { var cloned = new VirtualControl(Control);