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
This commit is contained in:
bd_ 2023-04-15 18:11:30 +09:00 committed by GitHub
parent 9dc44d8ccc
commit a300622bb2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 384 additions and 88 deletions

View File

@ -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>()
{

View File

@ -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();
}
}

View File

@ -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();
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4039f724f1af412c98e176c44f0d7734
timeCreated: 1681547765

View File

@ -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)

View File

@ -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"));
}
}

View File

@ -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",

View File

@ -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": "グループの初期設定にする",

View File

@ -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
{

View File

@ -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);

View File

@ -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; }
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d26aaffe335843959445c6a976d517a7
timeCreated: 1681386441

View File

@ -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]

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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)
{

View File

@ -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,

View File

@ -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"
}
}
}
}

View File

@ -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: {}

View File

@ -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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View 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.
![Menu Install Target](menu-install-target.png)
## 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).

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

@ -70,25 +70,33 @@ In some cases you might want more complex toggles. This can be done by adding a
![Sample control group object](control-group.png)
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:
![Clothing menu item: Default](clothes-0.png)
![Clothing menu item: Blanchir](clothes-1.png)
![Clothing menu item: SailorOnepiece](clothes-2.png)
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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View File

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