diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index 0f5278651..4262a6fb6 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -362,6 +362,12 @@ }, "url": "https://packages.unity.com" }, + "com.utj.profilerreader": { + "version": "file:jp.unity3d.profilerreader", + "depth": 0, + "source": "embedded", + "dependencies": {} + }, "com.unity.modules.ai": { "version": "1.0.0", "depth": 0, diff --git a/Packages/src/Editor/ParticleBatchAssigner.cs b/Packages/src/Editor/ParticleBatchAssigner.cs new file mode 100644 index 000000000..1045c01d6 --- /dev/null +++ b/Packages/src/Editor/ParticleBatchAssigner.cs @@ -0,0 +1,249 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; + +#if UNITY_2021_2_OR_NEWER +using UnityEditor.SceneManagement; +#elif UNITY_2018_3_OR_NEWER +using UnityEditor.Experimental.SceneManagement; +#endif + +namespace Coffee.UIExtensions +{ + /// + /// Editor window that batch-assigns multiple ParticleSystem GameObjects/effects + /// into a UIParticle's "Particles" list in one click. + /// + public class ParticleBatchAssigner : EditorWindow + { + private UIParticle _target; + private readonly List _sources = new List(); + private Vector2 _scroll; + private bool _expandChildren; + + [MenuItem("Tools/UIParticle/Batch Particle Assigner")] + private static void Open() + { + var win = GetWindow("Particle Batch"); + win.minSize = new Vector2(340, 460); + } + + private void OnGUI() + { + EditorGUILayout.Space(); + EditorGUILayout.LabelField("UIParticle 批量特效配置", EditorStyles.boldLabel); + EditorGUILayout.HelpBox( + "选中多个特效 GameObject(或 ParticleSystem),拖到下方源区;选好目标 UIParticle 后一键批量添加。", + MessageType.None); + + // ---- 目标 ---- + using (new EditorGUILayout.HorizontalScope()) + { + _target = (UIParticle)EditorGUILayout.ObjectField( + "目标 UIParticle", _target, typeof(UIParticle), true); + if (GUILayout.Button("用选中", GUILayout.Width(60))) + { + _target = Selection.objects + .Select(o => (o as UIParticle) ?? (o as GameObject)?.GetComponent()) + .FirstOrDefault(u => u != null); + } + } + + EditorGUILayout.Space(); + + // ---- 特效源 ---- + EditorGUILayout.LabelField("特效源", EditorStyles.boldLabel); + _expandChildren = EditorGUILayout.ToggleLeft( + "展开子 ParticleSystem(默认关:只取根,与原 Inspector 拖动一致)", _expandChildren); + DrawDropArea(); + DrawSourceList(); + + EditorGUILayout.Space(); + + // ---- 操作 ---- + using (new EditorGUI.DisabledGroupScope(_target == null || _sources.Count == 0)) + { + if (GUILayout.Button("批量添加到 UIParticle", GUILayout.Height(32))) + { + ApplyToTarget(true); + } + + using (new EditorGUILayout.HorizontalScope()) + { + if (GUILayout.Button("替换目标列表")) + { + ApplyToTarget(false); + } + + if (GUILayout.Button("清空源")) + { + _sources.Clear(); + GUI.changed = true; + } + } + } + + if (_target == null) + { + EditorGUILayout.HelpBox("请先指定一个目标 UIParticle。", MessageType.Info); + } + } + + private void DrawDropArea() + { + var rect = GUILayoutUtility.GetRect(0, 44, GUILayout.ExpandWidth(true)); + var style = new GUIStyle(GUI.skin.box) + { + alignment = TextAnchor.MiddleCenter, + normal = { textColor = new Color(0.6f, 0.6f, 0.6f) } + }; + GUI.Box(rect, "把多个特效 GameObject / ParticleSystem 拖到这里", style); + + var evt = Event.current; + if (rect.Contains(evt.mousePosition) + && (evt.type == EventType.DragUpdated || evt.type == EventType.DragPerform)) + { + var refs = DragAndDrop.objectReferences; + var valid = refs.Any(IsValidSource); + DragAndDrop.visualMode = valid + ? DragAndDropVisualMode.Copy + : DragAndDropVisualMode.Rejected; + if (evt.type == EventType.DragPerform && valid) + { + DragAndDrop.AcceptDrag(); + foreach (var o in refs) + { + AddSource(o); + } + + evt.Use(); + } + } + } + + private void DrawSourceList() + { + var expanded = GetParticleSystems(_sources, _expandChildren).Count(); + EditorGUILayout.LabelField( + $"源 {_sources.Count} 个 → 展开出 {expanded} 个 ParticleSystem", + EditorStyles.miniLabel); + + _scroll = EditorGUILayout.BeginScrollView(_scroll, GUILayout.MinHeight(90)); + for (var i = _sources.Count - 1; i >= 0; i--) + { + using (new EditorGUILayout.HorizontalScope()) + { + EditorGUILayout.ObjectField(_sources[i], typeof(Object), true); + if (GUILayout.Button("×", GUILayout.Width(24))) + { + _sources.RemoveAt(i); + } + } + } + + EditorGUILayout.EndScrollView(); + } + + private void AddSource(Object o) + { + if (o == null || !IsValidSource(o)) return; + if (_sources.Contains(o)) return; + _sources.Add(o); + } + + private static bool IsValidSource(Object o) + { + return o is ParticleSystem || o is GameObject; + } + + private static IEnumerable GetParticleSystems(IEnumerable sources, bool expandChildren) + { + var seen = new HashSet(); + foreach (var o in sources) + { + if (o == null) continue; + if (o is ParticleSystem ps) + { + if (seen.Add(ps)) yield return ps; + } + else if (o is GameObject go) + { + if (expandChildren) + { + // 展开所有子 ParticleSystem(含禁用/嵌套):用于一个 GameObject 含多个并联粒子的复杂特效 + foreach (var c in go.GetComponentsInChildren(true)) + { + if (seen.Add(c)) yield return c; + } + } + else + { + // 默认:只取根节点上的 ParticleSystem,与「拖 GameObject 到 Inspector 列表」行为完全一致 + var rootPs = go.GetComponent(); + if (rootPs != null && seen.Add(rootPs)) yield return rootPs; + } + } + } + } + + private void ApplyToTarget(bool append) + { + if (_target == null) return; + + Undo.RecordObject(_target, append ? "Batch add particles" : "Replace particles"); + + var so = new SerializedObject(_target); + var sp = so.FindProperty("m_Particles"); + so.Update(); + + if (!append) + { + sp.ClearArray(); + } + + // 现有引用去重 + var existing = new HashSet(); + for (var i = 0; i < sp.arraySize; i++) + { + var r = sp.GetArrayElementAtIndex(i).objectReferenceValue; + if (r != null) existing.Add(r); + } + + foreach (var ps in GetParticleSystems(_sources, _expandChildren)) + { + if (!existing.Add(ps)) continue; + sp.InsertArrayElementAtIndex(sp.arraySize); + sp.GetArrayElementAtIndex(sp.arraySize - 1).objectReferenceValue = ps; + } + + so.ApplyModifiedProperties(); + + // 刷新渲染器(不会清空列表) + _target.RefreshParticles(_target.particles); + + // Prefab 阶段保存 + SaveIfInPrefabStage(_target); + + EditorGUIUtility.PingObject(_target); + } + + private static void SaveIfInPrefabStage(Object obj) + { +#if UNITY_2018_3_OR_NEWER + var go = (obj as Component)?.gameObject; + if (go == null) return; + var stage = PrefabStageUtility.GetCurrentPrefabStage(); + if (stage != null && stage.scene.isLoaded && go.scene == stage.scene) + { +#if UNITY_2020_1_OR_NEWER + var assetPath = stage.assetPath; +#else + var assetPath = stage.prefabAssetPath; +#endif + PrefabUtility.SaveAsPrefabAsset(stage.prefabContentsRoot, assetPath); + } +#endif + } + } +} diff --git a/Packages/src/Editor/ParticleBatchAssigner.cs.meta b/Packages/src/Editor/ParticleBatchAssigner.cs.meta new file mode 100644 index 000000000..a2c72bdc9 --- /dev/null +++ b/Packages/src/Editor/ParticleBatchAssigner.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1f7e81e1435d7274796a28b07c081f75 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/ParticleScale3DResetter.cs b/Packages/src/Editor/ParticleScale3DResetter.cs new file mode 100644 index 000000000..a17913314 --- /dev/null +++ b/Packages/src/Editor/ParticleScale3DResetter.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +namespace Coffee.UIExtensions +{ + /// + /// One-click reset of UIParticle.m_Scale3D to (1,1,1). + /// The C# default was changed from (10,10,10) to (1,1,1), but already-serialized + /// UIParticle assets/scene objects keep their old (10,10,10) value. This menu fixes them. + /// + public static class ParticleScale3DResetter + { + [MenuItem("Tools/UIParticle/Reset Scale3D to 1")] + private static void ResetScale3D() + { + var done = new HashSet(); + var count = 0; + + // Loaded scene objects (including inactive). + foreach (var uip in Object.FindObjectsOfType(true)) + { + if (ResetOne(uip)) { done.Add(uip); count++; } + } + + // Selected prefab assets in the Project window. + foreach (var obj in Selection.objects) + { + var path = AssetDatabase.GetAssetPath(obj); + if (string.IsNullOrEmpty(path)) continue; + var asset = AssetDatabase.LoadAssetAtPath(path); + if (asset == null) continue; + foreach (var uip in asset.GetComponentsInChildren(true)) + { + if (!done.Contains(uip) && ResetOne(uip)) { done.Add(uip); count++; } + } + } + + AssetDatabase.SaveAssets(); + Debug.Log($"[UIParticle] Reset Scale3D to (1,1,1) on {count} UIParticle(s)."); + } + + private static bool ResetOne(Object uip) + { + var so = new SerializedObject(uip); + so.Update(); + var sp = so.FindProperty("m_Scale3D"); + if (sp == null) return false; + if (sp.vector3Value == new Vector3(1, 1, 1)) return false; + + sp.vector3Value = new Vector3(1, 1, 1); + so.ApplyModifiedProperties(); + EditorUtility.SetDirty(uip); + return true; + } + } +} diff --git a/Packages/src/Editor/ParticleScale3DResetter.cs.meta b/Packages/src/Editor/ParticleScale3DResetter.cs.meta new file mode 100644 index 000000000..3b2c44cf9 --- /dev/null +++ b/Packages/src/Editor/ParticleScale3DResetter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3670b336d10c266418ac9ad2d6747479 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Runtime/UIParticle.cs b/Packages/src/Runtime/UIParticle.cs index 3f3ffa6a5..cd600ba7e 100644 --- a/Packages/src/Runtime/UIParticle.cs +++ b/Packages/src/Runtime/UIParticle.cs @@ -64,7 +64,7 @@ namespace Coffee.UIExtensions [Tooltip("Scale the rendering particles. When the `3D` toggle is enabled, 3D scale (x, y, z) is supported.")] [SerializeField] - private Vector3 m_Scale3D = new Vector3(10, 10, 10); + private Vector3 m_Scale3D = new Vector3(1, 1, 1); [Tooltip("If you want to update material properties (e.g. _MainTex_ST, _Color) in AnimationClip, " + "use this to mark as animatable.")] diff --git a/Packages/src/Runtime/UIParticleRenderer.cs b/Packages/src/Runtime/UIParticleRenderer.cs index 814c137ab..8f18eb7a1 100644 --- a/Packages/src/Runtime/UIParticleRenderer.cs +++ b/Packages/src/Runtime/UIParticleRenderer.cs @@ -417,10 +417,11 @@ namespace Coffee.UIExtensions } else { - var diff = _particleSystem.transform.position - _parent.transform.position; + // Relative mode: place the ParticleSystem at its true local offset from the + // UIParticle. No (scale-1) amplification, so the on-UI position matches the + // in-scene relative position regardless of scale3D. s_CombineInstances[0].transform = canvasRenderer.transform.worldToLocalMatrix - * Matrix4x4.Translate(diff.GetScaled(scale - Vector3.one)) * GetWorldMatrix(psPos, scale); }