Added Unity project files

This commit is contained in:
2026-06-07 16:58:24 +01:00
parent 3cc05d260b
commit 23bbcab156
3942 changed files with 453676 additions and 0 deletions

View File

@ -0,0 +1,53 @@
using UnityEditor;
using UnityEngine;
using System.Collections.Generic;
using VRCSceneDescriptor = VRC.SDK3.Components.VRCSceneDescriptor;
namespace VRC.SDK3.Editor
{
[InitializeOnLoad]
public static class SpawnGizmoDrawer
{
private static VRCSceneDescriptor sceneDescriptor;
static SpawnGizmoDrawer()
{
SceneView.duringSceneGui += OnSceneGUI;
}
static void OnSceneGUI(SceneView sceneView)
{
if (Selection.activeTransform == null)
return;
if (sceneDescriptor == null)
{
sceneDescriptor = GameObject.FindFirstObjectByType<VRCSceneDescriptor>();
if (sceneDescriptor == null)
return;
}
Transform selected = Selection.activeTransform;
foreach (Transform spawn in sceneDescriptor.spawns)
{
if (spawn == null)
continue;
if (spawn == selected)
{
if (sceneDescriptor.spawnRadius != 0)
Handles.DrawWireDisc(spawn.position, Vector3.up, sceneDescriptor.spawnRadius);
else
{
float size = HandleUtility.GetHandleSize(spawn.position) * 0.5f;
Vector3 pos = spawn.position;
Vector3 offset1 = (Vector3.forward + Vector3.right).normalized * size;
Vector3 offset2 = (Vector3.forward - Vector3.right).normalized * size;
Handles.DrawLine(pos + offset1, pos - offset1);
Handles.DrawLine(pos + offset2, pos - offset2);
}
}
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 59a956eb4084499dbbc4004a6922c503
timeCreated: 1744910458

View File

@ -0,0 +1,166 @@
using System;
using System.Net;
using System.Threading.Tasks;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
using VRC.SDK3.Components;
using VRC.SDKBase.Editor.Api;
namespace VRC.SDK3.Editor
{
[CanEditMultipleObjects]
[CustomEditor(typeof(VRCAvatarPedestal))]
public class VRCAvatarPedestalEditor : VRCInspectorBase
{
private SerializedProperty _blueprintId;
private SerializedProperty _placement;
private SerializedProperty _changeAvatarsOnUse;
private SerializedProperty _scale;
private PropertyField _fieldBlueprintId;
private PropertyField _fieldPlacement;
private PropertyField _fieldChangeAvatarsOnUse;
private PropertyField _fieldScale;
private HelpBox _errorMessageBox;
private HelpBox _warningMessageBox;
private const string ERROR_PRIVATE_AVATAR = "This pedestal contains a private avatar ID. Only the author of the avatar will be able to see the pedestal and interact with it in VRChat.";
private const string ERROR_PRIVATE_OWN_AVATAR = "This pedestal contains a private avatar ID of your own. Only you will be able to see the pedestal and interact with it in VRChat.";
private const string ERROR_FAILED_TO_LOAD = "Failed to load avatar information. Make sure the avatar ID is valid and the avatar is public.";
private const string ERROR_UNAUTHORIZED = "Failed to load avatar information. Make sure you are logged in via the VRChat SDK Control Panel.";
private void OnEnable()
{
_blueprintId = serializedObject.FindProperty(nameof(VRCAvatarPedestal.blueprintId));
_placement = serializedObject.FindProperty(nameof(VRCAvatarPedestal.Placement));
_changeAvatarsOnUse = serializedObject.FindProperty(nameof(VRCAvatarPedestal.ChangeAvatarsOnUse));
_scale = serializedObject.FindProperty(nameof(VRCAvatarPedestal.scale));
}
public override void BuildInspectorGUI()
{
base.BuildInspectorGUI();
_fieldBlueprintId = AddFieldLabel(_blueprintId, "Avatar ID");
_errorMessageBox = new HelpBox(string.Empty, HelpBoxMessageType.Error)
{
style =
{
display = DisplayStyle.None
}
};
Root.Add(_errorMessageBox);
_warningMessageBox = new HelpBox(string.Empty, HelpBoxMessageType.Warning)
{
style =
{
display = DisplayStyle.None
}
};
Root.Add(_warningMessageBox);
_fieldPlacement = AddField(_placement);
_fieldChangeAvatarsOnUse = AddField(_changeAvatarsOnUse);
_fieldScale = AddField(_scale);
_fieldBlueprintId.RegisterValueChangeCallback(evt => AvatarChanged(evt.changedProperty.stringValue).ConfigureAwait(false));
AvatarChanged(_blueprintId.stringValue).ConfigureAwait(false);
}
private void ShowError(string message)
{
HideWarning();
_errorMessageBox.text = message;
_errorMessageBox.style.display = DisplayStyle.Flex;
}
private void HideError()
{
_errorMessageBox.style.display = DisplayStyle.None;
}
private void ShowWarning(string message)
{
HideError();
_warningMessageBox.text = message;
_warningMessageBox.style.display = DisplayStyle.Flex;
}
private void HideWarning()
{
_warningMessageBox.style.display = DisplayStyle.None;
}
private async Task AvatarChanged(string id)
{
if (string.IsNullOrWhiteSpace(id))
{
HideError();
HideWarning();
return;
}
try
{
// Attempt to log in if the user is not logged in
if (!Core.APIUser.IsLoggedIn)
{
// No void task completion source in .NET Standard, so we use a <bool> here
var resultTask = new TaskCompletionSource<bool>();
Core.APIUser.InitialFetchCurrentUser(_ => resultTask.SetResult(true), _ => resultTask.SetResult(false));
await resultTask.Task;
}
var avatarInfo = await VRCApi.GetAvatar(id, true);
// AVM Avatars should not trigger a warning or error
if (avatarInfo.ReleaseStatus != "public" && !avatarInfo.Lock)
{
if (avatarInfo.AuthorId == Core.APIUser.CurrentUser?.id)
{
Core.Logger.LogWarning(ERROR_PRIVATE_OWN_AVATAR);
ShowWarning(ERROR_PRIVATE_OWN_AVATAR);
}
else
{
Core.Logger.LogError(ERROR_PRIVATE_AVATAR);
ShowError(ERROR_PRIVATE_AVATAR);
}
return;
}
// If everything is ok - we can hide all the messages
HideWarning();
HideError();
}
catch (ApiErrorException e)
{
if (e.StatusCode == HttpStatusCode.NotFound)
{
Core.Logger.LogError(ERROR_PRIVATE_AVATAR);
ShowError(ERROR_PRIVATE_AVATAR);
return;
}
if (e.StatusCode == HttpStatusCode.Unauthorized)
{
Core.Logger.LogWarning(ERROR_UNAUTHORIZED);
ShowWarning(ERROR_UNAUTHORIZED);
return;
}
Core.Logger.LogException(e);
Core.Logger.LogError(e.ErrorMessage);
Core.Logger.LogWarning(ERROR_FAILED_TO_LOAD);
ShowWarning(ERROR_FAILED_TO_LOAD);
}
catch (Exception e)
{
Core.Logger.LogException(e);
Core.Logger.LogWarning(ERROR_FAILED_TO_LOAD);
ShowWarning(ERROR_FAILED_TO_LOAD);
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 92daf15a37b84806a147aa8127651a17
timeCreated: 1746046539

View File

@ -0,0 +1,328 @@
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using VRC.SDK3.Components;
namespace VRC.SDK3.Editor
{
[InitializeOnLoad]
public static class VRCCameraDollyDrawer
{
private static readonly Color[] SplineColors =
{
new(0f, 0.7f, 0.7f),
new(1f, 0.5f, 0f),
new(0.6f, 0f, 0.8f),
new(0.6f, 0.4f, 0.2f),
new(0.2f, 0.6f, 0.9f),
new(0.9f, 0.6f, 0.7f),
new(0.3f, 0.3f, 0.7f),
Color.yellow,
new(0.8f, 0.3f, 0.3f),
Color.magenta,
};
private static VRCCameraDollyAnimation ANIMATION;
static VRCCameraDollyDrawer()
{
SceneView.duringSceneGui += OnSceneGUI;
}
static void OnSceneGUI(SceneView sceneView)
{
// don't draw if gizmos are off
if (!SceneView.currentDrawingSceneView.drawGizmos) return;
Transform[] selection = Selection.transforms;
if (selection == null || selection.Length == 0) return;
// check each selected object
foreach (Transform selected in selection)
{
VRCCameraDollyPath path;
// if an animation is selected, draw all of its paths
if (selected.TryGetComponent(out ANIMATION))
{
if (ANIMATION.Paths == null) return;
// draw paths
int pathIndex;
for (pathIndex = 0; pathIndex < ANIMATION.Paths.Length; pathIndex++)
{
path = ANIMATION.Paths[pathIndex];
if (path == null) continue;
DrawPath(ANIMATION, path);
DrawPoints(ANIMATION, path);
}
}
else
{
// if a path is selected, draw it
if (selected.TryGetComponent(out path))
ANIMATION = path.GetComponentInParent<VRCCameraDollyAnimation>();
// if a point is selected, also draw its path
else if (selected.TryGetComponent(out VRCCameraDollyPathPoint point))
{
ANIMATION = point.GetComponentInParent<VRCCameraDollyAnimation>();
path = point.GetComponentInParent<VRCCameraDollyPath>();
}
// if selected object isn't an animation, path, or point, don't draw anything
else return;
DrawPath(ANIMATION, path);
DrawPoints(ANIMATION, path);
}
}
}
private static void DrawPath(VRCCameraDollyAnimation animation, VRCCameraDollyPath path)
{
if (!IsPathValid(animation, path)) return;
int index = path.transform.GetSiblingIndex();
Handles.color = animation.gameObject.activeInHierarchy
? SplineColors[index % SplineColors.Length]
: Color.gray;
var points = path.Points
.Where(p => p != null)
.Select(p => p.transform.position)
.ToList();
VRCCameraDollyAnimation.VRCCameraDollyPathType pathType = animation.PathType;
if (points.Count < 4)
{
pathType = VRCCameraDollyAnimation.VRCCameraDollyPathType.Linear;
}
if (pathType == VRCCameraDollyAnimation.VRCCameraDollyPathType.Linear)
{
for (int i = 0; i <= points.Count - 2; i++)
{
Handles.DrawLine(points[i], points[i+1], 5);
}
}
else
{
Vector3 prev = GetPointOnSpline(points, 0f, pathType);
for (int i = 1; i <= VRCCameraDollyAnimation.RESOLUTION; i++)
{
float t = i / (float)VRCCameraDollyAnimation.RESOLUTION;
Vector3 curr = GetPointOnSpline(points, t, pathType);
Handles.DrawLine(prev, curr, 5);
prev = curr;
}
}
// draw path label in the path's color
Handles.Label(points[0] + new Vector3(0,0.3f,0), $"Path {index+1}",
new GUIStyle { fontSize = 25, normal = new GUIStyleState { textColor = Handles.color } });
}
private static void DrawPoints(VRCCameraDollyAnimation animation, VRCCameraDollyPath path)
{
if (!IsPathValid(animation, path)) return;
for (int i = 0; i < path.Points.Length; i++)
{
var point = path.Points[i];
if (point == null) continue;
if (animation.PathType != VRCCameraDollyAnimation.VRCCameraDollyPathType.Linear &&
path.Points.Length >= 4 && (i == 0 || i == path.Points.Length - 1))
{
Handles.color = Color.red;
Handles.DrawWireCube(point.transform.position, new Vector3(0.15f, 0.15f, 0.15f));
Handles.Label(point.transform.position + new Vector3(0,0.1f,0), $"{(i == 0 ? "Start" : "End")} Anchor");
// anchors show a dotted line
Handles.color = Color.blue;
if (i == 0)
Handles.DrawDottedLine(point.transform.position, path.Points[1].transform.position, 5);
else
Handles.DrawDottedLine(path.Points[i - 1].transform.position, point.transform.position,
5);
}
else
{
Vector3 pos = point.transform.position;
Quaternion rot = point.transform.rotation;
Handles.color = Color.grey;
Handles.SphereHandleCap(0, pos, rot, 0.1f, EventType.Repaint);
Handles.Label(point.transform.position + new Vector3(0,0.1f,0), $"Point {i+1}");
}
}
}
static bool IsPathValid(VRCCameraDollyAnimation animation, VRCCameraDollyPath path)
{
if (animation == null ||
path == null ||
path.Points == null ||
path.Points.Length < 2)
return false;
return true;
}
static Vector3 GetPointOnSpline(List<Vector3> points, float t, VRCCameraDollyAnimation.VRCCameraDollyPathType pathType)
{
return pathType switch
{
VRCCameraDollyAnimation.VRCCameraDollyPathType.Linear => Linear(points, t),
VRCCameraDollyAnimation.VRCCameraDollyPathType.Loose => CatmullRom(points, t),
VRCCameraDollyAnimation.VRCCameraDollyPathType.Fitted => BSpline(points, t),
_ => Vector3.zero
};
}
static Vector3 Linear(List<Vector3> pts, float t)
{
int i = Mathf.FloorToInt(t * (pts.Count - 1));
i = Mathf.Clamp(i, 0, pts.Count - 2);
float localT = t * (pts.Count - 1) - i;
return Vector3.Lerp(pts[i], pts[i + 1], localT);
}
static Vector3 CatmullRom(List<Vector3> points, float t)
{
if (points.Count < 4) return Linear(points, t);
int numSections = points.Count - 3;
float u = t * numSections;
int i = Mathf.Clamp(Mathf.FloorToInt(u), 0, numSections - 1);
u -= i;
Vector3 p0 = points[i];
Vector3 p1 = points[i + 1];
Vector3 p2 = points[i + 2];
Vector3 p3 = points[i + 3];
return 0.5f * (
2f * p1 +
(-p0 + p2) * u +
(2f * p0 - 5f * p1 + 4f * p2 - p3) * u * u +
(-p0 + 3f * p1 - 3f * p2 + p3) * u * u * u
);
}
private static Vector3 BSpline(List<Vector3> points, float t)
{
if (points.Count < 4) return Linear(points, t);
int numSections = points.Count - 3;
float u = t * numSections;
int i = Mathf.Clamp(Mathf.FloorToInt(u), 0, numSections - 1);
u -= i;
Vector3 p0 = points[i];
Vector3 p1 = points[i + 1];
Vector3 p2 = points[i + 2];
Vector3 p3 = points[i + 3];
return GetBSplinePoint(p0, p1, p2, p3, u);
}
private static Vector3 GetBSplinePoint(Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float t)
{
float t2 = t * t;
float t3 = t2 * t;
float b0 = (-t3 + 3f * t2 - 3f * t + 1f) / 6f;
float b1 = (3f * t3 - 6f * t2 + 4f) / 6f;
float b2 = (-3f * t3 + 3f * t2 + 3f * t + 1f) / 6f;
float b3 = t3 / 6f;
Vector3 position = b0 * p0 + b1 * p1 + b2 * p2 + b3 * p3;
return position;
}
}
[CustomEditor(typeof(VRCCameraDollyAnimation))]
public class VRCCameraDollyAnimationEditor : UnityEditor.Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector();
if (GUILayout.Button("Collect Paths & Points"))
{
var animation = (VRCCameraDollyAnimation)target;
SerializedObject animationSO = new SerializedObject(animation);
SerializedProperty pathsProp = animationSO.FindProperty("Paths");
pathsProp.ClearArray();
int pathIndex = 0;
foreach (Transform path in animation.transform)
{
if (!path.TryGetComponent<VRCCameraDollyPath>(out var pathComponent)) continue;
SerializedObject pathSO = new SerializedObject(pathComponent);
SerializedProperty pointsProp = pathSO.FindProperty("Points");
pointsProp.ClearArray();
int pointIndex = 0;
foreach (Transform point in path)
{
if (!point.TryGetComponent<VRCCameraDollyPathPoint>(out var pointComponent)) continue;
if (pointComponent != null)
{
pointsProp.InsertArrayElementAtIndex(pointIndex);
pointsProp.GetArrayElementAtIndex(pointIndex).objectReferenceValue = pointComponent;
pointIndex++;
}
}
pathSO.ApplyModifiedProperties();
EditorUtility.SetDirty(pathComponent);
pathsProp.InsertArrayElementAtIndex(pathIndex);
pathsProp.GetArrayElementAtIndex(pathIndex).objectReferenceValue = pathComponent;
pathIndex++;
}
animationSO.ApplyModifiedProperties();
EditorUtility.SetDirty(animation);
EditorSceneManager.MarkSceneDirty(animation.gameObject.scene);
}
}
}
[CustomEditor(typeof(VRCCameraDollyPath))]
public class VRCCameraDollyPathEditor : UnityEditor.Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector();
if (GUILayout.Button("Collect Points"))
{
var path = (VRCCameraDollyPath)target;
SerializedObject pathSO = new SerializedObject(path);
SerializedProperty pointsProp = pathSO.FindProperty("Points");
pointsProp.ClearArray();
int pointIndex = 0;
foreach (Transform point in path.transform)
{
if (!point.TryGetComponent<VRCCameraDollyPathPoint>(out var pointComponent)) continue;
if (pointComponent != null)
{
pointsProp.InsertArrayElementAtIndex(pointIndex);
pointsProp.GetArrayElementAtIndex(pointIndex).objectReferenceValue = pointComponent;
pointIndex++;
}
}
pathSO.ApplyModifiedProperties();
EditorUtility.SetDirty(path);
EditorSceneManager.MarkSceneDirty(path.gameObject.scene);
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6348c1cdc3444611a7fdbac6d268dc58
timeCreated: 1748027556

View File

@ -0,0 +1,59 @@
/*
#if VRC_SDK_VRCSDK3
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using System;
[CustomEditor(typeof(VRC.SDK3.Components.VRCDestructibleUdon))]
public class VRCDestructibleUdonEditor : Editor
{
VRC.SDK3.Components.VRCDestructibleUdon myTarget;
void OnEnable()
{
if (myTarget == null)
myTarget = (VRC.SDK3.Components.VRCDestructibleUdon)target;
}
string[] UdonMethods = null;
string[] UdonVariables = null;
public override void OnInspectorGUI()
{
var udon = myTarget.GetComponent<VRC.Udon.UdonBehaviour>();
if (udon != null)
{
#if VRC_CLIENT
myTarget.UdonMethodApplyDamage = EditorGUILayout.TextField("On Apply Damage", myTarget.UdonMethodApplyDamage);
myTarget.UdonMethodApplyHealing= EditorGUILayout.TextField("On Apply Healing", myTarget.UdonMethodApplyHealing);
myTarget.UdonVariableCurrentHealth= EditorGUILayout.TextField("Current Health Variable", myTarget.UdonVariableCurrentHealth);
myTarget.UdonVariableMaxHealth = EditorGUILayout.TextField("On Max Health Variable", myTarget.UdonVariableMaxHealth);
#else
List<string> methods = new List<string>(udon.GetPrograms());
methods.Insert(0, "-none-");
myTarget.UdonMethodApplyDamage = DrawUdonProgramPicker("On Apply Damage", myTarget.UdonMethodApplyDamage, methods);
myTarget.UdonMethodApplyHealing = DrawUdonProgramPicker("On Apply Healing", myTarget.UdonMethodApplyHealing, methods);
List<string> variables = new List<string>(udon.publicVariables.VariableSymbols);
variables.Insert(0, "-none-");
myTarget.UdonVariableCurrentHealth = DrawUdonProgramPicker("Current Health Variable", myTarget.UdonVariableCurrentHealth, variables);
myTarget.UdonVariableMaxHealth = DrawUdonProgramPicker("Max Health Variable", myTarget.UdonVariableMaxHealth, variables);
#endif
}
}
string DrawUdonProgramPicker(string title, string current, List<string> choices)
{
int index = choices.IndexOf(current);
if (index == -1)
index = 0;
int value = EditorGUILayout.Popup(title, index, choices.ToArray());
if (value != 0)
return choices[value];
return current;
}
}
#endif
*/

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d57b23c04034119448f23c5fdbc57662
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,25 @@
using UnityEditor;
using UnityEngine;
using VRC.SDK3.Midi;
using VRC.SDKBase.Midi;
#if (UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN) && !UNITY_ANDROID
namespace VRC.SDK3.Editor
{
[CustomEditor(typeof(VRCMidiListener))]
public class VRCMidiListenerEditor : UnityEditor.Editor
{
#if UNITY_STANDALONE_WIN
[RuntimeInitializeOnLoadMethod]
public static void InitializeMidi()
{
VRCMidiHandler.OnLog = (message) => Debug.Log(message);
VRCMidiHandler.Initialize = () =>
{
return VRCMidiHandler.OpenMidiInput<VRCPortMidiInput>(
EditorPrefs.GetString(VRCMidiWindow.DEVICE_NAME_STRING));
};
}
#endif
}
}
#endif

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5b1017097f3d46cd949892e0fce3ece9
timeCreated: 1612853300

View File

@ -0,0 +1,319 @@
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using VRC.SDK3.Midi;
public class VRCMidiEditorVisualizer : EditorWindow
{
private MidiFile _midiFile;
private Vector2 _scroll;
private bool _hasGenerated;
private float _maxDistanceNotes;
private PreviewRenderUtility _previewRenderUtility;
private Texture _outputTexture;
private Mesh _meshMidi;
private Mesh _meshSideNotes;
private Mesh _meshTime;
private Material _mainMaterial;
private List<int> _usedChannels;
private Dictionary<int, Color> _colors;
public static void Init(MidiFile midiFile)
{
// Get existing open window or if none, make a new one:
VRCMidiEditorVisualizer window = GetWindow<VRCMidiEditorVisualizer>();
window._midiFile = midiFile;
window.Show();
}
private void OnEnable()
{
_previewRenderUtility = new PreviewRenderUtility(false);
var camera = _previewRenderUtility.camera;
camera.orthographic = true;
camera.nearClipPlane = 0.3f;
camera.farClipPlane = 10;
camera.orthographicSize = 20f;
camera.transform.position = new Vector3(0f, 2f, 0f);
camera.transform.LookAt(Vector3.zero);
InititlizeData();
wantsMouseMove = true;
}
private void InititlizeData()
{
_mainMaterial = new Material(Shader.Find("VRChat/Mobile/Toon Lit"));
_meshMidi = new Mesh();
_meshSideNotes = new Mesh();
_meshTime = new Mesh();
_hasGenerated = false;
}
public void OnDisable()
{
if (_previewRenderUtility != null)
{
_previewRenderUtility.Cleanup();
}
if (_meshMidi != null)
{
DestroyImmediate(_meshMidi);
DestroyImmediate(_meshSideNotes);
DestroyImmediate(_meshTime);
DestroyImmediate(_mainMaterial);
}
}
void OnGUI()
{
if (_midiFile == null)
{
EditorGUILayout.HelpBox("Editor was created without Midi File.", MessageType.Error);
return;
}
if (_meshMidi == null)
{
InititlizeData();
}
if (!_hasGenerated)
{
GenerateNoteObject();
GenerateMeshTime();
}
GenerateSideNotes();
ShowChannelLegend();
EditorGUILayout.BeginHorizontal();
// Main Note area
_scroll = EditorGUILayout.BeginScrollView(_scroll, false, false, GUILayout.ExpandWidth(true));
Rect rect = GUILayoutUtility.GetRect(_maxDistanceNotes*10, 1280);
EditorGUILayout.EndScrollView();
rect.width = position.width-16;
rect.height = position.height - EditorGUIUtility.singleLineHeight*2 -3;
rect.y += EditorGUIUtility.singleLineHeight+3;
var camera = _previewRenderUtility.camera;
camera.orthographicSize = rect.height / 20;
camera.transform.position = new Vector3(rect.width / 20 + _scroll.x / 10, 2, 128 - rect.height / 20 - _scroll.y / 10);
_previewRenderUtility.BeginPreview(rect, GUIStyle.none);
_previewRenderUtility.DrawMesh(_meshTime, Matrix4x4.identity, _mainMaterial, 0);
_previewRenderUtility.DrawMesh(_meshMidi,Matrix4x4.identity,_mainMaterial,0);
_previewRenderUtility.DrawMesh(_meshSideNotes, Vector3.right * (_scroll.x / 10), Quaternion.identity, _mainMaterial,0);
_previewRenderUtility.camera.Render();
_previewRenderUtility.EndAndDrawPreview(rect);
EditorGUILayout.EndHorizontal();
if (Event.current.type == EventType.MouseMove)
Repaint();
}
private void ShowChannelLegend()
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Channels:");
foreach (int channel in _usedChannels)
{
EditorGUILayout.LabelField(channel.ToString(), new GUIStyle(){alignment = TextAnchor.MiddleCenter, normal = new GUIStyleState(){textColor = _colors[channel]}}, new GUILayoutOption[]{GUILayout.Width(20)});
}
EditorGUILayout.EndHorizontal();
}
public void GenerateNoteObject()
{
_meshMidi.Clear();
_usedChannels = new List<int>();
MidiData data = _midiFile.data;
List<Vector3> positions = new List<Vector3>();
List<int> tri = new List<int>();
List<Color> colors = new List<Color>();
// Iterate over the list once to get all the used channels
foreach (var track in data.tracks)
{
foreach (var block in track.blocks)
{
if(!_usedChannels.Contains(block.channel)) _usedChannels.Add(block.channel);
}
}
// Generate colors for all channels, spread across spectrum evenly
_colors = new Dictionary<int, Color>();
for (int i = 0; i < _usedChannels.Count; i++)
{
float normalizedPosition = Mathf.InverseLerp(0, _usedChannels.Count, i);
_colors.Add(_usedChannels[i], Color.HSVToRGB(normalizedPosition, .9f, .9f));
}
int index = 0;
for (int TrackIndex = 0; TrackIndex < data.tracks.Length; TrackIndex++)
{
MidiData.MidiTrack track = data.tracks[TrackIndex];
MidiData.MidiBlock[] blocks = track.blocks;
for(int blockIndex = 0; blockIndex < blocks.Length; blockIndex++)
{
MidiData.MidiBlock block = blocks[blockIndex];
float endTime = block.endTimeMs / 100 + 10;
if ( _maxDistanceNotes < endTime)
{
_maxDistanceNotes = endTime;
}
positions.Add(new Vector3((block.startTimeMs / 100f) + 10, 0, block.note ));
positions.Add(new Vector3(endTime, 0, block.note ));
positions.Add(new Vector3((block.startTimeMs / 100f) + 10, 0, block.note + 1f));
positions.Add(new Vector3(endTime, 0, block.note + 1f));
tri.Add(index);
tri.Add(index + 2);
tri.Add(index + 1);
tri.Add(index + 1);
tri.Add(index + 2);
tri.Add(index + 3);
index += 4;
colors.Add(_colors[block.channel]);
colors.Add(_colors[block.channel]);
colors.Add(_colors[block.channel]);
colors.Add(_colors[block.channel]);
if(!_usedChannels.Contains(block.channel)) _usedChannels.Add(block.channel);
}
}
var camera = _previewRenderUtility.camera;
camera.transform.position = new Vector3(_maxDistanceNotes/2, 2f, 64);
_meshMidi.vertices = positions.ToArray();
_meshMidi.triangles = tri.ToArray();
_meshMidi.colors = colors.ToArray();
_hasGenerated = true;
}
public void GenerateSideNotes()
{
_meshSideNotes.Clear();
List<Vector3> positions = new List<Vector3>();
List<int> tri = new List<int>();
List<Color> colors = new List<Color>();
int index = 0;
int skip = -1;
bool whiteNote = true;
Color color;
for (int Notes = 0; Notes < 128; Notes++)
{
skip++;
skip %= 12;
if (skip == 5 || skip == 0)
{
whiteNote = !whiteNote;
}
whiteNote = !whiteNote;
if (whiteNote)
{
positions.Add(new Vector3(0, 0.3f, Notes + 0.05f));
positions.Add(new Vector3(10, 0.3f, Notes + 0.05f));
positions.Add(new Vector3(0, 0.3f, Notes + 0.95f));
positions.Add(new Vector3(10, 0.3f, Notes + 0.95f));
color = Color.white;
}
else
{
positions.Add(new Vector3(0, 0.3f, Notes + 0.05f));
positions.Add(new Vector3(10, 0.3f, Notes + 0.05f));
positions.Add(new Vector3(0, 0.3f, Notes + 0.95f));
positions.Add(new Vector3(10, 0.3f, Notes + 0.95f));
color = Color.black;
}
if(Mathf.FloorToInt((1280 - (GUIUtility.GUIToScreenPoint(Event.current.mousePosition).y + _scroll.y - position.y - EditorGUIUtility.singleLineHeight*2.3f))/ 10) == Notes)
{
color = Color.gray;
}
colors.Add(color);
colors.Add(color);
colors.Add(color);
colors.Add(color);
tri.Add(index);
tri.Add(index + 2);
tri.Add(index + 1);
tri.Add(index + 1);
tri.Add(index + 2);
tri.Add(index + 3);
index += 4;
}
_meshSideNotes.vertices = positions.ToArray();
_meshSideNotes.triangles = tri.ToArray();
_meshSideNotes.colors = colors.ToArray();
}
private void GenerateMeshTime()
{
_meshTime.Clear();
List<Vector3> positions = new List<Vector3>();
List<int> tri = new List<int>();
List<Color> colors = new List<Color>();
int index = 0;
int amount = Mathf.CeilToInt(((_maxDistanceNotes-10) / 600) * _midiFile.data.bpm);
Color color = Color.black;
for (int beats = 0; beats < amount; beats++)
{
float offset = (beats * (600f / _midiFile.data.bpm)) + 10;
positions.Add(new Vector3(-0.1f + offset, -0.3f, 0));
positions.Add(new Vector3(0.1f + offset, -0.3f, 0));
positions.Add(new Vector3(-0.1f + offset, -0.3f, 128));
positions.Add(new Vector3(0.1f + offset, -0.3f, 128));
colors.Add(color);
colors.Add(color);
colors.Add(color);
colors.Add(color);
tri.Add(index);
tri.Add(index + 2);
tri.Add(index + 1);
tri.Add(index + 1);
tri.Add(index + 2);
tri.Add(index + 3);
index += 4;
}
_meshTime.vertices = positions.ToArray();
_meshTime.triangles = tri.ToArray();
_meshTime.colors = colors.ToArray();
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 88390b175b72faa49b553b95cbcac908
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,144 @@
using UnityEditor;
using UnityEngine;
using VRC.SDK3.Midi;
namespace VRC.SDK3.Editor
{
[CustomEditor(typeof(VRCMidiPlayer))]
public class VRCMidiPlayerEditor : UnityEditor.Editor
{
private VRCMidiPlayer _player;
public bool displayDebugBlocks;
// Serialized Properties
private SerializedProperty _midiFileProp;
private SerializedProperty _audioSourceProp;
private SerializedProperty _targetBehavioursProp;
private const string MidiAssetReloadKey = "MidiAssetReloaded";
void OnEnable()
{
// Fetch the objects from the GameObject script to display in the inspector
_midiFileProp = serializedObject.FindProperty(nameof(VRCMidiPlayer.midiFile));
_audioSourceProp = serializedObject.FindProperty(nameof(VRCMidiPlayer.audioSource));
_targetBehavioursProp = serializedObject.FindProperty(nameof(VRCMidiPlayer.targetBehaviours));
_player = (VRCMidiPlayer)target;
}
private void Awake()
{
displayDebugBlocks = EditorPrefs.GetBool(GetPrefsNameFor(nameof(displayDebugBlocks)), false);
}
private static string GetPrefsNameFor(string value)
{
return $"VRCMidiPlayerEditor.{value}";
}
public override void OnInspectorGUI()
{
bool _isReady = _player.midiFile != null &&
_player.audioSource != null &&
_player.audioSource.clip != null &&
_player.targetBehaviours.Length > 0 &&
_player.targetBehaviours[0] != null ;
if (_isReady)
{
EditorGUILayout.HelpBox("✔ Midi Player is Ready!", MessageType.None);
}
else
{
EditorGUILayout.HelpBox("Not Ready - see messages below.", MessageType.Warning);
}
// Display Midi File Field
EditorGUILayout.PropertyField(
_midiFileProp,
new GUIContent(
"Midi File", null,
"The MIDI file in SMF format whose data you want to trigger."
));
// Ensure a Midi File is set before continuing
if (!_player.midiFile)
{
EditorGUILayout.HelpBox("Choose a Midi File to continue setting up the Player.", MessageType.Info);
serializedObject.ApplyModifiedProperties();
return;
}
// Display Audio Source Field
EditorGUILayout.PropertyField(
_audioSourceProp,
new GUIContent(
"Audio Source", null,
"The AudioSource component with the audio clip corresponding to your MIDI data."
));
// Add audio source if it's not set
if (!_player.audioSource)
{
EditorGUILayout.HelpBox("Set or Create an AudioSource to continue.", MessageType.Info);
if (GUILayout.Button("Create One Here"))
{
_player.audioSource = _player.gameObject.AddComponent<AudioSource>();
}
serializedObject.ApplyModifiedProperties();
return;
}
// Force reimport midi assets one time per session
if (_player.midiFile != null && _player.midiFile.audioClip == null)
{
string key = $"{MidiAssetReloadKey}-{_player.midiFile.GetInstanceID()}";
if (!SessionState.GetBool(key, false))
{
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(_player.midiFile), ImportAssetOptions.ForceUpdate);
SessionState.SetBool(key, true);
}
}
// Automatically set AudioSource clip from MidiAsset if possible
else if (_player.midiFile.audioClip != null && _player.audioSource.clip == null || _player.audioSource.clip != _player.midiFile.audioClip )
{
_player.audioSource.clip = _player.midiFile.audioClip;
}
if (_player.audioSource.clip == null)
{
EditorGUILayout.HelpBox("You need to set the AudioClip in the AudioSource.", MessageType.Warning);
serializedObject.ApplyModifiedProperties();
return;
}
// Display target UdonBehaviours field
EditorGUILayout.PropertyField(
_targetBehavioursProp,
new GUIContent(
"Target Behaviours", null,
"An array of UdonBehaviours which will have MIDI Note On and Off events sent to them"
));
// Exit Early if there are not target behaviours set
if (_player.targetBehaviours.Length == 0 || _player.targetBehaviours[0] == null)
{
EditorGUILayout.HelpBox("Set some target UdonBehaviours above to continue.", MessageType.Info);
serializedObject.ApplyModifiedProperties();
return;
}
bool openVisualizer = GUILayout.Button("Open Visualizer");
if (openVisualizer)
{
VRCMidiEditorVisualizer.Init(_player.midiFile);
}
// Apply changes to the serializedProperty - always do this at the end of OnInspectorGUI.
serializedObject.ApplyModifiedProperties();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 76fb4f8f2f07c024795a6acecfeaefcc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,244 @@
#if VRC_SDK_VRCSDK3 && UNITY_EDITOR
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using VRC.SDKBase;
using VRC.SDKBase.Editor.Versioning;
using VRCPickup = VRC.SDK3.Components.VRCPickup;
namespace VRC.SDK3.Editor
{
[CanEditMultipleObjects]
[CustomEditor(typeof(VRCPickup))]
public class VRCPickupEditor3 : VRCInspectorBase
{
private SerializedProperty propVersion;
private SerializedProperty propMomentumTransferMethod;
private SerializedProperty propDisallowTheft;
private SerializedProperty propExactGun;
private SerializedProperty propExactGrip;
private SerializedProperty propAllowManipulationWhenEquipped;
private SerializedProperty propOrientation;
private SerializedProperty propAutoHold;
private SerializedProperty propInteractionText;
private SerializedProperty propUseText;
private SerializedProperty propThrowVelocityBoostMinSpeed;
private SerializedProperty propThrowVelocityBoostScale;
private SerializedProperty propPickupable;
private SerializedProperty propProximity;
private PropertyField fieldMomentumTransferMethod;
private PropertyField fieldDisallowTheft;
private PropertyField fieldExactGun;
private PropertyField fieldExactGrip;
private PropertyField fieldAllowManipulationWhenEquipped;
private PropertyField fieldOrientation;
private PropertyField fieldAutoHold;
private PropertyField fieldInteractionText;
private PropertyField fieldUseText;
private PropertyField fieldThrowVelocityBoostMinSpeed;
private PropertyField fieldThrowVelocityBoostScale;
private PropertyField fieldPickupable;
private PropertyField fieldProximity;
private ComponentVersionUI<VRCPickup.Version> versionUI;
private void OnEnable()
{
propVersion = serializedObject.FindProperty(nameof(VRCPickup.version));
propMomentumTransferMethod = serializedObject.FindProperty(nameof(VRCPickup.MomentumTransferMethod));
propDisallowTheft = serializedObject.FindProperty(nameof(VRCPickup.DisallowTheft));
propExactGun = serializedObject.FindProperty(nameof(VRCPickup.ExactGun));
propExactGrip = serializedObject.FindProperty(nameof(VRCPickup.ExactGrip));
propAllowManipulationWhenEquipped = serializedObject.FindProperty(nameof(VRCPickup.allowManipulationWhenEquipped));
propOrientation = serializedObject.FindProperty(nameof(VRCPickup.orientation));
propAutoHold = serializedObject.FindProperty(nameof(VRCPickup.AutoHold));
propInteractionText = serializedObject.FindProperty(nameof(VRCPickup.InteractionText));
propUseText = serializedObject.FindProperty(nameof(VRCPickup.UseText));
propThrowVelocityBoostMinSpeed = serializedObject.FindProperty(nameof(VRCPickup.ThrowVelocityBoostMinSpeed));
propThrowVelocityBoostScale = serializedObject.FindProperty(nameof(VRCPickup.ThrowVelocityBoostScale));
propPickupable = serializedObject.FindProperty(nameof(VRCPickup.pickupable));
propProximity = serializedObject.FindProperty(nameof(VRCPickup.proximity));
}
#region Inspector GUI Construction (Common Pattern for All Custom Editors)
public override void BuildInspectorGUI()
{
base.BuildInspectorGUI();
// Add version system
var migrator = new VRCPickupVersionMigrator(propAutoHold, propOrientation, propInteractionText, OnVersionChanged);
versionUI = AddVersionSystem(propVersion, migrator,
"https://creators.vrchat.com/worlds/components/vrc_pickup#versions");
// Create AutoHold enum field (shown in V1.0, hidden in V1.1+ when toggle is used)
fieldAutoHold = AddField(propAutoHold);
fieldAutoHold.RegisterValueChangeCallback(AutoHoldCallback);
fieldUseText = AddFieldTooltip(propUseText,
"Text to display describing action for clicking button, when this pickup is already being held.");
fieldInteractionText = AddFieldTooltip(propInteractionText,
"Text displayed when user hovers over the pickup.");
fieldProximity = AddField(propProximity);
fieldOrientation = AddField(propOrientation);
fieldOrientation.RegisterValueChangeCallback(OrientationCallback);
fieldExactGun = AddField(propExactGun);
fieldExactGrip = AddField(propExactGrip);
fieldPickupable = AddField(propPickupable);
fieldDisallowTheft = AddField(propDisallowTheft);
fieldAllowManipulationWhenEquipped = AddField(propAllowManipulationWhenEquipped);
fieldMomentumTransferMethod = AddField(propMomentumTransferMethod);
fieldThrowVelocityBoostMinSpeed = AddField(propThrowVelocityBoostMinSpeed);
fieldThrowVelocityBoostScale = AddField(propThrowVelocityBoostScale);
// Now that all fields are created, we can update their visibility based on version
var pickup = target as VRCPickup;
SetupAutoHoldField(pickup.version);
RefreshAllFieldVisibility(pickup.version);
}
// Creates the toggle field for modern AutoHold UI (V1.1+)
private void SetupAutoHoldField(VRCPickup.Version version)
{
var container = fieldAutoHold.parent;
if (!fieldAutoHold.ClassListContains("auto-hold-enum-field"))
fieldAutoHold.AddToClassList("auto-hold-enum-field");
var toggleField = container.Query().Name("autoHoldToggle").First() as Toggle;
if (toggleField == null)
{
toggleField = new Toggle("Auto Hold")
{
name = "autoHoldToggle",
tooltip = "When enabled, the pickup will stay in hand when trigger is released"
};
toggleField.AddToClassList("auto-hold-toggle-field");
toggleField.RegisterValueChangedCallback(evt => {
serializedObject.Update();
propAutoHold.enumValueIndex = evt.newValue ?
(int)VRC_Pickup.AutoHoldMode.Yes :
(int)VRC_Pickup.AutoHoldMode.No;
serializedObject.ApplyModifiedProperties();
AutoHoldChanged();
});
container.Insert(container.IndexOf(fieldAutoHold) + 1, toggleField);
}
}
// Switches between enum dropdown (V1.0) and toggle (V1.1+) for AutoHold field
private void UpdateAutoHoldFieldVisibility(VRCPickup.Version version)
{
var container = fieldAutoHold.parent;
var toggleField = container.Query().Name("autoHoldToggle").First() as Toggle;
if (toggleField == null) return;
if (IsModernVersion(version))
{
fieldAutoHold.style.display = DisplayStyle.None;
toggleField.style.display = DisplayStyle.Flex;
UpdateToggleFromEnumValue(toggleField);
}
else
{
fieldAutoHold.style.display = DisplayStyle.Flex;
toggleField.style.display = DisplayStyle.None;
}
}
// Syncs toggle state with underlying enum value without triggering callbacks
private void UpdateToggleFromEnumValue(Toggle toggle)
{
bool treatAsAutoHold = propAutoHold.enumValueIndex
is (int)VRC_Pickup.AutoHoldMode.Yes
or (int)VRC_Pickup.AutoHoldMode.Sometimes;
toggle.SetValueWithoutNotify(treatAsAutoHold);
}
#endregion
#region VRC Pickup-Specific Helper Methods
// Checks if version uses modern UI (toggle instead of enum dropdown)
private bool IsModernVersion(VRCPickup.Version version)
{
return version > VRCPickup.Version.Version_1_0;
}
private void AutoHoldCallback(SerializedPropertyChangeEvent evt)
{
AutoHoldChanged();
}
// Updates UseText field visibility when AutoHold setting changes
private void AutoHoldChanged()
{
bool canDisplayUseText = propAutoHold.enumValueIndex
is (int)VRC_Pickup.AutoHoldMode.Yes
or (int)VRC_Pickup.AutoHoldMode.Sometimes
or (int)VRC_Pickup.AutoHoldMode.AutoDetect; // Use text can appear if gun or grip transforms are assigned.
fieldUseText.SetVisible(canDisplayUseText);
// Update version upgrade info visibility
versionUI?.RefreshUpgradeInfo();
}
private void OrientationCallback(SerializedPropertyChangeEvent evt) => OrientationChanged();
// Shows/hides ExactGun and ExactGrip fields based on pickup orientation
private void OrientationChanged()
{
switch ((VRC_Pickup.PickupOrientation)propOrientation.enumValueIndex)
{
case VRC_Pickup.PickupOrientation.Any:
fieldExactGun.SetVisible(true);
fieldExactGrip.SetVisible(true);
break;
case VRC_Pickup.PickupOrientation.Grip:
fieldExactGun.SetVisible(false);
fieldExactGrip.SetVisible(true);
break;
case VRC_Pickup.PickupOrientation.Gun:
fieldExactGun.SetVisible(true);
fieldExactGrip.SetVisible(false);
break;
}
// Update version upgrade info since orientation affects AutoDetect migration
versionUI?.RefreshUpgradeInfo();
}
#endregion
#region Version-Dependent UI Management (Pattern for Versioned Components)
// Refreshes all field visibility based on current version and field states
private void RefreshAllFieldVisibility(VRCPickup.Version version)
{
versionUI?.RefreshUpgradeInfo();
UpdateAutoHoldFieldVisibility(version);
OrientationChanged();
AutoHoldChanged();
}
// Called when version changes to update UI elements
private void OnVersionChanged(VRCPickup.Version newVersion)
{
RefreshAllFieldVisibility(newVersion);
}
#endregion
}
}
#endif

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a8cc4c1876b26174fbaeb062178a6bda
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,154 @@
using UnityEditor;
using VRC.SDK3.Components;
using VRC.SDKBase;
using VRC.SDKBase.Editor.Versioning;
namespace VRC.SDK3.Editor
{
/// <summary>
/// Version migration logic for VRCPickup
/// </summary>
internal class VRCPickupVersionMigrator : ComponentVersionMigrator<VRCPickup.Version>
{
private readonly SerializedProperty autoHoldProperty;
private readonly SerializedProperty orientationProperty;
private readonly SerializedProperty interactionTextProperty;
private readonly System.Action<VRCPickup.Version> onVersionChangedCallback;
public VRCPickupVersionMigrator(SerializedProperty autoHoldProperty, SerializedProperty orientationProperty, SerializedProperty interactionTextProperty, System.Action<VRCPickup.Version> onVersionChangedCallback = null)
{
this.autoHoldProperty = autoHoldProperty;
this.orientationProperty = orientationProperty;
this.interactionTextProperty = interactionTextProperty;
this.onVersionChangedCallback = onVersionChangedCallback;
}
// Creates the success dialog message explaining what specific changes were made during upgrade
public override string GetUpgradeChangesMessage(VRCPickup.Version oldVersion, VRCPickup.Version newVersion, SerializedObject serializedObject)
{
if (newVersion <= oldVersion) return null;
// Handle multi-selection with simplified message
if (serializedObject.isEditingMultipleObjects)
return $"Version upgraded successfully!\n{serializedObject.targetObjects.Length} pickup(s) upgraded to Version 1.1.";
var orientation = (VRC_Pickup.PickupOrientation)orientationProperty.enumValueIndex;
var message = "Version upgraded successfully!\n";
int oldAutoHoldValue = autoHoldProperty.enumValueIndex;
var changes = new System.Collections.Generic.List<string>();
if (oldAutoHoldValue == (int)VRC_Pickup.AutoHoldMode.Sometimes)
{
changes.Add("Changed 'Sometimes' to 'Yes' for better controller compatibility");
}
else if (oldAutoHoldValue == (int)VRC_Pickup.AutoHoldMode.AutoDetect)
{
bool willBeAutoHold = VRC_Pickup.IsGlobalAutoHoldPickup((VRC_Pickup.AutoHoldMode)oldAutoHoldValue, orientation);
var newValue = willBeAutoHold ? "Yes" : "No";
var change = $"Changed 'Auto Detect' to '{newValue}' based on orientation ({orientation})";
if (newValue == "No")
{
change += "\n'Any' orientation pickups don't use auto-hold";
}
else
{
change += "\nGrip/Gun orientation pickups use auto-hold";
}
changes.Add(change);
}
if (changes.Count > 0)
{
message += "Changes made:\n" + string.Join("\n", changes);
}
else
{
message += "No changes needed - your settings are already compatible with this version.";
}
return message;
}
// Performs the actual data migration when upgrading to a specific version
public override void MigrateToVersion(VRCPickup.Version oldVersion, VRCPickup.Version newVersion, SerializedObject serializedObject)
{
// Handle migration from old busted AutoHold to Global AutoHold
if (oldVersion == VRCPickup.Version.Version_1_0)
{
var currentOrientation = (VRC_Pickup.PickupOrientation)orientationProperty.enumValueIndex;
var currentAutoHoldMode = (VRC_Pickup.AutoHoldMode)autoHoldProperty.enumValueIndex;
bool willBeAutoHold = VRC_Pickup.IsGlobalAutoHoldPickup(currentAutoHoldMode, currentOrientation);
var newAutoHoldMode = willBeAutoHold ? VRC_Pickup.AutoHoldMode.Yes : VRC_Pickup.AutoHoldMode.No;
if ((int)newAutoHoldMode != autoHoldProperty.enumValueIndex)
{
autoHoldProperty.enumValueIndex = (int)newAutoHoldMode;
autoHoldProperty.serializedObject.ApplyModifiedProperties();
}
}
}
// Determines when to show the upgrade info box at the top of the inspector
public override bool ShouldShowUpgradePrompt(VRCPickup.Version version, SerializedObject serializedObject)
{
// Always show upgrade for ANY Version 1.0 pickup in multi-selection
if (serializedObject.isEditingMultipleObjects)
return version == VRCPickup.Version.Version_1_0;
return version == VRCPickup.Version.Version_1_0 &&
(autoHoldProperty.enumValueIndex == (int)VRC_Pickup.AutoHoldMode.Sometimes ||
autoHoldProperty.enumValueIndex == (int)VRC_Pickup.AutoHoldMode.AutoDetect);
}
// Returns message about why they should upgrade
public override string GetUpgradePromptText(VRCPickup.Version currentVersion, VRCPickup.Version latestVersion)
{
// Multi-selection mode - simplified message
if (autoHoldProperty.serializedObject.isEditingMultipleObjects)
return "Selected pickups use older settings.\nUpgrade all to Version 1.1 for better compatibility and simplified options.";
int currentAutoHoldValue = autoHoldProperty.enumValueIndex;
var orientation = (VRC_Pickup.PickupOrientation)orientationProperty.enumValueIndex;
bool willBeAutoHold = VRC_Pickup.IsGlobalAutoHoldPickup((VRC_Pickup.AutoHoldMode)currentAutoHoldValue, orientation);
// Handle Sometimes - always problematic
if (currentAutoHoldValue == (int)VRC_Pickup.AutoHoldMode.Sometimes)
{
return "This pickup uses older auto-hold settings that don't work for many controllers.\n" +
"Upgrade to Version 1.1 for better compatibility and reduced hand fatigue.";
}
// Handle AutoDetect - check what it will become
if (currentAutoHoldValue == (int)VRC_Pickup.AutoHoldMode.AutoDetect)
{
if (willBeAutoHold)
{
return "This pickup uses older auto-hold settings that don't work for many controllers.\n" +
"Upgrade to Version 1.1 for better compatibility and reduced hand fatigue.";
}
}
// Handle No and AutoDetect that becomes No - won't change function but will simplify UI
if (!willBeAutoHold)
{
return "This pickup uses older auto-hold settings.\nUpgrading won't change the way it functions,\n" +
"but it will simplify the options below.";
}
// Default fallback
return "Upgrade to Version 1.1 for improved compatibility and simplified settings.";
}
// Called after version change to notify editor for UI updates
public override void OnVersionChanged(VRCPickup.Version version, SerializedObject serializedObject)
{
onVersionChangedCallback?.Invoke(version);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e5f6789012345678901234abcdef0123
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,52 @@
#if VRC_SDK_VRCSDK3 && UNITY_EDITOR
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using System;
using UnityEngine.UIElements;
using VRCStation = VRC.SDK3.Components.VRCStation;
namespace VRC.SDK3.Editor
{
[CanEditMultipleObjects]
[CustomEditor(typeof(VRCStation))]
public class VRCPlayerStationEditor3 : VRCInspectorBase
{
private SerializedProperty propPlayerMobility;
private SerializedProperty propCanUseStationFromStation;
private SerializedProperty propAnimatorController;
private SerializedProperty propDisableStationExit;
private SerializedProperty propSeated;
private SerializedProperty propStationEnterPlayerLocation;
private SerializedProperty propStationExitPlayerLocation;
private void OnEnable()
{
propPlayerMobility = serializedObject.FindProperty(nameof(VRCStation.PlayerMobility));
propCanUseStationFromStation = serializedObject.FindProperty(nameof(VRCStation.canUseStationFromStation));
propAnimatorController = serializedObject.FindProperty(nameof(VRCStation.animatorController));
propDisableStationExit = serializedObject.FindProperty(nameof(VRCStation.disableStationExit));
propSeated = serializedObject.FindProperty(nameof(VRCStation.seated));
propStationEnterPlayerLocation = serializedObject.FindProperty(nameof(VRCStation.stationEnterPlayerLocation));
propStationExitPlayerLocation = serializedObject.FindProperty(nameof(VRCStation.stationExitPlayerLocation));
}
public override void BuildInspectorGUI()
{
base.BuildInspectorGUI();
AddField(propPlayerMobility);
AddField(propSeated);
AddField(propDisableStationExit);
AddField(propCanUseStationFromStation);
AddField(propStationEnterPlayerLocation);
AddField(propStationExitPlayerLocation);
AddField(propAnimatorController);
}
}
}
#endif

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8901d07a685ca424492a3cabff506184
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,51 @@
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
using VRC.SDKBase;
using VRCPortalMarker = VRC.SDK3.Components.VRCPortalMarker;
namespace VRC.SDK3.Editor
{
[CanEditMultipleObjects]
[CustomEditor(typeof(VRCPortalMarker))]
public class VRCPortalMarkerEditor : VRCInspectorBase
{
private SerializedProperty propWorld;
private SerializedProperty propRoomId;
private SerializedProperty propCustomPortalName;
private PropertyField fieldWorld;
private PropertyField fieldRoomId;
private PropertyField fieldCustomPortalName;
private HelpBox helpBoxTag;
private void OnEnable()
{
propWorld = serializedObject.FindProperty(nameof(VRCPortalMarker.world));
propRoomId = serializedObject.FindProperty(nameof(VRCPortalMarker.roomId));
propCustomPortalName = serializedObject.FindProperty(nameof(VRCPortalMarker.customPortalName));
}
public override void BuildInspectorGUI()
{
base.BuildInspectorGUI();
fieldRoomId = AddFieldLabel(propRoomId, "World ID");
fieldCustomPortalName = AddField(propCustomPortalName);
fieldWorld = AddField(propWorld);
fieldWorld.RegisterValueChangeCallback(evt => WorldChanged());
WorldChanged();
}
private void WorldChanged()
{
bool value = (VRC_PortalMarker.VRChatWorld)propWorld.enumValueIndex == VRC_PortalMarker.VRChatWorld.None;
fieldRoomId.SetEnabled(value);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d0eab965d110d6a41b44e8c56b81f7bd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,213 @@
#if VRC_SDK_VRCSDK3 && UNITY_EDITOR
using System;
using System.Linq;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using VRC.Core;
using VRCSceneDescriptor = VRC.SDK3.Components.VRCSceneDescriptor;
namespace VRC.SDK3.Editor
{
[CanEditMultipleObjects]
[CustomEditor(typeof(VRCSceneDescriptor))]
public class VRCSceneDescriptorEditor3 : VRCInspectorBase
{
private VRCSceneDescriptor sceneDescriptor;
private SerializedProperty propSpawns;
private SerializedProperty propSpawnRadius;
private SerializedProperty propSpawnOrder;
private SerializedProperty propSpawnOrientation;
private SerializedProperty propReferenceCamera;
private SerializedProperty propRespawnHeightY;
private SerializedProperty propObjectBehaviourAtRespawnHeight;
private SerializedProperty propForbidUserPortals;
private SerializedProperty propUnityVersion;
private SerializedProperty propDynamicPrefabs;
private SerializedProperty propDynamicMaterials;
private SerializedProperty propNetworkIDs;
private SerializedProperty propPortraitCameraPositionOffset;
private SerializedProperty propPortraitCameraRotationOffset;
private SerializedProperty propInteractPassthrough;
[NonSerialized] private string[] layerNames;
private int mask;
private const string INTERACTION_HELPBOX_LABEL =
"Interaction through User layers is blocked by default. Use the \"Interact Passthrough\" mask to define layers that will be transparent to interaction (allow interactions to pass through).";
private const string INTERACTION_HELPBOX_URL = "https://creators.vrchat.com/worlds/layers/#interaction-block-and-passthrough-on-vrchat-layers";
private const int USER_LAYER_START = 22;
private const int USER_LAYER_COUNT = 10;
private const float HANDLE_SIZE = 0.1f;
private static readonly Color RespawnHeightGizmoColor = new(0, 1, 0, 0.25f);
private Vector3[] respawnHeightGizmoCorners;
private void OnEnable()
{
sceneDescriptor = (VRCSceneDescriptor)target;
propSpawns = serializedObject.FindProperty(nameof(VRCSceneDescriptor.spawns));
propSpawnRadius = serializedObject.FindProperty(nameof(VRCSceneDescriptor.spawnRadius));
propSpawnOrder = serializedObject.FindProperty(nameof(VRCSceneDescriptor.spawnOrder));
propSpawnOrientation = serializedObject.FindProperty(nameof(VRCSceneDescriptor.spawnOrientation));
propReferenceCamera = serializedObject.FindProperty(nameof(VRCSceneDescriptor.ReferenceCamera));
propRespawnHeightY = serializedObject.FindProperty(nameof(VRCSceneDescriptor.RespawnHeightY));
propObjectBehaviourAtRespawnHeight = serializedObject.FindProperty(nameof(VRCSceneDescriptor.ObjectBehaviourAtRespawnHeight));
propForbidUserPortals = serializedObject.FindProperty(nameof(VRCSceneDescriptor.ForbidUserPortals));
propUnityVersion = serializedObject.FindProperty(nameof(VRCSceneDescriptor.unityVersion));
propDynamicPrefabs = serializedObject.FindProperty(nameof(VRCSceneDescriptor.DynamicPrefabs));
propDynamicMaterials = serializedObject.FindProperty(nameof(VRCSceneDescriptor.DynamicMaterials));
propPortraitCameraPositionOffset = serializedObject.FindProperty(nameof(VRCSceneDescriptor.portraitCameraPositionOffset));
propPortraitCameraRotationOffset = serializedObject.FindProperty(nameof(VRCSceneDescriptor.portraitCameraRotationOffset));
propInteractPassthrough = serializedObject.FindProperty(nameof(VRCSceneDescriptor.interactThruLayers));
// Using NetworkIDCollection here doesn't expose the actual list of network IDs in the inspector
propNetworkIDs = serializedObject.FindProperty("NetworkIDs");
GetRespawnHeightGizmoPlaneCorners(false);
PopulateUserLayerNames();
HierarchyChanged();
EditorApplication.hierarchyChanged += HierarchyChanged;
}
private void OnDisable()
{
EditorApplication.hierarchyChanged -= HierarchyChanged;
}
private void OnSceneGUI()
{
var handlePosition = new Vector3(
sceneDescriptor.transform.position.x,
sceneDescriptor.RespawnHeightY,
sceneDescriptor.transform.position.z);
var handleSize = HandleUtility.GetHandleSize(handlePosition) * HANDLE_SIZE;
EditorGUI.BeginChangeCheck();
Vector3 newHandlePosition = Handles.FreeMoveHandle(handlePosition, handleSize, Vector3.up, Handles.DotHandleCap);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(sceneDescriptor, "Move Respawn Height Y");
sceneDescriptor.RespawnHeightY = newHandlePosition.y;
EditorUtility.SetDirty(sceneDescriptor);
}
GetRespawnHeightGizmoPlaneCorners();
Handles.DrawSolidRectangleWithOutline(respawnHeightGizmoCorners, RespawnHeightGizmoColor, Color.green);
foreach (Transform spawn in sceneDescriptor.spawns)
{
if (!spawn) continue;
Vector3 position = spawn.position;
Handles.color = Color.white;
if (sceneDescriptor.spawnRadius > 0)
Handles.DrawWireDisc(position, Vector3.up, sceneDescriptor.spawnRadius);
else
{
float size = HandleUtility.GetHandleSize(spawn.position) * 0.5f;
Vector3 pos = spawn.position;
Vector3 offset1 = (Vector3.forward + Vector3.right).normalized * size;
Vector3 offset2 = (Vector3.forward - Vector3.right).normalized * size;
Handles.DrawLine(pos + offset1, pos - offset1);
Handles.DrawLine(pos + offset2, pos - offset2);
}
Handles.color = Color.green;
Handles.DrawLine(position, position + Vector3.up);
Handles.color = Color.blue;
Handles.DrawLine(position, position + spawn.forward);
}
Handles.color = Color.white;
}
private void HierarchyChanged()
{
// This cannot be a RequireComponent because VRC.Core isn't included in VRC.Base.
if (!sceneDescriptor.GetComponent<PipelineManager>())
{
sceneDescriptor.gameObject.AddComponent<PipelineManager>();
}
if (sceneDescriptor.spawns == null || sceneDescriptor.spawns.Length == 0)
{
Undo.RecordObject(sceneDescriptor, "Grabbed new Spawn Position");
sceneDescriptor.spawns = new[] { sceneDescriptor.transform };
Debug.LogWarning($"Scene Descriptor spawns were empty, adding a default Spawn.");
}
}
public override void BuildInspectorGUI()
{
base.BuildInspectorGUI();
AddField(propSpawns);
AddFieldTooltip(propSpawnRadius, "Players spawn at a random spot within the radius. Set to zero to have players spawn at the exact spawn position.");
AddField(propSpawnOrder);
AddField(propSpawnOrientation);
AddField(propReferenceCamera);
AddField(propRespawnHeightY);
AddField(propForbidUserPortals);
AddField(propObjectBehaviourAtRespawnHeight);
AddField(propNetworkIDs, "Network IDs", "The Network ID Collection");
MaskField fieldPassthrough = new ("Interact Passthrough");
fieldPassthrough.AddToClassList("unity-base-field__aligned");
fieldPassthrough.choices = layerNames.ToList();
fieldPassthrough.BindProperty(propInteractPassthrough);
Root.Add(fieldPassthrough);
HelpBox helpBox = new (INTERACTION_HELPBOX_LABEL, HelpBoxMessageType.Info);
Button buttonDocs = new () { text = "Docs" };
buttonDocs.clicked += () => Application.OpenURL(INTERACTION_HELPBOX_URL);
helpBox.Add(buttonDocs);
Root.Add(helpBox);
}
private void PopulateUserLayerNames()
{
if (layerNames == null)
{
layerNames = new string[USER_LAYER_COUNT];
}
for (int i = 0; i < USER_LAYER_COUNT; ++i)
{
string layerName = LayerMask.LayerToName(USER_LAYER_START + i);
if (string.IsNullOrWhiteSpace(layerName))
{
layerNames[i] = $"<<layer {USER_LAYER_START + i}>>";
}
else
{
layerNames[i] = layerName;
}
}
}
private void GetRespawnHeightGizmoPlaneCorners(bool checkRespawnHeightChanged = true)
{
// only get new corners if respawn height has changed
if (checkRespawnHeightChanged &&
Mathf.Approximately(respawnHeightGizmoCorners[0].y, sceneDescriptor.RespawnHeightY))
{
return;
}
respawnHeightGizmoCorners = new[]
{
new Vector3(sceneDescriptor.transform.position.x-5, sceneDescriptor.RespawnHeightY, sceneDescriptor.transform.position.z-5),
new Vector3(sceneDescriptor.transform.position.x-5, sceneDescriptor.RespawnHeightY, sceneDescriptor.transform.position.z+5),
new Vector3(sceneDescriptor.transform.position.x+5, sceneDescriptor.RespawnHeightY, sceneDescriptor.transform.position.z+5),
new Vector3(sceneDescriptor.transform.position.x+5, sceneDescriptor.RespawnHeightY, sceneDescriptor.transform.position.z-5)
};
}
}
}
#endif

View File

@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 4b2b9ac625bc5b04c887ff9ee9b5fdbe
timeCreated: 1450463561
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,77 @@
#if VRC_SDK_VRCSDK3
using UnityEngine;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
using VRCSpatialAudioSource = VRC.SDK3.Components.VRCSpatialAudioSource;
namespace VRC.SDK3.Editor
{
[CanEditMultipleObjects]
[CustomEditor(typeof(VRCSpatialAudioSource))]
public class VRCSpatialAudioSourceEditor3 : VRCInspectorBase
{
private const string SHOW_ADVANCED_OPTIONS_KEY = "VRC.SDK3.Components.VRCSpatialAudioSource.ShowAdvancedOptions";
private SerializedProperty propGain;
private SerializedProperty propNear;
private SerializedProperty propFar;
private SerializedProperty propVolumetricRadius;
private SerializedProperty propEnableSpatialization;
private SerializedProperty propUseAudioSourceVolumeCurve;
private PropertyField fieldEnableSpatialization;
private PropertyField fieldUseAudioSourceVolumeCurve;
private VRCSpatialAudioSource script;
private AudioSource source;
private void OnEnable()
{
script = (VRCSpatialAudioSource)target;
source = script.GetComponent<AudioSource>();
propGain = serializedObject.FindProperty(nameof(VRCSpatialAudioSource.Gain));
propNear = serializedObject.FindProperty(nameof(VRCSpatialAudioSource.Near));
propFar = serializedObject.FindProperty(nameof(VRCSpatialAudioSource.Far));
propVolumetricRadius = serializedObject.FindProperty(nameof(VRCSpatialAudioSource.VolumetricRadius));
propEnableSpatialization = serializedObject.FindProperty(nameof(VRCSpatialAudioSource.EnableSpatialization));
propUseAudioSourceVolumeCurve = serializedObject.FindProperty(nameof(VRCSpatialAudioSource.UseAudioSourceVolumeCurve));
}
public override void BuildInspectorGUI()
{
base.BuildInspectorGUI();
AddField(propGain);
AddField(propFar);
Foldout foldout = AddKeyedFoldout("Advanced Options", SHOW_ADVANCED_OPTIONS_KEY);
foldout.Add(AddField(propNear));
foldout.Add(AddField(propVolumetricRadius));
fieldEnableSpatialization = AddField(propEnableSpatialization);
fieldEnableSpatialization.RegisterValueChangeCallback(EnableSpatializationCallback);
foldout.Add(fieldEnableSpatialization);
fieldUseAudioSourceVolumeCurve = AddField(propUseAudioSourceVolumeCurve);
foldout.Add(fieldUseAudioSourceVolumeCurve);
UseAudioSourceVolumeCurveChanged();
}
private void EnableSpatializationCallback(SerializedPropertyChangeEvent evt) => UseAudioSourceVolumeCurveChanged();
private void UseAudioSourceVolumeCurveChanged()
{
source.spatialize = propEnableSpatialization.boolValue;
fieldUseAudioSourceVolumeCurve.SetVisible(propEnableSpatialization.boolValue);
}
}
}
#endif

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3f8f999a8e1ebee4588f94a8a618d7c6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,314 @@
using TMPro;
using TMPro.EditorUtilities;
using UnityEditor;
using UnityEditor.Presets;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace VRC.SDK3.Components.Editor
{
public class VRCTMP
{
private static TMP_DefaultControls.Resources s_StandardResources;
private const string kUILayerName = "Default";
private const string kStandardSpritePath = "UI/Skin/UISprite.psd";
private const string kBackgroundSpritePath = "UI/Skin/Background.psd";
private const string kInputFieldBackgroundPath = "UI/Skin/InputFieldBackground.psd";
private const string kKnobPath = "UI/Skin/Knob.psd";
private const string kCheckmarkPath = "UI/Skin/Checkmark.psd";
private const string kDropdownArrowPath = "UI/Skin/DropdownArrow.psd";
private const string kMaskPath = "UI/Skin/UIMask.psd";
private static TMP_DefaultControls.Resources GetStandardResources()
{
if (s_StandardResources.standard == null)
{
s_StandardResources.standard = AssetDatabase.GetBuiltinExtraResource<Sprite>(kStandardSpritePath);
s_StandardResources.background = AssetDatabase.GetBuiltinExtraResource<Sprite>(kBackgroundSpritePath);
s_StandardResources.inputField = AssetDatabase.GetBuiltinExtraResource<Sprite>(kInputFieldBackgroundPath);
s_StandardResources.knob = AssetDatabase.GetBuiltinExtraResource<Sprite>(kKnobPath);
s_StandardResources.checkmark = AssetDatabase.GetBuiltinExtraResource<Sprite>(kCheckmarkPath);
s_StandardResources.dropdown = AssetDatabase.GetBuiltinExtraResource<Sprite>(kDropdownArrowPath);
s_StandardResources.mask = AssetDatabase.GetBuiltinExtraResource<Sprite>(kMaskPath);
}
return s_StandardResources;
}
[MenuItem("GameObject/UI/Text - TextMeshPro (VRC)", false, 9)]
private static void CreateTextVRCTMP(MenuCommand menuCommand)
{
GameObject go = TMP_DefaultControls.CreateText(GetStandardResources());
TextMeshProUGUI textComponent = go.GetComponent<TextMeshProUGUI>();
textComponent.fontSize = TMP_Settings.defaultFontSize;
textComponent.color = Color.white;
textComponent.text = "New Text";
PlaceUIElementRoot(go, menuCommand);
}
[MenuItem("GameObject/UI/Button - TextMeshPro (VRC)", false, 10)]
public static void AddButton(MenuCommand menuCommand)
{
GameObject go = TMP_DefaultControls.CreateButton(GetStandardResources());
// Override font size
TMP_Text textComponent = go.GetComponentInChildren<TMP_Text>();
textComponent.fontSize = 24;
PlaceUIElementRoot(go, menuCommand);
}
[MenuItem("GameObject/UI/Dropdown - TextMeshPro (VRC)", false, 11)]
private static void CreateTextVRCDropdown(MenuCommand menuCommand)
{
GameObject go = TMP_DefaultControls.CreateDropdown(GetStandardResources());
PlaceUIElementRoot(go, menuCommand);
}
[MenuItem("GameObject/UI/Input Field - TextMeshPro (VRC)", false, 12)]
private static void CreateTextVRCInputfield(MenuCommand menuCommand)
{
GameObject go = TMP_DefaultControls.CreateInputField(GetStandardResources());
PlaceUIElementRoot(go, menuCommand);
}
// TMP Object creation code from TextMeshPro edited to place in a vrc way
private static void PlaceUIElementRoot(GameObject element, MenuCommand menuCommand)
{
GameObject parent = menuCommand.context as GameObject;
bool explicitParentChoice = true;
if (parent == null)
{
parent = GetOrCreateCanvasGameObject();
explicitParentChoice = false;
// If in Prefab Mode, Canvas has to be part of Prefab contents,
// otherwise use Prefab root instead.
PrefabStage prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
if (prefabStage != null && !prefabStage.IsPartOfPrefabContents(parent))
parent = prefabStage.prefabContentsRoot;
}
if (parent.GetComponentsInParent<Canvas>(true).Length == 0)
{
// Create canvas under context GameObject,
// and make that be the parent which UI element is added under.
GameObject canvas = CreateNewUI();
Undo.SetTransformParent(canvas.transform, parent.transform, "");
parent = canvas;
}
GameObjectUtility.EnsureUniqueNameForSibling(element);
SetParentAndAlign(element, parent);
if (!explicitParentChoice) // not a context click, so center in sceneview
SetPositionVisibleinSceneView(parent.GetComponent<RectTransform>(), element.GetComponent<RectTransform>());
// This call ensure any change made to created Objects after they where registered will be part of the Undo.
Undo.RegisterFullObjectHierarchyUndo(parent == null ? element : parent, "");
// We have to fix up the undo name since the name of the object was only known after reparenting it.
Undo.SetCurrentGroupName("Create " + element.name);
Selection.activeGameObject = element;
}
// Helper function that returns a Canvas GameObject; preferably a parent of the selection, or other existing Canvas.
public static GameObject GetOrCreateCanvasGameObject()
{
GameObject selectedGo = Selection.activeGameObject;
// Try to find a gameobject that is the selected GO or one if its parents.
Canvas canvas = (selectedGo != null) ? selectedGo.GetComponentInParent<Canvas>() : null;
if (IsValidCanvas(canvas))
return canvas.gameObject;
// No canvas in selection or its parents? Then use any valid canvas.
// We have to find all loaded Canvases, not just the ones in main scenes.
Canvas[] canvasArray = StageUtility.GetCurrentStageHandle().FindComponentsOfType<Canvas>();
for (int i = 0; i < canvasArray.Length; i++)
if (IsValidCanvas(canvasArray[i]))
return canvasArray[i].gameObject;
// No canvas in the scene at all? Then create a new one.
return CreateNewUI();
}
static bool IsValidCanvas(Canvas canvas)
{
if (canvas == null || !canvas.gameObject.activeInHierarchy)
return false;
// It's important that the non-editable canvas from a prefab scene won't be rejected,
// but canvases not visible in the Hierarchy at all do. Don't check for HideAndDontSave.
if (EditorUtility.IsPersistent(canvas) || (canvas.hideFlags & HideFlags.HideInHierarchy) != 0)
return false;
if (StageUtility.GetStageHandle(canvas.gameObject) != StageUtility.GetCurrentStageHandle())
return false;
return true;
}
private static void SetParentAndAlign(GameObject child, GameObject parent)
{
if (parent == null)
return;
Undo.SetTransformParent(child.transform, parent.transform, "");
RectTransform rectTransform = child.transform as RectTransform;
if (rectTransform)
{
rectTransform.anchoredPosition = Vector2.zero;
Vector3 localPosition = rectTransform.localPosition;
localPosition.z = 0;
rectTransform.localPosition = localPosition;
}
else
{
child.transform.localPosition = Vector3.zero;
}
child.transform.localRotation = Quaternion.identity;
child.transform.localScale = Vector3.one;
SetLayerRecursively(child, parent.layer);
}
private static void SetLayerRecursively(GameObject go, int layer)
{
go.layer = layer;
Transform t = go.transform;
for (int i = 0; i < t.childCount; i++)
SetLayerRecursively(t.GetChild(i).gameObject, layer);
}
public static GameObject CreateNewUI()
{
// Root for the UI
var root = new GameObject("Canvas");
root.layer = LayerMask.NameToLayer(kUILayerName);
Canvas canvas = root.AddComponent<Canvas>();
RectTransform canvasRTransform = canvas.GetComponent<RectTransform>();
canvasRTransform.localScale = new Vector3(0.001f, 0.001f,0.001f);
canvasRTransform.sizeDelta = new Vector2(1000, 1000);
SceneView sceneView = SceneView.lastActiveSceneView;
if (sceneView == null && SceneView.sceneViews.Count > 0)
sceneView = SceneView.sceneViews[0] as SceneView;
// Couldn't find a SceneView. Don't set position.
if (sceneView != null && sceneView.camera != null)
{
Camera camera = sceneView.camera;
canvasRTransform.position = camera.transform.position + camera.transform.forward * 2;
}
canvas.renderMode = RenderMode.WorldSpace;
root.AddComponent<CanvasScaler>();
root.AddComponent<GraphicRaycaster>();
root.AddComponent<VRCUiShape>();
// Works for all stages.
StageUtility.PlaceGameObjectInCurrentStage(root);
bool customScene = false;
PrefabStage prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
if (prefabStage != null)
{
root.transform.SetParent(prefabStage.prefabContentsRoot.transform, false);
customScene = true;
}
Undo.RegisterCreatedObjectUndo(root, "Create " + root.name);
// If there is no event system add one...
// No need to place event system in custom scene as these are temporary anyway.
// It can be argued for or against placing it in the user scenes,
// but let's not modify scene user is not currently looking at.
if (!customScene)
CreateEventSystem(false);
return root;
}
private static void SetPositionVisibleinSceneView(RectTransform canvasRTransform, RectTransform itemTransform)
{
// Find the best scene view
SceneView sceneView = SceneView.lastActiveSceneView;
if (sceneView == null && SceneView.sceneViews.Count > 0)
sceneView = SceneView.sceneViews[0] as SceneView;
// Couldn't find a SceneView. Don't set position.
if (sceneView == null || sceneView.camera == null)
return;
// Create world space Plane from canvas position.
Camera camera = sceneView.camera;
Vector3 position = Vector3.zero;
Vector2 localPlanePosition;
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRTransform, new Vector2(camera.pixelWidth / 2, camera.pixelHeight / 2), camera, out localPlanePosition))
{
// Adjust for canvas pivot
localPlanePosition.x = localPlanePosition.x + canvasRTransform.sizeDelta.x * canvasRTransform.pivot.x;
localPlanePosition.y = localPlanePosition.y + canvasRTransform.sizeDelta.y * canvasRTransform.pivot.y;
localPlanePosition.x = Mathf.Clamp(localPlanePosition.x, 0, canvasRTransform.sizeDelta.x);
localPlanePosition.y = Mathf.Clamp(localPlanePosition.y, 0, canvasRTransform.sizeDelta.y);
// Adjust for anchoring
position.x = localPlanePosition.x - canvasRTransform.sizeDelta.x * itemTransform.anchorMin.x;
position.y = localPlanePosition.y - canvasRTransform.sizeDelta.y * itemTransform.anchorMin.y;
Vector3 minLocalPosition;
minLocalPosition.x = canvasRTransform.sizeDelta.x * (0 - canvasRTransform.pivot.x) + itemTransform.sizeDelta.x * itemTransform.pivot.x;
minLocalPosition.y = canvasRTransform.sizeDelta.y * (0 - canvasRTransform.pivot.y) + itemTransform.sizeDelta.y * itemTransform.pivot.y;
Vector3 maxLocalPosition;
maxLocalPosition.x = canvasRTransform.sizeDelta.x * (1 - canvasRTransform.pivot.x) - itemTransform.sizeDelta.x * itemTransform.pivot.x;
maxLocalPosition.y = canvasRTransform.sizeDelta.y * (1 - canvasRTransform.pivot.y) - itemTransform.sizeDelta.y * itemTransform.pivot.y;
position.x = Mathf.Clamp(position.x, minLocalPosition.x, maxLocalPosition.x);
position.y = Mathf.Clamp(position.y, minLocalPosition.y, maxLocalPosition.y);
}
itemTransform.anchoredPosition = position;
itemTransform.localRotation = Quaternion.identity;
itemTransform.localScale = Vector3.one;
}
private static void CreateEventSystem(bool select)
{
CreateEventSystem(select, null);
}
private static void CreateEventSystem(bool select, GameObject parent)
{
var esys = Object.FindFirstObjectByType<EventSystem>();
if (esys == null)
{
var eventSystem = new GameObject("EventSystem");
GameObjectUtility.SetParentAndAlign(eventSystem, parent);
esys = eventSystem.AddComponent<EventSystem>();
eventSystem.AddComponent<StandaloneInputModule>();
Undo.RegisterCreatedObjectUndo(eventSystem, "Create " + eventSystem.name);
}
if (select && esys != null)
{
Selection.activeGameObject = esys.gameObject;
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 577bc48ccbd86fc4e8804e4ed36a6dac
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: