Last active
April 9, 2021 20:14
-
-
Save aki-lua87/7bc804bfda4f58c335060be4e8a06884 to your computer and use it in GitHub Desktop.
This file contains 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
using UnityEngine; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System; | |
using VRC.SDK3.Avatars.ScriptableObjects; | |
using VRC.SDK3.Avatars.Components; | |
using UnityEditor.Animations; | |
using static VRC.SDK3.Avatars.Components.VRCAvatarDescriptor; | |
using UnityEditor; | |
using System.IO; | |
using static VRC.SDK3.Avatars.ScriptableObjects.VRCExpressionsMenu.Control; | |
using VRC.SDKBase; | |
using System.Reflection; | |
using System.Text.RegularExpressions; | |
public class RadialInventory : EditorWindow | |
{ | |
[MenuItem("Editor/RadialInventory")] | |
private static void Create() | |
{ | |
GetWindow<RadialInventory>("RadialInventory"); | |
} | |
private bool propGroup_IsOpen = true; | |
private SORadialInventory Values; | |
public Vector2 ScrollPosition = Vector2.zero; | |
public Stack<PropInfo> RemoveProps = new Stack<PropInfo>(); | |
public Stack<GroupInfo> RemoveGroups = new Stack<GroupInfo>(); | |
public VRCAvatarDescriptor beforeRoot = null; | |
public bool ShowAdvanceSettings = false; | |
private void OnGUI() | |
{ | |
if (Values == null) | |
{ | |
//初期化部分 | |
Values = ScriptableObject.CreateInstance<SORadialInventory>(); | |
} | |
using (var scrollScope = new EditorGUILayout.ScrollViewScope(ScrollPosition)) | |
{ | |
ScrollPosition = scrollScope.scrollPosition; | |
EditorGUILayout.LabelField("Settings"); | |
EditorGUILayoutEx.Separator(); | |
var generatedItemsPath = ""; | |
using (new GUILayout.VerticalScope()) | |
{ | |
//GameObject類を設定する欄 | |
//Descriptorとかを弄るアバター | |
Values.AvatarRoot = (VRCAvatarDescriptor)EditorGUILayout.ObjectField("AvatarRoot", Values.AvatarRoot, typeof(VRCAvatarDescriptor), true); | |
if (Values.AvatarRoot != null) | |
{ | |
//AvatarRootが設定されたらいろいろ表示する | |
//作業用フォルダーの存在確認と生成 | |
var name = string.Join("", Values.AvatarRoot.gameObject.name.ToArray().Where(c => !Path.GetInvalidFileNameChars().Contains(c))).Trim(); | |
generatedItemsPath = "Assets/RadialInventory/GeneratedItems/" + name + "/"; | |
if (!AssetDatabase.IsValidFolder(generatedItemsPath)) | |
CreateFolderRecursively(generatedItemsPath); | |
} | |
else | |
{ | |
Values.Groups.Clear(); | |
beforeRoot = null; | |
} | |
//AvatarRootが変更されたら設定を復元 | |
if (Values.AvatarRoot != beforeRoot) | |
{ | |
if (Values.AvatarRoot != null) | |
{ | |
SORadialInventory settings = (SORadialInventory)AssetDatabase.LoadAssetAtPath(generatedItemsPath + "SavedSettings.asset", typeof(SORadialInventory)); | |
if (settings != null) | |
RestoreSettings(settings); | |
else | |
Values.Groups.Clear(); | |
} | |
else | |
Values.Groups.Clear(); | |
beforeRoot = Values.AvatarRoot; | |
} | |
Values.UseWriteDefaults = EditorGUILayout.Toggle("WriteDefaults", Values.UseWriteDefaults); | |
Values.SaveParameter = EditorGUILayout.Toggle("SaveParameter", Values.SaveParameter); | |
if (Values != null && Values.AvatarRoot != null && Values.AvatarRoot.baseAnimationLayers != null && | |
Values.AvatarRoot.baseAnimationLayers.Length >= 5 && Values.AvatarRoot.baseAnimationLayers[4].animatorController != null) | |
{ | |
var controller = (AnimatorController)Values.AvatarRoot.baseAnimationLayers[4].animatorController; | |
var layers = controller.layers.Where(n => !n.name.StartsWith("RILayerG")); | |
foreach(var layer in layers) | |
{ | |
if(layer.stateMachine != null) | |
{ | |
if(layer.stateMachine.states.Any(n => n.state.writeDefaultValues != Values.UseWriteDefaults)) | |
{ | |
EditorGUILayout.HelpBox("WriteDefaultsがFXレイヤー内で統一されていません。\n" + | |
"このままでも動作はしますが、表情切り替えにバグが発生する場合があります。\n" + | |
"WriteDefaultsのチェックを切り替えてもエラーメッセージが消えない場合は\n" + | |
"使用している他のアバターギミックなどを確認してみてください。", MessageType.Warning); | |
break; | |
} | |
} | |
} | |
} | |
} | |
EditorGUILayoutEx.Separator(); | |
EditorGUILayout.LabelField(""); | |
using (new GUILayout.HorizontalScope()) | |
{ | |
propGroup_IsOpen = EditorGUILayout.Foldout(propGroup_IsOpen, "PropGroup"); | |
ShowAdvanceSettings = EditorGUILayout.Toggle(" Show Adv. Settings", ShowAdvanceSettings, GUILayout.MaxWidth(180f)); | |
} | |
if (propGroup_IsOpen) | |
{ | |
//PropGroupの一覧 | |
EditorGUILayoutEx.Separator(); | |
using (new GUILayout.VerticalScope()) | |
{ | |
int group_index = 0; | |
foreach (var group in Values.Groups) | |
{ | |
using (new GUILayout.VerticalScope(GUI.skin.box)) | |
{ | |
using (new GUILayout.HorizontalScope()) | |
{ | |
Values.Groups[group_index].GroupName = EditorGUILayout.TextField(Values.Groups[group_index].GroupName, GUILayout.MinWidth(40f)); | |
EditorGUILayout.LabelField(" Icon", GUILayout.Width(40f)); | |
Values.Groups[group_index].GroupIcon = (Texture2D)EditorGUILayout.ObjectField(Values.Groups[group_index].GroupIcon, typeof(Texture2D), false); | |
EditorGUILayout.LabelField("IsExclusive", GUILayout.Width(80f)); | |
Values.Groups[group_index].ExclusiveMode = EditorGUILayout.Toggle(Values.Groups[group_index].ExclusiveMode, GUILayout.Width(30f)); | |
if (Values.Groups.Count > 1) | |
{ | |
//PropGroupの削除ボタン(ここで削除するとエラーが発生するから後でまとめて削除) | |
//PropGroupが1個以下のときは非表示 | |
if (GUILayout.Button("-", GUILayout.Width(20f))) | |
{ | |
RemoveGroups.Push(group); | |
} | |
} | |
++group_index; | |
} | |
EditorGUILayoutEx.Separator(); | |
using (new GUILayout.VerticalScope()) | |
{ | |
//PropGroup内のPropの一覧 | |
int prop_index = 0; | |
foreach (var prop in group.Props) | |
{ | |
using (new GUILayout.HorizontalScope()) | |
{ | |
EditorGUILayout.LabelField("Prop" + (++prop_index).ToString(), GUILayout.Width(50f)); | |
prop.TargetObject = (GameObject)EditorGUILayout.ObjectField(prop.TargetObject, typeof(GameObject), true, GUILayout.MinWidth(10f)); | |
EditorGUILayout.LabelField("Icon", GUILayout.Width(40f)); | |
prop.PropIcon = (Texture2D)EditorGUILayout.ObjectField(prop.PropIcon, typeof(Texture2D), false); | |
if(ShowAdvanceSettings) | |
{ | |
EditorGUILayout.LabelField("CustomAnim", GUILayout.Width(80f)); | |
prop.CustomAnim = (AnimationClip)EditorGUILayout.ObjectField(prop.CustomAnim, typeof(AnimationClip), false, GUILayout.MinWidth(10f)); | |
} | |
EditorGUILayout.LabelField("Default", GUILayout.Width(50f)); | |
prop.IsDefaultEnabled = EditorGUILayout.Toggle(prop.IsDefaultEnabled, GUILayout.Width(30f)); | |
EditorGUILayout.LabelField("Local", GUILayout.Width(40f)); | |
prop.LocalOnly = EditorGUILayout.Toggle(prop.LocalOnly, GUILayout.Width(30f)); | |
if (group.Props.Count > 1) | |
{ | |
//PropGroup内のPropの削除ボタン(ここで削除するとエラーが発生するから後でまとめて削除) | |
//Propが1個以下のときは非表示 | |
if (GUILayout.Button("-", GUILayout.Width(20f))) | |
{ | |
RemoveProps.Push(prop); | |
} | |
} | |
} | |
} | |
while (RemoveProps.Any()) | |
group.Props.Remove(RemoveProps.Pop()); | |
if (group.Props.Count < (group.ExclusiveMode ? 8 : 7)) | |
{ | |
//Propの追加ボタン | |
//8個以上のときは非表示 | |
if (GUILayout.Button("+")) | |
{ | |
group.Props.Add(new PropInfo()); | |
} | |
} | |
} | |
EditorGUILayoutEx.Separator(); | |
} | |
} | |
while (RemoveGroups.Any()) | |
{ | |
Values.Groups.Remove(RemoveGroups.Pop()); | |
} | |
if (Values.Groups.Count < 8) | |
{ | |
//PropGroupの追加ボタン | |
//8個以上のときは非表示 | |
if (GUILayout.Button("+")) | |
{ | |
var list = new GroupInfo(); | |
list.Props.Add(new PropInfo()); | |
Values.Groups.Add(list); | |
list.GroupName = "Group" + Values.Groups.Count.ToString(); | |
} | |
} | |
} | |
EditorGUILayoutEx.Separator(); | |
} | |
EditorGUILayout.LabelField(""); | |
List<string> errorTxt = new List<string>(); | |
if (Values.AvatarRoot != null) | |
{ | |
//適用条件を満たしているかのチェック | |
var hasNullProp = Values.Groups.Any(group => group.Props.Count(prop => prop.TargetObject == null) > 0); | |
var hasOverGroup = (Values.Groups.Count > 8) || Values.Groups.Any(n => n.Props.Count > (n.ExclusiveMode ? 8 : 7)); | |
var hasNullGroup = Values.Groups.Any(n => n.Props.Count == 0); | |
if (Values.Groups.Count <= 0) | |
errorTxt.Add("グループが登録されていません。"); | |
if (hasNullProp) | |
errorTxt.Add("GameObjectが設定されていないPropがあります。"); | |
if (hasOverGroup) | |
errorTxt.Add("Propの数が7つ(ExclusiveModeでは8つ)以上あるグループがあります。"); | |
if (hasNullGroup) | |
errorTxt.Add("Propが登録されていないグループがあります。"); | |
if (GUILayout.Button(SystemStrings.RemoveAllGeneratedObjectsButtonText)) | |
{ | |
RemoveAllGeneratedObjects(); | |
} | |
} | |
else | |
errorTxt.Add("AvatarRootが登録されていません。"); | |
EditorGUILayoutEx.Separator(); | |
//適用ボタン | |
//PropGroupとPropの数が1個以上 GameObjectが適切に設定されているときだけ表示 | |
if (errorTxt.Count > 0) | |
{ | |
using (new GUILayout.VerticalScope(GUI.skin.box)) | |
{ | |
foreach (var text in errorTxt) | |
EditorGUILayout.HelpBox(text, MessageType.Error); | |
} | |
} | |
else if(GUILayout.Button(SystemStrings.ApplyButtonText)) | |
{ | |
//設定を保存 | |
SaveSettingsToFile(generatedItemsPath); | |
//アバターに適用 | |
ApplyToAvatar(generatedItemsPath); | |
} | |
} | |
} | |
public void RemoveAllGeneratedObjects() | |
{ | |
AnimatorController controller = null; | |
if (Values.AvatarRoot.baseAnimationLayers.Length >= 5 && Values.AvatarRoot.baseAnimationLayers[4].animatorController != null) | |
{ | |
controller = (AnimatorController)Values.AvatarRoot.baseAnimationLayers[4].animatorController; | |
int ofs = 0; | |
var max = controller.layers.Count(); | |
foreach (var layerIndex in Enumerable.Range(0, max)) | |
{ | |
var name = controller.layers[layerIndex + ofs].name; | |
if (name.StartsWith("RadInvLayer") || name.StartsWith("RadBitLayer")) | |
{ | |
controller.RemoveLayer(layerIndex + ofs); | |
ofs--; | |
} | |
} | |
ofs = 0; | |
max = controller.parameters.Count(); | |
foreach (var paramIndex in Enumerable.Range(0, max)) | |
{ | |
var name = controller.parameters[paramIndex + ofs].name; | |
if (name.StartsWith("RadInvStatus")) | |
{ | |
controller.RemoveParameter(paramIndex + ofs); | |
ofs--; | |
} | |
} | |
} | |
if (Values.AvatarRoot.expressionParameters != null) | |
{ | |
var expressionParam = Values.AvatarRoot.expressionParameters; | |
foreach (var v in Enumerable.Range(0, expressionParam.parameters.Count())) | |
{ | |
if (expressionParam.parameters[v].name.StartsWith("RadInvStatus")) | |
{ | |
expressionParam.parameters[v].name = ""; | |
expressionParam.parameters[v].valueType = VRCExpressionParameters.ValueType.Int; | |
} | |
} | |
} | |
if (Values.AvatarRoot.expressionsMenu != null) | |
{ | |
var menu = Values.AvatarRoot.expressionsMenu; | |
var ofs = 0; | |
foreach (var v in Enumerable.Range(0, menu.controls.Count())) | |
{ | |
if (menu.controls[v].name == "RadialInventory") | |
{ | |
menu.controls.RemoveAt(v + ofs); | |
ofs--; | |
} | |
} | |
} | |
} | |
public void GetFromV1(string generatedItemsPath) | |
{ | |
Values.Groups.Clear(); | |
var rootPath = GetGameObjectPath(Values.AvatarRoot.gameObject); | |
foreach (var v in Enumerable.Range(1, 8)) | |
{ | |
AnimationClip group_default = (AnimationClip)AssetDatabase.LoadAssetAtPath(generatedItemsPath + "Animations/" + "G" + v.ToString() + "DEFAULT.anim", typeof(AnimationClip)); | |
if (group_default != null) | |
{ | |
EditorCurveBinding[] bindings = AnimationUtility.GetCurveBindings(group_default); | |
var groupInfo = new GroupInfo(); | |
groupInfo.GroupName = "Group" + (Values.Groups.Count + 1).ToString(); | |
groupInfo.ExclusiveMode = true; | |
foreach (var bind in bindings) | |
{ | |
AnimationCurve curve = AnimationUtility.GetEditorCurve(group_default, bind); | |
var obj = FindGameObjectFromPath(bind.path, Values.AvatarRoot.gameObject); | |
if (obj != null && curve != null && curve.keys.Length > 0) | |
{ | |
var defaultStatus = Convert.ToBoolean((int)curve.keys[0].value); | |
var propInfo = new PropInfo(); | |
propInfo.TargetObject = obj; | |
propInfo.IsDefaultEnabled = defaultStatus; | |
groupInfo.Props.Add(propInfo); | |
} | |
} | |
Values.Groups.Add(groupInfo); | |
} | |
} | |
} | |
public void ApplyToAvatar(string generatedItemsPath) | |
{ | |
//VRCExpressionParametersの取得or新規作成 | |
VRCExpressionParameters expressionParameters = Values.AvatarRoot.expressionParameters; | |
if (Values.AvatarRoot.expressionParameters == null) | |
{ | |
var param = CreateExpressionParameters(generatedItemsPath + "RadInvExpressionParameters.asset"); | |
expressionParameters = param; | |
Values.AvatarRoot.expressionParameters = expressionParameters; | |
} | |
if (!CheckHasParameterSpace(expressionParameters, Values.Groups.Count)) | |
{ | |
EditorUtility.DisplayDialog("Radial Inventory System", "エラー : Parametersに空きがありません", "OK"); | |
return; | |
} | |
//AnimatorControllerの取得or新規作成 | |
AnimatorController controller = GetOrCreateAnimator(generatedItemsPath); | |
//自動生成されたAnimationsフォルダを一旦削除 | |
ReCreateFolder(generatedItemsPath + "Animations"); | |
//メインメニューの取得or新規作成 | |
var risMainMenu = GetOrCreateAsset(generatedItemsPath + "RadInvMainMenu.asset", typeof(VRCExpressionsMenu)) as VRCExpressionsMenu; | |
risMainMenu.controls = new List<VRCExpressionsMenu.Control>(); | |
//RISメインメニューを追加するルートメニューを取得or新規作成 | |
VRCExpressionsMenu addDestination; | |
if (Values.AvatarRoot.expressionsMenu != null) | |
addDestination = Values.AvatarRoot.expressionsMenu; | |
else | |
{ | |
var rootMenu = GetOrCreateAsset(generatedItemsPath + "ExpressionsRootMenu.asset", typeof(VRCExpressionsMenu)) as VRCExpressionsMenu; | |
rootMenu.controls = new List<VRCExpressionsMenu.Control>(); | |
Values.AvatarRoot.expressionsMenu = addDestination = rootMenu; | |
} | |
//ルートメニューにRISメインメニューを追加 | |
var control = GetOrCreateMenuControl(addDestination, "RadialInventory", ControlType.SubMenu); | |
Texture2D icon = (Texture2D)AssetDatabase.LoadAssetAtPath("Assets/RadialInventory/icon.png", typeof(Texture2D)); | |
control.icon = icon; | |
control.subMenu = risMainMenu; | |
Dictionary<string, AnimationClip> objectToggleClips = new Dictionary<string, AnimationClip>(); | |
AnimatorState state; | |
AnimationClip clip; | |
List<AnimatorState[]> states = new List<AnimatorState[]>(); | |
ClearAllGeneratedStateMachine(controller); | |
var containLocalItem = Values.Groups.Any(group => group.Props.Any(prop => prop.LocalOnly)); | |
var isLocalParamName = "IsLocal"; | |
var containParameter = controller.parameters.Any(n => n.name == isLocalParamName); | |
if (containLocalItem && !containParameter) | |
controller.AddParameter(isLocalParamName, AnimatorControllerParameterType.Bool); | |
foreach (var groupIndex in Enumerable.Range(1, Values.Groups.Count)) | |
{ | |
states.Clear(); | |
var group = Values.Groups[groupIndex - 1]; | |
var clipOFF = new AnimationClip(); | |
var paramName = "RadInvStatusG" + groupIndex.ToString(); | |
//RIS用のParameterが存在しなかったら追加する(VRCExpressionParametersとControllerのParametersへ) | |
if (!CheckParameter(expressionParameters, paramName, controller, Values.SaveParameter)) | |
{ | |
EditorUtility.DisplayDialog("Radial Inventory System", "エラー : Parameterの追加に失敗しました", "OK"); | |
return; | |
} | |
EditorUtility.SetDirty(expressionParameters); | |
//RIS用のLayerを取得/生成する | |
AnimatorControllerLayer layer = GetOrCreateControllerLayer(controller, "RadInvLayerG" + groupIndex.ToString()); | |
//保存するためにレイヤーをサブアセットへ追加 | |
AnimatorStateMachine stateMachine = layer.stateMachine; | |
if (!AssetDatabase.IsSubAsset(stateMachine)) | |
AssetDatabase.AddObjectToAsset(stateMachine, controller); | |
stateMachine.hideFlags = HideFlags.HideInHierarchy; | |
stateMachine.name = "RILayerG" + groupIndex.ToString(); | |
//ビットマスクの生成 | |
var propCount = group.Props.Count; | |
var valueBase = 1 << propCount; | |
UpdateProgressBar("Creating bitmask.", 0, 1); | |
var bitMask = 1; | |
foreach (var v in Enumerable.Range(1, (propCount - 1))) | |
bitMask |= 1 << v; | |
var bitMaskRange = Enumerable.Range(0, bitMask + 1); | |
Debug.Log("BitMask : " + Convert.ToString(bitMask, 2).PadLeft(propCount, '0')); | |
Debug.Log("BitMaskCount : " + bitMaskRange.Count().ToString()); | |
var clipName = ""; | |
//Prop切り替え用メニューを取得(存在しなければ生成) | |
//Groupの数が1つだけなら生成しない | |
VRCExpressionsMenu propsMenu; | |
var targetParam = new VRCExpressionsMenu.Control.Parameter() { name = paramName }; | |
if (Values.Groups.Count > 1 && group.Props.Count > 1) | |
{ | |
propsMenu = GetOrCreateAsset(generatedItemsPath + "RadInvG" + groupIndex.ToString() + "Menu.asset", typeof(VRCExpressionsMenu)) as VRCExpressionsMenu; | |
propsMenu.controls = new List<VRCExpressionsMenu.Control>(); | |
//Groupの切り替えメニューを取得(存在しなければ生成) | |
//取得した上で現在のGroupをメニューに追加 | |
var menuName = string.IsNullOrEmpty(Values.Groups[groupIndex - 1].GroupName) ? "Group" + groupIndex.ToString() : Values.Groups[groupIndex - 1].GroupName; | |
var groupControl = GetOrCreateMenuControl(risMainMenu, menuName, ControlType.SubMenu); | |
groupControl.subMenu = propsMenu; | |
groupControl.icon = group.GroupIcon; | |
EditorUtility.SetDirty(risMainMenu); | |
} | |
else | |
{ | |
propsMenu = risMainMenu; | |
} | |
List<Dictionary<EditorCurveBinding, Vector2>> keyFramePairs = new List<Dictionary<EditorCurveBinding, Vector2>>(); | |
//x -> off value, y -> on value | |
foreach (var v in group.Props) | |
{ | |
var dict = new Dictionary<EditorCurveBinding, Vector2>(); | |
if (v.CustomAnim == null) | |
dict.Add(new EditorCurveBinding() { propertyName = "m_IsActive", path = GetGameObjectPath(v.TargetObject, Values.AvatarRoot.gameObject), type = v.TargetObject.GetType() }, new Vector2(0, 1)); | |
else | |
{ | |
AnimationClip group_default = v.CustomAnim; | |
EditorCurveBinding[] bindings = AnimationUtility.GetCurveBindings(group_default); | |
foreach (var bind in bindings) | |
{ | |
AnimationCurve curve = AnimationUtility.GetEditorCurve(group_default, bind); | |
if (curve != null && curve.keys.Length >= 2) | |
dict.Add(bind, new Vector2(curve.keys[0].value, curve.keys[1].value)); | |
} | |
} | |
keyFramePairs.Add(dict); | |
} | |
if (!group.ExclusiveMode) | |
{ | |
if (group.Props.Count > 1) | |
{ | |
//排他モードでないならProp切り替え用メニューにデフォルトボタンを追加 | |
control = GetOrCreateMenuControl(propsMenu, "Default", ControlType.Toggle); | |
control.parameter = targetParam; | |
control.value = valueBase; | |
Texture2D deficon = (Texture2D)AssetDatabase.LoadAssetAtPath("Assets/RadialInventory/reload.png", typeof(Texture2D)); | |
control.icon = deficon; | |
} | |
} | |
else | |
{ | |
state = null; | |
//排他モードならデフォルトステートを追加 | |
clip = CreateExclusivePropAnim(group.Props, null, keyFramePairs, true); | |
clipName = "G" + groupIndex.ToString() + "DEFAULT"; | |
objectToggleClips.Add(clipName, clip); | |
AssetDatabase.CreateAsset(clip, generatedItemsPath + "Animations/" + clipName + ".anim"); | |
EditorUtility.SetDirty(clip); | |
state = stateMachine.AddState(clipName, new Vector2(-240, 120 + 60)); | |
state.motion = clip; | |
state.writeDefaultValues = Values.UseWriteDefaults; | |
state.speed = 0; | |
state.cycleOffset = 0; | |
states.Add(new AnimatorState[] { state }); | |
} | |
foreach (var propIndex in Enumerable.Range(1, propCount)) | |
{ | |
UpdateProgressBar("Creating AnimationClip.", propIndex, propCount); | |
var prop = group.Props[propIndex - 1]; | |
if (group.ExclusiveMode) | |
{ | |
//現在のPropをONにするアニメーションを作成 | |
clip = CreateExclusivePropAnim(group.Props, prop, keyFramePairs); | |
clipName = "G" + groupIndex.ToString() + "P" + propIndex.ToString() + "ON"; | |
objectToggleClips.Add(clipName, clip); | |
AssetDatabase.CreateAsset(clip, generatedItemsPath + "Animations/" + clipName + ".anim"); | |
EditorUtility.SetDirty(clip); | |
state = stateMachine.AddState(clipName, new Vector2(0, 120 + 60 * propIndex)); | |
state.motion = clip; | |
state.writeDefaultValues = Values.UseWriteDefaults; | |
state.speed = 0; | |
state.cycleOffset = 0; | |
if (prop.LocalOnly) | |
{ | |
clip = new AnimationClip(); | |
foreach (var prop_subIndex in Enumerable.Range(0, propCount)) | |
{ | |
var prop_sub = group.Props[prop_subIndex]; | |
var keyframes = keyFramePairs[prop_subIndex]; | |
foreach (var v1 in keyFramePairs) | |
{ | |
foreach (var v2 in v1) | |
{ | |
var curve = new AnimationCurve(); | |
float status; | |
if (prop == prop_sub && prop.IsDefaultEnabled) | |
status = v2.Value.y; | |
else | |
status = v2.Value.x; | |
curve.AddKey(new Keyframe(0f, status)); | |
curve.AddKey(new Keyframe(1f / clip.frameRate, status)); | |
clip.SetCurve(v2.Key.path, v2.Key.type, v2.Key.propertyName, curve); | |
} | |
} | |
} | |
clipName = "G" + groupIndex.ToString() + "P" + propIndex.ToString() + "ON_REMOTE"; | |
objectToggleClips.Add(clipName, clip); | |
AssetDatabase.CreateAsset(clip, generatedItemsPath + "Animations/" + clipName + ".anim"); | |
EditorUtility.SetDirty(clip); | |
var state2 = stateMachine.AddState(clipName, new Vector2(0, -(120 + 60 * propIndex))); | |
state2.motion = clip; | |
state2.writeDefaultValues = Values.UseWriteDefaults; | |
state2.speed = 0; | |
state2.cycleOffset = 0; | |
states.Add(new AnimatorState[] { state, state2 }); | |
} | |
else | |
states.Add(new AnimatorState[] { state }); | |
} | |
//Prop切り替えメニューに現在のPropを追加 | |
control = GetOrCreateMenuControl(propsMenu, prop.TargetObject.name, ControlType.Toggle); | |
control.parameter = targetParam; | |
control.value = propIndex + (group.ExclusiveMode ? 0 : valueBase); | |
control.icon = prop.PropIcon; | |
} | |
//各ステートから各ステートへ移るトランジションを作成 | |
if (group.ExclusiveMode) | |
{ | |
foreach (var destIndex in Enumerable.Range(0, propCount + 1)) | |
{ | |
var destState = states[destIndex]; | |
if (destState.Length == 1) | |
{ | |
MakeAnyStateTransition(stateMachine, destState[0], paramName, destIndex, propCount).canTransitionToSelf = false; | |
} | |
else | |
{ | |
var transitonLocal = MakeAnyStateTransition(stateMachine, destState[0], paramName, destIndex, propCount); | |
transitonLocal.AddCondition(AnimatorConditionMode.If, 1, "IsLocal"); | |
transitonLocal.canTransitionToSelf = false; | |
transitonLocal = MakeAnyStateTransition(stateMachine, destState[1], paramName, destIndex, propCount); | |
transitonLocal.AddCondition(AnimatorConditionMode.IfNot, 1, "IsLocal"); | |
transitonLocal.canTransitionToSelf = false; | |
} | |
} | |
} | |
else | |
{ | |
var bitstateClip = new AnimationClip(); | |
float bitstateStatus = 0; | |
AnimationClipSettings settings = AnimationUtility.GetAnimationClipSettings(bitstateClip); | |
settings.loopTime = true; | |
AnimationUtility.SetAnimationClipSettings(bitstateClip, settings); | |
clipName = "G" + groupIndex.ToString() + "STATES"; | |
AssetDatabase.CreateAsset(bitstateClip, generatedItemsPath + "Animations/" + clipName + ".anim"); | |
var remoteBitstateClip = new AnimationClip(); | |
settings = AnimationUtility.GetAnimationClipSettings(remoteBitstateClip); | |
settings.loopTime = true; | |
AnimationUtility.SetAnimationClipSettings(remoteBitstateClip, settings); | |
clipName = "G" + groupIndex.ToString() + "STATES_REMOTE"; | |
AssetDatabase.CreateAsset(remoteBitstateClip, generatedItemsPath + "Animations/" + clipName + ".anim"); | |
var bitPatternStates = new List<AnimatorState[]>(); | |
var propRange = Enumerable.Range(0, propCount); | |
var localPropBitMasks = new List<int>(); | |
foreach(var v in propRange) | |
{ | |
if (group.Props[v].LocalOnly) | |
localPropBitMasks.Add(1 << v); | |
} | |
//各ビット状態に応じたステートの追加 | |
foreach (var bitPattern in bitMaskRange) | |
{ | |
var xPos = 240 * (bitPattern % propCount + 1); | |
var yPos = 60 * (bitPattern / propCount + 3); | |
state = stateMachine.AddState(Convert.ToString(bitPattern, 2).PadLeft(propCount, '0'), new Vector2(xPos, yPos)); | |
state.motion = bitstateClip; | |
state.writeDefaultValues = Values.UseWriteDefaults; | |
state.speed = 0; | |
state.cycleOffset = 1f / (bitMask + 1) * bitPattern; | |
var driver = state.AddStateMachineBehaviour<VRCAvatarParameterDriver>(); | |
driver.parameters = new List<VRC_AvatarParameterDriver.Parameter> | |
{ | |
new VRC_AvatarParameterDriver.Parameter { name = paramName, value = bitPattern } | |
}; | |
if (localPropBitMasks.Any(n => (n & bitPattern) != 0)) | |
{ | |
var stateRemote = stateMachine.AddState(Convert.ToString(bitPattern, 2).PadLeft(propCount, '0') + "_REMOTE", new Vector2(xPos, -yPos)); | |
stateRemote.motion = remoteBitstateClip; | |
stateRemote.writeDefaultValues = Values.UseWriteDefaults; | |
stateRemote.speed = 0; | |
stateRemote.cycleOffset = 1f / (bitMask + 1) * bitPattern; | |
driver = stateRemote.AddStateMachineBehaviour<VRCAvatarParameterDriver>(); | |
driver.parameters = new List<VRC_AvatarParameterDriver.Parameter> | |
{ | |
new VRC_AvatarParameterDriver.Parameter { name = paramName, value = bitPattern } | |
}; | |
bitPatternStates.Add(new AnimatorState[] { state, stateRemote }); | |
} | |
else | |
bitPatternStates.Add(new AnimatorState[] { state }); | |
} | |
var propIndexRange = Enumerable.Range(1, propCount); | |
var bitstateCurves = new List<List<AnimationCurve>>(); | |
foreach (var propIndex in propIndexRange) | |
bitstateCurves.Add(Enumerable.Range(0, keyFramePairs[propIndex - 1].Count).Select(n => new AnimationCurve()).ToList()); | |
var remoteBitstateCurves = new List<List<AnimationCurve>>(); | |
foreach (var propIndex in propIndexRange) | |
remoteBitstateCurves.Add(Enumerable.Range(0, keyFramePairs[propIndex - 1].Count).Select(n => new AnimationCurve()).ToList()); | |
//Parameterがビットパターン数+Prop番号になった時にProp番号のビットを反転させ、そのステートに移るトランジションを作成。 | |
var anyStateTransition = MakeAnyStateTransition(stateMachine, bitPatternStates[0][0], paramName, valueBase, propCount, AnimatorConditionMode.Equals, false, -valueBase); | |
anyStateTransition.canTransitionToSelf = false; | |
foreach (var bitPattern in bitMaskRange) | |
{ | |
var bitPatternState = bitPatternStates[bitPattern][0]; | |
anyStateTransition = stateMachine.AddAnyStateTransition(bitPatternState); | |
anyStateTransition.name = "->" + bitPatternState.name; | |
anyStateTransition.canTransitionToSelf = false; | |
anyStateTransition.AddCondition(AnimatorConditionMode.Equals, bitPattern, paramName); | |
if (bitPatternStates[bitPattern].Length == 2) | |
{ | |
anyStateTransition.AddCondition(AnimatorConditionMode.If, 1, "IsLocal"); | |
bitPatternState = bitPatternStates[bitPattern][1]; | |
anyStateTransition = stateMachine.AddAnyStateTransition(bitPatternState); | |
anyStateTransition.name = "->" + bitPatternState.name; | |
anyStateTransition.canTransitionToSelf = false; | |
anyStateTransition.AddCondition(AnimatorConditionMode.Equals, bitPattern, paramName); | |
anyStateTransition.AddCondition(AnimatorConditionMode.IfNot, 1, "IsLocal"); | |
} | |
foreach (var srcState in bitPatternStates[bitPattern]) | |
{ | |
foreach (var propIndex in propIndexRange) | |
{ | |
var propBit = 1 << (propIndex - 1); | |
var xorBit = bitPattern ^ propBit; | |
var destIsLocal = bitPatternStates[xorBit].Length == 2; | |
if (destIsLocal) | |
{ | |
MakeTransition(srcState, bitPatternStates[xorBit][0], paramName, valueBase + propIndex, propCount, AnimatorConditionMode.Equals, false, -valueBase) | |
.AddCondition(AnimatorConditionMode.If, 1f, "IsLocal"); | |
MakeTransition(srcState, bitPatternStates[xorBit][1], paramName, valueBase + propIndex, propCount, AnimatorConditionMode.Equals, false, -valueBase) | |
.AddCondition(AnimatorConditionMode.IfNot, 1f, "IsLocal"); | |
} | |
else | |
MakeTransition(srcState, bitPatternStates[xorBit][0], paramName, valueBase + propIndex, propCount, AnimatorConditionMode.Equals, false, -valueBase); | |
var pairs = keyFramePairs[propIndex - 1].ToList(); | |
foreach (var v in Enumerable.Range(0, pairs.Count)) | |
{ | |
var pair = pairs[v]; | |
var prop = group.Props[propIndex - 1]; | |
var isActive = (bitPattern & propBit) != 0; | |
if (isActive ^ prop.IsDefaultEnabled) | |
bitstateStatus = pair.Value.y; | |
else | |
bitstateStatus = pair.Value.x; | |
bitstateCurves[propIndex - 1][v].AddKey(new Keyframe(1f / bitstateClip.frameRate * bitPattern, bitstateStatus)); | |
if (prop.LocalOnly) | |
bitstateStatus = prop.IsDefaultEnabled ? pair.Value.y : pair.Value.x; | |
remoteBitstateCurves[propIndex - 1][v].AddKey(new Keyframe(1f / bitstateClip.frameRate * bitPattern, bitstateStatus)); | |
} | |
} | |
} | |
} | |
foreach (var propIndex in propIndexRange) | |
{ | |
var keyframes = keyFramePairs[propIndex - 1].ToList(); | |
foreach (var v in Enumerable.Range(0, keyframes.Count)) | |
{ | |
bitstateCurves[propIndex - 1][v].AddKey(new Keyframe(1f / bitstateClip.frameRate * (bitMask + 1), 1)); | |
remoteBitstateCurves[propIndex - 1][v].AddKey(new Keyframe(1f / bitstateClip.frameRate * (bitMask + 1), 1)); | |
var v2 = keyframes[v]; | |
bitstateClip.SetCurve(v2.Key.path, v2.Key.type, v2.Key.propertyName, bitstateCurves[propIndex - 1][v]); | |
remoteBitstateClip.SetCurve(v2.Key.path, v2.Key.type, v2.Key.propertyName, remoteBitstateCurves[propIndex - 1][v]); | |
} | |
} | |
EditorUtility.SetDirty(bitstateClip); | |
} | |
EditorUtility.SetDirty(propsMenu); | |
} | |
EditorUtility.ClearProgressBar(); | |
Values.AvatarRoot.customExpressions = true; | |
Values.AvatarRoot.customizeAnimationLayers = true; | |
Values.AvatarRoot.baseAnimationLayers[4] = new CustomAnimLayer() { animatorController = controller, isEnabled = true, type = AnimLayerType.FX, isDefault = false }; | |
Values.AvatarRoot.expressionParameters = expressionParameters; | |
EditorUtility.SetDirty(controller); | |
AssetDatabase.SaveAssets(); | |
AssetDatabase.Refresh(); | |
} | |
public bool CheckHasParameterSpace(VRCExpressionParameters expressionParameters, int groupCount) | |
{ | |
int currentCost = expressionParameters.CalcTotalCost(); | |
currentCost -= expressionParameters.parameters.Where(n => string.IsNullOrEmpty(n.name)).Sum(n => VRCExpressionParameters.TypeCost(n.valueType)); | |
int existParamCount = 0; | |
foreach(var v in Enumerable.Range(1, groupCount)) | |
{ | |
var param = expressionParameters.FindParameter("RadInvStatusG" + v.ToString()); | |
if (param != null) | |
{ | |
if (param.valueType != VRCExpressionParameters.ValueType.Int) | |
currentCost -= VRCExpressionParameters.TypeCost(param.valueType); | |
else | |
++existParamCount; | |
} | |
} | |
currentCost += (groupCount - existParamCount) * VRCExpressionParameters.TypeCost(VRCExpressionParameters.ValueType.Int); | |
if (currentCost <= VRCExpressionParameters.MAX_PARAMETER_COST) | |
return true; | |
return false; | |
} | |
public AnimatorStateTransition MakeAnyStateTransition(AnimatorStateMachine stateMachine, AnimatorState destState, string paramName, int v, int propsCount, AnimatorConditionMode mode = AnimatorConditionMode.Equals, bool binaryName = true, int binaryOffset = 0) | |
{ | |
AnimatorStateTransition transition; | |
transition = stateMachine.AddAnyStateTransition(destState); | |
transition.hasExitTime = false; | |
transition.exitTime = 0; | |
transition.hasFixedDuration = true; | |
transition.duration = 0; | |
transition.offset = 0; | |
if (binaryName) | |
transition.name = Convert.ToString(v + binaryOffset, 2).PadLeft(propsCount, '0') + "(->" + destState.name + ") [DEC:" + v.ToString() + "]"; | |
else | |
transition.name = (v + binaryOffset).ToString() + "(->" + destState.name + ")"; | |
transition.conditions = new AnimatorCondition[] { new AnimatorCondition() { mode = mode, parameter = paramName, threshold = v } }; | |
return transition; | |
} | |
public AnimatorStateTransition MakeTransition(AnimatorState srcState, AnimatorState destState, string paramName, int v, int propsCount, AnimatorConditionMode mode = AnimatorConditionMode.Equals, bool binaryName = true, int binaryOffset = 0) | |
{ | |
AnimatorStateTransition transition; | |
transition = srcState.AddTransition(destState); | |
transition.hasExitTime = false; | |
transition.exitTime = 0; | |
transition.hasFixedDuration = true; | |
transition.duration = 0; | |
transition.offset = 0; | |
if(binaryName) | |
transition.name = Convert.ToString(v + binaryOffset, 2).PadLeft(propsCount, '0') + "(->" + destState.name + ") [DEC:" + v.ToString() + "]"; | |
else | |
transition.name = (v + binaryOffset).ToString() + "(->" + destState.name + ")"; | |
transition.conditions = new AnimatorCondition[] { new AnimatorCondition() { mode = mode, parameter = paramName, threshold = v } }; | |
return transition; | |
} | |
public AnimatorStateTransition MakeTransition(AnimatorState srcState, AnimatorState destState, string paramName, int min, int max, int propsCount) | |
{ | |
AnimatorStateTransition transition; | |
transition = srcState.AddTransition(destState); | |
transition.hasExitTime = false; | |
transition.exitTime = 0; | |
transition.hasFixedDuration = true; | |
transition.duration = 0; | |
transition.offset = 0; | |
transition.name = Convert.ToString(min, 2).PadLeft(propsCount, '0') + "-" + Convert.ToString(max, 2).PadLeft(propsCount, '0') + "(->" + destState.name + ") [DEC:" + min.ToString() + "-" + max.ToString() + "]"; | |
transition.conditions = new AnimatorCondition[] | |
{ | |
new AnimatorCondition() { mode = AnimatorConditionMode.Greater, parameter = paramName, threshold = min - 1 }, | |
new AnimatorCondition() { mode = AnimatorConditionMode.Less, parameter = paramName, threshold = max + 1 }, | |
}; | |
return transition; | |
} | |
public void ClearAllGeneratedStateMachine(AnimatorController controller) | |
{ | |
var max = controller.layers.Count(); | |
int ofs = 0; | |
foreach (var layerIndex in Enumerable.Range(0, controller.layers.Count())) | |
{ | |
UpdateProgressBar("Get all generated state machines.", layerIndex, max); | |
var name = controller.layers[layerIndex + ofs].name; | |
if (name.StartsWith("RadInvLayer") || name.StartsWith("RadBitLayer")) | |
{ | |
controller.RemoveLayer(layerIndex + ofs); | |
ofs--; | |
} | |
} | |
} | |
public void UpdateProgressBar(string text, int count, int max) | |
{ | |
float progress = (float)count / max; | |
EditorUtility.DisplayProgressBar(text, count.ToString() + "/" + max.ToString() + " (" + (progress * 100) + "%)", progress); | |
} | |
public AnimationClip CreateExclusivePropAnim(List<PropInfo> props, PropInfo targetProp, List<Dictionary<EditorCurveBinding, Vector2>> keyframeList, bool generateDefault = false) | |
{ | |
var clip = new AnimationClip(); | |
foreach (var prop_index in Enumerable.Range(0, props.Count)) | |
{ | |
var prop = props[prop_index]; | |
var keyframes = keyframeList[prop_index]; | |
foreach (var v in keyframes) | |
{ | |
var curve = new AnimationCurve(); | |
float status; | |
if (generateDefault) | |
status = prop.IsDefaultEnabled ? v.Value.y : v.Value.x; | |
else if (targetProp == prop && (props.Count > 1 || (props.Count <= 1 && !targetProp.IsDefaultEnabled))) | |
status = v.Value.y; | |
else | |
status = v.Value.x; | |
curve.AddKey(new Keyframe(0f, status)); | |
curve.AddKey(new Keyframe(1f / clip.frameRate, status)); | |
clip.SetCurve(v.Key.path, v.Key.type, v.Key.propertyName, curve); | |
} | |
} | |
return clip; | |
} | |
public VRCExpressionsMenu.Control GetOrCreateMenuControl(VRCExpressionsMenu menu, string name, ControlType type) | |
{ | |
var control = menu.controls.FirstOrDefault(n => n.name == name && n.type == type); | |
if (control == null) | |
{ | |
control = new VRCExpressionsMenu.Control(); | |
control.name = name; | |
control.type = type; | |
menu.controls.Add(control); | |
} | |
return control; | |
} | |
public void ReCreateFolder(string path) | |
{ | |
if (AssetDatabase.IsValidFolder(path)) | |
DeleteAssetDirectory(path); | |
CreateFolderRecursively(path); | |
} | |
public UnityEngine.Object GetOrCreateAsset(string path, Type type) | |
{ | |
var obj = AssetDatabase.LoadAssetAtPath(path, type); | |
if (obj == null) | |
{ | |
obj = CreateInstance(type); | |
AssetDatabase.CreateAsset(obj, path); | |
} | |
return obj; | |
} | |
public AnimatorController GetOrCreateAnimator(string generatedItemsPath) | |
{ | |
AnimatorController controller = null; | |
if (Values != null && Values.AvatarRoot != null && Values.AvatarRoot.baseAnimationLayers != null && | |
Values.AvatarRoot.baseAnimationLayers.Length >= 5 && Values.AvatarRoot.baseAnimationLayers[4].animatorController != null) | |
controller = (AnimatorController)Values.AvatarRoot.baseAnimationLayers[4].animatorController; | |
else | |
controller = AnimatorController.CreateAnimatorControllerAtPath(generatedItemsPath + "FXRadialInventory.controller"); | |
return controller; | |
} | |
public void RestoreSettings(SORadialInventory srcValues) | |
{ | |
Values.Groups = srcValues.Groups.Select(n => | |
{ | |
var obj = (GroupInfo)n.Clone(); | |
if (obj != null && obj.Props != null) | |
foreach (var v in obj.Props) | |
if (v != null) | |
{ | |
v.TargetObject = FindGameObjectFromPath(v.TargetObjectPath, Values.AvatarRoot.gameObject); | |
} | |
return obj; | |
}).ToList(); | |
Values.SaveParameter = srcValues.SaveParameter; | |
Values.UseWriteDefaults = srcValues.UseWriteDefaults; | |
} | |
public void SaveSettingsToFile(string generatedItemsPath) | |
{ | |
var path = generatedItemsPath + "SavedSettings.asset"; | |
SORadialInventory settings = (SORadialInventory)AssetDatabase.LoadAssetAtPath(path, typeof(SORadialInventory)); | |
if (settings == null) | |
{ | |
settings = ScriptableObject.CreateInstance<SORadialInventory>(); | |
AssetDatabase.CreateAsset(settings, generatedItemsPath + "SavedSettings.asset"); | |
} | |
foreach (var asset in AssetDatabase.LoadAllAssetsAtPath(path)) | |
{ | |
if (AssetDatabase.IsSubAsset(asset)) | |
{ | |
DestroyImmediate(asset, true); | |
} | |
} | |
AssetDatabase.Refresh(); | |
AssetDatabase.SaveAssets(); | |
var groupID = 1; | |
settings.SaveParameter = Values.SaveParameter; | |
settings.UseWriteDefaults = Values.UseWriteDefaults; | |
settings.Groups = Values.Groups.Select(n => | |
{ | |
var obj = (GroupInfo)n.Clone(); | |
var propID = 1; | |
obj.Props.ForEach(v => | |
{ | |
v.name = "G" + groupID.ToString() + "Prop" + (propID++).ToString(); | |
v.TargetObjectPath = GetGameObjectPath(v.TargetObject, Values.AvatarRoot.gameObject); | |
AssetDatabase.AddObjectToAsset(v, settings); | |
}); | |
obj.name = "Group" + (groupID++).ToString(); | |
AssetDatabase.AddObjectToAsset(obj, settings); | |
return obj; | |
}).ToList(); | |
EditorUtility.SetDirty(settings); | |
AssetDatabase.SaveAssets(); | |
} | |
public AnimatorControllerLayer GetOrCreateControllerLayer(AnimatorController controller, string layerName, int weight = 1) | |
{ | |
AnimatorControllerLayer layer = controller.layers.FirstOrDefault(n => n.name == layerName); | |
if (layer == null) | |
{ | |
layer = new AnimatorControllerLayer(); | |
layer.defaultWeight = weight; | |
layer.blendingMode = AnimatorLayerBlendingMode.Override; | |
layer.name = layerName; | |
layer.stateMachine = new AnimatorStateMachine(); | |
controller.AddLayer(layer); | |
} | |
if (layer.stateMachine == null) | |
layer.stateMachine = new AnimatorStateMachine(); | |
return layer; | |
} | |
public VRCExpressionParameters CreateExpressionParameters(string path) | |
{ | |
var param = CreateInstance<VRCExpressionParameters>(); | |
param.parameters = new VRCExpressionParameters.Parameter[16]; | |
param.parameters[0] = new VRCExpressionParameters.Parameter() { name = "VRCEmote", valueType = VRCExpressionParameters.ValueType.Int }; | |
param.parameters[1] = new VRCExpressionParameters.Parameter() { name = "VRCFaceBlendH", valueType = VRCExpressionParameters.ValueType.Float }; | |
param.parameters[2] = new VRCExpressionParameters.Parameter() { name = "VRCFaceBlendV", valueType = VRCExpressionParameters.ValueType.Float }; | |
AssetDatabase.CreateAsset(param, path); | |
return param; | |
} | |
public bool CheckParameter(VRCExpressionParameters expressionParameters, string paramName, AnimatorController controller, bool saved) | |
{ | |
int i = -1; | |
var cost = expressionParameters.CalcTotalCost(); | |
var blankParams = expressionParameters.parameters.Where(n => string.IsNullOrEmpty(n.name)); | |
cost -= blankParams.Sum(n => VRCExpressionParameters.TypeCost(n.valueType)); | |
var param = expressionParameters.FindParameter(paramName); | |
if(param == null) | |
{ | |
if(blankParams.Any()) | |
{ | |
foreach(var v in Enumerable.Range(0, expressionParameters.parameters.Count())) | |
{ | |
if(string.IsNullOrEmpty(expressionParameters.parameters[v].name)) | |
{ | |
param = expressionParameters.parameters[v]; | |
var newCost = cost - VRCExpressionParameters.TypeCost(param.valueType) + VRCExpressionParameters.TypeCost(VRCExpressionParameters.ValueType.Int); | |
if (newCost <= VRCExpressionParameters.MAX_PARAMETER_COST) | |
{ | |
param.valueType = VRCExpressionParameters.ValueType.Int; | |
param.name = paramName; | |
break; | |
} | |
else | |
return false; | |
} | |
} | |
} | |
else | |
{ | |
if (cost + VRCExpressionParameters.TypeCost(VRCExpressionParameters.ValueType.Int) <= VRCExpressionParameters.MAX_PARAMETER_COST) | |
{ | |
var len = expressionParameters.parameters.Length; | |
Array.Resize(ref expressionParameters.parameters, len + 1); | |
param = new VRCExpressionParameters.Parameter(); | |
expressionParameters.parameters[len] = param; | |
expressionParameters.parameters[len].name = paramName; | |
expressionParameters.parameters[len].valueType = VRCExpressionParameters.ValueType.Int; | |
} | |
else | |
return false; | |
} | |
} | |
else if(param.valueType != VRCExpressionParameters.ValueType.Int) | |
{ | |
var newCost = cost - VRCExpressionParameters.TypeCost(param.valueType) + VRCExpressionParameters.TypeCost(VRCExpressionParameters.ValueType.Int); | |
if (newCost <= VRCExpressionParameters.MAX_PARAMETER_COST) | |
{ | |
param.valueType = VRCExpressionParameters.ValueType.Int; | |
} | |
else | |
return false; | |
} | |
param.saved = saved; | |
if (!controller.parameters.Any(n => n.name == paramName)) | |
controller.AddParameter(paramName, AnimatorControllerParameterType.Int); | |
else | |
{ | |
var first = controller.parameters.First(n => n.name == paramName); | |
if (first.type != AnimatorControllerParameterType.Int) | |
first.type = AnimatorControllerParameterType.Int; | |
} | |
return true; | |
} | |
public void CreateFolderRecursively(string path) | |
{ | |
Debug.Assert(path.StartsWith("Assets/"), "arg `path` of CreateFolderRecursively doesn't starts with `Assets/`"); | |
if (AssetDatabase.IsValidFolder(path)) return; | |
if (path[path.Length - 1] == '/') | |
{ | |
path = path.Substring(0, path.Length - 1); | |
} | |
var names = path.Split('/'); | |
for (int i = 1; i < names.Length; i++) | |
{ | |
var parent = string.Join("/", names.Take(i).ToArray()); | |
var target = string.Join("/", names.Take(i + 1).ToArray()); | |
var child = names[i]; | |
if (!AssetDatabase.IsValidFolder(target)) | |
{ | |
AssetDatabase.CreateFolder(parent, child); | |
} | |
} | |
} | |
public string GetGameObjectPath(GameObject obj, GameObject parent = null, int recCount = -1) | |
{ | |
string path = "/" + obj.name; | |
int i = 0; | |
string parentpath = ""; | |
if (parent != null) | |
parentpath = GetGameObjectPath(parent); | |
while (obj.transform.parent != null && (recCount == -1 || i < recCount)) | |
{ | |
++i; | |
obj = obj.transform.parent.gameObject; | |
path = "/" + obj.name + path; | |
} | |
Debug.Log("[old]path: " + path); | |
var parentpathLength = parentpath.Length; | |
if (!string.IsNullOrEmpty(parentpath)) | |
{ | |
path = path.Remove(1,parentpathLength); | |
} | |
Debug.Log("[new]path: " + path); | |
return path.TrimStart('/'); | |
} | |
private bool DeleteAssetDirectory(string assetPath) | |
{ | |
if (AssetDatabase.DeleteAsset(assetPath)) | |
{ | |
Debug.Log("Delete Asset: " + assetPath); | |
return true; | |
} | |
string[] dirpathlist = Directory.GetDirectories(assetPath); | |
foreach (string path in dirpathlist) | |
{ | |
if (false == DeleteAssetDirectory(path)) | |
{ | |
Debug.LogError("Delete Asset Directory Error: " + path); | |
return false; | |
} | |
} | |
string[] filepathlist = Directory.GetFiles(assetPath); | |
foreach (string path in filepathlist) | |
{ | |
if (path.EndsWith(".meta")) | |
{ | |
continue; | |
} | |
if (false == AssetDatabase.DeleteAsset(path)) | |
{ | |
Debug.LogError("Delete Asset Files Error: " + path); | |
return false; | |
} | |
} | |
Debug.Log("Delete Asset: " + assetPath); | |
return true; | |
} | |
public GameObject FindGameObjectFromPath(string path, GameObject root = null) | |
{ | |
var splittedPath = path.Split('/'); | |
GameObject currentObject = root; | |
foreach (var v in splittedPath) | |
{ | |
if (currentObject == null) | |
currentObject = GameObject.Find(v); | |
else | |
currentObject = currentObject.transform.Find(v)?.gameObject; | |
if (currentObject == null) | |
{ | |
Debug.Log("[Warn] currentObject == null"); | |
return null; | |
} | |
} | |
return currentObject; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
メリノさんでなんか変になるやつの修正
2.9向け