feat: support unsynced/saved toggles in new menu system (#276)
* chore: add support for synced/saved settings on menu actions * feat: move action defaults to control group * chore: finish the control group ui updates * docs: update tutorial * docs: update control group documentation * docs/ui: menu install target UI and docs
@ -138,7 +138,7 @@ namespace nadena.dev.modular_avatar.editor.ErrorReporting
|
||||
private static List<ErrorLog> CheckInternal(ModularAvatarMenuInstaller mi)
|
||||
{
|
||||
// TODO - check that target menu is in the avatar
|
||||
if (mi.menuToAppend == null && mi.GetComponent<MenuSourceComponent>() == null)
|
||||
if (mi.menuToAppend == null && mi.GetComponent<MenuSource>() == null)
|
||||
{
|
||||
return new List<ErrorLog>()
|
||||
{
|
||||
|
@ -2,19 +2,68 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.IMGUI.Controls;
|
||||
using UnityEngine;
|
||||
using static nadena.dev.modular_avatar.core.editor.Localization;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
internal class ControlGroupDefaultDropdown : AdvancedDropdown
|
||||
{
|
||||
public string[] _names { get; private set; }
|
||||
public ModularAvatarMenuItem[] _menuItems { get; private set; }
|
||||
|
||||
public event Action<ModularAvatarMenuItem> OnItemSelected;
|
||||
|
||||
public ControlGroupDefaultDropdown(ModularAvatarMenuItem[] menuItems) : base(new AdvancedDropdownState())
|
||||
{
|
||||
_names = menuItems.Select(n =>
|
||||
{
|
||||
if (n == null || n.gameObject == null)
|
||||
{
|
||||
return Localization.S("control_group.default_value.unset");
|
||||
}
|
||||
else
|
||||
{
|
||||
return n.gameObject.name;
|
||||
}
|
||||
}).ToArray();
|
||||
_menuItems = menuItems;
|
||||
}
|
||||
|
||||
protected override AdvancedDropdownItem BuildRoot()
|
||||
{
|
||||
var root = new AdvancedDropdownItem(S("control_group.default_value"));
|
||||
for (int i = 0; i < _names.Length; i++)
|
||||
{
|
||||
var item = new AdvancedDropdownItem(_names[i]);
|
||||
item.id = i;
|
||||
root.AddChild(item);
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
protected override void ItemSelected(AdvancedDropdownItem item)
|
||||
{
|
||||
OnItemSelected?.Invoke(_menuItems[item.id]);
|
||||
}
|
||||
}
|
||||
|
||||
[CustomEditor(typeof(ControlGroup))]
|
||||
internal class ControlGroupInspector : MAEditorBase
|
||||
{
|
||||
private bool _showInner;
|
||||
private SerializedProperty _isSynced, _isSaved, _defaultValue;
|
||||
private ControlGroupDefaultDropdown _dropdown;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
EditorApplication.hierarchyChanged += Invalidate;
|
||||
|
||||
_isSynced = serializedObject.FindProperty(nameof(ControlGroup.isSynced));
|
||||
_isSaved = serializedObject.FindProperty(nameof(ControlGroup.isSaved));
|
||||
_defaultValue = serializedObject.FindProperty(nameof(ControlGroup.defaultValue));
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
@ -31,11 +80,22 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
var menuItems = avatar.GetComponentsInChildren<ModularAvatarMenuItem>(true);
|
||||
|
||||
_menuItemActions = new List<Action>();
|
||||
var filteredMenuItems = new List<ModularAvatarMenuItem>();
|
||||
foreach (var menuItem in menuItems.Where(item => item.controlGroup == target))
|
||||
{
|
||||
var node = CreateMenuItemNode(menuItem);
|
||||
_menuItemActions.Add(node);
|
||||
filteredMenuItems.Add(menuItem);
|
||||
}
|
||||
|
||||
filteredMenuItems.Insert(0, null);
|
||||
_dropdown = new ControlGroupDefaultDropdown(filteredMenuItems.ToArray());
|
||||
_dropdown.OnItemSelected += (item) =>
|
||||
{
|
||||
serializedObject.Update();
|
||||
_defaultValue.objectReferenceValue = item;
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
};
|
||||
}
|
||||
|
||||
private Action CreateMenuItemNode(ModularAvatarMenuItem menuItem)
|
||||
@ -85,9 +145,43 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
};
|
||||
}
|
||||
|
||||
private Rect _dropdownButtonRect;
|
||||
|
||||
protected override void OnInnerInspectorGUI()
|
||||
{
|
||||
if (_menuItemActions == null) Invalidate();
|
||||
serializedObject.Update();
|
||||
|
||||
if (_menuItemActions == null || _dropdown == null) Invalidate();
|
||||
|
||||
EditorGUILayout.PropertyField(_isSynced, G("control_group.is_synced"));
|
||||
EditorGUILayout.PropertyField(_isSaved, G("control_group.is_saved"));
|
||||
//EditorGUILayout.PropertyField(_defaultValue, G("control_group.default_value")); // TODO - dropdown
|
||||
|
||||
if (_dropdown != null)
|
||||
{
|
||||
var label = G("control_group.default_value");
|
||||
var position = EditorGUILayout.GetControlRect(true);
|
||||
position = EditorGUI.PrefixLabel(position, label);
|
||||
|
||||
var currentValue = _defaultValue.objectReferenceValue;
|
||||
string selected;
|
||||
|
||||
if (currentValue == null || !(currentValue is ModularAvatarMenuItem item) ||
|
||||
!item.controlGroup == target)
|
||||
{
|
||||
selected = S("control_group.default_value.unset");
|
||||
}
|
||||
else
|
||||
{
|
||||
selected = item.gameObject.name;
|
||||
}
|
||||
|
||||
if (GUI.Button(position, selected, EditorStyles.popup))
|
||||
{
|
||||
_dropdown.Show(position);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
_showInner = EditorGUILayout.Foldout(_showInner, G("control_group.foldout.menu_items"));
|
||||
if (_showInner)
|
||||
@ -108,6 +202,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
}
|
||||
}
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
|
||||
Localization.ShowLanguageUI();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,16 @@
|
||||
using UnityEditor;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core.editor
|
||||
{
|
||||
[CustomEditor(typeof(ModularAvatarMenuInstallTarget))]
|
||||
internal class MenuInstallTargetEditor : MAEditorBase
|
||||
{
|
||||
protected override void OnInnerInspectorGUI()
|
||||
{
|
||||
serializedObject.Update();
|
||||
EditorGUILayout.PropertyField(
|
||||
serializedObject.FindProperty(nameof(ModularAvatarMenuInstallTarget.installer)));
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4039f724f1af412c98e176c44f0d7734
|
||||
timeCreated: 1681547765
|
@ -353,7 +353,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
foreach (var t in targets)
|
||||
{
|
||||
var installer = (ModularAvatarMenuInstaller) t;
|
||||
if (installer.GetComponent<MenuSourceComponent>() || installer.menuToAppend == null) continue;
|
||||
if (installer.GetComponent<MenuSource>() != null || installer.menuToAppend == null) continue;
|
||||
|
||||
var menu = installer.menuToAppend;
|
||||
if (menu.controls.Count == 0)
|
||||
|
@ -30,7 +30,6 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
private readonly SerializedProperty _value;
|
||||
private readonly SerializedProperty _submenu;
|
||||
private readonly SerializedProperty _controlGroup;
|
||||
private readonly SerializedProperty _isDefault;
|
||||
|
||||
private readonly ParameterGUI _parameterGUI;
|
||||
|
||||
@ -46,6 +45,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
private readonly SerializedProperty _prop_submenuSource;
|
||||
private readonly SerializedProperty _prop_otherObjSource;
|
||||
private readonly SerializedProperty _prop_isSynced, _prop_isSaved;
|
||||
|
||||
public bool AlwaysExpandContents = false;
|
||||
public bool ExpandContents = false;
|
||||
@ -92,7 +92,6 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
_name = gameObjects.FindProperty("m_Name");
|
||||
_controlGroup = obj.FindProperty(nameof(ModularAvatarMenuItem.controlGroup));
|
||||
_isDefault = obj.FindProperty(nameof(ModularAvatarMenuItem.isDefault));
|
||||
|
||||
var control = obj.FindProperty(nameof(ModularAvatarMenuItem.Control));
|
||||
|
||||
@ -110,6 +109,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
_prop_submenuSource = obj.FindProperty(nameof(ModularAvatarMenuItem.MenuSource));
|
||||
_prop_otherObjSource = obj.FindProperty(nameof(ModularAvatarMenuItem.menuSource_otherObjectChildren));
|
||||
_prop_isSynced = obj.FindProperty(nameof(ModularAvatarMenuItem.isSynced));
|
||||
_prop_isSaved = obj.FindProperty(nameof(ModularAvatarMenuItem.isSaved));
|
||||
_previewGUI = new MenuPreviewGUI(redraw);
|
||||
}
|
||||
|
||||
@ -133,7 +134,6 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
_submenu = _control.FindPropertyRelative(nameof(VRCExpressionsMenu.Control.subMenu));
|
||||
|
||||
_controlGroup = null;
|
||||
_isDefault = null;
|
||||
|
||||
_parameterGUI = new ParameterGUI(parameterReference, parameter, redraw);
|
||||
|
||||
@ -142,6 +142,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
_prop_submenuSource = null;
|
||||
_prop_otherObjSource = null;
|
||||
_prop_isSynced = null;
|
||||
_prop_isSaved = null;
|
||||
_previewGUI = new MenuPreviewGUI(redraw);
|
||||
}
|
||||
|
||||
@ -165,12 +167,11 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
if (_hasActions)
|
||||
{
|
||||
EditorGUILayout.PropertyField(_controlGroup, G("menuitem.prop.control_group"));
|
||||
if (_controlGroup.hasMultipleDifferentValues || _controlGroup.objectReferenceValue != null)
|
||||
|
||||
if (!_controlGroup.hasMultipleDifferentValues && _controlGroup.objectReferenceValue == null)
|
||||
{
|
||||
using (new EditorGUI.DisabledScope(_obj.isEditingMultipleObjects))
|
||||
{
|
||||
EditorGUILayout.PropertyField(_isDefault, G("menuitem.prop.is_default"));
|
||||
}
|
||||
EditorGUILayout.PropertyField(_prop_isSynced, G("menuitem.prop.is_synced"));
|
||||
EditorGUILayout.PropertyField(_prop_isSaved, G("menuitem.prop.is_saved"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -102,6 +102,10 @@
|
||||
"menuitem.prop.submenu_source.tooltip": "Where to find the items to put inside this submenu",
|
||||
"menuitem.prop.source_override": "Source object override",
|
||||
"menuitem.prop.source_override.tooltip": "If specified, this object will be used as the source for the contents of the submenu. Otherwise, children of this menu item will be used.",
|
||||
"menuitem.prop.is_saved": "Saved",
|
||||
"menuitem.prop.is_saved.tooltip": "If true, the value of this menu item will be saved when you change avatars or worlds.",
|
||||
"menuitem.prop.is_synced": "Synced",
|
||||
"menuitem.prop.is_synced.tooltip": "If true, the value of this menu item will be synced to other players across the network.",
|
||||
"menuitem.param.rotation": "Parameter: Rotation",
|
||||
"menuitem.param.rotation.tooltip": "The parameter to set based on the rotation of this menu item",
|
||||
"menuitem.param.horizontal": "Parameter: Horizontal",
|
||||
@ -120,6 +124,12 @@
|
||||
"menuitem.param.controlled_by_action": "<controlled by action>",
|
||||
"control_group.foldout.actions": "Actions",
|
||||
"control_group.foldout.menu_items": "Bound menu items",
|
||||
"control_group.is_saved": "Saved",
|
||||
"control_group.is_saved.tooltip": "If true, the value of this menu item will be saved when you change avatars or worlds.",
|
||||
"control_group.is_synced": "Synced",
|
||||
"control_group.is_synced.tooltip": "If true, the value of this menu item will be synced to other players across the network.",
|
||||
"control_group.default_value": "Initial setting",
|
||||
"control_group.default_value.unset": "(none selected)",
|
||||
"menuitem.prop.control_group": "Control Group",
|
||||
"menuitem.prop.control_group.tooltip": "Only one toggle in a given group can be selected at the same time",
|
||||
"menuitem.prop.is_default": "Is Group Default",
|
||||
|
@ -99,6 +99,10 @@
|
||||
"menuitem.prop.submenu_source.tooltip": "このサブメニューの内容をどこから引用するべきかを指定",
|
||||
"menuitem.prop.source_override": "引用元オブジェクト",
|
||||
"menuitem.prop.source_override.tooltip": "指定した場合は、指定したオブジェクトの子をメニューの内容として指定します。指定されてない場合はこのオブジェクト直下の子を使用します。",
|
||||
"menuitem.prop.is_saved": "保存する",
|
||||
"menuitem.prop.is_saved.tooltip": "有効になっていると、アバター変更やワールド移動するときこの設定が保持されます。",
|
||||
"menuitem.prop.is_synced": "同期する",
|
||||
"menuitem.prop.is_synced.tooltip": "有効の場合はほかのプレイヤーに同期されます。",
|
||||
"menuitem.param.rotation": "回転パラメーター名",
|
||||
"menuitem.param.rotation.tooltip": "このメニューアイテムの回転に連動するべきパラメーター",
|
||||
"menuitem.param.horizontal": "横パラメーター名",
|
||||
@ -117,6 +121,12 @@
|
||||
"menuitem.param.controlled_by_action": "<アクションで制御されています>",
|
||||
"control_group.foldout.actions": "アクション",
|
||||
"control_group.foldout.menu_items": "関連付けされたメニューアイテム",
|
||||
"control_group.is_saved": "保存する",
|
||||
"control_group.is_saved.tooltip": "有効になっていると、アバター変更やワールド移動するときこの設定が保持されます。",
|
||||
"control_group.is_synced": "同期する",
|
||||
"control_group.is_synced.tooltip": "有効の場合はほかのプレイヤーに同期されます。",
|
||||
"control_group.default_value": "初期値",
|
||||
"control_group.default_value.unset": "(どれも選択されない)",
|
||||
"menuitem.prop.control_group": "コントロールグループ",
|
||||
"menuitem.prop.control_group.tooltip": "同じグループ内では、一つのトグルしか起動できません",
|
||||
"menuitem.prop.is_default": "グループの初期設定にする",
|
||||
|
@ -70,7 +70,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
controller.parameters = parameters.ToArray();
|
||||
|
||||
int layersToInsert = 1; // TODO
|
||||
int layersToInsert = 2; // TODO
|
||||
|
||||
var rootBlendTree = GenerateRootBlendLayer(actions);
|
||||
AdjustAllBehaviors(controller, b =>
|
||||
@ -86,8 +86,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
}
|
||||
|
||||
var layerList = controller.layers.ToList();
|
||||
//layerList.Insert(0, GenerateBlendshapeBaseLayer(avatar));
|
||||
//rootBlendTree.defaultWeight = 1;
|
||||
layerList.Insert(0, GenerateBlendshapeBaseLayer(avatar));
|
||||
rootBlendTree.defaultWeight = 1;
|
||||
layerList.Insert(0, rootBlendTree);
|
||||
layerList[1].defaultWeight = 1;
|
||||
controller.layers = layerList.ToArray();
|
||||
@ -214,8 +214,8 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
List<VRCExpressionParameters.Parameter> expParameters = expParams.parameters.ToList();
|
||||
List<BlendTree> blendTrees = new List<BlendTree>();
|
||||
|
||||
Dictionary<Component, List<ModularAvatarMenuItem>> groupedItems =
|
||||
new Dictionary<Component, List<ModularAvatarMenuItem>>();
|
||||
Dictionary<ActionController, List<ModularAvatarMenuItem>> groupedItems =
|
||||
new Dictionary<ActionController, List<ModularAvatarMenuItem>>();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
@ -243,8 +243,16 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
foreach (var kvp in groupedItems)
|
||||
{
|
||||
// sort default first
|
||||
ModularAvatarMenuItem defaultItem = null;
|
||||
if (kvp.Key is ControlGroup cg)
|
||||
{
|
||||
defaultItem = cg.defaultValue;
|
||||
if (defaultItem == null || defaultItem.controlGroup != cg) defaultItem = null;
|
||||
}
|
||||
|
||||
var group = kvp.Value;
|
||||
group.Sort((a, b) => b.isDefault.CompareTo(a.isDefault));
|
||||
group.Sort((a, b) =>
|
||||
(b == defaultItem).CompareTo(a == defaultItem));
|
||||
|
||||
// Generate parameter
|
||||
var paramname = "_MA/A/" + kvp.Key.gameObject.name + "/" + (paramIndex++);
|
||||
@ -253,12 +261,20 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
? VRCExpressionParameters.ValueType.Int
|
||||
: VRCExpressionParameters.ValueType.Bool;
|
||||
|
||||
if (defaultItem == null)
|
||||
{
|
||||
group.Insert(0, null);
|
||||
}
|
||||
|
||||
bool isSaved = kvp.Key.isSavedProp, isSynced = kvp.Key.isSyncedProp;
|
||||
|
||||
expParameters.Add(new VRCExpressionParameters.Parameter()
|
||||
{
|
||||
name = paramname,
|
||||
defaultValue = 0, // TODO
|
||||
valueType = expParamType,
|
||||
saved = false, // TODO
|
||||
saved = isSaved,
|
||||
networkSynced = isSynced
|
||||
});
|
||||
acParameters.Add(new AnimatorControllerParameter()
|
||||
{
|
||||
@ -267,12 +283,12 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
defaultFloat = 0, // TODO
|
||||
});
|
||||
|
||||
var hasDefault = group[0].isDefault;
|
||||
for (int i = 0; i < group.Count; i++)
|
||||
{
|
||||
if (group[i] == null) continue;
|
||||
var control = group[i].Control;
|
||||
control.parameter = new VRCExpressionsMenu.Control.Parameter() {name = paramname};
|
||||
control.value = hasDefault ? i : i + 1;
|
||||
control.value = i;
|
||||
}
|
||||
|
||||
var blendTree = new BlendTree();
|
||||
@ -283,12 +299,7 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
List<ChildMotion> children = new List<ChildMotion>();
|
||||
|
||||
List<Motion> motions = GenerateMotions(group, bindings, out var inactiveMotion);
|
||||
|
||||
if (!hasDefault)
|
||||
{
|
||||
motions.Insert(0, inactiveMotion);
|
||||
}
|
||||
List<Motion> motions = GenerateMotions(group, bindings, kvp.Key);
|
||||
|
||||
for (int i = 0; i < motions.Count; i++)
|
||||
{
|
||||
@ -322,12 +333,14 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
void MergeCurves(
|
||||
IDictionary<MenuCurveBinding, (Component, AnimationCurve)> curves,
|
||||
ModularAvatarMenuItem item,
|
||||
ActionController controller,
|
||||
Func<SwitchedMenuAction, IDictionary<MenuCurveBinding, AnimationCurve>> getCurves,
|
||||
bool ignoreDuplicates
|
||||
)
|
||||
{
|
||||
foreach (var action in item.GetComponents<SwitchedMenuAction>())
|
||||
if (controller == null) return;
|
||||
|
||||
foreach (var action in controller.GetComponents<SwitchedMenuAction>())
|
||||
{
|
||||
var newCurves = getCurves(action);
|
||||
|
||||
@ -345,14 +358,14 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
binding,
|
||||
existing.Item1.gameObject.name,
|
||||
existing.Item1.GetType().Name,
|
||||
item.gameObject.name,
|
||||
item.GetType().Name
|
||||
}, binding.target, existing.Item1, item);
|
||||
controller.gameObject.name,
|
||||
controller.GetType().Name
|
||||
}, binding.target, existing.Item1, controller);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
curves.Add(binding, (item, curve));
|
||||
curves.Add(binding, (controller, curve));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -361,40 +374,34 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
private List<Motion> GenerateMotions(
|
||||
List<ModularAvatarMenuItem> items,
|
||||
Dictionary<MenuCurveBinding, Component> bindings,
|
||||
out Motion inactiveMotion)
|
||||
ActionController controller
|
||||
)
|
||||
{
|
||||
Dictionary<MenuCurveBinding, Component> newBindings = new Dictionary<MenuCurveBinding, Component>();
|
||||
|
||||
Dictionary<MenuCurveBinding, (Component, AnimationCurve)> inactiveCurves =
|
||||
new Dictionary<MenuCurveBinding, (Component, AnimationCurve)>();
|
||||
|
||||
var defaultItems = items.Where(i => i.isDefault).ToList();
|
||||
if (defaultItems.Count > 1)
|
||||
if (controller is ControlGroup)
|
||||
{
|
||||
BuildReport.LogFatal("animation_gen.multiple_defaults",
|
||||
strings: Array.Empty<object>(),
|
||||
objects: defaultItems.ToArray<UnityEngine.Object>()
|
||||
);
|
||||
defaultItems.RemoveRange(1, defaultItems.Count - 1);
|
||||
}
|
||||
|
||||
if (defaultItems.Count > 0)
|
||||
{
|
||||
MergeCurves(inactiveCurves, defaultItems[0], a => a.GetInactiveCurves(true), false);
|
||||
MergeCurves(inactiveCurves, controller, a => a.GetCurves(), false);
|
||||
}
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (defaultItems.Count == 0 || defaultItems[0] != item)
|
||||
{
|
||||
MergeCurves(inactiveCurves, item, a => a.GetInactiveCurves(false), true);
|
||||
}
|
||||
|
||||
var inactiveMotion = CurvesToMotion(inactiveCurves);
|
||||
var sampleItem = items.FirstOrDefault(i => i != null);
|
||||
String groupName = "(unknown group)";
|
||||
if (sampleItem != null)
|
||||
{
|
||||
groupName = (sampleItem.controlGroup != null
|
||||
? sampleItem.controlGroup.gameObject.name
|
||||
: sampleItem.gameObject.name);
|
||||
}
|
||||
|
||||
inactiveMotion = CurvesToMotion(inactiveCurves);
|
||||
var groupName = (items[0].controlGroup != null
|
||||
? items[0].controlGroup.gameObject.name
|
||||
: items[0].gameObject.name);
|
||||
inactiveMotion.name =
|
||||
groupName
|
||||
+ " (Inactive)";
|
||||
@ -403,8 +410,18 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
Dictionary<MenuCurveBinding, (Component, AnimationCurve)> activeCurves =
|
||||
new Dictionary<MenuCurveBinding, (Component, AnimationCurve)>();
|
||||
Dictionary<MenuCurveBinding, (Component, AnimationCurve)> activeCurves;
|
||||
|
||||
Motion clip;
|
||||
|
||||
if (item == null)
|
||||
{
|
||||
activeCurves = inactiveCurves;
|
||||
clip = inactiveMotion;
|
||||
}
|
||||
else
|
||||
{
|
||||
activeCurves = new Dictionary<MenuCurveBinding, (Component, AnimationCurve)>();
|
||||
|
||||
MergeCurves(activeCurves, item, a => a.GetCurves(), false);
|
||||
foreach (var kvp in inactiveCurves)
|
||||
@ -415,8 +432,10 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
}
|
||||
}
|
||||
|
||||
var clip = CurvesToMotion(activeCurves);
|
||||
clip = CurvesToMotion(activeCurves);
|
||||
clip.name = groupName + " (" + item.gameObject.name + ")";
|
||||
}
|
||||
|
||||
motions.Add(clip);
|
||||
|
||||
foreach (var binding in activeCurves)
|
||||
@ -430,12 +449,12 @@ namespace nadena.dev.modular_avatar.core.editor
|
||||
|
||||
foreach (var binding in newBindings)
|
||||
{
|
||||
if (bindings.ContainsKey(binding.Key))
|
||||
if (bindings.TryGetValue(binding.Key, out var bindingValue))
|
||||
{
|
||||
BuildReport.LogFatal("animation_gen.duplicate_binding", new object[]
|
||||
{
|
||||
binding.Key
|
||||
}, binding.Value, bindings[binding.Key]);
|
||||
}, binding.Value, bindingValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -94,7 +94,7 @@ namespace nadena.dev.modular_avatar.core.editor.menu
|
||||
|
||||
BuildReport.ReportingObject(installer, () =>
|
||||
{
|
||||
var menuSourceComp = installer.GetComponent<MenuSourceComponent>();
|
||||
var menuSourceComp = installer.GetComponent<MenuSource>();
|
||||
if (menuSourceComp != null)
|
||||
{
|
||||
PushNode(menuSourceComp);
|
||||
|
@ -0,0 +1,17 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core
|
||||
{
|
||||
/// <summary>
|
||||
/// Tag class that marks components that actions can be attached to.
|
||||
///
|
||||
/// Note that this is public due to C# protection rules, but is not a supported API for editor scripts and may
|
||||
/// change in point releases.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public abstract class ActionController : AvatarTagComponent
|
||||
{
|
||||
internal abstract bool isSyncedProp { get; }
|
||||
internal abstract bool isSavedProp { get; }
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d26aaffe335843959445c6a976d517a7
|
||||
timeCreated: 1681386441
|
@ -7,8 +7,8 @@ using Object = System.Object;
|
||||
|
||||
namespace nadena.dev.modular_avatar.core
|
||||
{
|
||||
[RequireComponent(typeof(ModularAvatarMenuItem))]
|
||||
[AddComponentMenu("Modular Avatar/MA Action Toggle Object")]
|
||||
[RequireComponent(typeof(ActionController))]
|
||||
public class ActionToggleObject : AvatarTagComponent, SwitchedMenuAction
|
||||
{
|
||||
[Serializable]
|
||||
|
@ -3,7 +3,14 @@
|
||||
namespace nadena.dev.modular_avatar.core
|
||||
{
|
||||
[AddComponentMenu("Modular Avatar/MA Control Group")]
|
||||
public class ControlGroup : AvatarTagComponent
|
||||
public class ControlGroup : ActionController
|
||||
{
|
||||
public bool isSynced = true;
|
||||
public bool isSaved = true;
|
||||
|
||||
public ModularAvatarMenuItem defaultValue;
|
||||
|
||||
internal override bool isSyncedProp => isSynced;
|
||||
internal override bool isSavedProp => isSaved;
|
||||
}
|
||||
}
|
@ -88,7 +88,7 @@ namespace nadena.dev.modular_avatar.core.menu
|
||||
{
|
||||
foreach (Transform t in root.transform)
|
||||
{
|
||||
var source = t.GetComponent<MenuSourceComponent>();
|
||||
var source = t.GetComponent<MenuSource>();
|
||||
if (source != null) context.PushNode(source);
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ namespace nadena.dev.modular_avatar.core
|
||||
}
|
||||
|
||||
[AddComponentMenu("Modular Avatar/MA Menu Item")]
|
||||
public class ModularAvatarMenuItem : MenuSourceComponent
|
||||
public class ModularAvatarMenuItem : ActionController, MenuSource
|
||||
{
|
||||
public VRCExpressionsMenu.Control Control;
|
||||
public SubmenuSource MenuSource;
|
||||
@ -22,19 +22,30 @@ namespace nadena.dev.modular_avatar.core
|
||||
public GameObject menuSource_otherObjectChildren;
|
||||
|
||||
[FormerlySerializedAs("toggleGroup")] public ControlGroup controlGroup;
|
||||
public bool isDefault;
|
||||
|
||||
/// <summary>
|
||||
/// If no control group is set (and an action is linked), this controls whether this control is synced.
|
||||
/// </summary>
|
||||
public bool isSynced = true;
|
||||
|
||||
public bool isSaved = true;
|
||||
|
||||
internal override bool isSyncedProp => isSynced;
|
||||
internal override bool isSavedProp => isSaved;
|
||||
|
||||
protected override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
|
||||
RuntimeUtil.InvalidateMenu();
|
||||
|
||||
if (Control == null)
|
||||
{
|
||||
Control = new VRCExpressionsMenu.Control();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Visit(NodeContext context)
|
||||
public void Visit(NodeContext context)
|
||||
{
|
||||
if (Control == null)
|
||||
{
|
||||
|
@ -62,6 +62,15 @@
|
||||
"dependencies": {},
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.postprocessing": {
|
||||
"version": "3.1.1",
|
||||
"depth": 1,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
"com.unity.modules.physics": "1.0.0"
|
||||
},
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.settings-manager": {
|
||||
"version": "1.0.1",
|
||||
"depth": 1,
|
||||
@ -165,6 +174,20 @@
|
||||
"dependencies": {},
|
||||
"hash": "ba9c16e482e7a376db18e9a85e24fabc04649d37"
|
||||
},
|
||||
"dev.onevr.vrworldtoolkit": {
|
||||
"version": "file:dev.onevr.vrworldtoolkit",
|
||||
"depth": 0,
|
||||
"source": "embedded",
|
||||
"dependencies": {
|
||||
"com.unity.postprocessing": "3.1.1"
|
||||
}
|
||||
},
|
||||
"lyuma.av3emulator": {
|
||||
"version": "file:lyuma.av3emulator",
|
||||
"depth": 0,
|
||||
"source": "embedded",
|
||||
"dependencies": {}
|
||||
},
|
||||
"nadena.dev.modular-avatar": {
|
||||
"version": "file:nadena.dev.modular-avatar",
|
||||
"depth": 0,
|
||||
@ -173,6 +196,12 @@
|
||||
"com.unity.nuget.newtonsoft-json": "2.0.0"
|
||||
}
|
||||
},
|
||||
"vrchat.blackstartx.gesture-manager": {
|
||||
"version": "file:vrchat.blackstartx.gesture-manager",
|
||||
"depth": 0,
|
||||
"source": "embedded",
|
||||
"dependencies": {}
|
||||
},
|
||||
"com.unity.modules.ai": {
|
||||
"version": "1.0.0",
|
||||
"depth": 0,
|
||||
|
@ -5,21 +5,30 @@
|
||||
},
|
||||
"com.vrchat.core.vpm-resolver": {
|
||||
"version": "0.1.13"
|
||||
},
|
||||
"vrchat.blackstartx.gesture-manager": {
|
||||
"version": "3.8.3"
|
||||
}
|
||||
},
|
||||
"locked": {
|
||||
"com.vrchat.avatars": {
|
||||
"version": "3.1.11",
|
||||
"version": "3.1.13",
|
||||
"dependencies": {
|
||||
"com.vrchat.base": "3.1.11"
|
||||
"com.vrchat.base": "3.1.13"
|
||||
}
|
||||
},
|
||||
"com.vrchat.base": {
|
||||
"version": "3.1.11",
|
||||
"version": "3.1.13",
|
||||
"dependencies": {}
|
||||
},
|
||||
"com.vrchat.core.vpm-resolver": {
|
||||
"version": "0.1.17"
|
||||
},
|
||||
"vrchat.blackstartx.gesture-manager": {
|
||||
"version": "3.8.3",
|
||||
"dependencies": {
|
||||
"com.vrchat.avatars": "3.1.x"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -619,7 +619,19 @@ PlayerSettings:
|
||||
webGLThreadsSupport: 0
|
||||
webGLWasmStreaming: 0
|
||||
scriptingDefineSymbols:
|
||||
1: VRC_SDK_VRCSDK3
|
||||
1: VRC_SDK_VRCSDK3;UNITY_POST_PROCESSING_STACK_V2
|
||||
7: UNITY_POST_PROCESSING_STACK_V2
|
||||
13: UNITY_POST_PROCESSING_STACK_V2
|
||||
14: UNITY_POST_PROCESSING_STACK_V2
|
||||
19: UNITY_POST_PROCESSING_STACK_V2
|
||||
21: UNITY_POST_PROCESSING_STACK_V2
|
||||
25: UNITY_POST_PROCESSING_STACK_V2
|
||||
27: UNITY_POST_PROCESSING_STACK_V2
|
||||
28: UNITY_POST_PROCESSING_STACK_V2
|
||||
29: UNITY_POST_PROCESSING_STACK_V2
|
||||
30: UNITY_POST_PROCESSING_STACK_V2
|
||||
32: UNITY_POST_PROCESSING_STACK_V2
|
||||
33: UNITY_POST_PROCESSING_STACK_V2
|
||||
platformArchitecture: {}
|
||||
scriptingBackend: {}
|
||||
il2cppCompilerConfiguration: {}
|
||||
|
@ -10,7 +10,25 @@ When you want to create an option that can be in one of several states - for exa
|
||||
|
||||
## How do I use it?
|
||||
|
||||
The control group component itself has no configuration; simply add it to a Game Object, and point your [Menu Item](menu-item.md) components at the Control Group.
|
||||
The control group can be added to any game object, as long as it's inside your avatar. Feel free to put it somewhere convenient for you.
|
||||
Add a control group to a Game Object, and point your [Menu Item](menu-item.md) components at the Control Group.
|
||||
The control group can be added to any game object, as long as it's inside your avatar, and does not contain another
|
||||
MA Menu Item. Feel free to put it somewhere convenient for you.
|
||||
|
||||
Note that the control group is only used with menu items driving [action components](action-toggle-object.md); for traditional toggles driving animator parameters, simply set those toggles to the same parameter name.
|
||||
You can configure the following options on a control group:
|
||||
* Saved: If set, the current setting of the associated toggles will be preserved upon changing worlds or avatars
|
||||
* Synced: If set, the current setting of the associated toggles will be synced and visible to other players
|
||||
* Initial setting: The initial setting of this control group. If you choose "(none selected)", the default will be to
|
||||
have no toggles selected. Otherwise, the specified toggle will be selected by default. Note that if you set a default
|
||||
toggle, deselecting all toggles won't be possible.
|
||||
|
||||
The "Bound menu items" section allows you to see all the menu items linked to this control group.
|
||||
|
||||
Note that the control group is only used with menu items driving [action components](action-toggle-object.md); for
|
||||
traditional toggles driving animator parameters, simply set those toggles to the same parameter name.
|
||||
|
||||
### Attaching Actions to control groups
|
||||
|
||||
You can attach actions such as [Action Toggle Object](action-toggle-object.md) to a control group. These actions will be
|
||||
applied as defaults when selecting a menu item which does not specify what to do with a particular object. This can be
|
||||
used to, for example, turn off the initial outfit in an outfit selector, so that you don't need to copy those toggles
|
||||
to every alternate outfit.
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 99 KiB |
18
docs/docs/reference/menu-install-target.md
Normal file
@ -0,0 +1,18 @@
|
||||
# Menu Install Target
|
||||
|
||||
The Menu Install Target component is a component used to support the "Select Menu" button on the [MA Menu Installer](menu-installer) component.
|
||||
It "pulls" the menu from the MA Menu Installer component, and installs it based on the position of the game object it is
|
||||
attached to.
|
||||
|
||||

|
||||
|
||||
## When should I use it?
|
||||
|
||||
Modular Avatar will create this component when necessary, when you use the "select menu" button on the
|
||||
[MA Menu Installer](menu-installer) component. In most cases it is not necessary to create it manually.
|
||||
|
||||
## What does it do?
|
||||
|
||||
This component will override the target menu option on the menu installer that is selected; the menu installer will
|
||||
instead act as if its menu had been copy-pasted to the location of the Menu Install Target. This allows for prefabs that
|
||||
use Menu Installers to be integrated into the [object-based menu system](../tutorials/menu).
|
BIN
docs/docs/reference/menu-install-target.png
Normal file
After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 52 KiB |
@ -70,25 +70,33 @@ In some cases you might want more complex toggles. This can be done by adding a
|
||||
|
||||

|
||||
|
||||
Here we have a clothing menu which has a control group on it. The control group has no settings on it; it's just there to tie together the menu items. Here's what those look like:
|
||||
Here we have a clothing menu which has a control group on it. The main job of the control group is to to tie together the menu items, so that only one can be selected at a time. Here's what those look like:
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
As you can see, each of these menu items has a "Toggle" type, and a "MA Action Toggle Object" component. The "MA Action Toggle Object" component has a checkbox next to each object, which indicates whether that object should be shown when the menu item is selected.
|
||||
What's different here is that we've specified the control group object under "Control Group", and on the default object, we selected "Is Group Default".
|
||||
|
||||
As you can see, each of these menu items has a "Toggle" type, and a "MA Action Toggle Object" component.
|
||||
What's different here is that we've specified the control group object under "Control Group".
|
||||
When you specify a control group, only one menu item out of the control group can be selected at a time. This essentially means they'll be bound to the same parameter.
|
||||
|
||||
You can select one item out of your control group to be the default item. When you do this, the default item will be selected initially, and anything that it selects will be reversed when you select a different item. In this example, because we selected the "Kikyo_Blouse", "Kikyo_Coat", and "Kikyo_Skirt" objects ON in the default item, these will be toggled off when we select Blanchir or SailorOnepiece. You can override this on the individual other items by adding that item in explicitly.
|
||||
The control group can be placed on any object, provided that you don't have a Menu Item on that same game object. Feel
|
||||
free to put it wherever it makes sense in your hierarchy.
|
||||
|
||||
We've also set an additional Action Toggle Object on the control group itself. These toggles will be applied by default -
|
||||
this lets us turn off the default Kikyo outfit in all of our additional outfits that we're adding.
|
||||
|
||||
You can select one item out of your control group to be the default item on the control group. If you select the "(none
|
||||
selected)" option, then it'll be possible to turn off all toggles, selecting the control group defaults.
|
||||
|
||||
Finally, you can set on the control group whether the parameter is saved and/or synced.
|
||||
|
||||
### Limitations
|
||||
|
||||
This feature is in preview and has a number of limitations you should be aware of:
|
||||
|
||||
1. Objects can only be controlled by one control group or ungrouped toggle at this time.
|
||||
2. Currently, these toggles will not be saved. This means that if you change avatars or move between worlds, your toggles will not be saved.
|
||||
2. Only GameObjects can be toggled; there is not yet support for toggling individual components, blend shapes, etc.
|
||||
3. A toggle cannot both control actions and traditional animators at the same time.
|
||||
|
||||
These limitations will be improved on in future releases.
|
||||
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 80 KiB |
@ -72,24 +72,33 @@ Cubeの隣のチェックは、トグルがONになったときは表示す
|
||||
|
||||

|
||||
|
||||
こちらはControl Groupがついている衣装切り替えメニューです。Control group自体には設定項目がなく、メニューアイテムを一括りにするためだけにあります。各メニューアイテムも見てみましょう。
|
||||
こちらはControl Groupがついている衣装切り替えメニューです。Control groupの主な仕事はメニューアイテムを一括りにして、同時に一つまでしか選択できないようにするためにあります。各メニューアイテムも見てみましょう。
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
見ての通り、どれも「Toggle」タイプで、「MA Action Toggle Object」コンポーネントがついています。違うのは、Control Groupのオブジェクトを「コントロールグループ」に指定し、デフォルトの衣装に「グループの初期設定にする」をチェックしています。
|
||||
|
||||
見ての通り、どれも「Toggle」タイプで、「MA Action Toggle Object」コンポーネントがついています。
|
||||
違うのは、Control Groupのオブジェクトを「コントロールグループ」に指定しているところです。
|
||||
コントロールグループを指定すると、その中から一つのメニューアイテムしか設定できないようになります。同じパラメーターで連動するというわけです。
|
||||
|
||||
コントロールグループに連動するアイテムのうちから一つを初期設定にできます。すると、そのトグルが最初から設定される状態になり、そのトグルで表示されるものが他のトグルでは非表示になります(その逆もしかり)。この例の場合、「Kikyo_Blouse」、「Kikyo_Coat」、「Kikyo_Skirt」を初期項目でONにしたので、BlanchirとSailorOnepieceでは無効かされます。この挙動が不要の場合は、ほかの項目に該当オブジェクトを追加し、手動で設定できます。
|
||||
翻訳:コントロールグループは、同じゲームオブジェクトにMA Menu Itemがない限り、どこにでも置けます。ヒエラルキーのどこにでも置いても構いません。
|
||||
自分にとってわかりやすい所に置きましょう。
|
||||
|
||||
また、コントロールグループにもAction Toggle Objectを追加しました。こちらのトグルはデフォルトで設定されます。これでほかの衣装を選択しているとき、
|
||||
桔梗ちゃんのデフォルト衣装を簡単に切ることができます。
|
||||
|
||||
コントロールグループに連動するアイテムのうちから一つを初期設定にできます。設定しない場合は、どれも選択しないという状態がデフォルトになります。
|
||||
何も選択しない状態では、コントロールグループのデフォルトが適用されます。
|
||||
|
||||
最後に、コントロールグループのほうに保存・同期設定を調整できます。
|
||||
|
||||
### 制限
|
||||
|
||||
この機能は開発途中のもので、いくつか制限があります。
|
||||
|
||||
1. 一つのオブジェクトは一つのコントロールグループまたはグループに入っていないトグルにしか操作できない。
|
||||
2. 現在、トグルの状態が保存されません。アバター変更・ワールド移動で状態が保持されないということです。
|
||||
2. 現在、GameObjectしかトグルできません。コンポーネント単位のON/OFFやブレンドシェープの操作は未実装です。
|
||||
3. 一つのトグルでアクションと通常のアニメーターを両方操作できません。
|
||||
|
||||
今後のリリースで改善していく予定です。
|
||||
|