This commit is contained in:
2026-08-20 16:29:40 +08:00
parent 821758e7d6
commit 43295eebe1
7 changed files with 338 additions and 3 deletions
+6
View File
@@ -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,
@@ -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
{
/// <summary>
/// Editor window that batch-assigns multiple ParticleSystem GameObjects/effects
/// into a UIParticle's "Particles" list in one click.
/// </summary>
public class ParticleBatchAssigner : EditorWindow
{
private UIParticle _target;
private readonly List<Object> _sources = new List<Object>();
private Vector2 _scroll;
private bool _expandChildren;
[MenuItem("Tools/UIParticle/Batch Particle Assigner")]
private static void Open()
{
var win = GetWindow<ParticleBatchAssigner>("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<UIParticle>())
.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<ParticleSystem> GetParticleSystems(IEnumerable<Object> sources, bool expandChildren)
{
var seen = new HashSet<ParticleSystem>();
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<ParticleSystem>(true))
{
if (seen.Add(c)) yield return c;
}
}
else
{
// 默认:只取根节点上的 ParticleSystem,与「拖 GameObject 到 Inspector 列表」行为完全一致
var rootPs = go.GetComponent<ParticleSystem>();
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<Object>();
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
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1f7e81e1435d7274796a28b07c081f75
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,57 @@
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace Coffee.UIExtensions
{
/// <summary>
/// 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.
/// </summary>
public static class ParticleScale3DResetter
{
[MenuItem("Tools/UIParticle/Reset Scale3D to 1")]
private static void ResetScale3D()
{
var done = new HashSet<Object>();
var count = 0;
// Loaded scene objects (including inactive).
foreach (var uip in Object.FindObjectsOfType<UIParticle>(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<GameObject>(path);
if (asset == null) continue;
foreach (var uip in asset.GetComponentsInChildren<UIParticle>(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;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3670b336d10c266418ac9ad2d6747479
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+1 -1
View File
@@ -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.")]
+3 -2
View File
@@ -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);
}