diff --git a/Assets/Scripts/AssetBundles/AssetBundleLoader.cs b/Assets/Scripts/AssetBundles/AssetBundleLoader.cs
index d31ae28..db53c54 100644
--- a/Assets/Scripts/AssetBundles/AssetBundleLoader.cs
+++ b/Assets/Scripts/AssetBundles/AssetBundleLoader.cs
@@ -2,47 +2,34 @@
using UnityEngine;
using UnityEngine.Networking;
-// AssetBundle cache checker & loader with caching
-// worsk by loading .manifest file from server and parsing hash string from it
-
namespace UnityLibrary
{
public class AssetBundleLoader : MonoBehaviour
{
public string assetBundleURL = "http://localhost/bundle";
+ private string bundleName = "bundle";
void Start()
{
- //StartCoroutine(DownloadAndCache(assetBundleURL));
+ StartCoroutine(DownloadAndCache(assetBundleURL));
}
- ///
- /// load assetbundle manifest, check hash, load actual bundle with hash parameter to use caching
- /// instantiate gameobject
- ///
- /// full url to assetbundle file
- /// optional parameter to access specific asset from assetbundle
- ///
IEnumerator DownloadAndCache(string bundleURL, string assetName = "")
{
- // Wait for the Caching system to be ready
while (!Caching.ready)
{
yield return null;
}
- // if you want to always load from server, can clear cache first
- // Caching.CleanCache();
+ // Clear cache for previous versions of the asset bundle
+ Caching.ClearOtherCachedVersions(bundleName, Hash128.Parse("0"));
- // get current bundle hash from server, random value added to avoid caching
UnityWebRequest www = UnityWebRequest.Get(bundleURL + ".manifest?r=" + (Random.value * 9999999));
- Debug.Log("Loading manifest:" + bundleURL + ".manifest");
+ Debug.Log("Loading manifest: " + bundleURL + ".manifest");
- // wait for load to finish
- yield return www.Send();
+ yield return www.SendWebRequest();
- // if received error, exit
- if (www.isNetworkError == true)
+ if (www.isNetworkError)
{
Debug.LogError("www error: " + www.error);
www.Dispose();
@@ -50,43 +37,38 @@ IEnumerator DownloadAndCache(string bundleURL, string assetName = "")
yield break;
}
- // create empty hash string
- Hash128 hashString = (default(Hash128));// new Hash128(0, 0, 0, 0);
+ Hash128 hashString = default(Hash128);
- // check if received data contains 'ManifestFileVersion'
if (www.downloadHandler.text.Contains("ManifestFileVersion"))
{
- // extract hash string from the received data, TODO should add some error checking here
var hashRow = www.downloadHandler.text.ToString().Split("\n".ToCharArray())[5];
hashString = Hash128.Parse(hashRow.Split(':')[1].Trim());
- if (hashString.isValid == true)
+ if (hashString.isValid)
{
- // we can check if there is cached version or not
- if (Caching.IsVersionCached(bundleURL, hashString) == true)
+ if (Caching.IsVersionCached(bundleURL, hashString))
{
Debug.Log("Bundle with this hash is already cached!");
- } else
+ }
+ else
{
- Debug.Log("No cached version founded for this hash..");
+ Debug.Log("No cached version found for this hash..");
}
- } else
+ }
+ else
{
- // invalid loaded hash, just try loading latest bundle
- Debug.LogError("Invalid hash:" + hashString);
+ Debug.LogError("Invalid hash: " + hashString);
yield break;
}
-
- } else
+ }
+ else
{
Debug.LogError("Manifest doesn't contain string 'ManifestFileVersion': " + bundleURL + ".manifest");
yield break;
}
- // now download the actual bundle, with hashString parameter it uses cached version if available
www = UnityWebRequestAssetBundle.GetAssetBundle(bundleURL + "?r=" + (Random.value * 9999999), hashString, 0);
- // wait for load to finish
yield return www.SendWebRequest();
if (www.error != null)
@@ -97,42 +79,26 @@ IEnumerator DownloadAndCache(string bundleURL, string assetName = "")
yield break;
}
- // get bundle from downloadhandler
AssetBundle bundle = ((DownloadHandlerAssetBundle)www.downloadHandler).assetBundle;
-
GameObject bundlePrefab = null;
- // if no asset name is given, take the first/main asset
if (assetName == "")
{
bundlePrefab = (GameObject)bundle.LoadAsset(bundle.GetAllAssetNames()[0]);
- } else
- { // use asset name to access inside bundle
+ }
+ else
+ {
bundlePrefab = (GameObject)bundle.LoadAsset(assetName);
}
- // if we got something out
if (bundlePrefab != null)
{
-
- // instantiate at 0,0,0 and without rotation
Instantiate(bundlePrefab, Vector3.zero, Quaternion.identity);
-
- /*
- // fix pink shaders, NOTE: not always needed..
- foreach (Renderer r in go.GetComponentsInChildren(includeInactive: true))
- {
- // FIXME: creates multiple materials, not good
- var material = Shader.Find(r.material.shader.name);
- r.material.shader = null;
- r.material.shader = material;
- }*/
}
www.Dispose();
www = null;
- // try to cleanup memory
Resources.UnloadUnusedAssets();
bundle.Unload(false);
bundle = null;
diff --git a/Assets/Scripts/Camera/AutoResolution.cs b/Assets/Scripts/Camera/AutoResolution.cs
new file mode 100644
index 0000000..87fd874
--- /dev/null
+++ b/Assets/Scripts/Camera/AutoResolution.cs
@@ -0,0 +1,58 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+public class AutoResolution : MonoBehaviour
+{
+ public int setWidth = 1440;
+ public int setHeight = 2560;
+
+ private void Start()
+ {
+ // Get the main camera and its current dimensions
+ Camera camera = Camera.main;
+ Rect rect = camera.rect;
+
+ // Calculate the scale height and width of the screen
+ float scaleHeight = ((float)Screen.width / Screen.height) / ((float)9 / 16);
+ float scaleWidth = 1f / scaleHeight;
+
+ // Adjust the camera's dimensions based on the scale height and width
+ if (scaleHeight < 1)
+ {
+ rect.height = scaleHeight;
+ rect.y = (1f - scaleHeight) / 2f;
+ }
+ else
+ {
+ rect.width = scaleWidth;
+ rect.x = (1f - scaleWidth) / 2f;
+ }
+
+ camera.rect = rect;
+
+ SetResolution();
+ }
+
+ public void SetResolution()
+ {
+ // Get the current device's screen dimensions
+ int deviceWidth = Screen.width;
+ int deviceHeight = Screen.height;
+
+ // Set the screen resolution to the desired dimensions, while maintaining aspect ratio
+ Screen.SetResolution(setWidth, (int)(((float)deviceHeight / deviceWidth) * setWidth), true);
+
+ // Adjust the camera's dimensions based on the new resolution
+ if ((float)setWidth / setHeight < (float)deviceWidth / deviceHeight)
+ {
+ float newWidth = ((float)setWidth / setHeight) / ((float)deviceWidth / deviceHeight);
+ Camera.main.rect = new Rect((1f - newWidth) / 2f, 0f, newWidth, 1f);
+ }
+ else
+ {
+ float newHeight = ((float)deviceWidth / deviceHeight) / ((float)setWidth / setHeight);
+ Camera.main.rect = new Rect(0f, (1f - newHeight) / 2f, 1f, newHeight); // 새로운 Rect 적용
+ }
+ }
+}
diff --git a/Assets/Scripts/Docs/Graphics/Graphics_Blit.cs b/Assets/Scripts/Docs/Graphics/Graphics_Blit.cs
index 6760898..805f4ba 100644
--- a/Assets/Scripts/Docs/Graphics/Graphics_Blit.cs
+++ b/Assets/Scripts/Docs/Graphics/Graphics_Blit.cs
@@ -26,7 +26,7 @@ void OnPostRender()
{
// Copies source texture into destination render texture with a shader
// Destination RenderTexture is null to blit directly to screen
- Graphics.Blit(displayTexture, null, mat);
+ Graphics.Blit(displayTexture, null as RenderTexture, mat);
}
}
}
\ No newline at end of file
diff --git a/Assets/Scripts/Editor/BatchTools/CopyGameObjectNames.cs b/Assets/Scripts/Editor/BatchTools/CopyGameObjectNames.cs
new file mode 100644
index 0000000..40bf0d0
--- /dev/null
+++ b/Assets/Scripts/Editor/BatchTools/CopyGameObjectNames.cs
@@ -0,0 +1,59 @@
+// editor tool to copy names of selected GameObjects to clipboard as a list (so you can paste them in Excel or others..)
+
+using UnityEngine;
+using UnityEditor;
+using System.Text;
+using System.Linq;
+namespace UnityLibrary.Tools
+{
+ public class CopyGameObjectNames : EditorWindow
+ {
+ private string gameObjectNames = string.Empty;
+
+ [MenuItem("Tools/Copy GameObject Names")]
+ public static void ShowWindow()
+ {
+ GetWindow("Copy GameObject Names");
+ }
+
+ private void OnGUI()
+ {
+ GUILayout.Label("Copy Names of Selected GameObjects", EditorStyles.boldLabel);
+
+ if (GUILayout.Button("Fetch Names"))
+ {
+ FetchNames();
+ }
+
+ GUILayout.Label("GameObject Names:", EditorStyles.label);
+ gameObjectNames = EditorGUILayout.TextArea(gameObjectNames, GUILayout.Height(200));
+
+ if (GUILayout.Button("Copy to Clipboard"))
+ {
+ CopyToClipboard();
+ }
+ }
+
+ private void FetchNames()
+ {
+ StringBuilder sb = new StringBuilder();
+ GameObject[] selectedObjects = Selection.gameObjects;
+
+ // Sort the selected objects by their sibling index
+ var sortedObjects = selectedObjects.OrderBy(go => go.transform.GetSiblingIndex()).ToArray();
+
+ foreach (GameObject obj in sortedObjects)
+ {
+ sb.AppendLine(obj.name);
+ }
+
+ gameObjectNames = sb.ToString();
+ }
+
+ private void CopyToClipboard()
+ {
+ EditorGUIUtility.systemCopyBuffer = gameObjectNames;
+ Debug.Log("GameObject names copied to clipboard.");
+ }
+ }
+}
diff --git a/Assets/Scripts/Editor/BatchTools/ReplaceCharacterInGameObjectNames.cs b/Assets/Scripts/Editor/BatchTools/ReplaceCharacterInGameObjectNames.cs
new file mode 100644
index 0000000..105bc74
--- /dev/null
+++ b/Assets/Scripts/Editor/BatchTools/ReplaceCharacterInGameObjectNames.cs
@@ -0,0 +1,69 @@
+// editor tool to replace string from selected GameObject names
+
+using UnityEngine;
+using UnityEditor;
+using UnityEditor.SceneManagement;
+
+namespace UnityLibrary.Tools
+{
+ public class ReplaceCharacterInGameObjectNames : EditorWindow
+ {
+ private string searchString = "|";
+ private string replaceString = "@";
+
+ [MenuItem("Tools/Replace Characters in GameObject Names")]
+ public static void ShowWindow()
+ {
+ GetWindow("Replace Characters");
+ }
+
+ private void OnGUI()
+ {
+ GUILayout.Label("Replace Characters in Selected GameObject Names", EditorStyles.boldLabel);
+
+ searchString = EditorGUILayout.TextField("Search String", searchString);
+ replaceString = EditorGUILayout.TextField("Replace String", replaceString);
+
+ int selectedObjectCount = Selection.gameObjects.Length;
+ GUILayout.Label($"Selected GameObjects: {selectedObjectCount}", EditorStyles.label);
+
+ if (GUILayout.Button("Replace"))
+ {
+ ReplaceCharacters();
+ }
+ }
+
+ private void ReplaceCharacters()
+ {
+ GameObject[] selectedObjects = Selection.gameObjects;
+
+ if (selectedObjects.Length == 0)
+ {
+ Debug.LogWarning("No GameObjects selected.");
+ return;
+ }
+
+ // Start a new undo group
+ Undo.IncrementCurrentGroup();
+ Undo.SetCurrentGroupName("Replace Character in GameObject Names");
+ int undoGroup = Undo.GetCurrentGroup();
+
+ foreach (GameObject obj in selectedObjects)
+ {
+ if (obj.name.Contains(searchString))
+ {
+ Undo.RecordObject(obj, "Replace Character in GameObject Name");
+ obj.name = obj.name.Replace(searchString, replaceString);
+ EditorUtility.SetDirty(obj);
+ }
+ }
+
+ // End the undo group
+ Undo.CollapseUndoOperations(undoGroup);
+
+ EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
+
+ Debug.Log($"Replaced '{searchString}' with '{replaceString}' in the names of selected GameObjects.");
+ }
+ }
+}
diff --git a/Assets/Scripts/Editor/ContextMenu/BoxColliderFitChildren.cs b/Assets/Scripts/Editor/ContextMenu/BoxColliderFitChildren.cs
index 58f7b0d..ce3f298 100644
--- a/Assets/Scripts/Editor/ContextMenu/BoxColliderFitChildren.cs
+++ b/Assets/Scripts/Editor/ContextMenu/BoxColliderFitChildren.cs
@@ -1,7 +1,3 @@
-// Adjust Box Collider to fit child meshes inside
-// Usage: You have empty parent transform, with child meshes inside, add box collider to parent then use this
-// NOTE: Doesnt work if root transform is rotated
-
using UnityEngine;
using UnityEditor;
@@ -10,37 +6,66 @@ namespace UnityLibrary
public class BoxColliderFitChildren : MonoBehaviour
{
[MenuItem("CONTEXT/BoxCollider/Fit to Children")]
- static void FixSize(MenuCommand command)
+ static void FitColliderToChildren(MenuCommand command)
{
BoxCollider col = (BoxCollider)command.context;
- // record undo
+ // Record undo
Undo.RecordObject(col.transform, "Fit Box Collider To Children");
- // get child mesh bounds
- var b = GetRecursiveMeshBounds(col.gameObject);
+ // Get world-space bounds of all child meshes
+ var worldBounds = GetRecursiveMeshBounds(col.gameObject);
+
+ if (worldBounds.size == Vector3.zero)
+ {
+ Debug.LogWarning("No valid meshes found to fit the BoxCollider.");
+ return;
+ }
+
+ // Convert world-space center to local space
+ Vector3 localCenter = col.transform.InverseTransformPoint(worldBounds.center);
+
+ // Convert world-space size to local space
+ Vector3 localSize = col.transform.InverseTransformVector(worldBounds.size);
+
+ // Ensure size is positive
+ localSize = new Vector3(Mathf.Abs(localSize.x), Mathf.Abs(localSize.y), Mathf.Abs(localSize.z));
+
+ // Fix potential center flipping
+ if (Vector3.Dot(col.transform.right, Vector3.right) < 0)
+ {
+ localCenter.x = -localCenter.x;
+ }
+ if (Vector3.Dot(col.transform.up, Vector3.up) < 0)
+ {
+ localCenter.y = -localCenter.y;
+ }
+ if (Vector3.Dot(col.transform.forward, Vector3.forward) < 0)
+ {
+ localCenter.z = -localCenter.z;
+ }
- // set collider local center and size
- col.center = col.transform.root.InverseTransformVector(b.center) - col.transform.position;
- col.size = b.size;
+ // Apply to collider
+ col.center = localCenter;
+ col.size = localSize;
}
public static Bounds GetRecursiveMeshBounds(GameObject go)
{
- var r = go.GetComponentsInChildren();
- if (r.Length > 0)
- {
- var b = r[0].bounds;
- for (int i = 1; i < r.Length; i++)
- {
- b.Encapsulate(r[i].bounds);
- }
- return b;
- }
- else // TODO no renderers
+ Renderer[] renderers = go.GetComponentsInChildren();
+
+ if (renderers.Length == 0)
+ return new Bounds();
+
+ // Start with the first renderer’s bounds in world space
+ Bounds worldBounds = renderers[0].bounds;
+
+ for (int i = 1; i < renderers.Length; i++)
{
- return new Bounds(Vector3.one, Vector3.one);
+ worldBounds.Encapsulate(renderers[i].bounds);
}
+
+ return worldBounds;
}
}
}
diff --git a/Assets/Scripts/Editor/ContextMenu/LineRendererToLocalSpace.cs b/Assets/Scripts/Editor/ContextMenu/LineRendererToLocalSpace.cs
new file mode 100644
index 0000000..6cc6cc7
--- /dev/null
+++ b/Assets/Scripts/Editor/ContextMenu/LineRendererToLocalSpace.cs
@@ -0,0 +1,60 @@
+// converts line renderer points from world space to local space
+
+using UnityEngine;
+using UnityEditor;
+using UnityEditor.SceneManagement;
+
+namespace UnityLibrary.ContextMenu
+{
+ public static class LineRendererToLocalSpace
+ {
+ private const string MenuPath = "CONTEXT/LineRenderer/Convert Points To Local Space";
+
+ [MenuItem(MenuPath, true)]
+ private static bool Validate(MenuCommand command)
+ {
+ return command != null && command.context is LineRenderer;
+ }
+
+ [MenuItem(MenuPath)]
+ private static void Convert(MenuCommand command)
+ {
+ var lr = (LineRenderer)command.context;
+ if (lr == null) return;
+
+ int count = lr.positionCount;
+ if (count == 0) return;
+
+ Transform t = lr.transform;
+
+ Undo.RecordObject(lr, "Convert LineRenderer To Local Space");
+
+ // Get current positions in world space no matter what mode it's in.
+ Vector3[] world = new Vector3[count];
+ if (lr.useWorldSpace)
+ {
+ lr.GetPositions(world);
+ }
+ else
+ {
+ Vector3[] local = new Vector3[count];
+ lr.GetPositions(local);
+ for (int i = 0; i < count; i++)
+ world[i] = t.TransformPoint(local[i]);
+ }
+
+ // Convert world -> local, switch mode, write back.
+ Vector3[] newLocal = new Vector3[count];
+ for (int i = 0; i < count; i++)
+ newLocal[i] = t.InverseTransformPoint(world[i]);
+
+ lr.useWorldSpace = false;
+ lr.SetPositions(newLocal);
+
+ EditorUtility.SetDirty(lr);
+
+ if (!Application.isPlaying)
+ EditorSceneManager.MarkSceneDirty(lr.gameObject.scene);
+ }
+ }
+}
diff --git a/Assets/Scripts/Editor/ContextMenu/LineRendererToWorldSpace.cs b/Assets/Scripts/Editor/ContextMenu/LineRendererToWorldSpace.cs
new file mode 100644
index 0000000..90cc2d2
--- /dev/null
+++ b/Assets/Scripts/Editor/ContextMenu/LineRendererToWorldSpace.cs
@@ -0,0 +1,54 @@
+// converts LineRenderer points from local space to world space via context menu in Unity Editor
+
+using UnityEngine;
+using UnityEditor;
+using UnityEditor.SceneManagement;
+
+namespace UnityLibrary.ContextMenu
+{
+ public static class LineRendererToWorldSpace
+ {
+ private const string MenuPath = "CONTEXT/LineRenderer/Convert Points To World Space";
+
+ [MenuItem(MenuPath, true)]
+ private static bool Validate(MenuCommand command)
+ {
+ return command != null && command.context is LineRenderer;
+ }
+
+ [MenuItem(MenuPath)]
+ private static void Convert(MenuCommand command)
+ {
+ var lr = (LineRenderer)command.context;
+ if (lr == null) return;
+
+ if (lr.useWorldSpace)
+ {
+ Debug.Log("LineRenderer is already using World Space.");
+ return;
+ }
+
+ int count = lr.positionCount;
+ if (count == 0) return;
+
+ Transform t = lr.transform;
+
+ Undo.RecordObject(lr, "Convert LineRenderer To World Space");
+
+ Vector3[] local = new Vector3[count];
+ lr.GetPositions(local);
+
+ Vector3[] world = new Vector3[count];
+ for (int i = 0; i < count; i++)
+ world[i] = t.TransformPoint(local[i]);
+
+ lr.useWorldSpace = true;
+ lr.SetPositions(world);
+
+ EditorUtility.SetDirty(lr);
+
+ if (!Application.isPlaying)
+ EditorSceneManager.MarkSceneDirty(lr.gameObject.scene);
+ }
+ }
+}
diff --git a/Assets/Scripts/Editor/CustomInspector/CustomRectTransformCopyInspector.cs b/Assets/Scripts/Editor/CustomInspector/CustomRectTransformCopyInspector.cs
new file mode 100644
index 0000000..0a5de2b
--- /dev/null
+++ b/Assets/Scripts/Editor/CustomInspector/CustomRectTransformCopyInspector.cs
@@ -0,0 +1,114 @@
+#if UNITY_EDITOR
+using System;
+using System.Reflection;
+using UnityEngine;
+
+namespace UnityEditor
+{
+ [CustomEditor(typeof(RectTransform), true)]
+ [CanEditMultipleObjects]
+ public class CustomRectTransformCopyInspector : Editor
+ {
+ // Unity's built-in editor
+ Editor defaultEditor = null;
+ RectTransform rectTransform;
+
+ private static RectTransformData copiedData;
+
+ void OnEnable()
+ {
+ // Use reflection to get the default Unity RectTransform editor
+ defaultEditor = Editor.CreateEditor(targets, Type.GetType("UnityEditor.RectTransformEditor, UnityEditor"));
+ rectTransform = target as RectTransform;
+ }
+
+ void OnDisable()
+ {
+ // Destroy the default editor to avoid memory leaks
+ if (defaultEditor != null)
+ {
+ MethodInfo disableMethod = defaultEditor.GetType().GetMethod("OnDisable",
+ BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
+ if (disableMethod != null)
+ disableMethod.Invoke(defaultEditor, null);
+
+ DestroyImmediate(defaultEditor);
+ }
+ }
+
+ public override void OnInspectorGUI()
+ {
+ // Draw Unity's default RectTransform Inspector
+ defaultEditor.OnInspectorGUI();
+
+ // Add Copy and Paste buttons
+ EditorGUILayout.Space();
+ GUILayout.BeginHorizontal();
+
+ if (GUILayout.Button("C", GUILayout.Width(30))) // Copy
+ {
+ CopyRectTransform(rectTransform);
+ }
+
+ if (GUILayout.Button("P", GUILayout.Width(30))) // Paste
+ {
+ PasteRectTransform(rectTransform);
+ }
+
+ GUILayout.EndHorizontal();
+ }
+
+ private void CopyRectTransform(RectTransform rectTransform)
+ {
+ copiedData = new RectTransformData(rectTransform);
+ Debug.Log("RectTransform copied!");
+ }
+
+ private void PasteRectTransform(RectTransform rectTransform)
+ {
+ if (copiedData == null)
+ {
+ Debug.LogWarning("No RectTransform data to paste!");
+ return;
+ }
+
+ Undo.RecordObject(rectTransform, "Paste RectTransform");
+
+ copiedData.ApplyTo(rectTransform);
+ Debug.Log("RectTransform pasted!");
+
+ EditorUtility.SetDirty(rectTransform);
+ }
+
+ private class RectTransformData
+ {
+ public Vector2 anchorMin;
+ public Vector2 anchorMax;
+ public Vector2 anchoredPosition;
+ public Vector2 sizeDelta;
+ public Vector2 pivot;
+ public Quaternion rotation;
+
+ public RectTransformData(RectTransform rectTransform)
+ {
+ anchorMin = rectTransform.anchorMin;
+ anchorMax = rectTransform.anchorMax;
+ anchoredPosition = rectTransform.anchoredPosition;
+ sizeDelta = rectTransform.sizeDelta;
+ pivot = rectTransform.pivot;
+ rotation = rectTransform.rotation;
+ }
+
+ public void ApplyTo(RectTransform rectTransform)
+ {
+ rectTransform.anchorMin = anchorMin;
+ rectTransform.anchorMax = anchorMax;
+ rectTransform.anchoredPosition = anchoredPosition;
+ rectTransform.sizeDelta = sizeDelta;
+ rectTransform.pivot = pivot;
+ rectTransform.rotation = rotation;
+ }
+ }
+ }
+}
+#endif
diff --git a/Assets/Scripts/Editor/CustomInspector/CustomRectTransformInspector.cs b/Assets/Scripts/Editor/CustomInspector/CustomRectTransformInspector.cs
new file mode 100644
index 0000000..02c51af
--- /dev/null
+++ b/Assets/Scripts/Editor/CustomInspector/CustomRectTransformInspector.cs
@@ -0,0 +1,294 @@
+// source https://gist.github.com/GieziJo/f80bcb24c4caa68ebfb204148ccd4b18
+// ===============================
+// AUTHOR : J. Giezendanner
+// CREATE DATE : 12.03.2020
+// MODIFIED DATE :
+// PURPOSE : Adds helper functions to the RectTransform to align the rect to the anchors and vise-versa
+// SPECIAL NOTES : Sources for certain informations:
+// Display anchors gizmos:
+// https://forum.unity.com/threads/recttransform-custom-editor-ontop-of-unity-recttransform-custom-editor.455925/
+// Draw default inspector:
+// https://forum.unity.com/threads/extending-instead-of-replacing-built-in-inspectors.407612/
+// ===============================
+// Change History:
+//==================================
+
+#if UNITY_EDITOR
+using System;
+using System.Reflection;
+using UnityEditor.SceneManagement;
+using UnityEngine;
+
+
+namespace UnityEditor
+{
+ [CustomEditor(typeof(RectTransform), true)]
+ [CanEditMultipleObjects]
+ public class CustomRectTransformInspector : Editor
+ {
+ //Unity's built-in editor
+ Editor defaultEditor = null;
+ RectTransform rectTransform;
+
+ bool rect2Anchors_foldout = false;
+ bool anchors2Rect_foldout = false;
+ bool rect2Anchors__previousState = false;
+ bool anchors2Rect_previousState = false;
+
+ private bool playerPrefsChecked = false;
+
+ void OnEnable()
+ {
+ //When this inspector is created, also create the built-in inspector
+ defaultEditor = Editor.CreateEditor(targets, Type.GetType("UnityEditor.RectTransformEditor, UnityEditor"));
+ rectTransform = target as RectTransform;
+ }
+
+ void OnDisable()
+ {
+ //When OnDisable is called, the default editor we created should be destroyed to avoid memory leakage.
+ //Also, make sure to call any required methods like OnDisable
+
+ if (defaultEditor != null)
+ {
+ MethodInfo disableMethod = defaultEditor.GetType().GetMethod("OnDisable",
+ BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
+ if (disableMethod != null)
+ disableMethod.Invoke(defaultEditor, null);
+ DestroyImmediate(defaultEditor);
+ }
+ }
+
+ void checkPlayerPrefs()
+ {
+ rect2Anchors_foldout = PlayerPrefs.GetInt("giezi_tools_rect2Anchors_foldout_bool", 0) != 0;
+ anchors2Rect_foldout = PlayerPrefs.GetInt("giezi_tools_anchors2Rect_foldout_bool", 0) != 0;
+
+ rect2Anchors__previousState = rect2Anchors_foldout;
+ anchors2Rect_previousState = anchors2Rect_foldout;
+ }
+
+
+ public override void OnInspectorGUI()
+ {
+ if (!playerPrefsChecked)
+ {
+ checkPlayerPrefs();
+ playerPrefsChecked = true;
+ }
+
+ defaultEditor.OnInspectorGUI();
+
+
+ if (rectTransform.parent != null)
+ {
+ var centerButtonStyle = new GUIStyle(GUI.skin.button);
+ centerButtonStyle.fontStyle = FontStyle.Bold;
+
+ EditorGUILayout.Space();
+ EditorGUILayout.LabelField("Helper Functions", EditorStyles.boldLabel);
+
+ rect2Anchors_foldout = EditorGUILayout.Foldout(rect2Anchors_foldout, "Set Rect to Anchors");
+
+ if (rect2Anchors_foldout)
+ {
+ GUILayout.BeginHorizontal();
+ GUILayout.BeginVertical();
+ if (GUILayout.Button("Top Left"))
+ setRectValue("topLeft");
+ if (GUILayout.Button("Left"))
+ setRectValue("left");
+ if (GUILayout.Button("Bottom Left"))
+ setRectValue("bottomLeft");
+ GUILayout.EndVertical();
+ GUILayout.BeginVertical();
+ if (GUILayout.Button("Top"))
+ setRectValue("top");
+ if (GUILayout.Button("All", centerButtonStyle))
+ setRectValue("all");
+ if (GUILayout.Button("Bottom"))
+ setRectValue("bottom");
+ GUILayout.EndVertical();
+ GUILayout.BeginVertical();
+ if (GUILayout.Button("Top Right"))
+ setRectValue("topRight");
+ if (GUILayout.Button("Right"))
+ setRectValue("right");
+ if (GUILayout.Button("Bottom Right"))
+ setRectValue("bottomRight");
+ GUILayout.EndVertical();
+ GUILayout.EndHorizontal();
+ }
+
+ anchors2Rect_foldout = EditorGUILayout.Foldout(anchors2Rect_foldout, "Set Anchors to Rect");
+
+ if (anchors2Rect_foldout)
+ {
+ GUILayout.BeginHorizontal();
+ GUILayout.BeginVertical();
+ if (GUILayout.Button("Top Left"))
+ setAnchorsToRect("topLeft");
+ if (GUILayout.Button("Left"))
+ setAnchorsToRect("left");
+ if (GUILayout.Button("Bottom Left"))
+ setAnchorsToRect("bottomLeft");
+ GUILayout.EndVertical();
+ GUILayout.BeginVertical();
+ if (GUILayout.Button("Top"))
+ setAnchorsToRect("top");
+ if (GUILayout.Button("All", centerButtonStyle))
+ setAnchorsToRect("all");
+ if (GUILayout.Button("Bottom"))
+ setAnchorsToRect("bottom");
+ GUILayout.EndVertical();
+ GUILayout.BeginVertical();
+ if (GUILayout.Button("Top Right"))
+ setAnchorsToRect("topRight");
+ if (GUILayout.Button("Right"))
+ setAnchorsToRect("right");
+ if (GUILayout.Button("Bottom Right"))
+ setAnchorsToRect("bottomRight");
+ GUILayout.EndVertical();
+ GUILayout.EndHorizontal();
+ }
+
+
+ if (rect2Anchors_foldout != rect2Anchors__previousState)
+ {
+ rect2Anchors__previousState = rect2Anchors_foldout;
+ PlayerPrefs.SetInt("giezi_tools_rect2Anchors_foldout_bool", rect2Anchors_foldout ? 1 : 0);
+ }
+
+ if (anchors2Rect_foldout != anchors2Rect_previousState)
+ {
+ anchors2Rect_previousState = anchors2Rect_foldout;
+ PlayerPrefs.SetInt("giezi_tools_anchors2Rect_foldout_bool", anchors2Rect_foldout ? 1 : 0);
+ }
+ }
+ }
+
+
+ private void OnSceneGUI()
+ {
+ MethodInfo onSceneGUI_Method = defaultEditor.GetType()
+ .GetMethod("OnSceneGUI", BindingFlags.NonPublic | BindingFlags.Instance);
+ onSceneGUI_Method.Invoke(defaultEditor, null);
+ }
+
+
+ private void setAnchorsToRect(string field)
+ {
+ Vector2 anchorMax = new Vector2();
+ Vector2 anchorMin = new Vector2();
+ var parent = rectTransform.parent;
+ anchorMin.x = rectTransform.offsetMin.x / parent.GetComponent().rect.size.x;
+ anchorMin.y = rectTransform.offsetMin.y / parent.GetComponent().rect.size.y;
+ anchorMax.x = rectTransform.offsetMax.x / parent.GetComponent().rect.size.x;
+ anchorMax.y = rectTransform.offsetMax.y / parent.GetComponent().rect.size.y;
+
+
+ switch (field)
+ {
+ case "topLeft":
+ anchorMax.x = 0;
+ rectTransform.anchorMax += anchorMax;
+ rectTransform.offsetMax = new Vector2(rectTransform.offsetMax.x, 0);
+
+ anchorMin.y = 0;
+ rectTransform.anchorMin += anchorMin;
+ rectTransform.offsetMin = new Vector2(0, rectTransform.offsetMin.y);
+ break;
+ case "top":
+ anchorMax.x = 0;
+ rectTransform.anchorMax += anchorMax;
+ rectTransform.offsetMax = new Vector2(rectTransform.offsetMax.x, 0);
+ break;
+ case "topRight":
+ rectTransform.anchorMax += anchorMax;
+ rectTransform.offsetMax = Vector2.zero;
+ break;
+ case "bottomLeft":
+ rectTransform.anchorMin += anchorMin;
+ rectTransform.offsetMin = Vector2.zero;
+ break;
+ case "bottom":
+ anchorMin.x = 0;
+ rectTransform.anchorMin += anchorMin;
+ rectTransform.offsetMin = new Vector2(rectTransform.offsetMin.x, 0);
+ break;
+ case "bottomRight":
+ anchorMin.x = 0;
+ rectTransform.anchorMin += anchorMin;
+ rectTransform.offsetMin = new Vector2(rectTransform.offsetMin.x, 0);
+ anchorMax.y = 0;
+ rectTransform.anchorMax += anchorMax;
+ rectTransform.offsetMax = new Vector2(0, rectTransform.offsetMax.y);
+ break;
+ case "left":
+ anchorMin.y = 0;
+ rectTransform.anchorMin += anchorMin;
+ rectTransform.offsetMin = new Vector2(0, rectTransform.offsetMin.y);
+ break;
+ case "right":
+ anchorMax.y = 0;
+ rectTransform.anchorMax += anchorMax;
+ rectTransform.offsetMax = new Vector2(0, rectTransform.offsetMax.y);
+ break;
+ case "all":
+ rectTransform.anchorMax += anchorMax;
+ rectTransform.anchorMin += anchorMin;
+ rectTransform.offsetMin = Vector2.zero;
+ rectTransform.offsetMax = Vector2.zero;
+ break;
+ }
+
+ handleChange();
+ }
+
+
+ private void setRectValue(string field)
+ {
+ switch (field)
+ {
+ case "topLeft":
+ rectTransform.offsetMax = new Vector2(rectTransform.offsetMax.x, 0);
+ rectTransform.offsetMin = new Vector2(0, rectTransform.offsetMin.y);
+ break;
+ case "top":
+ rectTransform.offsetMax = new Vector2(rectTransform.offsetMax.x, 0);
+ break;
+ case "topRight":
+ rectTransform.offsetMax = Vector2.zero;
+ break;
+ case "bottomLeft":
+ rectTransform.offsetMin = Vector2.zero;
+ break;
+ case "bottom":
+ rectTransform.offsetMin = new Vector2(rectTransform.offsetMin.x, 0);
+ break;
+ case "bottomRight":
+ rectTransform.offsetMin = new Vector2(rectTransform.offsetMin.x, 0);
+ rectTransform.offsetMax = new Vector2(0, rectTransform.offsetMax.y);
+ break;
+ case "left":
+ rectTransform.offsetMin = new Vector2(0, rectTransform.offsetMin.y);
+ break;
+ case "right":
+ rectTransform.offsetMax = new Vector2(0, rectTransform.offsetMax.y);
+ break;
+ case "all":
+ rectTransform.offsetMin = new Vector2(0, 0);
+ rectTransform.offsetMax = new Vector2(0, 0);
+ break;
+ }
+
+ handleChange();
+ }
+
+ private void handleChange()
+ {
+ EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
+ }
+ }
+}
+#endif
diff --git a/Assets/Scripts/Editor/CustomInspector/TransformEditor.cs b/Assets/Scripts/Editor/CustomInspector/TransformEditor.cs
new file mode 100644
index 0000000..152989e
--- /dev/null
+++ b/Assets/Scripts/Editor/CustomInspector/TransformEditor.cs
@@ -0,0 +1,436 @@
+// source https://gist.github.com/unitycoder/e5e6384f087639c0d9edc93aa3820468
+
+using UnityEngine;
+using UnityEditor;
+
+namespace OddTales.Framework.Core.EditorExtension
+{
+ ///
+ /// Custom inspector for Transform component. Using only DrawDefaultInspector would give different display.
+ /// Script based on Unity wiki implementation : https://wiki.unity3d.com/index.php/TransformInspector
+ /// Buttons to reset, copy, paste Transform values.
+ /// Context menu to round/truncate values, hide/show tools.
+ ///
+ [CanEditMultipleObjects, CustomEditor(typeof(Transform))]
+ public class TransformEditor : Editor
+ {
+ private const float FIELD_WIDTH = 212.0f;
+ private const bool WIDE_MODE = true;
+
+ private const float POSITION_MAX = 100000.0f;
+
+ private static GUIContent positionGUIContent = new GUIContent(LocalString("Position"));
+ private static GUIContent rotationGUIContent = new GUIContent(LocalString("Rotation"));
+ private static GUIContent scaleGUIContent = new GUIContent(LocalString("Scale"));
+
+ private static string positionWarningText = LocalString("Due to floating-point precision limitations, it is recommended to bring the world coordinates of the GameObject within a smaller range.");
+
+ private SerializedProperty positionProperty, rotationProperty, scaleProperty;
+
+ private static Vector3? positionClipboard = null;
+ private static Quaternion? rotationClipboard = null;
+ private static Vector3? scaleClipboard = null;
+
+ private const string SHOW_TOOLS_KEY = "TransformEditor_ShowTools";
+ private const string SHOW_RESET_TOOLS_KEY = "TransformEditor_ShowResetTools";
+ private const string SHOW_PASTE_TOOLS_KEY = "TransformEditor_ShowPasteTools";
+ private const string SHOW_ADVANCED_PASTE_TOOLS_KEY = "TransformEditor_ShowAdvancedPasteTools";
+ private const string SHOW_CLIPBOARD_INFORMATIONS_KEY = "TransformEditor_ShowClipboardInformations";
+ private const string SHOW_SHORTCUTS_KEY = "TransformEditor_ShowHelpbox";
+
+
+#if UNITY_2017_3_OR_NEWER
+ private static System.Reflection.MethodInfo getLocalizedStringMethod;
+#endif
+
+
+ /// Get translated Transform label
+ private static string LocalString(string text)
+ {
+#if UNITY_2017_3_OR_NEWER
+ // Since Unity 2017.3, static class LocalizationDatabase is no longer public. Need to use reflection to access it.
+ if (getLocalizedStringMethod == null)
+ {
+ System.Reflection.Assembly assembly = typeof(UnityEditor.EditorWindow).Assembly;
+ System.Type localizationDatabaseType = assembly.GetType("UnityEditor.LocalizationDatabase");
+
+ getLocalizedStringMethod = localizationDatabaseType.GetMethod("GetLocalizedString");
+ }
+
+ return (string)getLocalizedStringMethod.Invoke(null, new object[] { text });
+#else
+ return LocalizationDatabase.GetLocalizedString(text);
+#endif
+ }
+
+ public void OnEnable()
+ {
+ positionProperty = serializedObject.FindProperty("m_LocalPosition");
+ rotationProperty = serializedObject.FindProperty("m_LocalRotation");
+ scaleProperty = serializedObject.FindProperty("m_LocalScale");
+
+ // Init options
+ if (!EditorPrefs.HasKey(SHOW_TOOLS_KEY)) EditorPrefs.SetBool(SHOW_TOOLS_KEY, true);
+ if (!EditorPrefs.HasKey(SHOW_RESET_TOOLS_KEY)) EditorPrefs.SetBool(SHOW_RESET_TOOLS_KEY, true);
+ if (!EditorPrefs.HasKey(SHOW_PASTE_TOOLS_KEY)) EditorPrefs.SetBool(SHOW_PASTE_TOOLS_KEY, true);
+ if (!EditorPrefs.HasKey(SHOW_ADVANCED_PASTE_TOOLS_KEY)) EditorPrefs.SetBool(SHOW_ADVANCED_PASTE_TOOLS_KEY, true);
+ if (!EditorPrefs.HasKey(SHOW_CLIPBOARD_INFORMATIONS_KEY)) EditorPrefs.SetBool(SHOW_CLIPBOARD_INFORMATIONS_KEY, true);
+ if (!EditorPrefs.HasKey(SHOW_SHORTCUTS_KEY)) EditorPrefs.SetBool(SHOW_SHORTCUTS_KEY, true);
+ }
+
+
+ public override void OnInspectorGUI()
+ {
+ Rect beginRect = GUILayoutUtility.GetRect(0, 0);
+
+ EditorGUIUtility.wideMode = TransformEditor.WIDE_MODE;
+ EditorGUIUtility.labelWidth = EditorGUIUtility.currentViewWidth - TransformEditor.FIELD_WIDTH; // align field to right of inspector
+
+ serializedObject.Update();
+
+ EditorGUIUtility.labelWidth = 60; // To allow float fields to expand when inspector width is increased
+
+ // Position GUI
+ EditorGUILayout.BeginHorizontal();
+ PositionPropertyField(positionProperty, positionGUIContent); // Note : Can't add generic menu if we use EditorGUILayout.PropertyField instead
+ if (EditorPrefs.GetBool(SHOW_TOOLS_KEY) && EditorPrefs.GetBool(SHOW_RESET_TOOLS_KEY))
+ {
+ if (GUILayout.Button("Reset", GUILayout.Width(50)))
+ {
+ Undo.RecordObjects(targets, "Reset Positions");
+ for (int i = 0; i < targets.Length; i++)
+ {
+ ((Transform)targets[i]).localPosition = Vector3.zero;
+ }
+ GUI.FocusControl(null);
+ }
+ }
+ EditorGUILayout.EndHorizontal();
+
+ // Rotation GUI
+ EditorGUILayout.BeginHorizontal();
+ RotationPropertyField(rotationProperty, rotationGUIContent); // Note : Can't add generic menu if we use EditorGUILayout.PropertyField instead
+ if (EditorPrefs.GetBool(SHOW_TOOLS_KEY) && EditorPrefs.GetBool(SHOW_RESET_TOOLS_KEY))
+ {
+ if (GUILayout.Button("Reset", GUILayout.Width(50)))
+ {
+ Undo.RecordObjects(targets, "Reset Rotations");
+ for (int i = 0; i < targets.Length; i++)
+ {
+ TransformUtils.SetInspectorRotation(((Transform)targets[i]), Vector3.zero);
+ }
+ GUI.FocusControl(null);
+ }
+
+ }
+ EditorGUILayout.EndHorizontal();
+
+ // Scale GUI
+ EditorGUILayout.BeginHorizontal();
+ ScalePropertyField(scaleProperty, scaleGUIContent); // Note : Can't add generic menu if we use EditorGUILayout.PropertyField instead
+ if (EditorPrefs.GetBool(SHOW_TOOLS_KEY) && EditorPrefs.GetBool(SHOW_RESET_TOOLS_KEY))
+ {
+ if (GUILayout.Button("Reset", GUILayout.Width(50)))
+ {
+ Undo.RecordObjects(targets, "Reset Scales");
+ for (int i = 0; i < targets.Length; i++)
+ {
+ ((Transform)targets[i]).localScale = Vector3.one;
+ }
+ GUI.FocusControl(null);
+ }
+ }
+ EditorGUILayout.EndHorizontal();
+
+
+ if (!ValidatePosition(((Transform)target).position)) EditorGUILayout.HelpBox(positionWarningText, MessageType.Warning); // Display floating-point warning message if values are too high
+
+ if (EditorPrefs.GetBool(SHOW_TOOLS_KEY))
+ {
+ // Paste Tools GUI
+ if (EditorPrefs.GetBool(SHOW_PASTE_TOOLS_KEY))
+ {
+ GUILayout.BeginHorizontal();
+ if (GUILayout.Button("Copy"))
+ {
+ positionClipboard = ((Transform)target).localPosition;
+ rotationClipboard = ((Transform)target).localRotation;
+ scaleClipboard = ((Transform)target).localScale;
+ }
+
+ if (!positionClipboard.HasValue) EditorGUI.BeginDisabledGroup(true);
+ if (GUILayout.Button("Paste"))
+ {
+ Undo.RecordObjects(targets, "Paste Clipboard Values");
+ for (int i = 0; i < targets.Length; i++)
+ {
+ ((Transform)targets[i]).localPosition = positionClipboard.Value;
+ ((Transform)targets[i]).localRotation = rotationClipboard.Value;
+ ((Transform)targets[i]).localScale = scaleClipboard.Value;
+ }
+ GUI.FocusControl(null);
+ }
+ if (!positionClipboard.HasValue) EditorGUI.EndDisabledGroup();
+ GUILayout.EndHorizontal();
+ }
+
+ // Advanced Paste Tools GUI
+ if (EditorPrefs.GetBool(SHOW_ADVANCED_PASTE_TOOLS_KEY))
+ {
+ GUILayout.BeginHorizontal();
+
+ if (!positionClipboard.HasValue) EditorGUI.BeginDisabledGroup(true);
+ if (GUILayout.Button("Paste position"))
+ {
+ Undo.RecordObjects(targets, "Paste Position Clipboard Value");
+ for (int i = 0; i < targets.Length; i++)
+ {
+ ((Transform)targets[i]).localPosition = positionClipboard.Value;
+ }
+ GUI.FocusControl(null);
+ }
+
+ if (GUILayout.Button("Paste rotation"))
+ {
+ Undo.RecordObjects(targets, "Paste Rotation Clipboard Value");
+ for (int i = 0; i < targets.Length; i++)
+ {
+ ((Transform)targets[i]).rotation = rotationClipboard.Value;
+ }
+ GUI.FocusControl(null);
+ }
+
+ if (GUILayout.Button("Paste scale"))
+ {
+ Undo.RecordObjects(targets, "Paste Scale Clipboard Value");
+ for (int i = 0; i < targets.Length; i++)
+ {
+ ((Transform)targets[i]).localScale = scaleClipboard.Value;
+ }
+ GUI.FocusControl(null);
+ }
+ if (!positionClipboard.HasValue) EditorGUI.EndDisabledGroup();
+
+ GUILayout.EndHorizontal();
+ }
+
+ // Clipboard GUI
+ if (EditorPrefs.GetBool(SHOW_CLIPBOARD_INFORMATIONS_KEY))
+ {
+ if (positionClipboard.HasValue && rotationClipboard.HasValue && scaleClipboard.HasValue)
+ {
+
+ GUIStyle helpboxStyle = new GUIStyle(EditorStyles.helpBox);
+ helpboxStyle.richText = true;
+
+ EditorGUILayout.TextArea("Clipboard values :\n" +
+ "Position : " + positionClipboard.Value.ToString("f2") + "\n" +
+ "Rotation : " + rotationClipboard.Value.ToString("f2") + "\n" +
+ "Scale : " + scaleClipboard.Value.ToString("f2"), helpboxStyle);
+ }
+ }
+
+
+ // Shortcuts GUI - Related to InspectorShortcuts.cs https://github.com/VoxelBoy/Useful-Unity-Scripts/blob/master/InspectorShortcuts.cs
+ if (EditorPrefs.GetBool(SHOW_SHORTCUTS_KEY))
+ {
+ EditorGUILayout.HelpBox("Inspector shortcuts :\n" +
+ "Toggle inspector lock : Ctrl + Shift + L\n" +
+ "Toggle inspector mode : Ctrl + Shift + D", MessageType.None);
+ }
+ }
+ Rect endRect = GUILayoutUtility.GetLastRect();
+ endRect.y += endRect.height;
+
+
+ #region Context Menu
+ Rect componentRect = new Rect(beginRect.x, beginRect.y, beginRect.width, endRect.y - beginRect.y);
+ //EditorGUI.DrawRect(componentRect, Color.green); // Debug : display GenericMenu zone
+
+ Event currentEvent = Event.current;
+
+ if (currentEvent.type == EventType.ContextClick)
+ {
+ if (componentRect.Contains(currentEvent.mousePosition))
+ {
+ GUI.FocusControl(null);
+
+ GenericMenu menu = new GenericMenu();
+
+ menu.AddItem(new GUIContent("Display/Tools"), EditorPrefs.GetBool(SHOW_TOOLS_KEY), ToggleOption, SHOW_TOOLS_KEY);
+ menu.AddSeparator("Display/");
+ menu.AddItem(new GUIContent("Display/Reset Tools"), EditorPrefs.GetBool(SHOW_RESET_TOOLS_KEY), ToggleOption, SHOW_RESET_TOOLS_KEY);
+ menu.AddItem(new GUIContent("Display/Paste Tools"), EditorPrefs.GetBool(SHOW_PASTE_TOOLS_KEY), ToggleOption, SHOW_PASTE_TOOLS_KEY);
+ menu.AddItem(new GUIContent("Display/Advanced Paste Tools"), EditorPrefs.GetBool(SHOW_ADVANCED_PASTE_TOOLS_KEY), ToggleOption, SHOW_ADVANCED_PASTE_TOOLS_KEY);
+ menu.AddItem(new GUIContent("Display/Clipboard informations"), EditorPrefs.GetBool(SHOW_CLIPBOARD_INFORMATIONS_KEY), ToggleOption, SHOW_CLIPBOARD_INFORMATIONS_KEY);
+ menu.AddItem(new GUIContent("Display/Shortcuts informations"), EditorPrefs.GetBool(SHOW_SHORTCUTS_KEY), ToggleOption, SHOW_SHORTCUTS_KEY);
+
+ // Round menu
+ menu.AddItem(new GUIContent("Round/Three Decimals"), false, Round, 3);
+ menu.AddItem(new GUIContent("Round/Two Decimals"), false, Round, 2);
+ menu.AddItem(new GUIContent("Round/One Decimal"), false, Round, 1);
+ menu.AddItem(new GUIContent("Round/Integer"), false, Round, 0);
+
+ // Truncate menu
+ menu.AddItem(new GUIContent("Truncate/Three Decimals"), false, Truncate, 3);
+ menu.AddItem(new GUIContent("Truncate/Two Decimals"), false, Truncate, 2);
+ menu.AddItem(new GUIContent("Truncate/One Decimal"), false, Truncate, 1);
+ menu.AddItem(new GUIContent("Truncate/Integer"), false, Truncate, 0);
+
+ menu.ShowAsContext();
+ currentEvent.Use();
+ }
+ }
+ #endregion
+
+ serializedObject.ApplyModifiedProperties();
+ }
+
+
+ private bool ValidatePosition(Vector3 position)
+ {
+ if (Mathf.Abs(position.x) > POSITION_MAX) return false;
+ if (Mathf.Abs(position.y) > POSITION_MAX) return false;
+ if (Mathf.Abs(position.z) > POSITION_MAX) return false;
+ return true;
+ }
+
+ private void PositionPropertyField(SerializedProperty positionProperty, GUIContent content)
+ {
+ Transform transform = (Transform)targets[0];
+ Vector3 localPosition = transform.localPosition;
+ for (int i = 0; i < targets.Length; i++)
+ {
+ if (!localPosition.Equals(((Transform)targets[i]).localPosition))
+ {
+ EditorGUI.showMixedValue = true;
+ break;
+ }
+ }
+
+ EditorGUI.BeginChangeCheck();
+ Vector3 newLocalPosition = EditorGUILayout.Vector3Field(content, localPosition);
+ if (EditorGUI.EndChangeCheck())
+ {
+ Undo.RecordObjects(targets, "Position Changed");
+ for (int i = 0; i < targets.Length; i++)
+ {
+ ((Transform)targets[i]).localPosition = newLocalPosition;
+ }
+ positionProperty.serializedObject.SetIsDifferentCacheDirty();
+ }
+ EditorGUI.showMixedValue = false;
+ }
+
+ private void RotationPropertyField(SerializedProperty rotationProperty, GUIContent content)
+ {
+ Transform transform = (Transform)targets[0];
+ Vector3 localRotation = TransformUtils.GetInspectorRotation(transform);
+
+
+ for (int i = 0; i < targets.Length; i++)
+ {
+ if (!localRotation.Equals(TransformUtils.GetInspectorRotation((Transform)targets[i])))
+ {
+ EditorGUI.showMixedValue = true;
+ break;
+ }
+ }
+
+ EditorGUI.BeginChangeCheck();
+ Vector3 eulerAngles = EditorGUILayout.Vector3Field(content, localRotation);
+ if (EditorGUI.EndChangeCheck())
+ {
+ Undo.RecordObjects(targets, "Rotation Changed");
+ for (int i = 0; i < targets.Length; i++)
+ {
+ //((Transform)targets[i]).localEulerAngles = eulerAngles;
+ TransformUtils.SetInspectorRotation(((Transform)targets[i]), eulerAngles);
+ }
+ rotationProperty.serializedObject.SetIsDifferentCacheDirty();
+ }
+ EditorGUI.showMixedValue = false;
+ }
+
+ private void ScalePropertyField(SerializedProperty scaleProperty, GUIContent content)
+ {
+ Transform transform = (Transform)targets[0];
+ Vector3 localScale = transform.localScale;
+ for (int i = 0; i < targets.Length; i++)
+ {
+ if (!localScale.Equals(((Transform)targets[i]).localScale))
+ {
+ EditorGUI.showMixedValue = true;
+ break;
+ }
+ }
+
+ EditorGUI.BeginChangeCheck();
+ Vector3 newLocalScale = EditorGUILayout.Vector3Field(content, localScale);
+ if (EditorGUI.EndChangeCheck())
+ {
+ Undo.RecordObjects(targets, "Scale Changed");
+ for (int i = 0; i < targets.Length; i++)
+ {
+ ((Transform)targets[i]).localScale = newLocalScale;
+ }
+ scaleProperty.serializedObject.SetIsDifferentCacheDirty();
+ }
+ EditorGUI.showMixedValue = false;
+ }
+
+
+ #region Generic Menu Callbacks
+ private void ToggleOption(object obj)
+ {
+ EditorPrefs.SetBool(obj.ToString(), !EditorPrefs.GetBool(obj.ToString()));
+ }
+
+ /// Round all values of the Transform to a given number of decimals
+ private void Round(object objNumberOfDecimals)
+ {
+ int numberOfDecimals = (int)objNumberOfDecimals;
+
+ Undo.RecordObjects(targets, "Round to " + numberOfDecimals + " decimals");
+ for (int i = 0; i < targets.Length; i++)
+ {
+ ((Transform)targets[i]).localPosition = RoundVector(((Transform)targets[i]).localPosition, numberOfDecimals);
+ ((Transform)targets[i]).localEulerAngles = RoundVector(((Transform)targets[i]).localEulerAngles, numberOfDecimals);
+ ((Transform)targets[i]).localScale = RoundVector(((Transform)targets[i]).localScale, numberOfDecimals);
+ }
+ }
+
+ /// Round all components of a Vector3
+ private Vector3 RoundVector(Vector3 vector, int numberOfDecimals)
+ {
+ vector.x = Mathf.Round(vector.x * Mathf.Pow(10.0f, (float)numberOfDecimals)) / Mathf.Pow(10.0f, (float)numberOfDecimals);
+ vector.y = Mathf.Round(vector.y * Mathf.Pow(10.0f, (float)numberOfDecimals)) / Mathf.Pow(10.0f, (float)numberOfDecimals);
+ vector.z = Mathf.Round(vector.z * Mathf.Pow(10.0f, (float)numberOfDecimals)) / Mathf.Pow(10.0f, (float)numberOfDecimals);
+ return vector;
+ }
+
+ /// Truncate all values of the Transform to a given number of decimals
+ private void Truncate(object objNumberOfDecimals)
+ {
+ int numberOfDecimals = (int)objNumberOfDecimals;
+
+ Undo.RecordObjects(targets, "Truncate to " + numberOfDecimals + " decimals");
+ for (int i = 0; i < targets.Length; i++)
+ {
+ ((Transform)targets[i]).localPosition = TruncateVector(((Transform)targets[i]).localPosition, numberOfDecimals);
+ ((Transform)targets[i]).localEulerAngles = TruncateVector(((Transform)targets[i]).localEulerAngles, numberOfDecimals);
+ ((Transform)targets[i]).localScale = TruncateVector(((Transform)targets[i]).localScale, numberOfDecimals);
+ }
+ }
+
+ /// Truncate all components of a Vector3
+ private Vector3 TruncateVector(Vector3 vector, int numberOfDecimals)
+ {
+ vector.x = Mathf.Floor(vector.x * Mathf.Pow(10.0f, (float)numberOfDecimals)) / Mathf.Pow(10.0f, (float)numberOfDecimals);
+ vector.y = Mathf.Floor(vector.y * Mathf.Pow(10.0f, (float)numberOfDecimals)) / Mathf.Pow(10.0f, (float)numberOfDecimals);
+ vector.z = Mathf.Floor(vector.z * Mathf.Pow(10.0f, (float)numberOfDecimals)) / Mathf.Pow(10.0f, (float)numberOfDecimals);
+ return vector;
+ }
+ #endregion
+ }
+}
diff --git a/Assets/Scripts/Editor/Importers/AnimationClipListImporter.cs b/Assets/Scripts/Editor/Importers/AnimationClipListImporter.cs
new file mode 100644
index 0000000..733ac03
--- /dev/null
+++ b/Assets/Scripts/Editor/Importers/AnimationClipListImporter.cs
@@ -0,0 +1,65 @@
+// Checks for a .txt file with the same name as an imported .fbx file (in Assets/Models/ folder), containing a list of animation clips to add to the ModelImporter.
+// .txt file should be tab-delimited with the following columns: "title", "start frame", "end frame" (and optional description, not used).
+// example:
+// Take0 10 40 asdf
+// Take1 50 80 wasdf..
+
+using System;
+using System.IO;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEditor;
+
+namespace UnityLibrary.Importers
+{
+ public class AnimationClipListImporter : AssetPostprocessor
+ {
+ void OnPreprocessModel()
+ {
+ ModelImporter modelImporter = assetImporter as ModelImporter;
+ if (modelImporter == null) return;
+
+ string assetPath = assetImporter.assetPath;
+ if (!assetPath.StartsWith("Assets/Models") || !assetPath.EndsWith(".fbx", StringComparison.OrdinalIgnoreCase)) return;
+
+ string txtPath = Path.ChangeExtension(assetPath, ".txt");
+ if (!File.Exists(txtPath)) return;
+
+ try
+ {
+ List clips = new List();
+ string[] lines = File.ReadAllLines(txtPath);
+
+ foreach (string line in lines)
+ {
+ string[] parts = line.Split('\t');
+ if (parts.Length < 3) continue; // Ensure we have at least "title, start, end"
+
+ string title = parts[0].Trim();
+ if (!int.TryParse(parts[1], out int startFrame) || !int.TryParse(parts[2], out int endFrame))
+ continue;
+
+ ModelImporterClipAnimation clip = new ModelImporterClipAnimation
+ {
+ name = title,
+ firstFrame = startFrame,
+ lastFrame = endFrame,
+ loopTime = false
+ };
+
+ clips.Add(clip);
+ }
+
+ if (clips.Count > 0)
+ {
+ modelImporter.clipAnimations = clips.ToArray();
+ Debug.Log($"Added {clips.Count} animation clips to {assetPath}");
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.LogError($"Failed to process animation data for {assetPath}: {ex.Message}");
+ }
+ }
+ }
+}
diff --git a/Assets/Scripts/Editor/Tools/AndroidStoreCaptureTool.cs b/Assets/Scripts/Editor/Tools/AndroidStoreCaptureTool.cs
new file mode 100644
index 0000000..8a73f3a
--- /dev/null
+++ b/Assets/Scripts/Editor/Tools/AndroidStoreCaptureTool.cs
@@ -0,0 +1,513 @@
+// AndroidStoreCaptureTool.cs
+// Put this file anywhere under an "Editor" folder.
+// Usage:
+// 1) Enter Play Mode.
+// 2) Open: Tools/Android Store Capture
+// 3) Pick output folder and click "Capture All Presets"
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using UnityEditor;
+using UnityEngine;
+
+namespace UnityLibrary.Tools
+{
+ public class AndroidStoreCaptureTool : EditorWindow
+ {
+ [Serializable]
+ private class Preset
+ {
+ public string name; // base file name (without _WxH)
+ public int width;
+ public int height;
+ public CropMode cropMode;
+
+ public Preset(string name, int w, int h, CropMode cropMode)
+ {
+ this.name = name;
+ width = w;
+ height = h;
+ this.cropMode = cropMode;
+ }
+ }
+
+ private enum CropMode
+ {
+ Stretch, // no crop, just scale to target (may distort)
+ CropToFit // center-crop to target aspect, then scale (no distortion)
+ }
+
+ private string _outputFolder = "StoreCaptures";
+ private int _phoneCount = 2; // Play Console: 2-8 phone screenshots
+
+ // Jobs
+ private class CaptureJob
+ {
+ public Preset preset;
+ public string filename;
+ }
+
+ private readonly Queue _queue = new Queue();
+ private bool _isRunning;
+
+ // Hidden helper MonoBehaviour that runs coroutines in Play Mode
+ private CaptureHelper _helper;
+
+ // Presets based on Play Console rules in your message.
+ // Phone/tablet sizes are common choices within allowed ranges.
+ private List BuildPresets()
+ {
+ var list = new List();
+
+ // App icon and feature graphic
+ list.Add(new Preset("appicon", 512, 512, CropMode.CropToFit));
+ list.Add(new Preset("featuregraphic", 1024, 500, CropMode.CropToFit));
+
+ // Phone screenshots (2-8). 9:16 or 16:9. Each side 320..3840.
+ // We capture portrait by default; toggle to landscape if you want.
+ for (int i = 1; i <= Mathf.Clamp(_phoneCount, 2, 8); i++)
+ list.Add(new Preset("phone_" + i.ToString("00"), 1080, 1920, CropMode.CropToFit));
+
+ // 7-inch tablet screenshots (allowed: 320..3840 each side)
+ list.Add(new Preset("tablet7_01", 1920, 1200, CropMode.CropToFit)); // landscape 16:10
+ list.Add(new Preset("tablet7_02", 1200, 1920, CropMode.CropToFit)); // portrait 10:16
+
+ // 10-inch tablet screenshots (each side 1080..7680)
+ list.Add(new Preset("tablet10_01", 2560, 1600, CropMode.CropToFit)); // landscape 16:10
+ list.Add(new Preset("tablet10_02", 1600, 2560, CropMode.CropToFit)); // portrait 10:16
+
+ return list;
+ }
+
+ [MenuItem("Tools/Android Store Capture")]
+ public static void Open()
+ {
+ var w = GetWindow("Android Store Capture");
+ w.minSize = new Vector2(420, 340);
+ w.Show();
+ }
+
+ private void OnDisable()
+ {
+ StopRunner();
+ }
+
+ private void OnGUI()
+ {
+ EditorGUILayout.LabelField("Capture from Game View (Play Mode)", EditorStyles.boldLabel);
+
+ using (new EditorGUILayout.VerticalScope("box"))
+ {
+ EditorGUILayout.LabelField("Output", EditorStyles.boldLabel);
+
+ EditorGUILayout.BeginHorizontal();
+ _outputFolder = EditorGUILayout.TextField("Folder", _outputFolder);
+ if (GUILayout.Button("Browse", GUILayout.Width(80)))
+ {
+ string picked = EditorUtility.OpenFolderPanel("Pick output folder", Application.dataPath, "");
+ if (!string.IsNullOrEmpty(picked))
+ {
+ // Make it project-relative when possible
+ string proj = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
+ string full = Path.GetFullPath(picked);
+ if (full.StartsWith(proj, StringComparison.OrdinalIgnoreCase))
+ {
+ _outputFolder = full.Substring(proj.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+ }
+ else
+ {
+ _outputFolder = full;
+ }
+ }
+ }
+ EditorGUILayout.EndHorizontal();
+
+ _phoneCount = EditorGUILayout.IntSlider("Phone screenshots (2-8)", _phoneCount, 2, 8);
+ }
+
+ using (new EditorGUILayout.VerticalScope("box"))
+ {
+ EditorGUILayout.LabelField("Actions", EditorStyles.boldLabel);
+
+ if (!EditorApplication.isPlaying)
+ {
+ EditorGUILayout.HelpBox("Enter Play Mode first. This tool captures the rendered Game View.", MessageType.Warning);
+ }
+
+ GUI.enabled = EditorApplication.isPlaying && !_isRunning;
+ if (GUILayout.Button("Capture All Presets"))
+ {
+ EnqueueAll();
+ StartRunner();
+ }
+
+ if (GUILayout.Button("Capture Only Icon + Feature Graphic"))
+ {
+ EnqueueIconAndFeatureOnly();
+ StartRunner();
+ }
+ GUI.enabled = true;
+
+ GUI.enabled = _isRunning;
+ if (GUILayout.Button("Stop"))
+ {
+ StopRunner();
+ }
+ GUI.enabled = true;
+
+ if (_isRunning)
+ {
+ EditorGUILayout.Space(6);
+ EditorGUILayout.LabelField("Running...", EditorStyles.boldLabel);
+ EditorGUILayout.LabelField("Remaining", _queue.Count.ToString());
+ }
+ }
+
+ using (new EditorGUILayout.VerticalScope("box"))
+ {
+ EditorGUILayout.LabelField("Notes", EditorStyles.boldLabel);
+ EditorGUILayout.LabelField("- Files are named like: appicon_512x512.png, featuregraphic_1024x500.png");
+ EditorGUILayout.LabelField("- Phone screenshots are named like: phone_01_1080x1920.png");
+ EditorGUILayout.LabelField("- Captures center-crop to match target aspect (no stretching).");
+ }
+ }
+
+ private void EnqueueAll()
+ {
+ _queue.Clear();
+
+ string folder = ResolveOutputFolder();
+ Directory.CreateDirectory(folder);
+
+ foreach (var p in BuildPresets())
+ {
+ string fn = $"{p.name}_{p.width}x{p.height}.png";
+ _queue.Enqueue(new CaptureJob { preset = p, filename = Path.Combine(folder, fn) });
+ }
+ }
+
+ private void EnqueueIconAndFeatureOnly()
+ {
+ _queue.Clear();
+
+ string folder = ResolveOutputFolder();
+ Directory.CreateDirectory(folder);
+
+ var icon = new Preset("appicon", 512, 512, CropMode.CropToFit);
+ var feature = new Preset("featuregraphic", 1024, 500, CropMode.CropToFit);
+
+ _queue.Enqueue(new CaptureJob { preset = icon, filename = Path.Combine(folder, $"appicon_512x512.png") });
+ _queue.Enqueue(new CaptureJob { preset = feature, filename = Path.Combine(folder, $"featuregraphic_1024x500.png") });
+ }
+
+ private string ResolveOutputFolder()
+ {
+ // If user gave absolute path, use it. Otherwise, place under project root.
+ if (Path.IsPathRooted(_outputFolder))
+ return _outputFolder;
+
+ string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
+ return Path.Combine(projectRoot, _outputFolder);
+ }
+
+ private CaptureHelper EnsureHelper()
+ {
+ if (_helper != null) return _helper;
+
+ var go = new GameObject("[AndroidStoreCaptureHelper]")
+ {
+ hideFlags = HideFlags.HideAndDontSave
+ };
+ _helper = go.AddComponent();
+ return _helper;
+ }
+
+ private void StartRunner()
+ {
+ if (_isRunning) return;
+ if (!EditorApplication.isPlaying) return;
+
+ _isRunning = true;
+
+ GetMainGameView();
+
+ var helper = EnsureHelper();
+ helper.StartCoroutine(RunCaptures());
+ }
+
+ private void StopRunner()
+ {
+ _isRunning = false;
+ _queue.Clear();
+
+ if (_helper != null)
+ {
+ _helper.StopAllCoroutines();
+ DestroyImmediate(_helper.gameObject);
+ _helper = null;
+ }
+ }
+
+ private IEnumerator RunCaptures()
+ {
+ while (_queue.Count > 0)
+ {
+ if (!EditorApplication.isPlaying)
+ {
+ StopRunner();
+ yield break;
+ }
+
+ var job = _queue.Dequeue();
+
+ SetGameViewSize(job.preset.width, job.preset.height);
+
+ // Wait for the GameView to resize and re-render
+ for (int i = 0; i < 6; i++)
+ yield return null;
+
+ // Wait for end of frame — this is required for ScreenCapture to work
+ yield return new WaitForEndOfFrame();
+
+ try
+ {
+ ProcessCaptureJob(job);
+ }
+ catch (Exception ex)
+ {
+ Debug.LogError("Capture failed: " + ex);
+ }
+
+ Repaint();
+ }
+
+ _isRunning = false;
+ AssetDatabase.Refresh();
+ Debug.Log("Android Store Capture: All captures finished.");
+ Repaint();
+
+ if (_helper != null)
+ {
+ DestroyImmediate(_helper.gameObject);
+ _helper = null;
+ }
+ }
+
+ private void ProcessCaptureJob(CaptureJob job)
+ {
+ int targetW = job.preset.width;
+ int targetH = job.preset.height;
+
+ Texture2D src = ScreenCapture.CaptureScreenshotAsTexture();
+ if (src == null)
+ {
+ Debug.LogError($"CaptureScreenshotAsTexture returned null for {job.filename}. Skipping.");
+ return;
+ }
+
+ Texture2D processed;
+ if (job.preset.cropMode == CropMode.CropToFit)
+ processed = CropToAspectThenScale(src, targetW, targetH);
+ else
+ processed = ScaleTexture(src, targetW, targetH);
+
+ byte[] png = processed.EncodeToPNG();
+ File.WriteAllBytes(job.filename, png);
+
+ DestroyImmediate(src);
+ if (processed != src)
+ DestroyImmediate(processed);
+
+ Debug.Log("Saved: " + job.filename);
+ }
+
+ private static Texture2D CropToAspectThenScale(Texture2D src, int targetW, int targetH)
+ {
+ float srcAspect = (float)src.width / src.height;
+ float dstAspect = (float)targetW / targetH;
+
+ int cropW = src.width;
+ int cropH = src.height;
+
+ if (srcAspect > dstAspect)
+ {
+ // too wide -> crop width
+ cropW = Mathf.RoundToInt(src.height * dstAspect);
+ cropH = src.height;
+ }
+ else
+ {
+ // too tall -> crop height
+ cropW = src.width;
+ cropH = Mathf.RoundToInt(src.width / dstAspect);
+ }
+
+ // Crop from top-left: x starts at 0, y starts from top
+ int x0 = 0;
+ int y0 = src.height - cropH;
+
+ Color[] pixels = src.GetPixels(x0, y0, cropW, cropH);
+ Texture2D cropped = new Texture2D(cropW, cropH, TextureFormat.RGBA32, false);
+ cropped.SetPixels(pixels);
+ cropped.Apply(false, false);
+
+ Texture2D scaled = ScaleTexture(cropped, targetW, targetH);
+ DestroyImmediate(cropped);
+ return scaled;
+ }
+
+ private static Texture2D ScaleTexture(Texture2D src, int targetW, int targetH)
+ {
+ Texture2D dst = new Texture2D(targetW, targetH, TextureFormat.RGBA32, false);
+
+ for (int y = 0; y < targetH; y++)
+ {
+ float v = (targetH == 1) ? 0f : (float)y / (targetH - 1);
+ for (int x = 0; x < targetW; x++)
+ {
+ float u = (targetW == 1) ? 0f : (float)x / (targetW - 1);
+ Color c = SampleBilinear(src, u, v);
+ dst.SetPixel(x, y, c);
+ }
+ }
+
+ dst.Apply(false, false);
+ return dst;
+ }
+
+ private static Color SampleBilinear(Texture2D tex, float u, float v)
+ {
+ float x = u * (tex.width - 1);
+ float y = v * (tex.height - 1);
+
+ int x0 = Mathf.Clamp((int)Mathf.Floor(x), 0, tex.width - 1);
+ int y0 = Mathf.Clamp((int)Mathf.Floor(y), 0, tex.height - 1);
+ int x1 = Mathf.Clamp(x0 + 1, 0, tex.width - 1);
+ int y1 = Mathf.Clamp(y0 + 1, 0, tex.height - 1);
+
+ float tx = x - x0;
+ float ty = y - y0;
+
+ Color c00 = tex.GetPixel(x0, y0);
+ Color c10 = tex.GetPixel(x1, y0);
+ Color c01 = tex.GetPixel(x0, y1);
+ Color c11 = tex.GetPixel(x1, y1);
+
+ Color a = Color.Lerp(c00, c10, tx);
+ Color b = Color.Lerp(c01, c11, tx);
+ return Color.Lerp(a, b, ty);
+ }
+
+ // ---------------------------
+ // GameView sizing (internal)
+ // ---------------------------
+
+ private static EditorWindow GetMainGameView()
+ {
+ Type t = Type.GetType("UnityEditor.GameView,UnityEditor");
+ if (t == null) return null;
+
+ // Try "GetMainGameView" first (older Unity versions)
+ MethodInfo getMain = t.GetMethod("GetMainGameView", BindingFlags.NonPublic | BindingFlags.Static);
+ if (getMain != null)
+ {
+ var result = getMain.Invoke(null, null) as EditorWindow;
+ if (result != null) return result;
+ }
+
+ // Fallback: try "GetMainGameViewRenderRect" or just find an open GameView window
+ var gameView = GetWindow(t, false, null, false);
+ return gameView;
+ }
+
+ private static void SetGameViewSize(int width, int height)
+ {
+ // Creates/uses a fixed resolution entry in the current platform group, then selects it.
+ // Unity does not expose this publicly; reflection is used.
+
+ Type sizesType = Type.GetType("UnityEditor.GameViewSizes,UnityEditor");
+ Type sizeType = Type.GetType("UnityEditor.GameViewSize,UnityEditor");
+ Type groupType = Type.GetType("UnityEditor.GameViewSizeGroupType,UnityEditor");
+
+ if (sizesType == null || sizeType == null || groupType == null)
+ return;
+
+ var instanceProp = sizesType.GetProperty("instance", BindingFlags.Public | BindingFlags.Static);
+ if (instanceProp == null) return;
+ object sizesInstance = instanceProp.GetValue(null, null);
+ if (sizesInstance == null) return;
+
+ MethodInfo getGroup = sizesType.GetMethod("GetGroup");
+ if (getGroup == null) return;
+ object group = getGroup.Invoke(sizesInstance, new object[] { (int)Enum.Parse(groupType, "Standalone") });
+ if (group == null) return;
+
+ // Find existing
+ MethodInfo getBuiltinCount = group.GetType().GetMethod("GetBuiltinCount");
+ MethodInfo getCustomCount = group.GetType().GetMethod("GetCustomCount");
+ MethodInfo getGameViewSize = group.GetType().GetMethod("GetGameViewSize");
+
+ if (getBuiltinCount == null || getCustomCount == null || getGameViewSize == null) return;
+
+ int builtin = (int)getBuiltinCount.Invoke(group, null);
+ int custom = (int)getCustomCount.Invoke(group, null);
+
+ int total = builtin + custom;
+ int foundIndex = -1;
+
+ for (int i = 0; i < total; i++)
+ {
+ object gvSize = getGameViewSize.Invoke(group, new object[] { i });
+ if (gvSize == null) continue;
+ var widthProp = gvSize.GetType().GetProperty("width");
+ var heightProp = gvSize.GetType().GetProperty("height");
+ if (widthProp == null || heightProp == null) continue;
+
+ int w = (int)widthProp.GetValue(gvSize, null);
+ int h = (int)heightProp.GetValue(gvSize, null);
+
+ if (w == width && h == height)
+ {
+ foundIndex = i;
+ break;
+ }
+ }
+
+ if (foundIndex < 0)
+ {
+ // Add custom size
+ Type gvSizeType = Type.GetType("UnityEditor.GameViewSizeType,UnityEditor");
+ if (gvSizeType == null) return;
+ object fixedRes = Enum.Parse(gvSizeType, "FixedResolution");
+
+ ConstructorInfo ctor = sizeType.GetConstructor(new[] { gvSizeType, typeof(int), typeof(int), typeof(string) });
+ if (ctor == null) return;
+ object newSize = ctor.Invoke(new object[] { fixedRes, width, height, width + "x" + height });
+
+ MethodInfo addCustom = group.GetType().GetMethod("AddCustomSize");
+ if (addCustom == null) return;
+ addCustom.Invoke(group, new object[] { newSize });
+
+ custom = (int)getCustomCount.Invoke(group, null);
+ foundIndex = builtin + (custom - 1);
+ }
+
+ // Select size in GameView
+ EditorWindow gv = GetMainGameView();
+ if (gv == null) return;
+
+ Type gvType = gv.GetType();
+ PropertyInfo selectedSizeIndex = gvType.GetProperty("selectedSizeIndex", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+ if (selectedSizeIndex != null)
+ selectedSizeIndex.SetValue(gv, foundIndex, null);
+
+ gv.Repaint();
+ }
+
+ // Hidden MonoBehaviour to run coroutines from the editor tool
+ private class CaptureHelper : MonoBehaviour { }
+ }
+}
diff --git a/Assets/Scripts/Editor/Tools/FindWhatButtonCallsMyMethod.cs b/Assets/Scripts/Editor/Tools/FindWhatButtonCallsMyMethod.cs
new file mode 100644
index 0000000..e67f45b
--- /dev/null
+++ b/Assets/Scripts/Editor/Tools/FindWhatButtonCallsMyMethod.cs
@@ -0,0 +1,69 @@
+// prints out which buttons in current scene are referencing your given method
+
+using UnityEngine;
+using UnityEngine.UI;
+using UnityEditor;
+using UnityEngine.Events;
+using System.Reflection;
+
+namespace UnityLibrary
+{
+ public class FindWhatButtonCallsMyMethod : EditorWindow
+ {
+ private string methodName = "MyMethodHere";
+
+ [MenuItem("Tools/Find Buttons with Method")]
+ public static void ShowWindow()
+ {
+ GetWindow("FindWhatButtonCallsMyMethod");
+ }
+
+ private void OnGUI()
+ {
+ GUILayout.Label("Find Buttons that call Method", EditorStyles.boldLabel);
+ methodName = EditorGUILayout.TextField("Method Name:", methodName);
+
+ if (GUILayout.Button("Find Buttons"))
+ {
+ FindButtonsWithMethod();
+ }
+ }
+
+ private void FindButtonsWithMethod()
+ {
+ Button[] allButtons = FindObjectsOfType