Added Unity project files
This commit is contained in:
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 59a956eb4084499dbbc4004a6922c503
|
||||
timeCreated: 1744910458
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 92daf15a37b84806a147aa8127651a17
|
||||
timeCreated: 1746046539
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6348c1cdc3444611a7fdbac6d268dc58
|
||||
timeCreated: 1748027556
|
||||
@ -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
|
||||
*/
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d57b23c04034119448f23c5fdbc57662
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5b1017097f3d46cd949892e0fce3ece9
|
||||
timeCreated: 1612853300
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 88390b175b72faa49b553b95cbcac908
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 76fb4f8f2f07c024795a6acecfeaefcc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a8cc4c1876b26174fbaeb062178a6bda
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e5f6789012345678901234abcdef0123
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8901d07a685ca424492a3cabff506184
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d0eab965d110d6a41b44e8c56b81f7bd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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
|
||||
@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4b2b9ac625bc5b04c887ff9ee9b5fdbe
|
||||
timeCreated: 1450463561
|
||||
licenseType: Free
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3f8f999a8e1ebee4588f94a8a618d7c6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 577bc48ccbd86fc4e8804e4ed36a6dac
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user