DC优化
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Coffee.UIEffectInternal;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.Profiling;
|
||||
using UnityEngine.UI;
|
||||
using UnityEditor;
|
||||
#if TIMELINE_ENABLE
|
||||
using UnityEngine.Timeline;
|
||||
#endif
|
||||
|
||||
[assembly: InternalsVisibleTo("UIEffect")]
|
||||
[assembly: InternalsVisibleTo("Coffee.UIEffect.Editor")]
|
||||
|
||||
namespace Coffee.UIEffects
|
||||
{
|
||||
[ExecuteAlways]
|
||||
[DisallowMultipleComponent]
|
||||
public abstract class UIEffectBase : UIBehaviour, IMeshModifier, IMaterialModifier, ICanvasRaycastFilter
|
||||
#if TIMELINE_ENABLE
|
||||
, ITimeControl
|
||||
#endif
|
||||
{
|
||||
private static readonly InternalObjectPool<UIEffectContext> s_ContextPool =
|
||||
new InternalObjectPool<UIEffectContext>(() => new UIEffectContext(), x => true, x => x.Reset());
|
||||
|
||||
private Graphic _graphic;
|
||||
private Material _material;
|
||||
private UIEffectContext _context;
|
||||
private Action _onBeforeCanvasRebuild;
|
||||
private Action _onAfterCanvasRebuild;
|
||||
private bool _canModifyMesh;
|
||||
private Matrix4x4 _prevTransformHash;
|
||||
private Canvas _canvas;
|
||||
private bool _canvasCached = false;
|
||||
private bool _isContextDirty;
|
||||
|
||||
public Material effectMaterial => _material;
|
||||
public Graphic graphic => _graphic != null ? _graphic : _graphic = GetComponent<Graphic>();
|
||||
public virtual uint effectId => (uint)GetHashCode();
|
||||
public virtual float actualSamplingScale => 1;
|
||||
public virtual bool canModifyShape => true;
|
||||
|
||||
protected Canvas canvas
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_canvasCached) return _canvas;
|
||||
|
||||
_canvasCached = true;
|
||||
_canvas = GetComponentInParent<Canvas>();
|
||||
if (_canvas == null || !_canvas.isActiveAndEnabled)
|
||||
{
|
||||
_canvas = null;
|
||||
_canvasCached = false;
|
||||
}
|
||||
|
||||
return _canvas;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual UIEffectContext context
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_context == null)
|
||||
{
|
||||
_context = s_ContextPool.Rent();
|
||||
UpdateContext(_context);
|
||||
}
|
||||
|
||||
return _context;
|
||||
}
|
||||
}
|
||||
|
||||
protected Material GetCurrentMaterial()
|
||||
{
|
||||
if (!isActiveAndEnabled) return null;
|
||||
|
||||
var g = graphic;
|
||||
if (g == null) return null;
|
||||
|
||||
var canvasRenderer = g.canvasRenderer;
|
||||
if (canvasRenderer == null || canvasRenderer.materialCount == 0) return null;
|
||||
|
||||
return canvasRenderer.GetMaterial();
|
||||
}
|
||||
|
||||
public virtual RectTransform transitionRoot => transform as RectTransform;
|
||||
|
||||
protected override void OnEnable()
|
||||
{
|
||||
_canModifyMesh = true;
|
||||
_onBeforeCanvasRebuild ??= OnBeforeCanvasRebuild;
|
||||
_onAfterCanvasRebuild ??= OnAfterCanvasRebuild;
|
||||
UIExtraCallbacks.onBeforeCanvasRebuild += _onBeforeCanvasRebuild;
|
||||
UIExtraCallbacks.onAfterCanvasRebuild += _onAfterCanvasRebuild;
|
||||
UpdateContext(context);
|
||||
SetMaterialDirty();
|
||||
SetVerticesDirty();
|
||||
}
|
||||
|
||||
protected override void OnDisable()
|
||||
{
|
||||
if (_onBeforeCanvasRebuild != null)
|
||||
{
|
||||
UIExtraCallbacks.onBeforeCanvasRebuild -= _onBeforeCanvasRebuild;
|
||||
}
|
||||
|
||||
if (_onAfterCanvasRebuild != null)
|
||||
{
|
||||
UIExtraCallbacks.onAfterCanvasRebuild -= _onAfterCanvasRebuild;
|
||||
}
|
||||
|
||||
MaterialRepository.Release(ref _material);
|
||||
SetMaterialDirty();
|
||||
SetVerticesDirty();
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
_onBeforeCanvasRebuild = null;
|
||||
_graphic = null;
|
||||
s_ContextPool.Return(ref _context);
|
||||
}
|
||||
|
||||
private void OnBeforeCanvasRebuild()
|
||||
{
|
||||
// If the mesh is not modified in the previous frame, try to modify it if possible.
|
||||
if (!_canModifyMesh && CanModifyMesh())
|
||||
{
|
||||
_canModifyMesh = true;
|
||||
SetVerticesDirty();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAfterCanvasRebuild()
|
||||
{
|
||||
if (_material == null || graphic == null || canvas == null || context == null) return;
|
||||
|
||||
if (_isContextDirty)
|
||||
{
|
||||
_isContextDirty = false;
|
||||
ApplyContextToMaterial(GetCurrentMaterial());
|
||||
}
|
||||
|
||||
if (!context.useViewMatrix) return;
|
||||
|
||||
context.UpdateViewMatrix(GetCurrentMaterial(), transitionRoot, canvas.rootCanvas);
|
||||
}
|
||||
|
||||
protected override void OnCanvasHierarchyChanged()
|
||||
{
|
||||
_canvasCached = false;
|
||||
_canvas = null;
|
||||
}
|
||||
|
||||
public void ModifyMesh(Mesh mesh)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void ModifyMesh(VertexHelper vh)
|
||||
{
|
||||
if (!isActiveAndEnabled || context == null) return;
|
||||
|
||||
_canModifyMesh = CanModifyMesh();
|
||||
if (_canModifyMesh)
|
||||
{
|
||||
context.ModifyMesh(graphic, canvas, transitionRoot, vh, canModifyShape);
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanModifyMesh()
|
||||
{
|
||||
if (graphic == null || !graphic.isActiveAndEnabled) return false;
|
||||
if (!isActiveAndEnabled) return false;
|
||||
|
||||
// The transitionRoot is same as the transform => true.
|
||||
var root = transitionRoot;
|
||||
if (transform == root) return true;
|
||||
|
||||
// The transitionRoot is not visible => false.
|
||||
var scale1 = root.lossyScale;
|
||||
var scale2 = transform.lossyScale;
|
||||
return !Mathf.Approximately(scale1.x * scale1.y * scale2.x * scale2.y, 0);
|
||||
}
|
||||
|
||||
public virtual Material GetModifiedMaterial(Material baseMaterial)
|
||||
{
|
||||
if (baseMaterial == null || !isActiveAndEnabled || context == null || !context.willModifyMaterial)
|
||||
{
|
||||
MaterialRepository.Release(ref _material);
|
||||
return baseMaterial;
|
||||
}
|
||||
|
||||
Profiler.BeginSample("(UIE)[UIEffect] GetModifiedMaterial");
|
||||
var samplingScaleId = (uint)(Mathf.InverseLerp(0.01f, 100, actualSamplingScale) * uint.MaxValue);
|
||||
// 使用特效配置哈希作为材质共享 key,相同配置的特效共享同一材质,从而可以合批。
|
||||
// root 仅在特效需要视图矩阵(transition/gradation/detail/shiny)时才影响材质。
|
||||
var configId = context.configId;
|
||||
var rootId = context.useViewMatrix && transitionRoot ? (uint)transitionRoot.GetInstanceID() : 0u;
|
||||
var hash = new Hash128((uint)baseMaterial.GetInstanceID(), configId, samplingScaleId, rootId);
|
||||
if (!MaterialRepository.Valid(hash, _material))
|
||||
{
|
||||
Profiler.BeginSample("(UIE)[UIEffect] GetModifiedMaterial > Get or create material");
|
||||
MaterialRepository.Get(hash, ref _material, x => new Material(x)
|
||||
{
|
||||
shader = UIEffectProjectSettings.shaderRegistry.FindOptionalShader(x.shader,
|
||||
"(UIEffect)",
|
||||
"Hidden/{0} (UIEffect)",
|
||||
"Hidden/UI/Default (UIEffect)"),
|
||||
hideFlags = HideFlags.HideAndDontSave
|
||||
}, baseMaterial);
|
||||
Profiler.EndSample();
|
||||
}
|
||||
|
||||
_material.CopyPropertiesFromMaterial(baseMaterial);
|
||||
ApplyContextToMaterial(_material);
|
||||
Profiler.EndSample();
|
||||
return _material;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
protected override void OnValidate()
|
||||
{
|
||||
UpdateContext(context);
|
||||
SetVerticesDirty();
|
||||
SetMaterialDirty();
|
||||
}
|
||||
|
||||
protected override void Reset()
|
||||
{
|
||||
OnValidate();
|
||||
}
|
||||
|
||||
internal virtual void SetEnablePreviewIfSelected(GameObject[] selection)
|
||||
{
|
||||
}
|
||||
#endif
|
||||
|
||||
protected override void OnRectTransformDimensionsChange()
|
||||
{
|
||||
if (!isActiveAndEnabled) return;
|
||||
SetVerticesDirty();
|
||||
}
|
||||
|
||||
protected override void OnDidApplyAnimationProperties()
|
||||
{
|
||||
UpdateContext(context);
|
||||
SetVerticesDirty();
|
||||
SetMaterialDirty();
|
||||
}
|
||||
|
||||
public virtual void SetVerticesDirty()
|
||||
{
|
||||
if (graphic != null)
|
||||
{
|
||||
graphic.SetVerticesDirty();
|
||||
GraphicProxy.Find(graphic).SetVerticesDirty(graphic, enabled);
|
||||
Misc.QueuePlayerLoopUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void SetMaterialDirty()
|
||||
{
|
||||
if (graphic != null)
|
||||
{
|
||||
graphic.SetMaterialDirty();
|
||||
SetMaterialContextDirty();
|
||||
Misc.QueuePlayerLoopUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void SetMaterialContextDirty()
|
||||
{
|
||||
_isContextDirty = true;
|
||||
}
|
||||
|
||||
internal void ReleaseMaterial()
|
||||
{
|
||||
MaterialRepository.Release(ref _material);
|
||||
}
|
||||
|
||||
internal abstract void UpdateContext(UIEffectContext c);
|
||||
|
||||
public virtual void ApplyContextToMaterial(Material material)
|
||||
{
|
||||
if (!isActiveAndEnabled || context == null || material == null) return;
|
||||
|
||||
_isContextDirty = false;
|
||||
context.ApplyToMaterial(material, actualSamplingScale);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
Profiler.BeginSample("(Editor/UIE)[UIEffect] ApplyContextToMaterial Post Process");
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
SetEnablePreviewIfSelected(Selection.gameObjects);
|
||||
Misc.QueuePlayerLoopUpdate();
|
||||
}
|
||||
|
||||
UIEffectProjectSettings.shaderRegistry.RegisterVariant(material, "UI > UIEffect");
|
||||
Profiler.EndSample();
|
||||
#endif
|
||||
}
|
||||
|
||||
public abstract void SetRate(float rate, UIEffectTweener.CullingMask cullingMask);
|
||||
public abstract bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera);
|
||||
|
||||
public void SetTime(double _)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnControlTimeStart()
|
||||
{
|
||||
if (this != null)
|
||||
{
|
||||
enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnControlTimeStop()
|
||||
{
|
||||
if (this != null)
|
||||
{
|
||||
enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,976 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Coffee.UIEffectInternal;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Profiling;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Coffee.UIEffects
|
||||
{
|
||||
public sealed class UIEffectContext
|
||||
{
|
||||
private static readonly UIEffectContext s_DefaultContext = new UIEffectContext();
|
||||
private static readonly List<UIVertex> s_WorkingVertices = new List<UIVertex>(1024 * 8);
|
||||
private static readonly int s_SrcBlend = Shader.PropertyToID("_SrcBlend");
|
||||
private static readonly int s_DstBlend = Shader.PropertyToID("_DstBlend");
|
||||
private static readonly int s_ToneIntensity = Shader.PropertyToID("_ToneIntensity");
|
||||
private static readonly int s_ColorFilter = Shader.PropertyToID("_ColorFilter");
|
||||
private static readonly int s_ColorValue = Shader.PropertyToID("_ColorValue");
|
||||
private static readonly int s_ColorIntensity = Shader.PropertyToID("_ColorIntensity");
|
||||
private static readonly int s_ColorGlow = Shader.PropertyToID("_ColorGlow");
|
||||
private static readonly int s_SamplingIntensity = Shader.PropertyToID("_SamplingIntensity");
|
||||
private static readonly int s_SamplingWidth = Shader.PropertyToID("_SamplingWidth");
|
||||
private static readonly int s_SamplingScale = Shader.PropertyToID("_SamplingScale");
|
||||
private static readonly int s_TransitionRate = Shader.PropertyToID("_TransitionRate");
|
||||
private static readonly int s_TransitionReverse = Shader.PropertyToID("_TransitionReverse");
|
||||
private static readonly int s_TransitionTex = Shader.PropertyToID("_TransitionTex");
|
||||
private static readonly int s_TransitionTex_ST = Shader.PropertyToID("_TransitionTex_ST");
|
||||
private static readonly int s_TransitionTex_Speed = Shader.PropertyToID("_TransitionTex_Speed");
|
||||
private static readonly int s_TransitionWidth = Shader.PropertyToID("_TransitionWidth");
|
||||
private static readonly int s_TransitionSoftness = Shader.PropertyToID("_TransitionSoftness");
|
||||
private static readonly int s_TransitionRange = Shader.PropertyToID("_TransitionRange");
|
||||
private static readonly int s_TransitionColorFilter = Shader.PropertyToID("_TransitionColorFilter");
|
||||
private static readonly int s_TransitionColor = Shader.PropertyToID("_TransitionColor");
|
||||
private static readonly int s_TransitionColorGlow = Shader.PropertyToID("_TransitionColorGlow");
|
||||
private static readonly int s_TransitionPatternReverse = Shader.PropertyToID("_TransitionPatternReverse");
|
||||
private static readonly int s_TransitionAutoPlaySpeed = Shader.PropertyToID("_TransitionAutoPlaySpeed");
|
||||
private static readonly int s_TransitionGradientTex = Shader.PropertyToID("_TransitionGradientTex");
|
||||
private static readonly int s_TargetColor = Shader.PropertyToID("_TargetColor");
|
||||
private static readonly int s_TargetRange = Shader.PropertyToID("_TargetRange");
|
||||
private static readonly int s_TargetSoftness = Shader.PropertyToID("_TargetSoftness");
|
||||
private static readonly int s_ShadowColorFilter = Shader.PropertyToID("_ShadowColorFilter");
|
||||
private static readonly int s_ShadowColor = Shader.PropertyToID("_ShadowColor");
|
||||
private static readonly int s_ShadowBlurIntensity = Shader.PropertyToID("_ShadowBlurIntensity");
|
||||
private static readonly int s_ShadowColorGlow = Shader.PropertyToID("_ShadowColorGlow");
|
||||
private static readonly int s_EdgeWidth = Shader.PropertyToID("_EdgeWidth");
|
||||
private static readonly int s_EdgeColorFilter = Shader.PropertyToID("_EdgeColorFilter");
|
||||
private static readonly int s_EdgeColor = Shader.PropertyToID("_EdgeColor");
|
||||
private static readonly int s_EdgeColorGlow = Shader.PropertyToID("_EdgeColorGlow");
|
||||
private static readonly int s_EdgeShinyAutoPlaySpeed = Shader.PropertyToID("_EdgeShinyAutoPlaySpeed");
|
||||
private static readonly int s_EdgeShinyRate = Shader.PropertyToID("_EdgeShinyRate");
|
||||
private static readonly int s_EdgeShinyWidth = Shader.PropertyToID("_EdgeShinyWidth");
|
||||
private static readonly int s_PatternArea = Shader.PropertyToID("_PatternArea");
|
||||
private static readonly int s_DetailIntensity = Shader.PropertyToID("_DetailIntensity");
|
||||
private static readonly int s_DetailThreshold = Shader.PropertyToID("_DetailThreshold");
|
||||
private static readonly int s_DetailColor = Shader.PropertyToID("_DetailColor");
|
||||
private static readonly int s_DetailTex = Shader.PropertyToID("_DetailTex");
|
||||
private static readonly int s_DetailTex_ST = Shader.PropertyToID("_DetailTex_ST");
|
||||
private static readonly int s_DetailTex_Speed = Shader.PropertyToID("_DetailTex_Speed");
|
||||
private static readonly int s_GradationIntensity = Shader.PropertyToID("_GradationIntensity");
|
||||
private static readonly int s_GradationColorFilter = Shader.PropertyToID("_GradationColorFilter");
|
||||
private static readonly int s_GradationColor1 = Shader.PropertyToID("_GradationColor1");
|
||||
private static readonly int s_GradationColor2 = Shader.PropertyToID("_GradationColor2");
|
||||
private static readonly int s_GradationColor3 = Shader.PropertyToID("_GradationColor3");
|
||||
private static readonly int s_GradationColor4 = Shader.PropertyToID("_GradationColor4");
|
||||
private static readonly int s_GradationTex = Shader.PropertyToID("_GradationTex");
|
||||
private static readonly int s_GradationTex_ST = Shader.PropertyToID("_GradationTex_ST");
|
||||
private static readonly int s_GradationRadial = Shader.PropertyToID("_GradationRadial");
|
||||
private static readonly int s_RootViewMatrix = Shader.PropertyToID("_RootViewMatrix");
|
||||
private static readonly int s_GradViewMatrix = Shader.PropertyToID("_GradViewMatrix");
|
||||
private static readonly int s_MirrorRootViewMatrix = Shader.PropertyToID("_MirrorRootViewMatrix");
|
||||
private static readonly int s_MirrorGradViewMatrix = Shader.PropertyToID("_MirrorGradViewMatrix");
|
||||
private static readonly int s_CanvasToWorldMatrix = Shader.PropertyToID("_CanvasToWorldMatrix");
|
||||
|
||||
private static readonly string[] s_ToneKeywords =
|
||||
{
|
||||
"",
|
||||
"TONE_GRAYSCALE",
|
||||
"TONE_SEPIA",
|
||||
"TONE_NEGATIVE",
|
||||
"TONE_RETRO",
|
||||
"TONE_POSTERIZE"
|
||||
};
|
||||
|
||||
private static readonly string[] s_ColorKeywords =
|
||||
{
|
||||
"",
|
||||
"COLOR_FILTER"
|
||||
};
|
||||
|
||||
private static readonly string[] s_SamplingKeywords =
|
||||
{
|
||||
"",
|
||||
"SAMPLING_BLUR_FAST",
|
||||
"SAMPLING_BLUR_MEDIUM",
|
||||
"SAMPLING_BLUR_DETAIL",
|
||||
"SAMPLING_PIXELATION",
|
||||
"SAMPLING_RGB_SHIFT",
|
||||
"SAMPLING_EDGE_LUMINANCE",
|
||||
"SAMPLING_EDGE_ALPHA"
|
||||
};
|
||||
|
||||
private static readonly string[] s_TransitionKeywords =
|
||||
{
|
||||
"",
|
||||
"TRANSITION_FADE",
|
||||
"TRANSITION_CUTOFF",
|
||||
"TRANSITION_DISSOLVE",
|
||||
"TRANSITION_SHINY",
|
||||
"TRANSITION_MASK",
|
||||
"TRANSITION_MELT",
|
||||
"TRANSITION_BURN",
|
||||
"TRANSITION_PATTERN",
|
||||
"TRANSITION_BLAZE"
|
||||
};
|
||||
|
||||
private static readonly string[] s_TargetKeywords =
|
||||
{
|
||||
"",
|
||||
"TARGET_HUE",
|
||||
"TARGET_LUMINANCE"
|
||||
};
|
||||
|
||||
private static readonly string[] s_EdgeKeywords =
|
||||
{
|
||||
"",
|
||||
"EDGE_PLAIN",
|
||||
"EDGE_SHINY"
|
||||
};
|
||||
|
||||
private static readonly string[] s_DetailKeywords =
|
||||
{
|
||||
"",
|
||||
"DETAIL_MASKING",
|
||||
"DETAIL_MULTIPLY",
|
||||
"DETAIL_ADDITIVE",
|
||||
"DETAIL_REPLACE",
|
||||
"DETAIL_MULTIPLY_ADDITIVE",
|
||||
"DETAIL_SUBTRACTIVE"
|
||||
};
|
||||
|
||||
private static readonly string[] s_GradationKeywords =
|
||||
{
|
||||
"",
|
||||
"GRADATION_GRADIENT",
|
||||
"GRADATION_COLOR2",
|
||||
"GRADATION_COLOR4"
|
||||
};
|
||||
|
||||
private static readonly Vector2[][] s_ShadowVectors = new[]
|
||||
{
|
||||
Array.Empty<Vector2>(), // None
|
||||
new[] { Vector2.one }, // Shadow
|
||||
new[] { Vector2.one, Vector2.right, Vector2.up }, // Shadow3
|
||||
new[] { Vector2.one, -Vector2.one, new Vector2(1, -1), new Vector2(-1, 1) }, // Outline
|
||||
new[]
|
||||
{
|
||||
Vector2.one, -Vector2.one, new Vector2(1, -1), new Vector2(-1, 1),
|
||||
Vector2.right, Vector2.up, Vector2.left, Vector2.down
|
||||
} // Outline8
|
||||
};
|
||||
|
||||
public static Func<UIVertex, Rect, UIVertex> onModifyVertex;
|
||||
public ToneFilter m_ToneFilter;
|
||||
public float m_ToneIntensity;
|
||||
|
||||
public ColorFilter m_ColorFilter;
|
||||
public float m_ColorIntensity;
|
||||
public Color m_Color;
|
||||
public bool m_ColorGlow;
|
||||
|
||||
public SamplingFilter m_SamplingFilter;
|
||||
public float m_SamplingIntensity;
|
||||
public float m_SamplingWidth;
|
||||
|
||||
public TransitionFilter m_TransitionFilter;
|
||||
public float m_TransitionRate;
|
||||
public bool m_TransitionReverse;
|
||||
public Texture m_TransitionTex;
|
||||
public Vector2 m_TransitionTexScale;
|
||||
public Vector2 m_TransitionTexOffset;
|
||||
public Vector2 m_TransitionTexSpeed;
|
||||
public float m_TransitionRotation;
|
||||
public bool m_TransitionKeepAspectRatio;
|
||||
public float m_TransitionWidth;
|
||||
public float m_TransitionSoftness;
|
||||
public MinMax01 m_TransitionRange;
|
||||
public ColorFilter m_TransitionColorFilter;
|
||||
public Color m_TransitionColor;
|
||||
public bool m_TransitionColorGlow;
|
||||
public bool m_TransitionPatternReverse;
|
||||
public float m_TransitionAutoPlaySpeed;
|
||||
public Gradient m_TransitionGradient;
|
||||
|
||||
public TargetMode m_TargetMode;
|
||||
public Color m_TargetColor;
|
||||
public float m_TargetRange;
|
||||
public float m_TargetSoftness;
|
||||
|
||||
public BlendMode m_SrcBlendMode;
|
||||
public BlendMode m_DstBlendMode;
|
||||
|
||||
public ShadowMode m_ShadowMode;
|
||||
public Vector2 m_ShadowDistance;
|
||||
public int m_ShadowIteration;
|
||||
public float m_ShadowFade;
|
||||
public float m_ShadowMirrorScale;
|
||||
public float m_ShadowBlurIntensity;
|
||||
public ColorFilter m_ShadowColorFilter;
|
||||
public Color m_ShadowColor;
|
||||
public bool m_ShadowColorGlow;
|
||||
|
||||
public EdgeMode m_EdgeMode;
|
||||
public float m_EdgeShinyRate;
|
||||
public float m_EdgeWidth;
|
||||
public ColorFilter m_EdgeColorFilter;
|
||||
public Color m_EdgeColor;
|
||||
public bool m_EdgeColorGlow;
|
||||
public float m_EdgeShinyWidth;
|
||||
public float m_EdgeShinyAutoPlaySpeed;
|
||||
public PatternArea m_PatternArea;
|
||||
|
||||
public GradationMode m_GradationMode;
|
||||
public float m_GradationIntensity;
|
||||
public GradationColorFilter m_GradationColorFilter;
|
||||
public Color m_GradationColor1;
|
||||
public Color m_GradationColor2;
|
||||
public Color m_GradationColor3;
|
||||
public Color m_GradationColor4;
|
||||
public Gradient m_GradationGradient;
|
||||
public float m_GradationOffset;
|
||||
public float m_GradationScale;
|
||||
public float m_GradationRotation;
|
||||
public TextureWrapMode m_GradationWrapMode;
|
||||
public bool m_GradationReverse;
|
||||
|
||||
public DetailFilter m_DetailFilter;
|
||||
public float m_DetailIntensity;
|
||||
public MinMax01 m_DetailThreshold;
|
||||
public Color m_DetailColor;
|
||||
public Texture m_DetailTex;
|
||||
public Vector2 m_DetailTexScale;
|
||||
public Vector2 m_DetailTexOffset;
|
||||
public Vector2 m_DetailTexSpeed;
|
||||
|
||||
public Flip m_Flip;
|
||||
|
||||
public bool willModifyMaterial => m_ToneFilter != ToneFilter.None
|
||||
|| m_ColorFilter != ColorFilter.None
|
||||
|| m_SamplingFilter != SamplingFilter.None
|
||||
|| m_TransitionFilter != TransitionFilter.None
|
||||
|| m_SrcBlendMode != BlendMode.One
|
||||
|| m_DstBlendMode != BlendMode.OneMinusSrcAlpha
|
||||
|| m_ShadowMode != ShadowMode.None
|
||||
|| m_EdgeMode != EdgeMode.None
|
||||
|| m_DetailFilter != DetailFilter.None
|
||||
|| m_GradationMode != GradationMode.None;
|
||||
|
||||
public bool willModifyVertex => willModifyMaterial;
|
||||
|
||||
public bool useViewMatrix => m_TransitionFilter != TransitionFilter.None
|
||||
|| m_GradationMode != GradationMode.None
|
||||
|| m_DetailFilter != DetailFilter.None
|
||||
|| m_EdgeMode == EdgeMode.Shiny;
|
||||
|
||||
/// <summary>
|
||||
/// 特效配置的稳定指纹。自动遍历所有序列化配置字段生成,因此插件新增属性时无需手动维护字段清单。
|
||||
/// 相同配置会得到相同指纹,从而可以共享同一个材质并参与合批。
|
||||
/// </summary>
|
||||
public uint configId
|
||||
{
|
||||
get
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var h = (uint)2166136261;
|
||||
var fields = s_ConfigFields;
|
||||
for (var i = 0; i < fields.Length; i++)
|
||||
{
|
||||
var f = fields[i];
|
||||
h = Mix(h, (uint)f.Name.GetHashCode());
|
||||
h = Mix(h, HashValue(f.GetValue(this)));
|
||||
}
|
||||
|
||||
// Avalanche 混淆(MurmurHash3 终混),让相近配置散列到不同指纹。
|
||||
h ^= h >> 16;
|
||||
h *= 0x85ebca6b;
|
||||
h ^= h >> 13;
|
||||
h *= 0xc2b2ae35;
|
||||
h ^= h >> 16;
|
||||
return h;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static uint Mix(uint h, uint v)
|
||||
{
|
||||
h ^= v;
|
||||
h *= 16777619;
|
||||
return h;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对任意配置值做内容哈希。值类型会逐字段展开,嵌套结构体(Color、Vector2/3/4、MinMax01 等)
|
||||
/// 无需单独写类型处理即可贡献全部数据。只有 UnityEngine.Object 和 Gradient 需要显式处理,
|
||||
/// 因为它们的默认哈希基于引用而非内容。
|
||||
/// </summary>
|
||||
private static uint HashValue(object value)
|
||||
{
|
||||
if (value == null) return 0u;
|
||||
|
||||
if (value is UnityEngine.Object obj)
|
||||
{
|
||||
return obj ? (uint)obj.GetInstanceID() : 0u;
|
||||
}
|
||||
|
||||
if (value is Gradient gradient)
|
||||
{
|
||||
return GradientHash(gradient);
|
||||
}
|
||||
|
||||
var type = value.GetType();
|
||||
if (type.IsPrimitive || type.IsEnum)
|
||||
{
|
||||
return unchecked((uint)value.GetHashCode());
|
||||
}
|
||||
|
||||
if (type.IsValueType)
|
||||
{
|
||||
return HashFields(value, type);
|
||||
}
|
||||
|
||||
return unchecked((uint)value.GetHashCode());
|
||||
}
|
||||
|
||||
private static uint HashFields(object boxed, Type type)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
if (!s_FieldCache.TryGetValue(type, out var fields))
|
||||
{
|
||||
fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
s_FieldCache[type] = fields;
|
||||
}
|
||||
|
||||
var h = (uint)2166136261;
|
||||
for (var i = 0; i < fields.Length; i++)
|
||||
{
|
||||
h = Mix(h, HashValue(fields[i].GetValue(boxed)));
|
||||
}
|
||||
|
||||
return h;
|
||||
}
|
||||
}
|
||||
|
||||
private static uint GradientHash(Gradient gradient)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var h = (uint)2166136261;
|
||||
|
||||
var colorKeys = gradient.colorKeys;
|
||||
for (var i = 0; i < colorKeys.Length; i++)
|
||||
{
|
||||
h = Mix(h, HashValue(colorKeys[i].color));
|
||||
h = Mix(h, HashValue(colorKeys[i].time));
|
||||
}
|
||||
|
||||
var alphaKeys = gradient.alphaKeys;
|
||||
for (var i = 0; i < alphaKeys.Length; i++)
|
||||
{
|
||||
h = Mix(h, HashValue(alphaKeys[i].alpha));
|
||||
h = Mix(h, HashValue(alphaKeys[i].time));
|
||||
}
|
||||
|
||||
return h;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly FieldInfo[] s_ConfigFields = typeof(UIEffectContext)
|
||||
.GetFields(BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
private static readonly Dictionary<Type, FieldInfo[]> s_FieldCache =
|
||||
new Dictionary<Type, FieldInfo[]>();
|
||||
|
||||
private Texture2D _gradationRampTex;
|
||||
private Texture2D _transitionRampTex;
|
||||
private bool _isGradientDirty = true;
|
||||
private bool _isTransitionGradientDirty = true;
|
||||
private static readonly Color[] s_Colors = new Color[256];
|
||||
|
||||
private static readonly InternalObjectPool<Texture2D> s_TexturePool = new InternalObjectPool<Texture2D>(
|
||||
() =>
|
||||
{
|
||||
var texture = new Texture2D(s_Colors.Length, 1, TextureFormat.RGBAFloat, false, false)
|
||||
{
|
||||
name = "GradationRamp",
|
||||
hideFlags = HideFlags.DontSave,
|
||||
wrapMode = TextureWrapMode.Repeat,
|
||||
anisoLevel = 0
|
||||
};
|
||||
|
||||
return texture;
|
||||
},
|
||||
texture => texture,
|
||||
_ => { });
|
||||
|
||||
private Texture2D gradationRampTex
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_GradationGradient == null) return null;
|
||||
if (_gradationRampTex == null) _gradationRampTex = s_TexturePool.Rent();
|
||||
if (!_isGradientDirty) return _gradationRampTex;
|
||||
_isGradientDirty = false;
|
||||
|
||||
var w = s_Colors.Length;
|
||||
for (var i = 0; i < w; i++)
|
||||
{
|
||||
var x = m_GradationReverse ? w - 1 - i : i;
|
||||
var t = (float)i / (w - 1);
|
||||
s_Colors[x] = ApplyColorSpace(m_GradationGradient.Evaluate(t));
|
||||
}
|
||||
|
||||
_gradationRampTex.wrapMode = m_GradationWrapMode;
|
||||
_gradationRampTex.filterMode = m_GradationGradient.mode == GradientMode.Blend
|
||||
? FilterMode.Bilinear
|
||||
: FilterMode.Point;
|
||||
_gradationRampTex.SetPixels(s_Colors);
|
||||
_gradationRampTex.Apply();
|
||||
return _gradationRampTex;
|
||||
}
|
||||
}
|
||||
|
||||
private Texture2D transitionRampTex
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_TransitionGradient == null) return null;
|
||||
if (_transitionRampTex == null) _transitionRampTex = s_TexturePool.Rent();
|
||||
if (!_isTransitionGradientDirty) return _transitionRampTex;
|
||||
_isTransitionGradientDirty = false;
|
||||
|
||||
var w = s_Colors.Length;
|
||||
for (var i = 0; i < w; i++)
|
||||
{
|
||||
var t = (float)i / (w - 1);
|
||||
s_Colors[i] = ApplyColorSpace(m_TransitionGradient.Evaluate(t));
|
||||
}
|
||||
|
||||
s_Colors[w - 1].a = 0;
|
||||
|
||||
_transitionRampTex.wrapMode = TextureWrapMode.Clamp;
|
||||
_transitionRampTex.filterMode = m_TransitionGradient.mode == GradientMode.Blend
|
||||
? FilterMode.Bilinear
|
||||
: FilterMode.Point;
|
||||
_transitionRampTex.SetPixels(s_Colors);
|
||||
_transitionRampTex.Apply();
|
||||
return _transitionRampTex;
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_isGradientDirty = true;
|
||||
_isTransitionGradientDirty = true;
|
||||
s_TexturePool.Return(ref _gradationRampTex);
|
||||
s_TexturePool.Return(ref _transitionRampTex);
|
||||
CopyFrom(s_DefaultContext);
|
||||
}
|
||||
|
||||
private void CopyFrom(UIEffectContext src)
|
||||
{
|
||||
var dst = this;
|
||||
dst.m_ToneFilter = src.m_ToneFilter;
|
||||
dst.m_ToneIntensity = src.m_ToneIntensity;
|
||||
|
||||
dst.m_ColorFilter = src.m_ColorFilter;
|
||||
dst.m_Color = src.m_Color;
|
||||
dst.m_ColorIntensity = src.m_ColorIntensity;
|
||||
dst.m_ColorGlow = src.m_ColorGlow;
|
||||
|
||||
dst.m_SamplingFilter = src.m_SamplingFilter;
|
||||
dst.m_SamplingIntensity = src.m_SamplingIntensity;
|
||||
dst.m_SamplingWidth = src.m_SamplingWidth;
|
||||
|
||||
dst.m_TransitionFilter = src.m_TransitionFilter;
|
||||
dst.m_TransitionRate = src.m_TransitionRate;
|
||||
dst.m_TransitionReverse = src.m_TransitionReverse;
|
||||
dst.m_TransitionTex = src.m_TransitionTex;
|
||||
dst.m_TransitionTexScale = src.m_TransitionTexScale;
|
||||
dst.m_TransitionTexOffset = src.m_TransitionTexOffset;
|
||||
dst.m_TransitionTexSpeed = src.m_TransitionTexSpeed;
|
||||
dst.m_TransitionKeepAspectRatio = src.m_TransitionKeepAspectRatio;
|
||||
dst.m_TransitionRotation = src.m_TransitionRotation;
|
||||
dst.m_TransitionWidth = src.m_TransitionWidth;
|
||||
dst.m_TransitionSoftness = src.m_TransitionSoftness;
|
||||
dst.m_TransitionRange = src.m_TransitionRange;
|
||||
dst.m_TransitionColor = src.m_TransitionColor;
|
||||
dst.m_TransitionColorFilter = src.m_TransitionColorFilter;
|
||||
dst.m_TransitionColorGlow = src.m_TransitionColorGlow;
|
||||
dst.m_TransitionPatternReverse = src.m_TransitionPatternReverse;
|
||||
dst.m_TransitionAutoPlaySpeed = src.m_TransitionAutoPlaySpeed;
|
||||
dst.m_TransitionGradient = src.m_TransitionGradient;
|
||||
|
||||
dst.m_TargetMode = src.m_TargetMode;
|
||||
dst.m_TargetColor = src.m_TargetColor;
|
||||
dst.m_TargetRange = src.m_TargetRange;
|
||||
dst.m_TargetSoftness = src.m_TargetSoftness;
|
||||
|
||||
dst.m_SrcBlendMode = src.m_SrcBlendMode;
|
||||
dst.m_DstBlendMode = src.m_DstBlendMode;
|
||||
|
||||
dst.m_ShadowMode = src.m_ShadowMode;
|
||||
dst.m_ShadowDistance = src.m_ShadowDistance;
|
||||
dst.m_ShadowIteration = src.m_ShadowIteration;
|
||||
dst.m_ShadowFade = src.m_ShadowFade;
|
||||
dst.m_ShadowMirrorScale = src.m_ShadowMirrorScale;
|
||||
dst.m_ShadowBlurIntensity = src.m_ShadowBlurIntensity;
|
||||
dst.m_ShadowColorFilter = src.m_ShadowColorFilter;
|
||||
dst.m_ShadowColor = src.m_ShadowColor;
|
||||
dst.m_ShadowColorGlow = src.m_ShadowColorGlow;
|
||||
|
||||
dst.m_EdgeMode = src.m_EdgeMode;
|
||||
dst.m_EdgeShinyRate = src.m_EdgeShinyRate;
|
||||
dst.m_EdgeWidth = src.m_EdgeWidth;
|
||||
dst.m_EdgeColorFilter = src.m_EdgeColorFilter;
|
||||
dst.m_EdgeColor = src.m_EdgeColor;
|
||||
dst.m_EdgeColorGlow = src.m_EdgeColorGlow;
|
||||
dst.m_EdgeShinyAutoPlaySpeed = src.m_EdgeShinyAutoPlaySpeed;
|
||||
dst.m_EdgeShinyWidth = src.m_EdgeShinyWidth;
|
||||
dst.m_PatternArea = src.m_PatternArea;
|
||||
|
||||
dst.m_GradationMode = src.m_GradationMode;
|
||||
dst.m_GradationIntensity = src.m_GradationIntensity;
|
||||
dst.m_GradationColorFilter = src.m_GradationColorFilter;
|
||||
dst.m_GradationColor1 = src.m_GradationColor1;
|
||||
dst.m_GradationColor2 = src.m_GradationColor2;
|
||||
dst.m_GradationColor3 = src.m_GradationColor3;
|
||||
dst.m_GradationColor4 = src.m_GradationColor4;
|
||||
dst.m_GradationGradient = src.m_GradationGradient;
|
||||
dst.m_GradationScale = src.m_GradationScale;
|
||||
dst.m_GradationOffset = src.m_GradationOffset;
|
||||
dst.m_GradationRotation = src.m_GradationRotation;
|
||||
dst.m_GradationWrapMode = src.m_GradationWrapMode;
|
||||
dst.m_GradationReverse = src.m_GradationReverse;
|
||||
|
||||
dst.m_DetailFilter = src.m_DetailFilter;
|
||||
dst.m_DetailIntensity = src.m_DetailIntensity;
|
||||
dst.m_DetailColor = src.m_DetailColor;
|
||||
dst.m_DetailThreshold = src.m_DetailThreshold;
|
||||
dst.m_DetailTex = src.m_DetailTex;
|
||||
dst.m_DetailTexScale = src.m_DetailTexScale;
|
||||
dst.m_DetailTexOffset = src.m_DetailTexOffset;
|
||||
dst.m_DetailTexSpeed = src.m_DetailTexSpeed;
|
||||
|
||||
dst.m_Flip = src.m_Flip;
|
||||
}
|
||||
|
||||
public void SetGradationDirty()
|
||||
{
|
||||
_isGradientDirty = true;
|
||||
}
|
||||
|
||||
public void SetTransitionGradationDirty()
|
||||
{
|
||||
_isTransitionGradientDirty = true;
|
||||
}
|
||||
|
||||
public void ApplyToMaterial(Material material, float actualSamplingScale = 1f)
|
||||
{
|
||||
if (material == null) return;
|
||||
|
||||
Profiler.BeginSample("(UIE)[UIEffect] ApplyToMaterial");
|
||||
|
||||
material.SetInt(s_SrcBlend, (int)m_SrcBlendMode);
|
||||
material.SetInt(s_DstBlend, (int)m_DstBlendMode);
|
||||
|
||||
material.SetFloat(s_ToneIntensity, Mathf.Clamp01(m_ToneIntensity));
|
||||
|
||||
material.SetInt(s_ColorFilter, (int)m_ColorFilter);
|
||||
material.SetColor(s_ColorValue, ApplyColorSpace(m_Color));
|
||||
material.SetFloat(s_ColorIntensity, Mathf.Clamp01(m_ColorIntensity));
|
||||
material.SetInt(s_ColorGlow, m_ColorGlow ? 1 : 0);
|
||||
|
||||
material.SetFloat(s_SamplingIntensity, Mathf.Clamp01(m_SamplingIntensity));
|
||||
material.SetFloat(s_SamplingWidth, m_SamplingWidth);
|
||||
material.SetFloat(s_SamplingScale, actualSamplingScale);
|
||||
|
||||
material.SetFloat(s_TransitionRate, Mathf.Clamp01(m_TransitionRate));
|
||||
material.SetInt(s_TransitionReverse, m_TransitionReverse ? 1 : 0);
|
||||
material.SetTexture(s_TransitionTex, m_TransitionFilter != 0 ? m_TransitionTex : null);
|
||||
material.SetVector(s_TransitionTex_ST,
|
||||
new Vector4(m_TransitionTexScale.x, m_TransitionTexScale.y,
|
||||
m_TransitionTexOffset.x, m_TransitionTexOffset.y));
|
||||
material.SetVector(s_TransitionTex_Speed, m_TransitionTexSpeed);
|
||||
material.SetFloat(s_TransitionWidth, Mathf.Clamp01(m_TransitionWidth));
|
||||
material.SetFloat(s_TransitionSoftness, Mathf.Clamp01(m_TransitionSoftness));
|
||||
material.SetVector(s_TransitionRange, new Vector2(m_TransitionRange.min, m_TransitionRange.max));
|
||||
material.SetInt(s_TransitionColorFilter, (int)m_TransitionColorFilter);
|
||||
material.SetColor(s_TransitionColor, ApplyColorSpace(m_TransitionColor));
|
||||
material.SetInt(s_TransitionColorGlow, m_TransitionColorGlow ? 1 : 0);
|
||||
material.SetInt(s_TransitionPatternReverse, m_TransitionPatternReverse ? 1 : 0);
|
||||
material.SetFloat(s_TransitionAutoPlaySpeed, m_TransitionAutoPlaySpeed);
|
||||
material.SetTexture(s_TransitionGradientTex,
|
||||
m_TransitionFilter == TransitionFilter.Blaze ? transitionRampTex : null);
|
||||
|
||||
material.SetColor(s_TargetColor, ApplyColorSpace(m_TargetColor));
|
||||
material.SetFloat(s_TargetRange, Mathf.Clamp01(m_TargetRange));
|
||||
material.SetFloat(s_TargetSoftness, Mathf.Clamp01(m_TargetSoftness));
|
||||
|
||||
switch (m_SamplingFilter)
|
||||
{
|
||||
case SamplingFilter.BlurFast:
|
||||
case SamplingFilter.BlurMedium:
|
||||
case SamplingFilter.BlurDetail:
|
||||
material.SetFloat(s_ShadowBlurIntensity, Mathf.Clamp01(m_ShadowBlurIntensity));
|
||||
break;
|
||||
default:
|
||||
material.SetFloat(s_ShadowBlurIntensity, Mathf.Clamp01(m_SamplingIntensity));
|
||||
break;
|
||||
}
|
||||
|
||||
material.SetInt(s_ShadowColorFilter, (int)m_ShadowColorFilter);
|
||||
material.SetColor(s_ShadowColor, ApplyColorSpace(m_ShadowColor));
|
||||
material.SetInt(s_ShadowColorGlow, m_ShadowColorGlow ? 1 : 0);
|
||||
|
||||
material.SetFloat(s_EdgeWidth, Mathf.Clamp01(m_EdgeWidth));
|
||||
material.SetInt(s_EdgeColorFilter, (int)m_EdgeColorFilter);
|
||||
material.SetColor(s_EdgeColor, ApplyColorSpace(m_EdgeColor));
|
||||
material.SetInt(s_EdgeColorGlow, m_EdgeColorGlow ? 1 : 0);
|
||||
material.SetFloat(s_EdgeShinyRate, Mathf.Clamp01(m_EdgeShinyRate));
|
||||
material.SetFloat(s_EdgeShinyWidth, Mathf.Clamp01(m_EdgeShinyWidth));
|
||||
material.SetFloat(s_EdgeShinyAutoPlaySpeed, m_EdgeShinyAutoPlaySpeed);
|
||||
material.SetInt(s_PatternArea, m_EdgeMode != EdgeMode.None ? (int)m_PatternArea : 0);
|
||||
|
||||
material.SetTexture(s_GradationTex, m_GradationMode != 0 ? gradationRampTex : null);
|
||||
material.SetVector(s_GradationTex_ST, GetGradationScaleAndOffset());
|
||||
|
||||
material.SetFloat(s_GradationIntensity, m_GradationIntensity);
|
||||
material.SetInt(s_GradationColorFilter, (int)m_GradationColorFilter);
|
||||
material.SetColor(s_GradationColor1, ApplyColorSpace(m_GradationColor1));
|
||||
material.SetColor(s_GradationColor2, ApplyColorSpace(m_GradationColor2));
|
||||
material.SetColor(s_GradationColor3, ApplyColorSpace(m_GradationColor3));
|
||||
material.SetColor(s_GradationColor4, ApplyColorSpace(m_GradationColor4));
|
||||
material.SetInt(s_GradationRadial, m_GradationMode == GradationMode.Radial
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
|| m_GradationMode == GradationMode.RadialDetail
|
||||
#pragma warning restore CS0612
|
||||
|| m_GradationMode == GradationMode.RadialGradient
|
||||
? 1
|
||||
: 0);
|
||||
|
||||
material.SetFloat(s_DetailIntensity, Mathf.Clamp01(m_DetailIntensity));
|
||||
material.SetColor(s_DetailColor, ApplyColorSpace(m_DetailColor));
|
||||
material.SetVector(s_DetailThreshold, new Vector2(m_DetailThreshold.min, m_DetailThreshold.max));
|
||||
material.SetTexture(s_DetailTex, m_DetailFilter != 0 ? m_DetailTex : null);
|
||||
material.SetVector(s_DetailTex_ST,
|
||||
new Vector4(m_DetailTexScale.x, m_DetailTexScale.y,
|
||||
m_DetailTexOffset.x, m_DetailTexOffset.y));
|
||||
material.SetVector(s_DetailTex_Speed, m_DetailTexSpeed);
|
||||
|
||||
SetKeyword(material, s_ToneKeywords, (int)m_ToneFilter);
|
||||
SetKeyword(material, s_ColorKeywords, m_ColorFilter != 0 || m_ShadowMode != 0 ? 1 : 0);
|
||||
SetKeyword(material, s_SamplingKeywords, (int)m_SamplingFilter);
|
||||
SetKeyword(material, s_TransitionKeywords, (int)m_TransitionFilter);
|
||||
SetKeyword(material, s_EdgeKeywords, (int)m_EdgeMode);
|
||||
SetKeyword(material, s_DetailKeywords, (int)m_DetailFilter);
|
||||
SetKeyword(material, s_TargetKeywords, (int)m_TargetMode);
|
||||
|
||||
switch (m_GradationMode)
|
||||
{
|
||||
case GradationMode.None:
|
||||
SetKeyword(material, s_GradationKeywords, 0);
|
||||
break;
|
||||
case GradationMode.HorizontalGradient:
|
||||
case GradationMode.VerticalGradient:
|
||||
case GradationMode.AngleGradient:
|
||||
case GradationMode.RadialGradient:
|
||||
SetKeyword(material, s_GradationKeywords, 1);
|
||||
break;
|
||||
case GradationMode.Horizontal:
|
||||
case GradationMode.Vertical:
|
||||
case GradationMode.DiagonalToRightBottom:
|
||||
case GradationMode.DiagonalToLeftBottom:
|
||||
case GradationMode.Angle:
|
||||
case GradationMode.Radial:
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
case GradationMode.RadialDetail:
|
||||
#pragma warning restore CS0612
|
||||
SetKeyword(material, s_GradationKeywords, 2);
|
||||
break;
|
||||
case GradationMode.Diagonal:
|
||||
SetKeyword(material, s_GradationKeywords, 3);
|
||||
break;
|
||||
}
|
||||
|
||||
Profiler.EndSample();
|
||||
}
|
||||
|
||||
public void SetEnablePreview(bool enable, Material material)
|
||||
{
|
||||
if (material == null) return;
|
||||
material.SetVector(s_TransitionTex_Speed, enable ? (Vector4)m_TransitionTexSpeed : Vector4.zero);
|
||||
material.SetVector(s_DetailTex_Speed, enable ? (Vector4)m_DetailTexSpeed : Vector4.zero);
|
||||
material.SetFloat(s_TransitionAutoPlaySpeed, enable ? m_TransitionAutoPlaySpeed : 0);
|
||||
material.SetFloat(s_EdgeShinyAutoPlaySpeed, enable ? m_EdgeShinyAutoPlaySpeed : 0);
|
||||
}
|
||||
|
||||
public void UpdateViewMatrix(Material material, RectTransform transitionRoot, Canvas canvas)
|
||||
{
|
||||
if (material == null) return;
|
||||
|
||||
var size = transitionRoot.rect.size;
|
||||
var scale = new Vector3(1f / size.x, 1f / size.y, 1f);
|
||||
if (m_TransitionKeepAspectRatio && !Mathf.Approximately(scale.x, scale.y))
|
||||
{
|
||||
scale.x = scale.y = Mathf.Min(scale.x, scale.y);
|
||||
}
|
||||
|
||||
if (0 != (m_Flip & Flip.Effect))
|
||||
{
|
||||
scale.x = 0 != (m_Flip & Flip.Horizontal) ? -scale.x : scale.x;
|
||||
scale.y = 0 != (m_Flip & Flip.Vertical) ? -scale.y : scale.y;
|
||||
}
|
||||
|
||||
var pivot = new Vector3(0.5f, 0.5f);
|
||||
var offset = (transitionRoot.pivot - (Vector2)pivot) * size;
|
||||
var w2LMat = Matrix4x4.Translate(offset) * transitionRoot.worldToLocalMatrix;
|
||||
|
||||
var rootRotation = Quaternion.Euler(0, 0, m_TransitionRotation);
|
||||
var rootScale = 1f / GetMultiplier(m_TransitionRotation);
|
||||
material.SetMatrix(s_RootViewMatrix, Matrix4x4.TRS(pivot, rootRotation, scale * rootScale) * w2LMat);
|
||||
|
||||
var rotation = GetGradationRotation();
|
||||
var gradRotation = Quaternion.Euler(0, 0, rotation);
|
||||
var gradScale = 1f / GetMultiplier(rotation);
|
||||
material.SetMatrix(s_GradViewMatrix, Matrix4x4.TRS(pivot, gradRotation, scale * gradScale) * w2LMat);
|
||||
|
||||
if (m_ShadowMode == ShadowMode.Mirror)
|
||||
{
|
||||
var mirrorInv = 1f / m_ShadowMirrorScale;
|
||||
var mirrorOffset = new Vector3(0, size.y / 2 * (mirrorInv + 1) - m_ShadowDistance.y * mirrorInv, 0);
|
||||
var mirrorScale = new Vector3(1, mirrorInv, 1);
|
||||
var udScale = new Vector3(scale.x, -scale.y, scale.z);
|
||||
var mirrorTranslation = Matrix4x4.TRS(mirrorOffset, Quaternion.identity, mirrorScale) * w2LMat;
|
||||
material.SetMatrix(
|
||||
s_MirrorRootViewMatrix,
|
||||
Matrix4x4.TRS(pivot, rootRotation, udScale * rootScale) * mirrorTranslation
|
||||
);
|
||||
material.SetMatrix(
|
||||
s_MirrorGradViewMatrix,
|
||||
Matrix4x4.TRS(pivot, gradRotation, udScale * gradScale) * mirrorTranslation
|
||||
);
|
||||
}
|
||||
|
||||
var useIdentity =
|
||||
canvas.renderMode == RenderMode.WorldSpace ||
|
||||
(canvas.renderMode != RenderMode.ScreenSpaceOverlay && canvas.worldCamera);
|
||||
|
||||
material.SetMatrix(
|
||||
s_CanvasToWorldMatrix,
|
||||
useIdentity ? Matrix4x4.identity : canvas.transform.localToWorldMatrix
|
||||
);
|
||||
}
|
||||
|
||||
private Vector4 GetGradationScaleAndOffset()
|
||||
{
|
||||
var gScale = 1 / m_GradationScale;
|
||||
switch (m_GradationMode)
|
||||
{
|
||||
case GradationMode.HorizontalGradient:
|
||||
case GradationMode.VerticalGradient:
|
||||
case GradationMode.AngleGradient:
|
||||
return new Vector4(gScale, 1, -0.5f * (gScale + 1) - m_GradationOffset, 0);
|
||||
case GradationMode.Radial:
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
case GradationMode.RadialDetail:
|
||||
#pragma warning restore CS0612
|
||||
{
|
||||
return new Vector4(gScale, 1, m_GradationOffset, 0);
|
||||
}
|
||||
default:
|
||||
return new Vector4(gScale, 1, m_GradationOffset * (gScale + 1) / 2 - gScale * 0.5f + 0.5f, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private float GetGradationRotation()
|
||||
{
|
||||
switch (m_GradationMode)
|
||||
{
|
||||
case GradationMode.DiagonalToLeftBottom:
|
||||
return 135;
|
||||
case GradationMode.DiagonalToRightBottom:
|
||||
return 45;
|
||||
case GradationMode.Vertical:
|
||||
return 90;
|
||||
case GradationMode.VerticalGradient:
|
||||
return -90;
|
||||
case GradationMode.Angle:
|
||||
case GradationMode.AngleGradient:
|
||||
return m_GradationRotation;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static float GetMultiplier(float deg)
|
||||
{
|
||||
var rad = Mathf.Deg2Rad * deg;
|
||||
var sin = Mathf.Sin(rad);
|
||||
var cos = Mathf.Cos(rad);
|
||||
return Mathf.Max(Mathf.Abs(cos - sin), Mathf.Abs(cos + sin));
|
||||
}
|
||||
|
||||
#if UNITY_2021_2_OR_NEWER
|
||||
private static void SetKeyword(Material material, Span<string> keywords, int index)
|
||||
#else
|
||||
private static void SetKeyword(Material material, string[] keywords, int index)
|
||||
#endif
|
||||
{
|
||||
var length = keywords.Length;
|
||||
if (index < length && !string.IsNullOrEmpty(keywords[index]))
|
||||
{
|
||||
material.EnableKeyword(keywords[index]);
|
||||
}
|
||||
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
if (i == index) continue;
|
||||
material.DisableKeyword(keywords[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public void ModifyMesh(Graphic graphic, Canvas canvas, RectTransform transitionRoot, VertexHelper vh,
|
||||
bool canModifyShape)
|
||||
{
|
||||
// Apply flip (without effect).
|
||||
ApplyFlipWithoutEffect(vh, m_Flip);
|
||||
|
||||
var count = vh.currentIndexCount;
|
||||
if (!willModifyVertex || count == 0) return;
|
||||
|
||||
var effectProxy = GraphicProxy.Find(graphic);
|
||||
var isText = effectProxy.IsText(graphic);
|
||||
effectProxy.OnPreModifyMesh(graphic, canvas);
|
||||
|
||||
var verts = s_WorkingVertices;
|
||||
var expandSize = effectProxy.ModifyExpandSize(graphic, GetExpandSize(canModifyShape));
|
||||
|
||||
// Get the rectangle to calculate the normalized position.
|
||||
vh.GetUIVertexStream(verts);
|
||||
var bundleSize = isText ? 6 : count;
|
||||
|
||||
// Modify vertices for each bundled-quad.
|
||||
for (var i = 0; i < count; i += bundleSize)
|
||||
{
|
||||
// min/max for bundled-quad
|
||||
UIVertexUtil.GetBounds(verts, i, bundleSize, out var bounds, out var uvMask);
|
||||
UIVertexUtil.Expand(verts, i, bundleSize, expandSize, bounds);
|
||||
for (var j = 0; j < bundleSize; j++)
|
||||
{
|
||||
var vt = verts[i + j];
|
||||
if (onModifyVertex != null)
|
||||
{
|
||||
vt = onModifyVertex(vt, uvMask);
|
||||
}
|
||||
else
|
||||
{
|
||||
vt.uv1 = new Vector4(uvMask.xMin, uvMask.yMin, uvMask.xMax, uvMask.yMax);
|
||||
}
|
||||
|
||||
verts[i + j] = vt;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply flip (with effect).
|
||||
ApplyFlipWithEffect(verts, m_Flip);
|
||||
|
||||
// Apply shadow.
|
||||
ApplyShadow(verts, transitionRoot, m_Flip);
|
||||
|
||||
vh.Clear();
|
||||
vh.AddUIVertexTriangleStream(verts);
|
||||
}
|
||||
|
||||
private void ApplyShadow(List<UIVertex> verts, RectTransform transitionRoot, Flip flip)
|
||||
{
|
||||
var distance = m_ShadowDistance;
|
||||
if (distance == Vector2.zero) return;
|
||||
|
||||
if ((flip & Flip.Shadow) != 0)
|
||||
{
|
||||
distance.x = (flip & Flip.Horizontal) != 0 ? -distance.x : distance.x;
|
||||
distance.y = (flip & Flip.Vertical) != 0 ? -distance.y : distance.y;
|
||||
}
|
||||
|
||||
switch (m_ShadowMode)
|
||||
{
|
||||
case ShadowMode.Shadow:
|
||||
case ShadowMode.Shadow3:
|
||||
case ShadowMode.Outline:
|
||||
case ShadowMode.Outline8:
|
||||
ShadowUtil.DoShadow(verts, s_ShadowVectors[(int)m_ShadowMode], distance, m_ShadowIteration,
|
||||
m_ShadowFade);
|
||||
break;
|
||||
case ShadowMode.Mirror:
|
||||
ShadowUtil.DoMirror(verts, distance, m_ShadowMirrorScale, m_ShadowFade, transitionRoot);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyFlipWithoutEffect(VertexHelper vh, Flip flip)
|
||||
{
|
||||
if ((flip & Flip.Effect) != 0) return;
|
||||
|
||||
var flipHorizontal = 0 != (flip & Flip.Horizontal);
|
||||
var flipVertical = 0 != (flip & Flip.Vertical);
|
||||
if (!flipHorizontal && !flipVertical) return;
|
||||
|
||||
UIVertexUtil.Flip(vh, flipHorizontal, flipVertical);
|
||||
}
|
||||
|
||||
private static void ApplyFlipWithEffect(List<UIVertex> verts, Flip flip)
|
||||
{
|
||||
if ((flip & Flip.Effect) == 0) return;
|
||||
|
||||
var flipHorizontal = 0 != (flip & Flip.Horizontal);
|
||||
var flipVertical = 0 != (flip & Flip.Vertical);
|
||||
if (!flipHorizontal && !flipVertical) return;
|
||||
|
||||
UIVertexUtil.Flip(verts, flipHorizontal, flipVertical);
|
||||
}
|
||||
|
||||
private Vector4 GetExpandSize(bool canModifyShape)
|
||||
{
|
||||
if (!canModifyShape) return Vector4.zero;
|
||||
|
||||
var expandSize = Vector4.zero;
|
||||
switch (m_SamplingFilter)
|
||||
{
|
||||
case SamplingFilter.BlurFast:
|
||||
expandSize += Vector4.one * 10;
|
||||
break;
|
||||
case SamplingFilter.BlurMedium:
|
||||
expandSize += Vector4.one * 15;
|
||||
break;
|
||||
case SamplingFilter.BlurDetail:
|
||||
expandSize += Vector4.one * 20;
|
||||
break;
|
||||
case SamplingFilter.RgbShift:
|
||||
expandSize.x += 40;
|
||||
expandSize.z += 40;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (m_TransitionFilter)
|
||||
{
|
||||
case TransitionFilter.Melt:
|
||||
expandSize.y += 40;
|
||||
break;
|
||||
case TransitionFilter.Burn:
|
||||
expandSize.w += 40;
|
||||
break;
|
||||
}
|
||||
|
||||
return expandSize;
|
||||
}
|
||||
|
||||
private static Color ApplyColorSpace(Color color)
|
||||
{
|
||||
return QualitySettings.activeColorSpace == ColorSpace.Linear ? color.linear : color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
# UIEffect 降 DC 改动总结
|
||||
|
||||
> 提交:`fa5f0cdb 降低DC的操作`(2026-08-26)
|
||||
> 涉及目录:`unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/`
|
||||
|
||||
## 一、背景与目标
|
||||
|
||||
UI_Effect 插件原本每个 `UIEffect` 组件都会生成**独立的材质实例**(缓存 key 使用 `effectId`,即组件的 `GetInstanceID()`,每个实例唯一),导致:
|
||||
|
||||
- 相同配置的特效无法共享材质 → 无法参与动态合批 → **Draw Call 偏高**;
|
||||
- 批量刷 UI 时材质创建开销大,伴随帧率波动。
|
||||
|
||||
目标:让**相同配置的 UIEffect 共享同一材质**,从而参与合批,降低 DC。
|
||||
|
||||
## 二、方案选型
|
||||
|
||||
| 方案 | 说明 | 结论 |
|
||||
| --- | --- | --- |
|
||||
| UIEffectReplica + Preset | 业务代码逐个改为副本组件,指向共享 preset | 需要改动大量业务文件、创建 preset 资产,侵入性大 |
|
||||
| **configId 配置指纹(最终采用)** | 在插件内部按"配置内容哈希"共享材质 | **业务代码零改动**,同配置自动共享合批 |
|
||||
|
||||
## 三、具体改动
|
||||
|
||||
### 1. `UIEffectContext.cs`(新增约 121 行)
|
||||
|
||||
核心:新增 `configId` 属性——特效配置的"稳定指纹"。
|
||||
|
||||
- **反射字段缓存**:`s_ConfigFields` 为 `static readonly FieldInfo[]`,`GetFields` 只在类型加载时执行一次,运行时无反射类型发现开销,避免批量刷新时的 GC 尖峰(这是此前"帧率不稳"问题的根因与修复点)。
|
||||
- **内容哈希 `HashValue`**:
|
||||
- `null` → 0;
|
||||
- `UnityEngine.Object` → `GetInstanceID()`(引用同一资产即同哈希);
|
||||
- `Gradient` → `GradientHash` 按 colorKeys / alphaKeys 逐 key 做内容哈希(不同实例但内容相同的渐变得到相同指纹);
|
||||
- 基元/枚举 → `GetHashCode()`;嵌套值类型(Color、Vector、MinMax01 等)→ `HashFields` 逐字段展开(`s_FieldCache` 按类型缓存)。
|
||||
- **雪崩混淆**:FNV-1a 混合后追加 MurmurHash3 终混,让相近配置散列开,降低哈希碰撞概率。
|
||||
|
||||
### 2. `UIEffectBase.cs`(`GetModifiedMaterial`)
|
||||
|
||||
材质缓存 key 由 `effectId` 改为配置指纹:
|
||||
|
||||
```csharp
|
||||
var configId = context.configId;
|
||||
var rootId = context.useViewMatrix && transitionRoot ? (uint)transitionRoot.GetInstanceID() : 0u;
|
||||
var hash = new Hash128((uint)baseMaterial.GetInstanceID(), configId, samplingScaleId, rootId);
|
||||
```
|
||||
|
||||
要点:
|
||||
|
||||
- `rootId` **仅在特效需要视图矩阵时**(transition / gradation / detail / shiny)才参与哈希,普通灰度(ToneFilter)等不受 root 影响,可共享范围更大;
|
||||
- `Hash128(baseMaterial, configId, samplingScaleId, rootId)` 相同 → 命中 `MaterialRepository` 中同一材质实例 → 参与动态合批。
|
||||
|
||||
### 3. `ShaderVariantRegistry.cs`(Profiler 采样修复)
|
||||
|
||||
`RegisterVariant` / `GetVariant` 的**所有提前 return 分支**均补上 `Profiler.EndSample()`,保证 `BeginSample`/`EndSample` 严格配对,修复控制台的:
|
||||
|
||||
```
|
||||
Non-matching Profiler.EndSample (Every EndSample call must have a preceding BeginSample call...)
|
||||
```
|
||||
|
||||
## 四、业务侧影响
|
||||
|
||||
- **业务文件零改动**:`EquipBuffItem`、`MapBlockItem`、`PetItem`、`V_RechargeItem`、`FightLinkHeroItem`、`AvatarItem` 等继续正常使用 `UIEffect` 控制灰度,无需迁移到 `UIEffectReplica`。
|
||||
- 同屏所有**同配置**灰度特效(如大量 `ToneFilter.Grayscale` 的图标)自动共享一个材质并合批。
|
||||
|
||||
## 五、效果
|
||||
|
||||
- N 个相同配置的 UIEffect:N 个材质实例 → **1 个共享材质**,可被 UGUI 动态合批;
|
||||
- DC 显著下降(受益程度取决于面板内同配置特效数量)。
|
||||
|
||||
## 六、注意事项与局限
|
||||
|
||||
1. **配置不同的特效无法互相合批**——每种不同配置组合仍各占一个材质/DC,属预期行为;
|
||||
2. `configId` 在画布重建时计算,字段遍历存在少量装箱开销,已通过静态缓存降到最低(普通 UI 不会每帧触发);
|
||||
3. 新增 `UIEffectContext` 公共字段会自动纳入指纹(遍历 public instance 字段),**无需手动维护字段清单**;
|
||||
4. 验证方式:打开 Frame Debugger / Profiler,确认同配置 UI 元素合并为同一批次;确认控制台不再出现 `Non-matching Profiler.EndSample`。
|
||||
@@ -0,0 +1,156 @@
|
||||
diff --git a/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectBase.cs b/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectBase.cs
|
||||
index c3ceb2fd..210257d0 100644
|
||||
--- a/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectBase.cs
|
||||
+++ b/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectBase.cs
|
||||
@@ -196,8 +196,11 @@ namespace Coffee.UIEffects
|
||||
|
||||
Profiler.BeginSample("(UIE)[UIEffect] GetModifiedMaterial");
|
||||
var samplingScaleId = (uint)(Mathf.InverseLerp(0.01f, 100, actualSamplingScale) * uint.MaxValue);
|
||||
- var rootId = (uint)(transitionRoot != null ? transitionRoot.GetHashCode() : 0);
|
||||
- var hash = new Hash128((uint)baseMaterial.GetHashCode(), effectId, samplingScaleId, rootId);
|
||||
+ // 使用特效配置哈希作为材质共享 key,相同配置的特效共享同一材质,从而可以合批。
|
||||
+ // root 仅在特效需要视图矩阵(transition/gradation/detail/shiny)时才影响材质。
|
||||
+ var configId = context.configId;
|
||||
+ var rootId = context.useViewMatrix && transitionRoot ? (uint)transitionRoot.GetInstanceID() : 0u;
|
||||
+ var hash = new Hash128((uint)baseMaterial.GetInstanceID(), configId, samplingScaleId, rootId);
|
||||
if (!MaterialRepository.Valid(hash, _material))
|
||||
{
|
||||
Profiler.BeginSample("(UIE)[UIEffect] GetModifiedMaterial > Get or create material");
|
||||
diff --git a/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectContext.cs b/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectContext.cs
|
||||
index c315f4cb..48b6ae4b 100644
|
||||
--- a/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectContext.cs
|
||||
+++ b/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectContext.cs
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
+using System.Reflection;
|
||||
using Coffee.UIEffectInternal;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Profiling;
|
||||
@@ -263,6 +264,126 @@ namespace Coffee.UIEffects
|
||||
|| m_DetailFilter != DetailFilter.None
|
||||
|| m_EdgeMode == EdgeMode.Shiny;
|
||||
|
||||
+ /// <summary>
|
||||
+ /// 特效配置的稳定指纹。自动遍历所有序列化配置字段生成,因此插件新增属性时无需手动维护字段清单。
|
||||
+ /// 相同配置会得到相同指纹,从而可以共享同一个材质并参与合批。
|
||||
+ /// </summary>
|
||||
+ public uint configId
|
||||
+ {
|
||||
+ get
|
||||
+ {
|
||||
+ unchecked
|
||||
+ {
|
||||
+ var h = (uint)2166136261;
|
||||
+ var fields = s_ConfigFields;
|
||||
+ for (var i = 0; i < fields.Length; i++)
|
||||
+ {
|
||||
+ var f = fields[i];
|
||||
+ h = Mix(h, (uint)f.Name.GetHashCode());
|
||||
+ h = Mix(h, HashValue(f.GetValue(this)));
|
||||
+ }
|
||||
+
|
||||
+ // Avalanche 混淆(MurmurHash3 终混),让相近配置散列到不同指纹。
|
||||
+ h ^= h >> 16;
|
||||
+ h *= 0x85ebca6b;
|
||||
+ h ^= h >> 13;
|
||||
+ h *= 0xc2b2ae35;
|
||||
+ h ^= h >> 16;
|
||||
+ return h;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ private static uint Mix(uint h, uint v)
|
||||
+ {
|
||||
+ h ^= v;
|
||||
+ h *= 16777619;
|
||||
+ return h;
|
||||
+ }
|
||||
+
|
||||
+ /// <summary>
|
||||
+ /// 对任意配置值做内容哈希。值类型会逐字段展开,嵌套结构体(Color、Vector2/3/4、MinMax01 等)
|
||||
+ /// 无需单独写类型处理即可贡献全部数据。只有 UnityEngine.Object 和 Gradient 需要显式处理,
|
||||
+ /// 因为它们的默认哈希基于引用而非内容。
|
||||
+ /// </summary>
|
||||
+ private static uint HashValue(object value)
|
||||
+ {
|
||||
+ if (value == null) return 0u;
|
||||
+
|
||||
+ if (value is UnityEngine.Object obj)
|
||||
+ {
|
||||
+ return obj ? (uint)obj.GetInstanceID() : 0u;
|
||||
+ }
|
||||
+
|
||||
+ if (value is Gradient gradient)
|
||||
+ {
|
||||
+ return GradientHash(gradient);
|
||||
+ }
|
||||
+
|
||||
+ var type = value.GetType();
|
||||
+ if (type.IsPrimitive || type.IsEnum)
|
||||
+ {
|
||||
+ return unchecked((uint)value.GetHashCode());
|
||||
+ }
|
||||
+
|
||||
+ if (type.IsValueType)
|
||||
+ {
|
||||
+ return HashFields(value, type);
|
||||
+ }
|
||||
+
|
||||
+ return unchecked((uint)value.GetHashCode());
|
||||
+ }
|
||||
+
|
||||
+ private static uint HashFields(object boxed, Type type)
|
||||
+ {
|
||||
+ unchecked
|
||||
+ {
|
||||
+ if (!s_FieldCache.TryGetValue(type, out var fields))
|
||||
+ {
|
||||
+ fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
+ s_FieldCache[type] = fields;
|
||||
+ }
|
||||
+
|
||||
+ var h = (uint)2166136261;
|
||||
+ for (var i = 0; i < fields.Length; i++)
|
||||
+ {
|
||||
+ h = Mix(h, HashValue(fields[i].GetValue(boxed)));
|
||||
+ }
|
||||
+
|
||||
+ return h;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ private static uint GradientHash(Gradient gradient)
|
||||
+ {
|
||||
+ unchecked
|
||||
+ {
|
||||
+ var h = (uint)2166136261;
|
||||
+
|
||||
+ var colorKeys = gradient.colorKeys;
|
||||
+ for (var i = 0; i < colorKeys.Length; i++)
|
||||
+ {
|
||||
+ h = Mix(h, HashValue(colorKeys[i].color));
|
||||
+ h = Mix(h, HashValue(colorKeys[i].time));
|
||||
+ }
|
||||
+
|
||||
+ var alphaKeys = gradient.alphaKeys;
|
||||
+ for (var i = 0; i < alphaKeys.Length; i++)
|
||||
+ {
|
||||
+ h = Mix(h, HashValue(alphaKeys[i].alpha));
|
||||
+ h = Mix(h, HashValue(alphaKeys[i].time));
|
||||
+ }
|
||||
+
|
||||
+ return h;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ private static readonly FieldInfo[] s_ConfigFields = typeof(UIEffectContext)
|
||||
+ .GetFields(BindingFlags.Public | BindingFlags.Instance);
|
||||
+
|
||||
+ private static readonly Dictionary<Type, FieldInfo[]> s_FieldCache =
|
||||
+ new Dictionary<Type, FieldInfo[]>();
|
||||
+
|
||||
private Texture2D _gradationRampTex;
|
||||
private Texture2D _transitionRampTex;
|
||||
private bool _isGradientDirty = true;
|
||||
@@ -0,0 +1,286 @@
|
||||
# 移植与验证手册
|
||||
|
||||
> 配套包:`DC优化归档/` | 验证过的环境:Unity 团结引擎(Tuanjie)+ UI Effect **5.11.3** + URP
|
||||
> 改动量:2 个文件、126 行新增、2 行删除。业务代码 **0 改动**。
|
||||
|
||||
---
|
||||
|
||||
## 第一章 动手前的前提核对(3 项,缺一不可)
|
||||
|
||||
### 1.1 UIEffect 必须是"本地源码包",不能是 PackageCache 只读包
|
||||
|
||||
打开目标工程的 `Packages/manifest.json`,找 `com.coffee.ui-effect`:
|
||||
|
||||
```jsonc
|
||||
// ✅ 可以改:file: 指向工程内源码目录
|
||||
"com.coffee.ui-effect": "file:../my_libs/unityLib/UIEffect-main/Packages/src",
|
||||
|
||||
// ❌ 改了白改:版本号形式会被拉进 Library/PackageCache/(只读,Unity 重新解包会覆盖)
|
||||
"com.coffee.ui-effect": "5.11.3"
|
||||
```
|
||||
|
||||
如果是 ❌ 形式,先转成本地包:
|
||||
1. 从 [github.com/mob-sakai/ParticleEffectForUGUI 同作者的 UIEffect 仓库](https://github.com/mob-sakai/UIEffect) 下载对应版本源码;
|
||||
2. 放到工程内目录(如 `<项目>/unity/my_libs/unityLib/UIEffect-main/Packages/src`);
|
||||
3. `manifest.json` 改成 `file:` 相对路径;
|
||||
4. Unity 重新编译,确认 `Library/PackageCache/` 下的 `com.coffee.ui-effect@*` 不再被引用。
|
||||
|
||||
> T5 的实际路径:`d:\Project\T5\T5_game\unity\my_libs\unityLib\UIEffect-main\Packages\src`
|
||||
> 需要改的两个文件都在它的 `Runtime/` 下。
|
||||
|
||||
### 1.2 版本核对:确认锚点代码存在
|
||||
|
||||
补丁靠"上下文行"定位,版本差异大就会 apply 失败。打开目标工程的两个文件,确认下面两段存在:
|
||||
|
||||
**`Runtime/UIEffectContext.cs`** —— 找到 `useViewMatrix` 属性(补丁插在它后面):
|
||||
```csharp
|
||||
public bool useViewMatrix => m_TransitionFilter != TransitionFilter.None
|
||||
|| m_GradationMode != GradationMode.None
|
||||
|| m_DetailFilter != DetailFilter.None
|
||||
|| m_EdgeMode == EdgeMode.Shiny;
|
||||
```
|
||||
|
||||
**`Runtime/UIEffectBase.cs`** —— 找到 `GetModifiedMaterial()` 里的这两行(补丁要替换它们):
|
||||
```csharp
|
||||
var rootId = (uint)(transitionRoot != null ? transitionRoot.GetHashCode() : 0);
|
||||
var hash = new Hash128((uint)baseMaterial.GetHashCode(), effectId, samplingScaleId, rootId);
|
||||
```
|
||||
|
||||
两段都在 → 走**方式 A**(git apply)。
|
||||
`useViewMatrix` 不存在(更老的版本)→ 走**方式 C**(手工贴),并按 §3.3 的降级写法处理 `rootId`。
|
||||
|
||||
### 1.3 记录基线 DC 数字
|
||||
|
||||
改之前先量一次,否则改完无法证明有效:
|
||||
- 打开目标面板(选 UIEffect 用得最多的那个,T5 用的是 `UI_LevelGiftPanel`);
|
||||
- `Window → Analysis → Frame Debugger`,Enable;
|
||||
- 记下左侧事件树里 `Canvas.BuildBatch` 下的 `Draw Mesh` 数量(或右下角 Batches 数字);
|
||||
- 截图存档。
|
||||
|
||||
---
|
||||
|
||||
## 第二章 三种移植方式
|
||||
|
||||
### 方式 A:git apply(推荐,最快最安全)
|
||||
|
||||
补丁里的路径是 `a/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/...`,
|
||||
即**相对目标工程 git 根目录**。T5 的 git 根是 `<项目>/T5_game`(不是 `<项目>`),别站错目录。
|
||||
|
||||
```powershell
|
||||
# 站到目标工程的 git 根目录
|
||||
cd <目标工程 git 根目录>
|
||||
|
||||
# 先干跑,看能不能对上(不写文件)
|
||||
git apply --check --3way "d:\Project\T5\DC优化归档\01_UIEffect材质共享降DC\uieffect_configId.patch"
|
||||
|
||||
# 没问题再真打
|
||||
git apply --3way "d:\Project\T5\DC优化归档\01_UIEffect材质共享降DC\uieffect_configId.patch"
|
||||
|
||||
# 确认只动了这 2 个文件
|
||||
git status --porcelain
|
||||
```
|
||||
|
||||
**目标工程 UIEffect 放在别的路径**(比如 `Assets/Plugins/UIEffect/`)时,用 `--directory` 或 `-p` 调整剥层级,例如:
|
||||
|
||||
```powershell
|
||||
# 补丁里 a/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/X.cs
|
||||
# 目标工程实际是 Assets/Plugins/UIEffect/Runtime/X.cs → 剥掉前 5 层再拼目标前缀
|
||||
git apply -p5 --directory="Assets/Plugins/UIEffect" --3way "<补丁路径>"
|
||||
```
|
||||
|
||||
`--check` 报错 `does not match index` → 目标文件已被改过或版本不同 → 转方式 B/C。
|
||||
|
||||
### 方式 B:直接覆盖两个 .cs(兜底,要求版本一致)
|
||||
|
||||
```powershell
|
||||
$dst = "<目标工程>\unity\my_libs\unityLib\UIEffect-main\Packages\src\Runtime"
|
||||
Copy-Item "d:\Project\T5\DC优化归档\01_UIEffect材质共享降DC\UIEffectContext.cs" $dst -Force
|
||||
Copy-Item "d:\Project\T5\DC优化归档\01_UIEffect材质共享降DC\UIEffectBase.cs" $dst -Force
|
||||
```
|
||||
|
||||
覆盖前**务必先备份原文件**(或确认在 git 里可回退):这两个文件是完整成品,覆盖会连带丢掉目标工程对它们的任何其它本地修改。
|
||||
|
||||
> `.meta` 文件不用管:这是"修改已存在的文件",不是"新增资源",guid 不变,prefab/场景引用不受影响。
|
||||
|
||||
### 方式 C:手工贴代码块(版本不同时的唯一选择)
|
||||
|
||||
三处改动,照着贴:
|
||||
|
||||
**C-1 `UIEffectContext.cs` 文件头加 using**
|
||||
```csharp
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection; // ← 新增这一行
|
||||
using Coffee.UIEffectInternal;
|
||||
using UnityEngine;
|
||||
```
|
||||
|
||||
**C-2 `UIEffectContext.cs` 在 `useViewMatrix` 属性之后插入 121 行**
|
||||
完整代码见本包 `01_UIEffect材质共享降DC/UIEffectContext.cs` 的 **267~385 行**(`configId` 属性 + `Mix` / `HashValue` / `HashFields` / `GradientHash` + `s_ConfigFields` / `s_FieldCache` 两个静态缓存)。
|
||||
整块是纯新增,不改任何原有代码,粘到 `useViewMatrix` 与 `private Texture2D _gradationRampTex;` 之间即可。
|
||||
|
||||
**C-3 `UIEffectBase.cs` 的 `GetModifiedMaterial()` 换 key**(T5 里约 196~203 行)
|
||||
```csharp
|
||||
Profiler.BeginSample("(UIE)[UIEffect] GetModifiedMaterial");
|
||||
var samplingScaleId = (uint)(Mathf.InverseLerp(0.01f, 100, actualSamplingScale) * uint.MaxValue);
|
||||
// 使用特效配置哈希作为材质共享 key,相同配置的特效共享同一材质,从而可以合批。
|
||||
// root 仅在特效需要视图矩阵(transition/gradation/detail/shiny)时才影响材质。
|
||||
var configId = context.configId;
|
||||
var rootId = context.useViewMatrix && transitionRoot ? (uint)transitionRoot.GetInstanceID() : 0u;
|
||||
var hash = new Hash128((uint)baseMaterial.GetInstanceID(), configId, samplingScaleId, rootId);
|
||||
if (!MaterialRepository.Valid(hash, _material))
|
||||
```
|
||||
|
||||
注意三个容易漏的细节:
|
||||
- `baseMaterial.GetHashCode()` 也要一起改成 `baseMaterial.GetInstanceID()`(`GetHashCode` 对 Object 不是稳定 ID);
|
||||
- 只改 `hash` 那一行、不改 `rootId`,会让 transition 类特效在不同 root 下错误共享材质(视觉错位);
|
||||
- `effectId` 属性**保留不删**,它是 public API,`UIEffectReplica` 还在 override 它。
|
||||
|
||||
---
|
||||
|
||||
## 第三章 移植后必做的 5 项检查
|
||||
|
||||
### 3.1 编译通过 + 无警告
|
||||
Unity Console 干净。报 `UIEffectContext does not contain a definition for 'useViewMatrix'` → 见 §3.3。
|
||||
|
||||
### 3.2 业务代码有没有绕过 context 直接改材质(**最重要**)
|
||||
|
||||
材质从"每实例独有"变成"同配置共享"后,**任何直接改 `effectMaterial` 的代码都会污染所有共享者**。
|
||||
|
||||
```powershell
|
||||
# 在目标工程业务脚本目录执行,命中即需逐个确认
|
||||
cd <目标工程>\unity\Assets
|
||||
Select-String -Path (Get-ChildItem -Recurse -Filter *.cs).FullName `
|
||||
-Pattern "effectMaterial|GetComponent<UIEffect>|UIEffect\[\]" | Select-Object Path, LineNumber, Line
|
||||
```
|
||||
|
||||
判定规则:
|
||||
- 只读(`var m = e.effectMaterial; if (m == null)`)→ 安全;
|
||||
- 写入(`e.effectMaterial.SetColor/SetFloat/SetTexture/...`)→ **危险**,必须改成"改 UIEffect 组件的序列化字段(走 context)"或"给该实例单独指定不同配置";
|
||||
- T5 实测:业务代码 0 命中,无需处理。
|
||||
|
||||
### 3.3 `useViewMatrix` 不存在的降级写法(老版本插件)
|
||||
|
||||
```csharp
|
||||
var configId = context.configId;
|
||||
// 老版本没有 useViewMatrix:保守起见让 root 始终参与,共享范围变小但结果一定正确
|
||||
var rootId = transitionRoot != null ? (uint)transitionRoot.GetInstanceID() : 0u;
|
||||
var hash = new Hash128((uint)baseMaterial.GetInstanceID(), configId, samplingScaleId, rootId);
|
||||
```
|
||||
或者自己在 `UIEffectContext` 里补一个等价属性(判断条件见 §1.2 的代码块)。
|
||||
|
||||
### 3.4 UIEffectReplica / UIEffectPreset 兼容性
|
||||
|
||||
已核对源码(`UIEffectReplica.cs` 的 `context` override),补丁后两条路径行为不变:
|
||||
|
||||
| Replica 工作模式 | context 来源 | 补丁前共享依据 | 补丁后共享依据 | 结论 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 跟随场景内的 target | `target.context`(同一对象) | `effectId = target.effectId` | 同一个 context → configId 必然相同 | 仍共享 |
|
||||
| 跟随 preset(或 target 不在场景) | 自己的 `base.context` | `effectId = preset.GetHashCode()` | preset 配置相同 → configId 相同 | 仍共享 |
|
||||
|
||||
如果目标工程用了 Replica,改完抽查一处 Replica 的显示是否正常即可(T5 只有 1 个 prefab 涉及)。
|
||||
|
||||
### 3.5 IL2CPP / 高 stripping 下的一致性(出真机包前做)
|
||||
|
||||
`configId` 依赖反射遍历字段。字段本身被 Unity 序列化系统引用,正常不会被裁剪,但**换平台后建议验一次**:
|
||||
|
||||
```csharp
|
||||
// 临时加在 UIEffectBase.GetModifiedMaterial 里,真机跑一次看日志
|
||||
Debug.Log($"[UIE] configId={configId} hash={hash} name={name}");
|
||||
```
|
||||
同一配置的元素在真机上应打出**相同的 configId**,且与编辑器一致。验完删掉这行日志。
|
||||
|
||||
---
|
||||
|
||||
## 第四章 控制台报错处理
|
||||
|
||||
### 4.1 `Non-matching Profiler.EndSample`
|
||||
|
||||
```
|
||||
Non-matching Profiler.EndSample (Every EndSample call must have a preceding BeginSample call...)
|
||||
```
|
||||
|
||||
成因:`ShaderVariantRegistry.cs` 的 `RegisterVariant` / `GetVariant` 里有**提前 return 分支没配对 EndSample**。
|
||||
|
||||
处理:文件在 `Runtime/Internal/Utilities/ShaderVariantRegistry.cs`。检查每个 `Profiler.BeginSample(...)` 之后的**所有** `return` 路径,return 前补 `Profiler.EndSample();`。
|
||||
|
||||
> T5 的 5.11.3 版本该文件配对本身已正确(`RegisterVariant` 在 301 行 Begin,307/327/333 三处 End 覆盖全部分支;`GetVariant` 在 338 行 Begin,358/370 两处 End),**无需修改**,所以本包没有归档这个补丁。
|
||||
> T11 当时是旧版本才需要补。
|
||||
|
||||
### 4.2 `NullReferenceException` 出现在 UIEffect 相关调用栈
|
||||
|
||||
九成是**删了 UIEffect 组件但业务代码还在 `GetComponent<UIEffect>()` 后用**。
|
||||
血泪教训(T5 实测踩过):**删任何 UI 组件前,先 grep 业务代码有没有引用它**,确认无引用再删。
|
||||
本补丁不删组件,正常不会触发;若你在做 DC 优化时顺手清理了"看起来没用的" UIEffect,请务必回查。
|
||||
|
||||
---
|
||||
|
||||
## 第五章 验证方法(怎么证明 DC 真的降了)
|
||||
|
||||
### 5.1 Frame Debugger 对比(最直接)
|
||||
|
||||
1. 打开同一个面板(跟 §1.3 基线同一个);
|
||||
2. `Window → Analysis → Frame Debugger` → Enable;
|
||||
3. 看 `Canvas.BuildBatch` 下的 `Draw Mesh` 数量,与基线对比;
|
||||
4. **关键验证点**:在事件树里点几个原本各自独占一批的灰度图标,看右下角 **Material / Texture** 是否变成同一个(材质名相同即共享成功)。
|
||||
|
||||
预期:同配置特效密集的面板,批次数量级下降(T5 等级礼包面板 ~101 → 30~40)。
|
||||
如果只降了一两个批次,说明该面板里 UIEffect 配置本来就各不相同(属正常,见 §7 原因 1)。
|
||||
|
||||
### 5.2 材质实例数对比(更硬的证据)
|
||||
|
||||
Play 模式下执行(临时挂个测试按钮或用 Console 执行):
|
||||
```csharp
|
||||
var all = Resources.FindObjectsOfTypeAll<Material>();
|
||||
Debug.Log($"UIEffect 材质数: {all.Count(m => m.shader != null && m.shader.name.Contains("UIEffect"))}");
|
||||
```
|
||||
补丁前:数量 ≈ 场景内 UIEffect 组件数;补丁后:数量 ≈ **不同配置的种类数**。
|
||||
|
||||
### 5.3 视觉回归(必做)
|
||||
|
||||
逐个检查这几类特效显示是否正常——它们走的是 `rootId` 参与哈希的分支,是本次改动唯一有行为差异的地方:
|
||||
- Transition(过渡/溶解)
|
||||
- Gradation(渐变)
|
||||
- Detail Filter(细节滤镜)
|
||||
- Edge Mode = Shiny(发光边缘)
|
||||
|
||||
普通灰度(ToneFilter.Grayscale)不涉及 root,风险最低。
|
||||
|
||||
---
|
||||
|
||||
## 第六章 回滚
|
||||
|
||||
```powershell
|
||||
cd <目标工程 git 根目录>
|
||||
|
||||
# 方式 A/B 打的补丁:一条命令还原这两个文件
|
||||
git checkout -- "unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectBase.cs" `
|
||||
"unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectContext.cs"
|
||||
```
|
||||
路径不同的按实际改。回滚后回 Unity 编译即可,不涉及资源/序列化数据,**无副作用**(材质缓存 key 是运行时的,不落盘)。
|
||||
|
||||
---
|
||||
|
||||
## 第七章 "改完了 DC 还是没降"的 7 个原因
|
||||
|
||||
按命中概率排序,逐条排:
|
||||
|
||||
1. **配置本来就各不相同**:一个面板里 10 个 UIEffect 用了 10 种参数组合 → 10 个材质,属预期。想再降只能统一美术配置(比如灰度就用同一套参数)。
|
||||
2. **中间夹了不能合批的元素**:合批要求"同一 Canvas + 同材质 + 同纹理 + 层级相邻"。UIEffect 元素之间插了别的图(不同图集)→ 批次被打断,即使材质相同也合不了。
|
||||
3. **Mask / RectMask2D 切割**:每个 Mask 内部独立成批,跨 Mask 永不合批。
|
||||
4. **不同图集(SpriteAtlas)**:材质相同但纹理不同 → 不合批。Frame Debugger 里看纹理名 `sactx-...-atlas-<hash>`,hash 不同就是不同图集。
|
||||
5. **Canvas 被拆成多个**(子物体挂了 `Canvas` 组件 / `SubCanvas`)→ 各自建批。
|
||||
6. **TextMeshPro 与 Image 混排**:TMP 用字体图集,跟普通 Image 不同纹理,天然分批。
|
||||
7. **补丁没生效**:改的是 `Library/PackageCache/` 下的副本(§1.1 没做对),或 Unity 没重新编译(改完必须等编译完成、必要时 `Assets → Reimport All`)。
|
||||
|
||||
排查工具与看图技巧见 `05_排查方法论/DC与帧率排查经验.md`。
|
||||
|
||||
---
|
||||
|
||||
## 第八章 升级插件时的注意事项
|
||||
|
||||
UIEffect 版本升级会**覆盖这两个文件**,补丁丢失。所以:
|
||||
|
||||
1. 补丁改动务必**提交进 git**,commit message 写明"UIEffect configId 降 DC 补丁"(T5 是 `c61b9012`,T11 是 `fa5f0cdb`);
|
||||
2. 升级插件后重跑本手册第一章的锚点核对,再按方式 A/C 重新打;
|
||||
3. 本包 `01_UIEffect材质共享降DC/` 里的两个成品 `.cs` 是 5.11.3 版本,**不要**用它覆盖更高版本的插件文件。
|
||||
@@ -0,0 +1,16 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace MFrame
|
||||
{
|
||||
/// <summary>
|
||||
/// 全屏遮挡组件:挂到面板预制体根节点(与 UI_PanelBase 同节点)即生效,移除组件即关闭。
|
||||
/// 打开本面板时自动隐藏下方层级中正在显示的面板(省去被遮住的 DrawCall),
|
||||
/// 关闭/销毁时自动恢复。由策划在编辑器中添加/移除本组件即可配置,无需改代码。
|
||||
/// 注意:仅用于完全铺满屏幕的面板(如通行证、商店、活动大页),半屏弹窗不要挂。
|
||||
/// </summary>
|
||||
public class FullScreenCover : MonoBehaviour
|
||||
{
|
||||
[Tooltip("勾选后:本面板被上层全屏面板遮挡时仍保持显示(仅用于必须常驻可见的特殊面板)。")]
|
||||
public bool isKeepShowWhenCovered = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using MFrame;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
/// <summary>
|
||||
/// 全屏面板遮挡工具:查找/添加/移除面板预制体上的 FullScreenCover 组件。
|
||||
/// 挂有该组件的面板 = 全屏面板(打开时自动隐藏下层 UI 省去 DrawCall,关闭/销毁自动恢复)。
|
||||
/// 策划可在此窗口自助管理,无需改代码。
|
||||
/// 说明:Project 窗口的 t: 搜索只能按资源类型过滤,搜不到“预制体里挂了某组件”,故提供本工具。
|
||||
/// </summary>
|
||||
public class FullScreenCoverTool : EditorWindow
|
||||
{
|
||||
private sealed class CoverInfo
|
||||
{
|
||||
public Object prefab; // 预制体资产引用(用于 Ping/打开)
|
||||
public string prefabPath; // 资源路径
|
||||
public string nodeName; // 组件所在节点名
|
||||
public bool enabled; // 组件是否启用
|
||||
public bool keepShow; // 被遮挡时保持显示
|
||||
}
|
||||
|
||||
private readonly List<CoverInfo> results = new List<CoverInfo>();
|
||||
private Vector2 scroll;
|
||||
private GameObject addTarget;
|
||||
private bool scanned;
|
||||
private string statusText = "尚未扫描,点击“扫描全工程”查找挂载 FullScreenCover 组件的面板预制体";
|
||||
|
||||
[MenuItem("Tools/UI/全屏面板遮挡工具")]
|
||||
private static void ShowWindow()
|
||||
{
|
||||
var win = GetWindow<FullScreenCoverTool>("全屏面板遮挡");
|
||||
win.minSize = new Vector2(540f, 320f);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// 打开窗口时自动扫描一次
|
||||
EditorApplication.delayCall += ScanAll;
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
"挂有 FullScreenCover 组件的面板 = 全屏面板:打开时自动隐藏下层 UI(省去被遮住的 DrawCall),关闭/销毁自动恢复。\n" +
|
||||
"注意:仅用于完全铺满屏幕的面板(如通行证、商店、活动大页),半屏弹窗不要挂。", MessageType.Info);
|
||||
|
||||
// —— 添加区 ——
|
||||
EditorGUILayout.LabelField("添加组件(拖入面板预制体)", EditorStyles.boldLabel);
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
addTarget = (GameObject)EditorGUILayout.ObjectField(addTarget, typeof(GameObject), false);
|
||||
using (new EditorGUI.DisabledScope(addTarget == null))
|
||||
{
|
||||
if (GUILayout.Button("添加组件", GUILayout.Width(80f)))
|
||||
{
|
||||
AddComponentToPrefab(AssetDatabase.GetAssetPath(addTarget));
|
||||
addTarget = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(4f);
|
||||
|
||||
// —— 扫描区 ——
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
if (GUILayout.Button("扫描全工程", GUILayout.Height(24f)))
|
||||
{
|
||||
ScanAll();
|
||||
}
|
||||
GUILayout.Label(statusText, EditorStyles.miniLabel);
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(4f);
|
||||
|
||||
// —— 结果列表 ——
|
||||
if (!scanned)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (results.Count == 0)
|
||||
{
|
||||
EditorGUILayout.HelpBox("没有找到任何挂载 FullScreenCover 的面板。可用上方“添加组件”为全屏面板配置。", MessageType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
scroll = EditorGUILayout.BeginScrollView(scroll);
|
||||
for (int i = 0; i < results.Count; i++)
|
||||
{
|
||||
DrawResultItem(results[i]);
|
||||
}
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
private void DrawResultItem(CoverInfo info)
|
||||
{
|
||||
using (new EditorGUILayout.VerticalScope("box"))
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
// 点击名称定位资源
|
||||
string title = info.prefab != null ? info.prefab.name : Path.GetFileName(info.prefabPath);
|
||||
if (GUILayout.Button(title, EditorStyles.boldLabel, GUILayout.Width(170f)))
|
||||
{
|
||||
EditorGUIUtility.PingObject(info.prefab);
|
||||
}
|
||||
|
||||
GUILayout.Label(info.enabled ? "已启用" : "已禁用",
|
||||
info.enabled ? EditorStyles.miniLabel : EditorStyles.whiteMiniLabel, GUILayout.Width(46f));
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
// 被遮挡时保持显示(勾选即写回预制体)
|
||||
bool newKeep = GUILayout.Toggle(info.keepShow, "被遮挡时保持显示", GUILayout.Width(132f));
|
||||
if (newKeep != info.keepShow)
|
||||
{
|
||||
info.keepShow = newKeep;
|
||||
SetKeepShow(info.prefabPath, newKeep);
|
||||
}
|
||||
|
||||
if (GUILayout.Button("打开", GUILayout.Width(46f)))
|
||||
{
|
||||
AssetDatabase.OpenAsset(info.prefab);
|
||||
}
|
||||
if (GUILayout.Button("移除组件", GUILayout.Width(68f)))
|
||||
{
|
||||
if (EditorUtility.DisplayDialog("移除 FullScreenCover",
|
||||
$"确定从 {Path.GetFileName(info.prefabPath)} 移除 FullScreenCover 组件?\n该面板将不再自动隐藏下层。",
|
||||
"移除", "取消"))
|
||||
{
|
||||
RemoveComponent(info.prefabPath);
|
||||
ScanAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
EditorGUILayout.LabelField(info.prefabPath, EditorStyles.miniLabel);
|
||||
if (!info.enabled)
|
||||
{
|
||||
EditorGUILayout.LabelField("⚠ 组件处于禁用状态,不会生效(在预制体根节点 Inspector 勾选启用)。", EditorStyles.miniLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// —— 扫描 ——
|
||||
|
||||
private void ScanAll()
|
||||
{
|
||||
string scriptGuid = GetScriptGuid();
|
||||
if (string.IsNullOrEmpty(scriptGuid))
|
||||
{
|
||||
EditorUtility.DisplayDialog("错误", "无法获取 FullScreenCover 脚本 GUID,请确认脚本已编译无报错。", "确定");
|
||||
return;
|
||||
}
|
||||
|
||||
string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab", new[] { "Assets" });
|
||||
results.Clear();
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < prefabGuids.Length; i++)
|
||||
{
|
||||
if (EditorUtility.DisplayCancelableProgressBar("全屏面板遮挡工具",
|
||||
$"扫描预制体中... ({i + 1}/{prefabGuids.Length})", (float)i / prefabGuids.Length))
|
||||
{
|
||||
break; // 用户取消
|
||||
}
|
||||
|
||||
string path = AssetDatabase.GUIDToAssetPath(prefabGuids[i]);
|
||||
if (string.IsNullOrEmpty(path) || !path.EndsWith(".prefab"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 文本快筛:只有包含组件脚本 guid 的预制体才真正加载,避免逐个解析
|
||||
if (!ContainsGuidFast(path, scriptGuid))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
||||
if (prefab == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
FullScreenCover[] comps = prefab.GetComponentsInChildren<FullScreenCover>(true);
|
||||
foreach (FullScreenCover comp in comps)
|
||||
{
|
||||
results.Add(new CoverInfo
|
||||
{
|
||||
prefab = prefab,
|
||||
prefabPath = path,
|
||||
nodeName = comp.gameObject.name,
|
||||
enabled = comp.enabled,
|
||||
keepShow = comp.isKeepShowWhenCovered
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
EditorUtility.ClearProgressBar();
|
||||
}
|
||||
|
||||
scanned = true;
|
||||
statusText = $"扫描完成:{results.Count} 个面板预制体挂载 FullScreenCover";
|
||||
Repaint();
|
||||
}
|
||||
|
||||
/// <summary>动态获取 FullScreenCover 脚本资产的 guid,避免硬编码。</summary>
|
||||
private static string GetScriptGuid()
|
||||
{
|
||||
GameObject probe = new GameObject("FullScreenCoverGuidProbe");
|
||||
try
|
||||
{
|
||||
MonoScript ms = MonoScript.FromMonoBehaviour(probe.AddComponent<FullScreenCover>());
|
||||
if (ms == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(ms));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.DestroyImmediate(probe);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>逐行读取文件快速判断是否包含 guid(找到即中断),比加载预制体快一个量级。</summary>
|
||||
private static bool ContainsGuidFast(string path, string guid)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (string line in File.ReadLines(path))
|
||||
{
|
||||
if (line.Contains(guid))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// 文件被占用等情况按未命中处理
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// —— 修改预制体 ——
|
||||
|
||||
/// <summary>以“加载内容-修改-保存-卸载”的标准流程安全修改预制体资产。</summary>
|
||||
private static void EditPrefab(string path, System.Action<GameObject> edit)
|
||||
{
|
||||
GameObject root = PrefabUtility.LoadPrefabContents(path);
|
||||
try
|
||||
{
|
||||
edit(root);
|
||||
PrefabUtility.SaveAsPrefabAsset(root, path);
|
||||
}
|
||||
finally
|
||||
{
|
||||
PrefabUtility.UnloadPrefabContents(root);
|
||||
}
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
|
||||
private void AddComponentToPrefab(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path) || !path.EndsWith(".prefab"))
|
||||
{
|
||||
EditorUtility.DisplayDialog("无法添加", "请拖入一个预制体资产(.prefab),而不是场景对象或模型。", "确定");
|
||||
return;
|
||||
}
|
||||
|
||||
bool already = false;
|
||||
EditPrefab(path, root =>
|
||||
{
|
||||
if (root.GetComponent<FullScreenCover>() != null)
|
||||
{
|
||||
already = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
root.AddComponent<FullScreenCover>();
|
||||
}
|
||||
});
|
||||
|
||||
ShowNotification(new GUIContent(already ? "该预制体已挂载组件" : "已添加 FullScreenCover"));
|
||||
ScanAll();
|
||||
}
|
||||
|
||||
private void RemoveComponent(string path)
|
||||
{
|
||||
EditPrefab(path, root =>
|
||||
{
|
||||
FullScreenCover[] comps = root.GetComponentsInChildren<FullScreenCover>(true);
|
||||
foreach (FullScreenCover comp in comps)
|
||||
{
|
||||
Object.DestroyImmediate(comp, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void SetKeepShow(string path, bool value)
|
||||
{
|
||||
EditPrefab(path, root =>
|
||||
{
|
||||
FullScreenCover[] comps = root.GetComponentsInChildren<FullScreenCover>(true);
|
||||
foreach (FullScreenCover comp in comps)
|
||||
{
|
||||
comp.isKeepShowWhenCovered = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
# FullScreenCover 全屏遮挡省 DC — 使用方法与避坑
|
||||
|
||||
> 源文件:`my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/FullScreenCover.cs`(组件,17 行)
|
||||
> 配套工具:`Assets/Editor/FullScreenCoverTool.cs`(菜单 `Tools/UI/全屏面板遮挡工具`)
|
||||
> 验证环境:Unity 团结引擎(Tuanjie)+ MFrame 框架 + URP
|
||||
> **一句话:全屏面板打开时,把它下面那些"反正被盖住看不见"的界面 `SetActive(false)`,这些界面就不再提交 DrawCall;全屏面板关闭时自动恢复。业务代码零改动,策划在编辑器里挂个组件就能配。**
|
||||
|
||||
---
|
||||
|
||||
## 第一章 它到底省了多少(T5 实测数据)
|
||||
|
||||
| 项目 | 优化前 | 优化后 |
|
||||
| --- | --- | --- |
|
||||
| 大厅整帧批次数 | 约 100 | **35** |
|
||||
| 其中 SSAO 占用 | 4 个 DC | 0(见 `03_渲染设置与资源清理/`) |
|
||||
| LevelGiftPanel 打开时 | 自身 101 批 | 见 `01_UIEffect材质共享降DC/` |
|
||||
|
||||
遮挡机制本身的收益验证方式很直观:**打开全屏面板时,去 Hierarchy 里看 MainPanel / HomePanel 是不是变灰了**(变灰 = `SetActive(false)` = 不再产生 DC)。这是用户在 T5 上亲手确认过的。
|
||||
|
||||
---
|
||||
|
||||
## 第二章 5 分钟配置(策划操作,不用碰代码)
|
||||
|
||||
### 方式 A:用编辑器工具(推荐)
|
||||
|
||||
菜单:`Tools/UI/全屏面板遮挡工具`
|
||||
|
||||
窗口打开时会**自动扫描全工程**,列出所有已挂组件的面板。三个功能区:
|
||||
|
||||
| 操作 | 怎么做 |
|
||||
| --- | --- |
|
||||
| **给新面板加遮挡** | 把面板预制体拖进上方"添加组件"槽 → 点【添加组件】 |
|
||||
| **改"被遮挡时保持显示"** | 在结果列表里直接勾/取消那个复选框,**勾完立刻写回预制体**,不用再点保存 |
|
||||
| **取消某个面板的遮挡** | 点该行【移除组件】→ 二次确认弹窗 → 移除 |
|
||||
| **定位预制体** | 点面板名字 = Ping 到 Project 窗口;点【打开】= 进预制体编辑模式 |
|
||||
|
||||
列表还会告诉你每个面板的**组件启用状态**。如果显示"已禁用",说明组件被人在 Inspector 里取消勾选了,这时**不会生效**,窗口会用黄字提醒你。
|
||||
|
||||
### 方式 B:手动挂(工具不在手边时)
|
||||
|
||||
1. 打开面板预制体,选中**根节点**(必须和 `UI_PanelBase` 在同一个节点上);
|
||||
2. Inspector → Add Component → 搜 `FullScreenCover`;
|
||||
3. 完事。不需要写任何代码,也不需要注册。
|
||||
|
||||
> ⚠️ **必须挂在根节点**。框架是在 `_TryInitGameObject` 里用 `transform.GetComponent<FullScreenCover>()` 找的(非递归),挂在子节点上**完全无效**且不会报错——这是最容易踩的空坑。
|
||||
|
||||
### `isKeepShowWhenCovered` 这个勾到底啥意思
|
||||
|
||||
它是**"我不要被隐藏"的豁免权**,配在**被遮挡的那个面板**身上,不是配在全屏面板身上。
|
||||
|
||||
```
|
||||
通行证(全屏,挂了 FullScreenCover)打开
|
||||
↓ 会去隐藏所有比它层级低的面板
|
||||
主界面 MainPanel → 没勾豁免 → 被隐藏(省 DC)✅
|
||||
某个常驻活动图标面板 → 勾了豁免 → 保持显示(仍然出 DC)
|
||||
```
|
||||
|
||||
**什么时候才该勾**:这个面板必须一直可见,哪怕被全屏页盖住。比如某些常驻的悬浮入口、全局提示条。
|
||||
|
||||
**绝大多数面板都不要勾**。勾了就等于放弃这个面板的省 DC 收益。T5 现在的 7 个面板**全部没勾**(keepShow=0),这是正确状态。
|
||||
|
||||
> 历史备注:T5 早期有 6 个面板被误勾成 keepShow=1,后来在提交 `c61b9012` 里统一改回 0。当时的判断是"商店和通行证业务互斥,不会同时出现",但勾了这个之后**未来任何更高层的全屏页叠加都会失效**,所以还是改回来了。
|
||||
|
||||
---
|
||||
|
||||
## 第三章 哪些面板能挂 / 绝对不能挂
|
||||
|
||||
### ✅ 可以挂
|
||||
|
||||
**完全铺满屏幕、打开后下层一点都看不见**的面板:
|
||||
|
||||
- 通行证、商店、抽卡、宠物、技能、神话英雄、等级礼包(T5 现有 7 个)
|
||||
- 活动大页、图鉴、邮件全屏页、设置大页
|
||||
|
||||
### ❌ 绝对不能挂
|
||||
|
||||
| 类型 | 为什么不能挂 |
|
||||
| --- | --- |
|
||||
| **半屏弹窗**(确认框、奖励弹窗、底部抽屉) | 它盖不住下层,但会把下层隐藏 → **玩家看到下半截界面凭空消失** |
|
||||
| **带半透明背景的面板** | 同理,透出来的部分会是黑的/空的 |
|
||||
| **顶部飘字、Toast、加载条** | 这些在高层级,本身不会去隐藏别人;挂了也没意义 |
|
||||
| **战斗中需要看到主界面的面板** | 比如伤害飘字叠加在大厅上的情况 |
|
||||
|
||||
**判断口诀**:把这个面板打开,**肉眼能不能看到它下面的任何界面**?能看到 → 不能挂。
|
||||
|
||||
---
|
||||
|
||||
## 第四章 原理(通俗版)
|
||||
|
||||
Unity 的 UI 只要 GameObject 是**激活状态**,哪怕被别的界面完全盖住、一个像素都看不见,它**照样提交 DrawCall**——GPU 老老实实画一遍然后被上面的盖掉。这是纯浪费。
|
||||
|
||||
`FullScreenCover` 做的事就是:**打开全屏面板的瞬间,把下面那些白画一遍的界面关掉(`SetActive(false)`);关闭全屏面板时再打开回来。**
|
||||
|
||||
关键点:
|
||||
|
||||
1. **不是销毁,是隐藏**。对象还在内存里,还在面板管理器的注册表里,重新激活是瞬间的,不需要重新加载资源。
|
||||
2. **只隐藏"比自己层级低"的面板**。判定条件是 `panel.panelLayerIdx < _layerIdx`(严格小于),所以同层和更高层的面板不受影响。
|
||||
3. **业务语义不变**。被隐藏的面板调 `isShowing()` 仍然返回 `true`(框架特意这么写的),业务逻辑感知不到自己被藏了。
|
||||
|
||||
层级从低到高(`UILayerIdx` 枚举):
|
||||
|
||||
```
|
||||
layer_0 背景层 → layer_1 游戏层 → layer_2 UI基础层 → layer_3 功能层
|
||||
→ layer_4 弹窗层 → layer_5 提示层 → layer_6 特效层 → layer_7 通知层 → layer_8 加载层
|
||||
```
|
||||
|
||||
所以挂在 `layer_4`(弹窗层)的通行证,打开时会隐藏 `layer_0~layer_3` 里正在显示的面板(大厅 MainPanel、HomePanel 等)。
|
||||
|
||||
---
|
||||
|
||||
## 第五章 原理(程序版:改了哪 4 个文件)
|
||||
|
||||
补丁:`fullscreen_cover_mechanism.patch`(4 文件,+474 行)
|
||||
|
||||
| 文件 | 改动 |
|
||||
| --- | --- |
|
||||
| `FullScreenCover.cs` | **新建**。17 行纯配置组件,只有一个 `isKeepShowWhenCovered` 字段 |
|
||||
| `iface/IPanelBase.cs` | **新增 5 个成员**:`panelLayerIdx`、`isKeepShowWhenCovered`、`isHiddenByCover`(只读)+ `SetCoverHidden()`、`RestoreFromCover()`(方法) |
|
||||
| `PanelBase.cs` | **核心逻辑**,见下表 |
|
||||
| `FullScreenCoverTool.cs` | **新建**编辑器工具(Assets/Editor 下,不进包) |
|
||||
|
||||
`PanelBase.cs` 的 6 个改动点:
|
||||
|
||||
| 位置 | 代码 | 作用 |
|
||||
| --- | --- | --- |
|
||||
| 字段 | `_fullScreenCover` / `_hiddenByCover` / `_panelsHiddenByMe` | 组件缓存 / 我被藏了吗 / 我藏了谁 |
|
||||
| `_TryInitGameObject` | `_fullScreenCover = transform.GetComponent<FullScreenCover>();` | 加载完预制体后取组件(**只取根节点**) |
|
||||
| `Show()` | `if (_fullScreenCover != null && _fullScreenCover.enabled) HidePanelsUnderThis();` | 打开时隐藏下层 |
|
||||
| `Show()` | `if (_hiddenByCover) RestoreFromCover();` | 被藏着的面板主动 Show 时先恢复自己 |
|
||||
| `___CloseForPanelMgr()` | 关闭动画回调里 `RestorePanelsHiddenByMe();` | 关闭时恢复下层 |
|
||||
| `_Destroy()` | 开头 `RestorePanelsHiddenByMe();` | 兜底:没走 Close 直接销毁也要恢复 |
|
||||
| `isShowing()` | `return _view.gameObject.activeSelf \|\| _hiddenByCover;` | 被藏着也算"正在显示" |
|
||||
|
||||
两个恢复函数都做了**幂等**处理:`RestorePanelsHiddenByMe()` 遍历时检查 `panel.isHiddenByCover`,`RestoreFromCover()` 开头检查 `if (!_hiddenByCover) return;`。重复调用不会出错。
|
||||
|
||||
`HidePanelsUnderThis()` 的筛选条件(4 个全满足才隐藏):
|
||||
|
||||
```csharp
|
||||
if (panel.panelLayerIdx < _layerIdx // 1. 层级比我低
|
||||
&& !panel.isKeepShowWhenCovered // 2. 没勾豁免
|
||||
&& !panel.isHiddenByCover // 3. 还没被别人藏着(避免重复记录)
|
||||
&& panel.isShowing()) // 4. 当前正在显示
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第六章 移植到新项目
|
||||
|
||||
### 6.1 前提核对(3 项,缺一不可)
|
||||
|
||||
| 前提 | 怎么查 | 不满足怎么办 |
|
||||
| --- | --- | --- |
|
||||
| 用的是 MFrame 的 `PanelBase<V>` + `PanelMgr` 面板体系 | 搜 `class PanelBase<V> : UIBase, IPanelBase` | 不是的话本机制**不能直接搬**,得按 6.3 的思路重写 |
|
||||
| 有 `UILayerIdx` 层级枚举,且每个面板知道自己层级 | 搜 `enum UILayerIdx` 和 `_layerIdx` | 需要自己补一个层级来源 |
|
||||
| 面板预制体根节点上有 `UI_PanelBase` | 打开任一面板预制体看根节点 | 组件要挂在同一节点 |
|
||||
|
||||
### 6.2 移植步骤
|
||||
|
||||
1. **应用补丁**(推荐,能看清每行改了什么):
|
||||
```powershell
|
||||
cd <新项目的 T5_game 对应目录>
|
||||
git apply --stat <归档路径>/02_全屏面板遮挡省DC/fullscreen_cover_mechanism.patch
|
||||
git apply --check <归档路径>/02_全屏面板遮挡省DC/fullscreen_cover_mechanism.patch
|
||||
git apply <归档路径>/02_全屏面板遮挡省DC/fullscreen_cover_mechanism.patch
|
||||
```
|
||||
`--check` 不报错才继续。报错通常是目标工程的 `PanelBase.cs` 版本与 T5 有差异 → 改用第 2 步。
|
||||
|
||||
2. **兜底:直接覆盖 4 个成品文件**(本目录里的 `FullScreenCover.cs`、`IPanelBase.cs`、`PanelBase.cs`、`FullScreenCoverTool.cs`)。
|
||||
⚠️ 覆盖 `PanelBase.cs` 前**必须先 diff**,确认新项目的 PanelBase 没有 T5 之外的自定义改动,否则会覆盖掉别人的代码。
|
||||
|
||||
3. **改命名空间**:如果新项目不叫 `MFrame`,全局替换。
|
||||
|
||||
4. **编译通过后配置面板**:用 `Tools/UI/全屏面板遮挡工具` 逐个给全屏面板挂组件。**一次只挂一个,挂完立刻进游戏验证**(打开→看 Hierarchy 变灰→关闭→看下层恢复)。
|
||||
|
||||
5. **验证收益**:`Window → Analysis → Frame Debugger`,对比挂载前后同一画面的批次数。
|
||||
|
||||
### 6.3 如果新项目不是 MFrame 体系
|
||||
|
||||
机制本身不依赖 MFrame,只需要三个能力:
|
||||
|
||||
1. 面板管理器能**枚举所有正在显示的面板**及其**层级**;
|
||||
2. 每个面板能**被隐藏 / 被恢复**(`SetActive(false/true)`);
|
||||
3. 面板**打开时**和**关闭完成时**各有一个可插入的时机点。
|
||||
|
||||
把 `HidePanelsUnderThis()` / `RestorePanelsHiddenByMe()` 两个方法照搬过去,接到这三个点上即可。**第七章的两个陷阱同样适用,务必一起看。**
|
||||
|
||||
---
|
||||
|
||||
## 第七章 ★两个已知陷阱(T5 真实事故复盘)
|
||||
|
||||
这两个都是**机制本身的设计边界**,不是 bug。移植到新项目时**必须一并注意**。
|
||||
|
||||
### 陷阱一:已经 Close 掉的面板被"复活"(T5 副本异常,真实发生)
|
||||
|
||||
**现象**:副本界面挂了 `FullScreenCover`,点"开始挑战"后出现异常,看起来像"主界面找不到了"。
|
||||
|
||||
**根因**(提交 `cdbbca4b` → `d9fc47e0` 处置):
|
||||
|
||||
| 时刻 | 发生了什么 | 状态 |
|
||||
| --- | --- | --- |
|
||||
| T0 | 打开 RaidSelectPanel(挂了 FullScreenCover + Scale 关闭动画 0.15s) | MainPanel/HomePanel 被 `SetActive(false)`,记进 RaidSelect 的 `_panelsHiddenByMe` |
|
||||
| T1 | 点"开始挑战" → 战斗加载完成回调里业务主动执行 `PanelMgr.Close(MainPanel)` + `Close(RaidSelectPanel)` | MainPanel **业务语义已关闭**,但物理对象还在(`autoDestroy=false`,Close 只是 inactive) |
|
||||
| T2 | 0.15 秒后 RaidSelectPanel 关闭动画播完 → `RestorePanelsHiddenByMe()` | 遍历列表,看到 MainPanel 的 `isHiddenByCover == true` → **`SetActive(true)` 把它复活了** |
|
||||
| T3 | 后续所有检查逻辑 | MainPanel 处于"**业务说它关了、物理对象却活着**"的矛盾状态,检查分支全乱 |
|
||||
|
||||
问题出在 `RestorePanelsHiddenByMe()` 的守卫**只看"是否被遮挡隐藏",不看"是否已被业务关闭"**:
|
||||
|
||||
```csharp
|
||||
if (panel != null && panel.isHiddenByCover) // ← 少了 isClosed 判断
|
||||
{
|
||||
panel.RestoreFromCover();
|
||||
}
|
||||
```
|
||||
|
||||
**T5 的处置**:提交 `d9fc47e0 debug` 里直接把 RaidSelectPanel 的 `FullScreenCover` 组件**摘掉了**(保守做法,不动框架)。所以现在的 7 个挂载面板里没有 RaidSelectPanel。
|
||||
|
||||
**⚠️ 框架层隐患至今未修**:`PanelBase.cs` 里**没有任何 `_closed` 守卫**(已确认,全文搜 `_closed`/`isClosed` 零命中)。只要再出现"某面板在被遮挡状态下被业务 Close,且它 `autoDestroy=false`",同样的复活就会重演。
|
||||
|
||||
**推荐的框架层修复(约 4 行,不破坏遮挡机制)**:
|
||||
|
||||
```csharp
|
||||
// ① IPanelBase.cs 新增只读成员
|
||||
/// <summary>业务是否已关闭(区别于"被遮挡隐藏")</summary>
|
||||
bool isClosed { get; }
|
||||
|
||||
// ② PanelBase.cs 新增字段与实现
|
||||
private bool _closed;
|
||||
bool IPanelBase.isClosed => _closed;
|
||||
|
||||
// ③ PanelBase.Show() 里,_data = data; 之后加一行
|
||||
_closed = false;
|
||||
|
||||
// ④ PanelBase.___CloseForPanelMgr(Action complete) 方法体第一行加
|
||||
_closed = true;
|
||||
|
||||
// ⑤ isShowing() 改为
|
||||
return !_closed && (_view.gameObject.activeSelf || _hiddenByCover);
|
||||
|
||||
// ⑥ RestorePanelsHiddenByMe() 的守卫改为
|
||||
if (panel != null && panel.isHiddenByCover && !panel.isClosed)
|
||||
```
|
||||
|
||||
**为什么 `_closed = true` 要放在 `___CloseForPanelMgr` 而不是 `Close()`**:
|
||||
`PanelBase.Close()` 只是转发给 `PanelMgr.Instance.Close(_registerModule)`,而业务代码里很多地方**直接调 `PanelMgr.Close(...)`**(比如 `EntrancePanel._CloseAllPanel`),根本不经过 `PanelBase.Close()`。`___CloseForPanelMgr` 是 PanelMgr 回调面板的**唯一内部关闭入口**,所有关闭路径都必经此处,守卫放这里才没有漏网。
|
||||
|
||||
**这个修复的安全性**:只增加一个"已关闭就别复活"的判断,不改隐藏逻辑、不改层级判定、不删任何资源。已关闭的面板本来就不该被显示出来。
|
||||
|
||||
### 陷阱二:被遮挡(inactive)的面板如果挂动画 tween,Close 会卡死
|
||||
|
||||
Unity 的硬规则:**`MonoBehaviour.Update()` 在 `SetActive(false)` 的对象上完全不运行。**
|
||||
|
||||
MFrame 的 `PanelTween_Scale` 这类动画 tween 是由 `AbsCom.Update()` 驱动的,关闭回调挂在 tween 播完之后。所以:
|
||||
|
||||
```
|
||||
面板被遮挡 → inactive → 此时业务 Close 它
|
||||
→ tween 的 Update 不跑 → 动画永远停在第一帧
|
||||
→ 关闭回调永不触发 → RestorePanelsHiddenByMe() 永不执行
|
||||
→ 这个面板永远关不掉,它藏着的下层面板也永远出不来
|
||||
```
|
||||
|
||||
**T5 当前安全的原因**:MainPanel 挂的是 `PanelTween_None`(同步回调,不依赖 Update),所以走的是"立即完成"路径,不会卡。
|
||||
|
||||
**规避方法(三选一)**:
|
||||
1. **给可能被遮挡的面板(尤其是主界面)用 `PanelTween_None`**,别用动画版 —— T5 现在就是这个状态;
|
||||
2. 业务在 Close 一个可能被遮挡的面板前,先确认它 `isHiddenByCover`,是的话先让它恢复;
|
||||
3. 框架层在 `___CloseForPanelMgr` 开头补一句:若 `_hiddenByCover` 则先 `RestoreFromCover()` 再播关闭动画。
|
||||
|
||||
**排查这个陷阱的特征**:面板"关不掉"、下层界面"再也不出现"、没有任何报错、Hierarchy 里那个面板一直是灰的。看到这三个特征同时出现,直接查这条。
|
||||
|
||||
---
|
||||
|
||||
## 第八章 排查技巧
|
||||
|
||||
### 8.1 ⚠️ 团结引擎的 guid 坑(文本反查会落空)
|
||||
|
||||
T5 用的是团结引擎(Tuanjie),`.cs.meta` 里的 guid 是**加密长串**(56 字符,含 `/` `+` `=`):
|
||||
|
||||
```
|
||||
guid: Dysd4X//V3yyisRZkbBdkO/Yqfm+oXtw45Vx0v70tF8fEn5k7g7wkdI=
|
||||
```
|
||||
|
||||
而 **prefab 里写入的不是这个字符串**,是转换后的 32 位 hex:
|
||||
|
||||
```
|
||||
FullScreenCover 在 prefab 中的 hex = 0a2deb478a85eef4883e8465d48c426f
|
||||
```
|
||||
|
||||
所以「拿 `.meta` 的 guid 去 grep prefab」**永远搜不到**(我亲自踩过,两种写法都失败)。
|
||||
|
||||
**正确做法(两条,任选)**:
|
||||
|
||||
```powershell
|
||||
# 做法一:搜组件的序列化字段名(最简单,不依赖 guid)
|
||||
Get-ChildItem "Assets\Resources" -Recurse -Include *.prefab |
|
||||
Where-Object { Select-String -Path $_.FullName -Pattern 'isKeepShowWhenCovered' -Quiet } |
|
||||
ForEach-Object { $_.Name }
|
||||
|
||||
# 做法二:用转换后的 hex(能顺便确认组件引用完整)
|
||||
Get-ChildItem "Assets\Resources" -Recurse -Include *.prefab |
|
||||
Where-Object { Select-String -Path $_.FullName -Pattern '0a2deb478a85eef4883e8465d48c426f' -Quiet }
|
||||
```
|
||||
|
||||
两种做法在 T5 上都返回**同样的 7 个面板**,可以互相验证。
|
||||
|
||||
> 移植到别的项目后 hex 会不同(跟 guid 绑定),用做法一最稳。
|
||||
|
||||
### 8.2 验证机制是否生效(3 步)
|
||||
|
||||
1. **Hierarchy 变灰法**(最快):Play → 打开全屏面板 → 看 Hierarchy 里 MainPanel/HomePanel 是否变灰 → 关闭 → 是否恢复。变灰即生效。
|
||||
2. **Frame Debugger 对照法**(量化):打开全屏面板,数批次数;然后临时把组件取消勾选(Inspector 里去掉 `FullScreenCover` 的启用勾)再数一次。差值就是省下的 DC。
|
||||
3. **组件状态自查**:`Tools/UI/全屏面板遮挡工具` 扫一遍,看有没有"已禁用"的黄字警告。
|
||||
|
||||
### 8.3 临时禁用做对照实验(不改预制体)
|
||||
|
||||
不需要移除组件,两种无损办法:
|
||||
|
||||
- **编辑器里**:选中预制体根节点 → Inspector 里把 `FullScreenCover` 组件**左上角的勾去掉** → Apply。框架判断的是 `_fullScreenCover.enabled`,取消勾选即完全失效。实验完再勾回来。
|
||||
- **运行时代码**(临时调试用):
|
||||
```csharp
|
||||
var cover = panel.view.GetComponent<MFrame.FullScreenCover>();
|
||||
if (cover != null) cover.enabled = false; // 之后 Show 就不会隐藏下层
|
||||
```
|
||||
|
||||
### 8.4 出问题时看哪里
|
||||
|
||||
| 症状 | 优先查 |
|
||||
| --- | --- |
|
||||
| 打开全屏面板后下层界面消失且不恢复 | 陷阱二(tween 卡死)+ 该面板 `autoDestroy` 配置 |
|
||||
| 已关闭的界面自己冒出来了 | 陷阱一(复活),查是谁在 Close 之后又 SetActive |
|
||||
| 挂了组件但完全没效果 | ①组件是否挂在**根节点** ②组件是否被禁用 ③面板层级是否真的比目标高 |
|
||||
| 半屏弹窗导致下层消失 | 挂错了,该面板不该挂(第三章) |
|
||||
|
||||
---
|
||||
|
||||
## 第九章 T5 当前挂载现状(截至归档时)
|
||||
|
||||
7 个面板,全部 `keepShow=0`(正确配置)、组件全部启用:
|
||||
|
||||
| 面板预制体 | 业务 | keepShow |
|
||||
| --- | --- | --- |
|
||||
| `UI_DrawCardPanel` | 抽卡 | 0 |
|
||||
| `UI_LevelGiftPanel` | 等级礼包 | 0 |
|
||||
| `UI_MythHeroPanel` | 神话英雄 | 0 |
|
||||
| `UI_PassPanel` | 通行证 | 0 |
|
||||
| `UI_PetPanel` | 宠物 | 0 |
|
||||
| `UI_ShopPanel` | 商店 | 0 |
|
||||
| `UI_SkillPanel` | 技能 | 0 |
|
||||
|
||||
**`UI_RaidSelectPanel`(副本选择)不在列表里** —— 它曾经挂过(`cdbbca4b` 加上,keepShow=1 → `c61b9012` 改 0 → `d9fc47e0` 整个组件摘掉),原因就是第七章陷阱一的事故。
|
||||
|
||||
**如果第七章的框架层守卫修好了,RaidSelectPanel 可以重新挂回去**(它是全屏面板,本身符合挂载条件),能再省一批 DC。在守卫没修之前**不要挂**。
|
||||
|
||||
---
|
||||
|
||||
## 本目录文件清单
|
||||
|
||||
| 文件 | 说明 |
|
||||
| --- | --- |
|
||||
| `FullScreenCover.cs` | 组件本体(17 行,挂预制体根节点) |
|
||||
| `IPanelBase.cs` | 接口(新增 5 个成员) |
|
||||
| `PanelBase.cs` | 核心实现(遮挡/恢复全部逻辑在这) |
|
||||
| `FullScreenCoverTool.cs` | 编辑器工具(放 `Assets/Editor/`,不进包) |
|
||||
| `fullscreen_cover_mechanism.patch` | 上述 4 文件的 git 标准补丁(+474 行) |
|
||||
| `FullScreenCover_使用方法与避坑.md` | 本文 |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 第十章 T11 移植增强记录(2026-09-10)
|
||||
|
||||
> 本目录的 4 个 `.cs` 成品文件已同步为 **T11_game 验证过的增强版**(含第七章两个陷阱的框架层防护)。
|
||||
> 原始 T5 版本保留在 `fullscreen_cover_mechanism_T5原始.patch`,仅供历史对照,移植时不要用它。
|
||||
|
||||
### 相比 T5 原版的 2 处增强(均落在 PanelBase.cs / IPanelBase.cs)
|
||||
|
||||
**增强 1:`_closed` 复活守卫**(= 第七章陷阱一的框架层修复,按原文档推荐方案落地)
|
||||
|
||||
- `IPanelBase` 新增 `bool isClosed { get; }`;
|
||||
- `PanelBase` 新增 `_closed` 字段:`Show()` 时置 false,`___CloseForPanelMgr()` 开头置 true;
|
||||
- `isShowing()` 改为 `return !_closed && (_view.gameObject.activeSelf || _hiddenByCover);`;
|
||||
- `RestorePanelsHiddenByMe()` 恢复守卫加 `!panel.isClosed`——**已关闭的面板永不会被"复活"**。
|
||||
|
||||
**增强 2:被遮挡面板关闭前先恢复**(= 第七章陷阱二的规避方法 3)
|
||||
|
||||
- `___CloseForPanelMgr()` 开头:若 `_hiddenByCover` 则先 `RestoreFromCover()` 再播关闭动画。
|
||||
- 防止面板在 inactive 状态下关闭时 tween 的 `Update` 不跑、关闭回调永不触发、下层面板永远出不来。
|
||||
|
||||
### 移植到新项目时的文件对应关系
|
||||
|
||||
| 本目录文件 | 目标位置 |
|
||||
| --- | --- |
|
||||
| `FullScreenCover.cs` | `my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/` |
|
||||
| `IPanelBase.cs` / `PanelBase.cs` | 同上(`iface/` 子目录)——**建议只看 `fullscreen_cover_t11_guard_mechanism.patch` 手工合入**,别整文件覆盖 |
|
||||
| `FullScreenCoverTool.cs` | `Assets/Editor/`(不进包) |
|
||||
|
||||
### T11 实际改动验证
|
||||
|
||||
`fullscreen_cover_t11_guard_mechanism.patch` 由 T11_game 仓库 `git diff` 直接导出,覆盖 `PanelBase.cs`(+135/-3)与 `IPanelBase.cs`(+28)两文件;`FullScreenCover.cs`、`FullScreenCoverTool.cs` 为全新文件,直接复制即可。
|
||||
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
|
||||
namespace MFrame
|
||||
{
|
||||
/// <summary>
|
||||
/// UI面板基础接口
|
||||
/// </summary>
|
||||
/// <typeparam name="T">面板数据类型参数</typeparam>
|
||||
public interface IPanelBase : IDestroy
|
||||
{
|
||||
/// <summary>
|
||||
/// 显示面板
|
||||
/// </summary>
|
||||
/// <param name="data">面板所需数据</param>
|
||||
void Show(PanelBaseData data, System.Action<IPanelBase> completeHandler);
|
||||
|
||||
void ___CloseForPanelMgr(Action complete);
|
||||
|
||||
|
||||
bool IsAutoDestroy();
|
||||
|
||||
/// <summary>
|
||||
/// 是否正在显示中
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
bool isShowing();
|
||||
|
||||
/// <summary>
|
||||
/// 是否正在加载中
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
bool isLoading();
|
||||
|
||||
/// <summary>
|
||||
/// 面板所在 UI 层级(全屏遮挡判定用:仅隐藏低于本层面板层级的面板)
|
||||
/// </summary>
|
||||
UILayerIdx panelLayerIdx { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 被上层全屏面板遮挡时是否仍保持显示(常驻特殊面板为 true)
|
||||
/// </summary>
|
||||
bool isKeepShowWhenCovered { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否正被上层全屏面板隐藏
|
||||
/// </summary>
|
||||
bool isHiddenByCover { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 业务是否已关闭(区别于“被遮挡隐藏”)
|
||||
/// </summary>
|
||||
bool isClosed { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 全屏遮挡优化:由上层全屏面板调用,隐藏本面板,省去被盖住的 DrawCall
|
||||
/// </summary>
|
||||
void SetCoverHidden();
|
||||
|
||||
/// <summary>
|
||||
/// 全屏遮挡优化:由隐藏者关闭/销毁时调用,恢复本面板显示(幂等)
|
||||
/// </summary>
|
||||
void RestoreFromCover();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MFrame
|
||||
{
|
||||
/// <summary>
|
||||
/// 面板创建委托,用于动态创建UI面板实例
|
||||
/// </summary>
|
||||
/// <returns>返回创建的面板实例</returns>
|
||||
public delegate IPanelBase PanelCreate(PanelBaseData data, System.Action<IPanelBase> completeHandler);
|
||||
|
||||
/// <summary>
|
||||
/// UI面板基类,提供基础UI功能和资源加载管理
|
||||
/// 继承自UIBase,用于实现具体的UI面板
|
||||
/// </summary>
|
||||
public class PanelBase<V> : UIBase, IPanelBase where V : UI_PanelBase
|
||||
{
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// UI层级索引,控制面板在UI层级中的显示顺序
|
||||
/// </summary>
|
||||
protected UILayerIdx _layerIdx;
|
||||
|
||||
/// <summary>
|
||||
/// 面板数据,用于存储和传递面板所需的数据
|
||||
/// </summary>
|
||||
protected PanelBaseData _data;
|
||||
|
||||
/// <summary>
|
||||
/// 面板视图组件,负责处理UI的具体显示和交互逻辑
|
||||
/// </summary>
|
||||
protected V _view;
|
||||
|
||||
/// <summary>
|
||||
/// 预制体资源路径
|
||||
/// </summary>
|
||||
protected string _url;
|
||||
|
||||
protected string _registerModule;
|
||||
|
||||
// 全屏遮挡以组件形式配置:在面板预制体根节点挂 FullScreenCover 组件即生效(策划可自助添加/移除)
|
||||
/// <summary>
|
||||
/// 面板预制体根节点上的 FullScreenCover 组件(未挂则为 null,即非全屏面板)
|
||||
/// </summary>
|
||||
private FullScreenCover _fullScreenCover;
|
||||
|
||||
/// <summary>
|
||||
/// 是否正被上层全屏面板隐藏
|
||||
/// </summary>
|
||||
private bool _hiddenByCover;
|
||||
|
||||
/// <summary>
|
||||
/// 被本面板(全屏遮挡)隐藏的下层面板列表
|
||||
/// </summary>
|
||||
private readonly List<IPanelBase> _panelsHiddenByMe = new List<IPanelBase>();
|
||||
|
||||
/// <summary>
|
||||
/// 业务是否已关闭(防止被遮挡隐藏的已关闭面板被“复活”)
|
||||
/// </summary>
|
||||
private bool _closed;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数,初始化面板基本属性
|
||||
/// </summary>
|
||||
/// <param name="autoDestroy">是否在关闭时自动销毁</param>
|
||||
public PanelBase(bool autoDestroy) : base(null)
|
||||
{
|
||||
_autoDestroy = autoDestroy;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在关闭界面的时候是否进行销毁
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool IsAutoDestroy()
|
||||
{
|
||||
return _autoDestroy;
|
||||
}
|
||||
|
||||
// —— 全屏遮挡:IPanelBase 接口实现 ——
|
||||
UILayerIdx IPanelBase.panelLayerIdx => _layerIdx;
|
||||
bool IPanelBase.isKeepShowWhenCovered => _fullScreenCover != null && _fullScreenCover.enabled && _fullScreenCover.isKeepShowWhenCovered;
|
||||
bool IPanelBase.isHiddenByCover => _hiddenByCover;
|
||||
bool IPanelBase.isClosed => _closed;
|
||||
|
||||
/// <summary>
|
||||
/// 全屏遮挡优化:由上层全屏面板调用,隐藏本面板(省去被盖住的 DrawCall)
|
||||
/// </summary>
|
||||
public void SetCoverHidden()
|
||||
{
|
||||
_hiddenByCover = true;
|
||||
if (_view != null)
|
||||
{
|
||||
_view.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 全屏遮挡优化:恢复本面板显示(幂等;面板视图已销毁时自动跳过)
|
||||
/// </summary>
|
||||
public void RestoreFromCover()
|
||||
{
|
||||
if (!_hiddenByCover)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_hiddenByCover = false;
|
||||
if (_view != null)
|
||||
{
|
||||
_view.gameObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 尝试异步加载UI预制体
|
||||
/// </summary>
|
||||
/// <param name="url">预制体资源路径</param>
|
||||
/// <param name="layerIdx">目标UI层级</param>
|
||||
/// <param name="data">面板初始化数据</param>
|
||||
protected virtual void _TryInit(string registerModule, string url, UILayerIdx layerIdx, PanelBaseData data, System.Action<IPanelBase> completeHandler = null)
|
||||
{
|
||||
_registerModule = registerModule;
|
||||
_url = url;
|
||||
// 初始化并启用资源加载器
|
||||
_layerIdx = layerIdx;
|
||||
ResLoaderMgr.Instance.TryLoadAsync(_registerModule, _url, (float progress) =>
|
||||
{
|
||||
_OnProgress(progress);
|
||||
}, (Object prefab) =>
|
||||
{
|
||||
if (prefab != null)
|
||||
{
|
||||
// 加载成功后实例化预制体并设置到指定层级
|
||||
_TryInitGameObject(prefab as GameObject, layerIdx, data, completeHandler);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 处理加载失败的情况
|
||||
_OnLoadFail();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化 UI预制体
|
||||
/// </summary>
|
||||
/// <param name="layerIdx">目标UI层级</param>
|
||||
/// <param name="data">面板初始化数据</param>
|
||||
protected virtual void _TryInitGameObject(GameObject prefab, UILayerIdx layerIdx, PanelBaseData data, System.Action<IPanelBase> completeHandler = null)
|
||||
{
|
||||
LayerItem litem = GameGlobalData.GetLayerItem(_layerIdx);
|
||||
GameObject go = UnityUtils.InstantiatePrefab(prefab, litem.transform);
|
||||
transform = go.transform;
|
||||
// 获取视图组件并完成初始化
|
||||
_view = transform.GetComponent<V>();
|
||||
// 缓存全屏遮挡组件(策划在预制体根节点上挂载,未挂则为 null)
|
||||
_fullScreenCover = transform.GetComponent<FullScreenCover>();
|
||||
_view.AddUpdateHandler(this.Update);
|
||||
_OnLoadComplete();
|
||||
Show(data, completeHandler);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public V view
|
||||
{
|
||||
get
|
||||
{
|
||||
return _view;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void _OnProgress(float progress)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 资源加载完成回调,子类可重写此方法实现自定义加载完成逻辑
|
||||
/// </summary>
|
||||
protected virtual void _OnLoadComplete()
|
||||
{
|
||||
foreach (var btn in this._view.btnsClose)
|
||||
{
|
||||
if (btn != null)
|
||||
this.AddBtnOnClick(btn, this.Close);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 资源加载失败回调,子类可重写此方法实现自定义失败处理逻辑
|
||||
/// </summary>
|
||||
protected virtual void _OnLoadFail()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打开面板,可传入自定义数据
|
||||
/// </summary>
|
||||
/// <param name="data">面板初始化数据</param>
|
||||
public void Show(PanelBaseData data, System.Action<IPanelBase> completeHandler)
|
||||
{
|
||||
_data = data;
|
||||
_closed = false;
|
||||
if (this._view != null)
|
||||
{
|
||||
// 若此前被上层全屏面板隐藏,主动 Show 视为恢复显示
|
||||
//(隐藏者列表中的遗留记录由 RestorePanelsHiddenByMe 的 isHiddenByCover 守卫失效,无需清理)
|
||||
if (_hiddenByCover)
|
||||
{
|
||||
RestoreFromCover();
|
||||
}
|
||||
this._view.gameObject.SetActive(true);
|
||||
}
|
||||
_OnShow();
|
||||
// 全屏遮挡面板(预制体根节点挂 FullScreenCover 组件且处于启用状态):
|
||||
// 打开时隐藏下层正在显示的面板,省去被盖住的 DrawCall
|
||||
if (_fullScreenCover != null && _fullScreenCover.enabled)
|
||||
{
|
||||
HidePanelsUnderThis();
|
||||
}
|
||||
_view.Show(() =>
|
||||
{
|
||||
completeHandler?.Invoke(this);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断面板是否正在显示
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool isShowing()
|
||||
{
|
||||
if (_view == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// 被上层全屏面板隐藏时仍视为“打开中”,保持业务语义不变;已关闭的面板不算
|
||||
return !_closed && (_view.gameObject.activeSelf || _hiddenByCover);
|
||||
}
|
||||
public bool isLoading()
|
||||
{
|
||||
return _view == null;
|
||||
}
|
||||
protected virtual void _OnShow()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 关闭面板
|
||||
/// </summary>
|
||||
public void Close()
|
||||
{
|
||||
PanelMgr.Instance.Close(_registerModule);
|
||||
}
|
||||
/// <summary>
|
||||
/// 关闭面板(内部使用)
|
||||
/// </summary>
|
||||
public void ___CloseForPanelMgr(System.Action complete)
|
||||
{
|
||||
// 标记业务已关闭,防止关闭过程中被遮挡机制“复活”
|
||||
_closed = true;
|
||||
// 若正被上层全屏面板隐藏(inactive 会导致关闭动画不更新而卡死),先恢复再播关闭动画
|
||||
if (_hiddenByCover)
|
||||
{
|
||||
RestoreFromCover();
|
||||
}
|
||||
_view.Close(() =>
|
||||
{
|
||||
// 关闭完成后恢复被本面板隐藏的下层面板
|
||||
RestorePanelsHiddenByMe();
|
||||
_OnClose();
|
||||
complete?.Invoke();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
protected virtual void _OnClose()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 全屏遮挡优化:隐藏本面板下方所有层级中正在显示的面板,
|
||||
/// 被完全遮住的 UI 不再提交 DrawCall(关闭/销毁时由 RestorePanelsHiddenByMe 恢复)。
|
||||
/// </summary>
|
||||
private void HidePanelsUnderThis()
|
||||
{
|
||||
Dictionary<string, PanelMsgItem> items = PanelMgr.Instance.items;
|
||||
if (items == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (KeyValuePair<string, PanelMsgItem> kvp in items)
|
||||
{
|
||||
PanelMsgItem item = kvp.Value;
|
||||
if (item.panel == null || ReferenceEquals(item.panel, this))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
IPanelBase panel = item.panel;
|
||||
if (panel.panelLayerIdx < _layerIdx
|
||||
&& !panel.isKeepShowWhenCovered
|
||||
&& !panel.isHiddenByCover
|
||||
&& panel.isShowing())
|
||||
{
|
||||
_panelsHiddenByMe.Add(panel);
|
||||
panel.SetCoverHidden();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 恢复被本面板隐藏的下层面板(幂等,可重复调用)
|
||||
/// </summary>
|
||||
private void RestorePanelsHiddenByMe()
|
||||
{
|
||||
int len = _panelsHiddenByMe.Count;
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
IPanelBase panel = _panelsHiddenByMe[i];
|
||||
// 已关闭的面板不恢复,防止“复活”已关闭的界面
|
||||
if (panel != null && panel.isHiddenByCover && !panel.isClosed)
|
||||
{
|
||||
panel.RestoreFromCover();
|
||||
}
|
||||
}
|
||||
_panelsHiddenByMe.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 销毁面板实例,清理资源防止内存泄漏
|
||||
/// </summary>
|
||||
protected override void _Destroy()
|
||||
{
|
||||
// 兼容未走 Close 直接销毁的路径,确保下层面板一定被恢复
|
||||
RestorePanelsHiddenByMe();
|
||||
|
||||
base._Destroy();
|
||||
|
||||
// 清理资源加载器
|
||||
ResLoaderMgr.Instance.TryRemoveLoads(_registerModule);
|
||||
|
||||
// 清除面板数据
|
||||
_data = null;
|
||||
_fullScreenCover = null;
|
||||
if (_view != null)
|
||||
{
|
||||
_view.RemoveUpdateHandler(this.Update);
|
||||
GameObject.Destroy(_view.gameObject);
|
||||
_view = null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,567 @@
|
||||
diff --git a/unity/Assets/Editor/FullScreenCoverTool.cs b/unity/Assets/Editor/FullScreenCoverTool.cs
|
||||
new file mode 100644
|
||||
index 00000000..ca2a1e59
|
||||
--- /dev/null
|
||||
+++ b/unity/Assets/Editor/FullScreenCoverTool.cs
|
||||
@@ -0,0 +1,316 @@
|
||||
+using System.Collections.Generic;
|
||||
+using System.IO;
|
||||
+using UnityEditor;
|
||||
+using UnityEngine;
|
||||
+using MFrame;
|
||||
+using Object = UnityEngine.Object;
|
||||
+
|
||||
+/// <summary>
|
||||
+/// 全屏面板遮挡工具:查找/添加/移除面板预制体上的 FullScreenCover 组件。
|
||||
+/// 挂有该组件的面板 = 全屏面板(打开时自动隐藏下层 UI 省去 DrawCall,关闭/销毁自动恢复)。
|
||||
+/// 策划可在此窗口自助管理,无需改代码。
|
||||
+/// 说明:Project 窗口的 t: 搜索只能按资源类型过滤,搜不到“预制体里挂了某组件”,故提供本工具。
|
||||
+/// </summary>
|
||||
+public class FullScreenCoverTool : EditorWindow
|
||||
+{
|
||||
+ private sealed class CoverInfo
|
||||
+ {
|
||||
+ public Object prefab; // 预制体资产引用(用于 Ping/打开)
|
||||
+ public string prefabPath; // 资源路径
|
||||
+ public string nodeName; // 组件所在节点名
|
||||
+ public bool enabled; // 组件是否启用
|
||||
+ public bool keepShow; // 被遮挡时保持显示
|
||||
+ }
|
||||
+
|
||||
+ private readonly List<CoverInfo> results = new List<CoverInfo>();
|
||||
+ private Vector2 scroll;
|
||||
+ private GameObject addTarget;
|
||||
+ private bool scanned;
|
||||
+ private string statusText = "尚未扫描,点击“扫描全工程”查找挂载 FullScreenCover 组件的面板预制体";
|
||||
+
|
||||
+ [MenuItem("Tools/UI/全屏面板遮挡工具")]
|
||||
+ private static void ShowWindow()
|
||||
+ {
|
||||
+ var win = GetWindow<FullScreenCoverTool>("全屏面板遮挡");
|
||||
+ win.minSize = new Vector2(540f, 320f);
|
||||
+ }
|
||||
+
|
||||
+ private void OnEnable()
|
||||
+ {
|
||||
+ // 打开窗口时自动扫描一次
|
||||
+ EditorApplication.delayCall += ScanAll;
|
||||
+ }
|
||||
+
|
||||
+ private void OnGUI()
|
||||
+ {
|
||||
+ EditorGUILayout.HelpBox(
|
||||
+ "挂有 FullScreenCover 组件的面板 = 全屏面板:打开时自动隐藏下层 UI(省去被遮住的 DrawCall),关闭/销毁自动恢复。\n" +
|
||||
+ "注意:仅用于完全铺满屏幕的面板(如通行证、商店、活动大页),半屏弹窗不要挂。", MessageType.Info);
|
||||
+
|
||||
+ // —— 添加区 ——
|
||||
+ EditorGUILayout.LabelField("添加组件(拖入面板预制体)", EditorStyles.boldLabel);
|
||||
+ using (new EditorGUILayout.HorizontalScope())
|
||||
+ {
|
||||
+ addTarget = (GameObject)EditorGUILayout.ObjectField(addTarget, typeof(GameObject), false);
|
||||
+ using (new EditorGUI.DisabledScope(addTarget == null))
|
||||
+ {
|
||||
+ if (GUILayout.Button("添加组件", GUILayout.Width(80f)))
|
||||
+ {
|
||||
+ AddComponentToPrefab(AssetDatabase.GetAssetPath(addTarget));
|
||||
+ addTarget = null;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ EditorGUILayout.Space(4f);
|
||||
+
|
||||
+ // —— 扫描区 ——
|
||||
+ using (new EditorGUILayout.HorizontalScope())
|
||||
+ {
|
||||
+ if (GUILayout.Button("扫描全工程", GUILayout.Height(24f)))
|
||||
+ {
|
||||
+ ScanAll();
|
||||
+ }
|
||||
+ GUILayout.Label(statusText, EditorStyles.miniLabel);
|
||||
+ }
|
||||
+
|
||||
+ EditorGUILayout.Space(4f);
|
||||
+
|
||||
+ // —— 结果列表 ——
|
||||
+ if (!scanned)
|
||||
+ {
|
||||
+ return;
|
||||
+ }
|
||||
+ if (results.Count == 0)
|
||||
+ {
|
||||
+ EditorGUILayout.HelpBox("没有找到任何挂载 FullScreenCover 的面板。可用上方“添加组件”为全屏面板配置。", MessageType.Warning);
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ scroll = EditorGUILayout.BeginScrollView(scroll);
|
||||
+ for (int i = 0; i < results.Count; i++)
|
||||
+ {
|
||||
+ DrawResultItem(results[i]);
|
||||
+ }
|
||||
+ EditorGUILayout.EndScrollView();
|
||||
+ }
|
||||
+
|
||||
+ private void DrawResultItem(CoverInfo info)
|
||||
+ {
|
||||
+ using (new EditorGUILayout.VerticalScope("box"))
|
||||
+ {
|
||||
+ using (new EditorGUILayout.HorizontalScope())
|
||||
+ {
|
||||
+ // 点击名称定位资源
|
||||
+ string title = info.prefab != null ? info.prefab.name : Path.GetFileName(info.prefabPath);
|
||||
+ if (GUILayout.Button(title, EditorStyles.boldLabel, GUILayout.Width(170f)))
|
||||
+ {
|
||||
+ EditorGUIUtility.PingObject(info.prefab);
|
||||
+ }
|
||||
+
|
||||
+ GUILayout.Label(info.enabled ? "已启用" : "已禁用",
|
||||
+ info.enabled ? EditorStyles.miniLabel : EditorStyles.whiteMiniLabel, GUILayout.Width(46f));
|
||||
+
|
||||
+ GUILayout.FlexibleSpace();
|
||||
+
|
||||
+ // 被遮挡时保持显示(勾选即写回预制体)
|
||||
+ bool newKeep = GUILayout.Toggle(info.keepShow, "被遮挡时保持显示", GUILayout.Width(132f));
|
||||
+ if (newKeep != info.keepShow)
|
||||
+ {
|
||||
+ info.keepShow = newKeep;
|
||||
+ SetKeepShow(info.prefabPath, newKeep);
|
||||
+ }
|
||||
+
|
||||
+ if (GUILayout.Button("打开", GUILayout.Width(46f)))
|
||||
+ {
|
||||
+ AssetDatabase.OpenAsset(info.prefab);
|
||||
+ }
|
||||
+ if (GUILayout.Button("移除组件", GUILayout.Width(68f)))
|
||||
+ {
|
||||
+ if (EditorUtility.DisplayDialog("移除 FullScreenCover",
|
||||
+ $"确定从 {Path.GetFileName(info.prefabPath)} 移除 FullScreenCover 组件?\n该面板将不再自动隐藏下层。",
|
||||
+ "移除", "取消"))
|
||||
+ {
|
||||
+ RemoveComponent(info.prefabPath);
|
||||
+ ScanAll();
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ EditorGUILayout.LabelField(info.prefabPath, EditorStyles.miniLabel);
|
||||
+ if (!info.enabled)
|
||||
+ {
|
||||
+ EditorGUILayout.LabelField("⚠ 组件处于禁用状态,不会生效(在预制体根节点 Inspector 勾选启用)。", EditorStyles.miniLabel);
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ // —— 扫描 ——
|
||||
+
|
||||
+ private void ScanAll()
|
||||
+ {
|
||||
+ string scriptGuid = GetScriptGuid();
|
||||
+ if (string.IsNullOrEmpty(scriptGuid))
|
||||
+ {
|
||||
+ EditorUtility.DisplayDialog("错误", "无法获取 FullScreenCover 脚本 GUID,请确认脚本已编译无报错。", "确定");
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab", new[] { "Assets" });
|
||||
+ results.Clear();
|
||||
+ try
|
||||
+ {
|
||||
+ for (int i = 0; i < prefabGuids.Length; i++)
|
||||
+ {
|
||||
+ if (EditorUtility.DisplayCancelableProgressBar("全屏面板遮挡工具",
|
||||
+ $"扫描预制体中... ({i + 1}/{prefabGuids.Length})", (float)i / prefabGuids.Length))
|
||||
+ {
|
||||
+ break; // 用户取消
|
||||
+ }
|
||||
+
|
||||
+ string path = AssetDatabase.GUIDToAssetPath(prefabGuids[i]);
|
||||
+ if (string.IsNullOrEmpty(path) || !path.EndsWith(".prefab"))
|
||||
+ {
|
||||
+ continue;
|
||||
+ }
|
||||
+
|
||||
+ // 文本快筛:只有包含组件脚本 guid 的预制体才真正加载,避免逐个解析
|
||||
+ if (!ContainsGuidFast(path, scriptGuid))
|
||||
+ {
|
||||
+ continue;
|
||||
+ }
|
||||
+
|
||||
+ GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
||||
+ if (prefab == null)
|
||||
+ {
|
||||
+ continue;
|
||||
+ }
|
||||
+ FullScreenCover[] comps = prefab.GetComponentsInChildren<FullScreenCover>(true);
|
||||
+ foreach (FullScreenCover comp in comps)
|
||||
+ {
|
||||
+ results.Add(new CoverInfo
|
||||
+ {
|
||||
+ prefab = prefab,
|
||||
+ prefabPath = path,
|
||||
+ nodeName = comp.gameObject.name,
|
||||
+ enabled = comp.enabled,
|
||||
+ keepShow = comp.isKeepShowWhenCovered
|
||||
+ });
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ finally
|
||||
+ {
|
||||
+ EditorUtility.ClearProgressBar();
|
||||
+ }
|
||||
+
|
||||
+ scanned = true;
|
||||
+ statusText = $"扫描完成:{results.Count} 个面板预制体挂载 FullScreenCover";
|
||||
+ Repaint();
|
||||
+ }
|
||||
+
|
||||
+ /// <summary>动态获取 FullScreenCover 脚本资产的 guid,避免硬编码。</summary>
|
||||
+ private static string GetScriptGuid()
|
||||
+ {
|
||||
+ GameObject probe = new GameObject("FullScreenCoverGuidProbe");
|
||||
+ try
|
||||
+ {
|
||||
+ MonoScript ms = MonoScript.FromMonoBehaviour(probe.AddComponent<FullScreenCover>());
|
||||
+ if (ms == null)
|
||||
+ {
|
||||
+ return null;
|
||||
+ }
|
||||
+ return AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(ms));
|
||||
+ }
|
||||
+ finally
|
||||
+ {
|
||||
+ Object.DestroyImmediate(probe);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ /// <summary>逐行读取文件快速判断是否包含 guid(找到即中断),比加载预制体快一个量级。</summary>
|
||||
+ private static bool ContainsGuidFast(string path, string guid)
|
||||
+ {
|
||||
+ try
|
||||
+ {
|
||||
+ foreach (string line in File.ReadLines(path))
|
||||
+ {
|
||||
+ if (line.Contains(guid))
|
||||
+ {
|
||||
+ return true;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ catch (IOException)
|
||||
+ {
|
||||
+ // 文件被占用等情况按未命中处理
|
||||
+ }
|
||||
+ return false;
|
||||
+ }
|
||||
+
|
||||
+ // —— 修改预制体 ——
|
||||
+
|
||||
+ /// <summary>以“加载内容-修改-保存-卸载”的标准流程安全修改预制体资产。</summary>
|
||||
+ private static void EditPrefab(string path, System.Action<GameObject> edit)
|
||||
+ {
|
||||
+ GameObject root = PrefabUtility.LoadPrefabContents(path);
|
||||
+ try
|
||||
+ {
|
||||
+ edit(root);
|
||||
+ PrefabUtility.SaveAsPrefabAsset(root, path);
|
||||
+ }
|
||||
+ finally
|
||||
+ {
|
||||
+ PrefabUtility.UnloadPrefabContents(root);
|
||||
+ }
|
||||
+ AssetDatabase.SaveAssets();
|
||||
+ }
|
||||
+
|
||||
+ private void AddComponentToPrefab(string path)
|
||||
+ {
|
||||
+ if (string.IsNullOrEmpty(path) || !path.EndsWith(".prefab"))
|
||||
+ {
|
||||
+ EditorUtility.DisplayDialog("无法添加", "请拖入一个预制体资产(.prefab),而不是场景对象或模型。", "确定");
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ bool already = false;
|
||||
+ EditPrefab(path, root =>
|
||||
+ {
|
||||
+ if (root.GetComponent<FullScreenCover>() != null)
|
||||
+ {
|
||||
+ already = true;
|
||||
+ }
|
||||
+ else
|
||||
+ {
|
||||
+ root.AddComponent<FullScreenCover>();
|
||||
+ }
|
||||
+ });
|
||||
+
|
||||
+ ShowNotification(new GUIContent(already ? "该预制体已挂载组件" : "已添加 FullScreenCover"));
|
||||
+ ScanAll();
|
||||
+ }
|
||||
+
|
||||
+ private void RemoveComponent(string path)
|
||||
+ {
|
||||
+ EditPrefab(path, root =>
|
||||
+ {
|
||||
+ FullScreenCover[] comps = root.GetComponentsInChildren<FullScreenCover>(true);
|
||||
+ foreach (FullScreenCover comp in comps)
|
||||
+ {
|
||||
+ Object.DestroyImmediate(comp, true);
|
||||
+ }
|
||||
+ });
|
||||
+ }
|
||||
+
|
||||
+ private void SetKeepShow(string path, bool value)
|
||||
+ {
|
||||
+ EditPrefab(path, root =>
|
||||
+ {
|
||||
+ FullScreenCover[] comps = root.GetComponentsInChildren<FullScreenCover>(true);
|
||||
+ foreach (FullScreenCover comp in comps)
|
||||
+ {
|
||||
+ comp.isKeepShowWhenCovered = value;
|
||||
+ }
|
||||
+ });
|
||||
+ }
|
||||
+}
|
||||
diff --git a/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/FullScreenCover.cs b/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/FullScreenCover.cs
|
||||
new file mode 100644
|
||||
index 00000000..48f6aaf3
|
||||
--- /dev/null
|
||||
+++ b/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/FullScreenCover.cs
|
||||
@@ -0,0 +1,16 @@
|
||||
+using UnityEngine;
|
||||
+
|
||||
+namespace MFrame
|
||||
+{
|
||||
+ /// <summary>
|
||||
+ /// 全屏遮挡组件:挂到面板预制体根节点(与 UI_PanelBase 同节点)即生效,移除组件即关闭。
|
||||
+ /// 打开本面板时自动隐藏下方层级中正在显示的面板(省去被遮住的 DrawCall),
|
||||
+ /// 关闭/销毁时自动恢复。由策划在编辑器中添加/移除本组件即可配置,无需改代码。
|
||||
+ /// 注意:仅用于完全铺满屏幕的面板(如通行证、商店、活动大页),半屏弹窗不要挂。
|
||||
+ /// </summary>
|
||||
+ public class FullScreenCover : MonoBehaviour
|
||||
+ {
|
||||
+ [Tooltip("勾选后:本面板被上层全屏面板遮挡时仍保持显示(仅用于必须常驻可见的特殊面板)。")]
|
||||
+ public bool isKeepShowWhenCovered = false;
|
||||
+ }
|
||||
+}
|
||||
diff --git a/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/PanelBase.cs b/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/PanelBase.cs
|
||||
index 98c6d79f..657a4024 100644
|
||||
--- a/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/PanelBase.cs
|
||||
+++ b/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/PanelBase.cs
|
||||
@@ -1,3 +1,4 @@
|
||||
+using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MFrame
|
||||
@@ -56,6 +57,54 @@ namespace MFrame
|
||||
return _autoDestroy;
|
||||
}
|
||||
|
||||
+ // 全屏遮挡以组件形式配置:在面板预制体根节点挂 FullScreenCover 组件即生效(策划可自助添加/移除)
|
||||
+ /// <summary>
|
||||
+ /// 面板预制体根节点上的 FullScreenCover 组件(未挂则为 null,即非全屏面板)
|
||||
+ /// </summary>
|
||||
+ private FullScreenCover _fullScreenCover;
|
||||
+
|
||||
+ /// <summary>
|
||||
+ /// 是否正被上层全屏面板隐藏
|
||||
+ /// </summary>
|
||||
+ private bool _hiddenByCover;
|
||||
+ /// <summary>
|
||||
+ /// 被本面板(全屏遮挡)隐藏的下层面板列表
|
||||
+ /// </summary>
|
||||
+ private readonly List<IPanelBase> _panelsHiddenByMe = new List<IPanelBase>();
|
||||
+
|
||||
+ // —— 全屏遮挡:IPanelBase 接口实现 ——
|
||||
+ UILayerIdx IPanelBase.panelLayerIdx => _layerIdx;
|
||||
+ bool IPanelBase.isKeepShowWhenCovered => _fullScreenCover != null && _fullScreenCover.enabled && _fullScreenCover.isKeepShowWhenCovered;
|
||||
+ bool IPanelBase.isHiddenByCover => _hiddenByCover;
|
||||
+
|
||||
+ /// <summary>
|
||||
+ /// 全屏遮挡优化:由上层全屏面板调用,隐藏本面板(省去被盖住的 DrawCall)
|
||||
+ /// </summary>
|
||||
+ public void SetCoverHidden()
|
||||
+ {
|
||||
+ _hiddenByCover = true;
|
||||
+ if (_view != null)
|
||||
+ {
|
||||
+ _view.gameObject.SetActive(false);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ /// <summary>
|
||||
+ /// 全屏遮挡优化:恢复本面板显示(幂等;面板视图已销毁时自动跳过)
|
||||
+ /// </summary>
|
||||
+ public void RestoreFromCover()
|
||||
+ {
|
||||
+ if (!_hiddenByCover)
|
||||
+ {
|
||||
+ return;
|
||||
+ }
|
||||
+ _hiddenByCover = false;
|
||||
+ if (_view != null)
|
||||
+ {
|
||||
+ _view.gameObject.SetActive(true);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
|
||||
/// <summary>
|
||||
/// 尝试异步加载UI预制体
|
||||
@@ -101,6 +150,8 @@ namespace MFrame
|
||||
transform = go.transform;
|
||||
// 获取视图组件并完成初始化
|
||||
_view = transform.GetComponent<V>();
|
||||
+ // 缓存全屏遮挡组件(策划在预制体根节点上挂载,未挂则为 null)
|
||||
+ _fullScreenCover = transform.GetComponent<FullScreenCover>();
|
||||
_view.AddUpdateHandler(this.Update);
|
||||
_OnLoadComplete();
|
||||
Show(data, completeHandler);
|
||||
@@ -150,8 +201,22 @@ namespace MFrame
|
||||
{
|
||||
_data = data;
|
||||
if (this._view != null)
|
||||
+ {
|
||||
+ // 若此前被上层全屏面板隐藏,主动 Show 视为恢复显示
|
||||
+ //(隐藏者列表中的遗留记录由 RestorePanelsHiddenByMe 的 isHiddenByCover 守卫失效,无需清理)
|
||||
+ if (_hiddenByCover)
|
||||
+ {
|
||||
+ RestoreFromCover();
|
||||
+ }
|
||||
this._view.gameObject.SetActive(true);
|
||||
+ }
|
||||
_OnShow();
|
||||
+ // 全屏遮挡面板(预制体根节点挂 FullScreenCover 组件且处于启用状态):
|
||||
+ // 打开时隐藏下层正在显示的面板,省去被盖住的 DrawCall
|
||||
+ if (_fullScreenCover != null && _fullScreenCover.enabled)
|
||||
+ {
|
||||
+ HidePanelsUnderThis();
|
||||
+ }
|
||||
_view.Show(() =>
|
||||
{
|
||||
completeHandler?.Invoke(this);
|
||||
@@ -168,7 +233,8 @@ namespace MFrame
|
||||
{
|
||||
return false;
|
||||
}
|
||||
- return _view.gameObject.activeSelf;
|
||||
+ // 被上层全屏面板隐藏时仍视为“打开中”,保持业务语义不变
|
||||
+ return _view.gameObject.activeSelf || _hiddenByCover;
|
||||
}
|
||||
public bool isLoading()
|
||||
{
|
||||
@@ -193,6 +259,8 @@ namespace MFrame
|
||||
{
|
||||
_view.Close(() =>
|
||||
{
|
||||
+ // 关闭完成后恢复被本面板隐藏的下层面板
|
||||
+ RestorePanelsHiddenByMe();
|
||||
_OnClose();
|
||||
complete?.Invoke();
|
||||
});
|
||||
@@ -204,6 +272,53 @@ namespace MFrame
|
||||
|
||||
}
|
||||
|
||||
+ /// <summary>
|
||||
+ /// 全屏遮挡优化:隐藏本面板下方所有层级中正在显示的面板,
|
||||
+ /// 被完全遮住的 UI 不再提交 DrawCall(关闭/销毁时由 RestorePanelsHiddenByMe 恢复)。
|
||||
+ /// </summary>
|
||||
+ private void HidePanelsUnderThis()
|
||||
+ {
|
||||
+ Dictionary<string, PanelMsgItem> items = PanelMgr.Instance.items;
|
||||
+ if (items == null)
|
||||
+ {
|
||||
+ return;
|
||||
+ }
|
||||
+ foreach (KeyValuePair<string, PanelMsgItem> kvp in items)
|
||||
+ {
|
||||
+ PanelMsgItem item = kvp.Value;
|
||||
+ if (item.panel == null || ReferenceEquals(item.panel, this))
|
||||
+ {
|
||||
+ continue;
|
||||
+ }
|
||||
+ IPanelBase panel = item.panel;
|
||||
+ if (panel.panelLayerIdx < _layerIdx
|
||||
+ && !panel.isKeepShowWhenCovered
|
||||
+ && !panel.isHiddenByCover
|
||||
+ && panel.isShowing())
|
||||
+ {
|
||||
+ _panelsHiddenByMe.Add(panel);
|
||||
+ panel.SetCoverHidden();
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ /// <summary>
|
||||
+ /// 恢复被本面板隐藏的下层面板(幂等,可重复调用)
|
||||
+ /// </summary>
|
||||
+ private void RestorePanelsHiddenByMe()
|
||||
+ {
|
||||
+ int len = _panelsHiddenByMe.Count;
|
||||
+ for (int i = 0; i < len; i++)
|
||||
+ {
|
||||
+ IPanelBase panel = _panelsHiddenByMe[i];
|
||||
+ if (panel != null && panel.isHiddenByCover)
|
||||
+ {
|
||||
+ panel.RestoreFromCover();
|
||||
+ }
|
||||
+ }
|
||||
+ _panelsHiddenByMe.Clear();
|
||||
+ }
|
||||
+
|
||||
|
||||
|
||||
/// <summary>
|
||||
@@ -211,8 +326,8 @@ namespace MFrame
|
||||
/// </summary>
|
||||
protected override void _Destroy()
|
||||
{
|
||||
-
|
||||
-
|
||||
+ // 兼容未走 Close 直接销毁的路径,确保下层面板一定被恢复
|
||||
+ RestorePanelsHiddenByMe();
|
||||
|
||||
base._Destroy();
|
||||
|
||||
@@ -221,6 +336,7 @@ namespace MFrame
|
||||
|
||||
// 清除面板数据
|
||||
_data = null;
|
||||
+ _fullScreenCover = null;
|
||||
if (_view != null)
|
||||
{
|
||||
_view.RemoveUpdateHandler(this.Update);
|
||||
diff --git a/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/iface/IPanelBase.cs b/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/iface/IPanelBase.cs
|
||||
index c0a2283c..48cd4d14 100644
|
||||
--- a/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/iface/IPanelBase.cs
|
||||
+++ b/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/iface/IPanelBase.cs
|
||||
@@ -31,7 +31,30 @@ namespace MFrame
|
||||
/// <returns></returns>
|
||||
bool isLoading();
|
||||
|
||||
+ /// <summary>
|
||||
+ /// 面板所在 UI 层级(全屏遮挡判定用:仅隐藏低于本层面板层级的面板)
|
||||
+ /// </summary>
|
||||
+ UILayerIdx panelLayerIdx { get; }
|
||||
|
||||
+ /// <summary>
|
||||
+ /// 被上层全屏面板遮挡时是否仍保持显示(常驻特殊面板为 true)
|
||||
+ /// </summary>
|
||||
+ bool isKeepShowWhenCovered { get; }
|
||||
+
|
||||
+ /// <summary>
|
||||
+ /// 是否正被上层全屏面板隐藏
|
||||
+ /// </summary>
|
||||
+ bool isHiddenByCover { get; }
|
||||
+
|
||||
+ /// <summary>
|
||||
+ /// 全屏遮挡优化:由上层全屏面板调用,隐藏本面板,省去被盖住的 DrawCall
|
||||
+ /// </summary>
|
||||
+ void SetCoverHidden();
|
||||
+
|
||||
+ /// <summary>
|
||||
+ /// 全屏遮挡优化:由隐藏者关闭/销毁时调用,恢复本面板显示(幂等)
|
||||
+ /// </summary>
|
||||
+ void RestoreFromCover();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,567 @@
|
||||
diff --git a/unity/Assets/Editor/FullScreenCoverTool.cs b/unity/Assets/Editor/FullScreenCoverTool.cs
|
||||
new file mode 100644
|
||||
index 00000000..ca2a1e59
|
||||
--- /dev/null
|
||||
+++ b/unity/Assets/Editor/FullScreenCoverTool.cs
|
||||
@@ -0,0 +1,316 @@
|
||||
+using System.Collections.Generic;
|
||||
+using System.IO;
|
||||
+using UnityEditor;
|
||||
+using UnityEngine;
|
||||
+using MFrame;
|
||||
+using Object = UnityEngine.Object;
|
||||
+
|
||||
+/// <summary>
|
||||
+/// 全屏面板遮挡工具:查找/添加/移除面板预制体上的 FullScreenCover 组件。
|
||||
+/// 挂有该组件的面板 = 全屏面板(打开时自动隐藏下层 UI 省去 DrawCall,关闭/销毁自动恢复)。
|
||||
+/// 策划可在此窗口自助管理,无需改代码。
|
||||
+/// 说明:Project 窗口的 t: 搜索只能按资源类型过滤,搜不到“预制体里挂了某组件”,故提供本工具。
|
||||
+/// </summary>
|
||||
+public class FullScreenCoverTool : EditorWindow
|
||||
+{
|
||||
+ private sealed class CoverInfo
|
||||
+ {
|
||||
+ public Object prefab; // 预制体资产引用(用于 Ping/打开)
|
||||
+ public string prefabPath; // 资源路径
|
||||
+ public string nodeName; // 组件所在节点名
|
||||
+ public bool enabled; // 组件是否启用
|
||||
+ public bool keepShow; // 被遮挡时保持显示
|
||||
+ }
|
||||
+
|
||||
+ private readonly List<CoverInfo> results = new List<CoverInfo>();
|
||||
+ private Vector2 scroll;
|
||||
+ private GameObject addTarget;
|
||||
+ private bool scanned;
|
||||
+ private string statusText = "尚未扫描,点击“扫描全工程”查找挂载 FullScreenCover 组件的面板预制体";
|
||||
+
|
||||
+ [MenuItem("Tools/UI/全屏面板遮挡工具")]
|
||||
+ private static void ShowWindow()
|
||||
+ {
|
||||
+ var win = GetWindow<FullScreenCoverTool>("全屏面板遮挡");
|
||||
+ win.minSize = new Vector2(540f, 320f);
|
||||
+ }
|
||||
+
|
||||
+ private void OnEnable()
|
||||
+ {
|
||||
+ // 打开窗口时自动扫描一次
|
||||
+ EditorApplication.delayCall += ScanAll;
|
||||
+ }
|
||||
+
|
||||
+ private void OnGUI()
|
||||
+ {
|
||||
+ EditorGUILayout.HelpBox(
|
||||
+ "挂有 FullScreenCover 组件的面板 = 全屏面板:打开时自动隐藏下层 UI(省去被遮住的 DrawCall),关闭/销毁自动恢复。\n" +
|
||||
+ "注意:仅用于完全铺满屏幕的面板(如通行证、商店、活动大页),半屏弹窗不要挂。", MessageType.Info);
|
||||
+
|
||||
+ // —— 添加区 ——
|
||||
+ EditorGUILayout.LabelField("添加组件(拖入面板预制体)", EditorStyles.boldLabel);
|
||||
+ using (new EditorGUILayout.HorizontalScope())
|
||||
+ {
|
||||
+ addTarget = (GameObject)EditorGUILayout.ObjectField(addTarget, typeof(GameObject), false);
|
||||
+ using (new EditorGUI.DisabledScope(addTarget == null))
|
||||
+ {
|
||||
+ if (GUILayout.Button("添加组件", GUILayout.Width(80f)))
|
||||
+ {
|
||||
+ AddComponentToPrefab(AssetDatabase.GetAssetPath(addTarget));
|
||||
+ addTarget = null;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ EditorGUILayout.Space(4f);
|
||||
+
|
||||
+ // —— 扫描区 ——
|
||||
+ using (new EditorGUILayout.HorizontalScope())
|
||||
+ {
|
||||
+ if (GUILayout.Button("扫描全工程", GUILayout.Height(24f)))
|
||||
+ {
|
||||
+ ScanAll();
|
||||
+ }
|
||||
+ GUILayout.Label(statusText, EditorStyles.miniLabel);
|
||||
+ }
|
||||
+
|
||||
+ EditorGUILayout.Space(4f);
|
||||
+
|
||||
+ // —— 结果列表 ——
|
||||
+ if (!scanned)
|
||||
+ {
|
||||
+ return;
|
||||
+ }
|
||||
+ if (results.Count == 0)
|
||||
+ {
|
||||
+ EditorGUILayout.HelpBox("没有找到任何挂载 FullScreenCover 的面板。可用上方“添加组件”为全屏面板配置。", MessageType.Warning);
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ scroll = EditorGUILayout.BeginScrollView(scroll);
|
||||
+ for (int i = 0; i < results.Count; i++)
|
||||
+ {
|
||||
+ DrawResultItem(results[i]);
|
||||
+ }
|
||||
+ EditorGUILayout.EndScrollView();
|
||||
+ }
|
||||
+
|
||||
+ private void DrawResultItem(CoverInfo info)
|
||||
+ {
|
||||
+ using (new EditorGUILayout.VerticalScope("box"))
|
||||
+ {
|
||||
+ using (new EditorGUILayout.HorizontalScope())
|
||||
+ {
|
||||
+ // 点击名称定位资源
|
||||
+ string title = info.prefab != null ? info.prefab.name : Path.GetFileName(info.prefabPath);
|
||||
+ if (GUILayout.Button(title, EditorStyles.boldLabel, GUILayout.Width(170f)))
|
||||
+ {
|
||||
+ EditorGUIUtility.PingObject(info.prefab);
|
||||
+ }
|
||||
+
|
||||
+ GUILayout.Label(info.enabled ? "已启用" : "已禁用",
|
||||
+ info.enabled ? EditorStyles.miniLabel : EditorStyles.whiteMiniLabel, GUILayout.Width(46f));
|
||||
+
|
||||
+ GUILayout.FlexibleSpace();
|
||||
+
|
||||
+ // 被遮挡时保持显示(勾选即写回预制体)
|
||||
+ bool newKeep = GUILayout.Toggle(info.keepShow, "被遮挡时保持显示", GUILayout.Width(132f));
|
||||
+ if (newKeep != info.keepShow)
|
||||
+ {
|
||||
+ info.keepShow = newKeep;
|
||||
+ SetKeepShow(info.prefabPath, newKeep);
|
||||
+ }
|
||||
+
|
||||
+ if (GUILayout.Button("打开", GUILayout.Width(46f)))
|
||||
+ {
|
||||
+ AssetDatabase.OpenAsset(info.prefab);
|
||||
+ }
|
||||
+ if (GUILayout.Button("移除组件", GUILayout.Width(68f)))
|
||||
+ {
|
||||
+ if (EditorUtility.DisplayDialog("移除 FullScreenCover",
|
||||
+ $"确定从 {Path.GetFileName(info.prefabPath)} 移除 FullScreenCover 组件?\n该面板将不再自动隐藏下层。",
|
||||
+ "移除", "取消"))
|
||||
+ {
|
||||
+ RemoveComponent(info.prefabPath);
|
||||
+ ScanAll();
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ EditorGUILayout.LabelField(info.prefabPath, EditorStyles.miniLabel);
|
||||
+ if (!info.enabled)
|
||||
+ {
|
||||
+ EditorGUILayout.LabelField("⚠ 组件处于禁用状态,不会生效(在预制体根节点 Inspector 勾选启用)。", EditorStyles.miniLabel);
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ // —— 扫描 ——
|
||||
+
|
||||
+ private void ScanAll()
|
||||
+ {
|
||||
+ string scriptGuid = GetScriptGuid();
|
||||
+ if (string.IsNullOrEmpty(scriptGuid))
|
||||
+ {
|
||||
+ EditorUtility.DisplayDialog("错误", "无法获取 FullScreenCover 脚本 GUID,请确认脚本已编译无报错。", "确定");
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab", new[] { "Assets" });
|
||||
+ results.Clear();
|
||||
+ try
|
||||
+ {
|
||||
+ for (int i = 0; i < prefabGuids.Length; i++)
|
||||
+ {
|
||||
+ if (EditorUtility.DisplayCancelableProgressBar("全屏面板遮挡工具",
|
||||
+ $"扫描预制体中... ({i + 1}/{prefabGuids.Length})", (float)i / prefabGuids.Length))
|
||||
+ {
|
||||
+ break; // 用户取消
|
||||
+ }
|
||||
+
|
||||
+ string path = AssetDatabase.GUIDToAssetPath(prefabGuids[i]);
|
||||
+ if (string.IsNullOrEmpty(path) || !path.EndsWith(".prefab"))
|
||||
+ {
|
||||
+ continue;
|
||||
+ }
|
||||
+
|
||||
+ // 文本快筛:只有包含组件脚本 guid 的预制体才真正加载,避免逐个解析
|
||||
+ if (!ContainsGuidFast(path, scriptGuid))
|
||||
+ {
|
||||
+ continue;
|
||||
+ }
|
||||
+
|
||||
+ GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
||||
+ if (prefab == null)
|
||||
+ {
|
||||
+ continue;
|
||||
+ }
|
||||
+ FullScreenCover[] comps = prefab.GetComponentsInChildren<FullScreenCover>(true);
|
||||
+ foreach (FullScreenCover comp in comps)
|
||||
+ {
|
||||
+ results.Add(new CoverInfo
|
||||
+ {
|
||||
+ prefab = prefab,
|
||||
+ prefabPath = path,
|
||||
+ nodeName = comp.gameObject.name,
|
||||
+ enabled = comp.enabled,
|
||||
+ keepShow = comp.isKeepShowWhenCovered
|
||||
+ });
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ finally
|
||||
+ {
|
||||
+ EditorUtility.ClearProgressBar();
|
||||
+ }
|
||||
+
|
||||
+ scanned = true;
|
||||
+ statusText = $"扫描完成:{results.Count} 个面板预制体挂载 FullScreenCover";
|
||||
+ Repaint();
|
||||
+ }
|
||||
+
|
||||
+ /// <summary>动态获取 FullScreenCover 脚本资产的 guid,避免硬编码。</summary>
|
||||
+ private static string GetScriptGuid()
|
||||
+ {
|
||||
+ GameObject probe = new GameObject("FullScreenCoverGuidProbe");
|
||||
+ try
|
||||
+ {
|
||||
+ MonoScript ms = MonoScript.FromMonoBehaviour(probe.AddComponent<FullScreenCover>());
|
||||
+ if (ms == null)
|
||||
+ {
|
||||
+ return null;
|
||||
+ }
|
||||
+ return AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(ms));
|
||||
+ }
|
||||
+ finally
|
||||
+ {
|
||||
+ Object.DestroyImmediate(probe);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ /// <summary>逐行读取文件快速判断是否包含 guid(找到即中断),比加载预制体快一个量级。</summary>
|
||||
+ private static bool ContainsGuidFast(string path, string guid)
|
||||
+ {
|
||||
+ try
|
||||
+ {
|
||||
+ foreach (string line in File.ReadLines(path))
|
||||
+ {
|
||||
+ if (line.Contains(guid))
|
||||
+ {
|
||||
+ return true;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ catch (IOException)
|
||||
+ {
|
||||
+ // 文件被占用等情况按未命中处理
|
||||
+ }
|
||||
+ return false;
|
||||
+ }
|
||||
+
|
||||
+ // —— 修改预制体 ——
|
||||
+
|
||||
+ /// <summary>以“加载内容-修改-保存-卸载”的标准流程安全修改预制体资产。</summary>
|
||||
+ private static void EditPrefab(string path, System.Action<GameObject> edit)
|
||||
+ {
|
||||
+ GameObject root = PrefabUtility.LoadPrefabContents(path);
|
||||
+ try
|
||||
+ {
|
||||
+ edit(root);
|
||||
+ PrefabUtility.SaveAsPrefabAsset(root, path);
|
||||
+ }
|
||||
+ finally
|
||||
+ {
|
||||
+ PrefabUtility.UnloadPrefabContents(root);
|
||||
+ }
|
||||
+ AssetDatabase.SaveAssets();
|
||||
+ }
|
||||
+
|
||||
+ private void AddComponentToPrefab(string path)
|
||||
+ {
|
||||
+ if (string.IsNullOrEmpty(path) || !path.EndsWith(".prefab"))
|
||||
+ {
|
||||
+ EditorUtility.DisplayDialog("无法添加", "请拖入一个预制体资产(.prefab),而不是场景对象或模型。", "确定");
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ bool already = false;
|
||||
+ EditPrefab(path, root =>
|
||||
+ {
|
||||
+ if (root.GetComponent<FullScreenCover>() != null)
|
||||
+ {
|
||||
+ already = true;
|
||||
+ }
|
||||
+ else
|
||||
+ {
|
||||
+ root.AddComponent<FullScreenCover>();
|
||||
+ }
|
||||
+ });
|
||||
+
|
||||
+ ShowNotification(new GUIContent(already ? "该预制体已挂载组件" : "已添加 FullScreenCover"));
|
||||
+ ScanAll();
|
||||
+ }
|
||||
+
|
||||
+ private void RemoveComponent(string path)
|
||||
+ {
|
||||
+ EditPrefab(path, root =>
|
||||
+ {
|
||||
+ FullScreenCover[] comps = root.GetComponentsInChildren<FullScreenCover>(true);
|
||||
+ foreach (FullScreenCover comp in comps)
|
||||
+ {
|
||||
+ Object.DestroyImmediate(comp, true);
|
||||
+ }
|
||||
+ });
|
||||
+ }
|
||||
+
|
||||
+ private void SetKeepShow(string path, bool value)
|
||||
+ {
|
||||
+ EditPrefab(path, root =>
|
||||
+ {
|
||||
+ FullScreenCover[] comps = root.GetComponentsInChildren<FullScreenCover>(true);
|
||||
+ foreach (FullScreenCover comp in comps)
|
||||
+ {
|
||||
+ comp.isKeepShowWhenCovered = value;
|
||||
+ }
|
||||
+ });
|
||||
+ }
|
||||
+}
|
||||
diff --git a/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/FullScreenCover.cs b/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/FullScreenCover.cs
|
||||
new file mode 100644
|
||||
index 00000000..48f6aaf3
|
||||
--- /dev/null
|
||||
+++ b/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/FullScreenCover.cs
|
||||
@@ -0,0 +1,16 @@
|
||||
+using UnityEngine;
|
||||
+
|
||||
+namespace MFrame
|
||||
+{
|
||||
+ /// <summary>
|
||||
+ /// 全屏遮挡组件:挂到面板预制体根节点(与 UI_PanelBase 同节点)即生效,移除组件即关闭。
|
||||
+ /// 打开本面板时自动隐藏下方层级中正在显示的面板(省去被遮住的 DrawCall),
|
||||
+ /// 关闭/销毁时自动恢复。由策划在编辑器中添加/移除本组件即可配置,无需改代码。
|
||||
+ /// 注意:仅用于完全铺满屏幕的面板(如通行证、商店、活动大页),半屏弹窗不要挂。
|
||||
+ /// </summary>
|
||||
+ public class FullScreenCover : MonoBehaviour
|
||||
+ {
|
||||
+ [Tooltip("勾选后:本面板被上层全屏面板遮挡时仍保持显示(仅用于必须常驻可见的特殊面板)。")]
|
||||
+ public bool isKeepShowWhenCovered = false;
|
||||
+ }
|
||||
+}
|
||||
diff --git a/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/PanelBase.cs b/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/PanelBase.cs
|
||||
index 98c6d79f..657a4024 100644
|
||||
--- a/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/PanelBase.cs
|
||||
+++ b/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/PanelBase.cs
|
||||
@@ -1,3 +1,4 @@
|
||||
+using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MFrame
|
||||
@@ -56,6 +57,54 @@ namespace MFrame
|
||||
return _autoDestroy;
|
||||
}
|
||||
|
||||
+ // 全屏遮挡以组件形式配置:在面板预制体根节点挂 FullScreenCover 组件即生效(策划可自助添加/移除)
|
||||
+ /// <summary>
|
||||
+ /// 面板预制体根节点上的 FullScreenCover 组件(未挂则为 null,即非全屏面板)
|
||||
+ /// </summary>
|
||||
+ private FullScreenCover _fullScreenCover;
|
||||
+
|
||||
+ /// <summary>
|
||||
+ /// 是否正被上层全屏面板隐藏
|
||||
+ /// </summary>
|
||||
+ private bool _hiddenByCover;
|
||||
+ /// <summary>
|
||||
+ /// 被本面板(全屏遮挡)隐藏的下层面板列表
|
||||
+ /// </summary>
|
||||
+ private readonly List<IPanelBase> _panelsHiddenByMe = new List<IPanelBase>();
|
||||
+
|
||||
+ // —— 全屏遮挡:IPanelBase 接口实现 ——
|
||||
+ UILayerIdx IPanelBase.panelLayerIdx => _layerIdx;
|
||||
+ bool IPanelBase.isKeepShowWhenCovered => _fullScreenCover != null && _fullScreenCover.enabled && _fullScreenCover.isKeepShowWhenCovered;
|
||||
+ bool IPanelBase.isHiddenByCover => _hiddenByCover;
|
||||
+
|
||||
+ /// <summary>
|
||||
+ /// 全屏遮挡优化:由上层全屏面板调用,隐藏本面板(省去被盖住的 DrawCall)
|
||||
+ /// </summary>
|
||||
+ public void SetCoverHidden()
|
||||
+ {
|
||||
+ _hiddenByCover = true;
|
||||
+ if (_view != null)
|
||||
+ {
|
||||
+ _view.gameObject.SetActive(false);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ /// <summary>
|
||||
+ /// 全屏遮挡优化:恢复本面板显示(幂等;面板视图已销毁时自动跳过)
|
||||
+ /// </summary>
|
||||
+ public void RestoreFromCover()
|
||||
+ {
|
||||
+ if (!_hiddenByCover)
|
||||
+ {
|
||||
+ return;
|
||||
+ }
|
||||
+ _hiddenByCover = false;
|
||||
+ if (_view != null)
|
||||
+ {
|
||||
+ _view.gameObject.SetActive(true);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
|
||||
/// <summary>
|
||||
/// 尝试异步加载UI预制体
|
||||
@@ -101,6 +150,8 @@ namespace MFrame
|
||||
transform = go.transform;
|
||||
// 获取视图组件并完成初始化
|
||||
_view = transform.GetComponent<V>();
|
||||
+ // 缓存全屏遮挡组件(策划在预制体根节点上挂载,未挂则为 null)
|
||||
+ _fullScreenCover = transform.GetComponent<FullScreenCover>();
|
||||
_view.AddUpdateHandler(this.Update);
|
||||
_OnLoadComplete();
|
||||
Show(data, completeHandler);
|
||||
@@ -150,8 +201,22 @@ namespace MFrame
|
||||
{
|
||||
_data = data;
|
||||
if (this._view != null)
|
||||
+ {
|
||||
+ // 若此前被上层全屏面板隐藏,主动 Show 视为恢复显示
|
||||
+ //(隐藏者列表中的遗留记录由 RestorePanelsHiddenByMe 的 isHiddenByCover 守卫失效,无需清理)
|
||||
+ if (_hiddenByCover)
|
||||
+ {
|
||||
+ RestoreFromCover();
|
||||
+ }
|
||||
this._view.gameObject.SetActive(true);
|
||||
+ }
|
||||
_OnShow();
|
||||
+ // 全屏遮挡面板(预制体根节点挂 FullScreenCover 组件且处于启用状态):
|
||||
+ // 打开时隐藏下层正在显示的面板,省去被盖住的 DrawCall
|
||||
+ if (_fullScreenCover != null && _fullScreenCover.enabled)
|
||||
+ {
|
||||
+ HidePanelsUnderThis();
|
||||
+ }
|
||||
_view.Show(() =>
|
||||
{
|
||||
completeHandler?.Invoke(this);
|
||||
@@ -168,7 +233,8 @@ namespace MFrame
|
||||
{
|
||||
return false;
|
||||
}
|
||||
- return _view.gameObject.activeSelf;
|
||||
+ // 被上层全屏面板隐藏时仍视为“打开中”,保持业务语义不变
|
||||
+ return _view.gameObject.activeSelf || _hiddenByCover;
|
||||
}
|
||||
public bool isLoading()
|
||||
{
|
||||
@@ -193,6 +259,8 @@ namespace MFrame
|
||||
{
|
||||
_view.Close(() =>
|
||||
{
|
||||
+ // 关闭完成后恢复被本面板隐藏的下层面板
|
||||
+ RestorePanelsHiddenByMe();
|
||||
_OnClose();
|
||||
complete?.Invoke();
|
||||
});
|
||||
@@ -204,6 +272,53 @@ namespace MFrame
|
||||
|
||||
}
|
||||
|
||||
+ /// <summary>
|
||||
+ /// 全屏遮挡优化:隐藏本面板下方所有层级中正在显示的面板,
|
||||
+ /// 被完全遮住的 UI 不再提交 DrawCall(关闭/销毁时由 RestorePanelsHiddenByMe 恢复)。
|
||||
+ /// </summary>
|
||||
+ private void HidePanelsUnderThis()
|
||||
+ {
|
||||
+ Dictionary<string, PanelMsgItem> items = PanelMgr.Instance.items;
|
||||
+ if (items == null)
|
||||
+ {
|
||||
+ return;
|
||||
+ }
|
||||
+ foreach (KeyValuePair<string, PanelMsgItem> kvp in items)
|
||||
+ {
|
||||
+ PanelMsgItem item = kvp.Value;
|
||||
+ if (item.panel == null || ReferenceEquals(item.panel, this))
|
||||
+ {
|
||||
+ continue;
|
||||
+ }
|
||||
+ IPanelBase panel = item.panel;
|
||||
+ if (panel.panelLayerIdx < _layerIdx
|
||||
+ && !panel.isKeepShowWhenCovered
|
||||
+ && !panel.isHiddenByCover
|
||||
+ && panel.isShowing())
|
||||
+ {
|
||||
+ _panelsHiddenByMe.Add(panel);
|
||||
+ panel.SetCoverHidden();
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ /// <summary>
|
||||
+ /// 恢复被本面板隐藏的下层面板(幂等,可重复调用)
|
||||
+ /// </summary>
|
||||
+ private void RestorePanelsHiddenByMe()
|
||||
+ {
|
||||
+ int len = _panelsHiddenByMe.Count;
|
||||
+ for (int i = 0; i < len; i++)
|
||||
+ {
|
||||
+ IPanelBase panel = _panelsHiddenByMe[i];
|
||||
+ if (panel != null && panel.isHiddenByCover)
|
||||
+ {
|
||||
+ panel.RestoreFromCover();
|
||||
+ }
|
||||
+ }
|
||||
+ _panelsHiddenByMe.Clear();
|
||||
+ }
|
||||
+
|
||||
|
||||
|
||||
/// <summary>
|
||||
@@ -211,8 +326,8 @@ namespace MFrame
|
||||
/// </summary>
|
||||
protected override void _Destroy()
|
||||
{
|
||||
-
|
||||
-
|
||||
+ // 兼容未走 Close 直接销毁的路径,确保下层面板一定被恢复
|
||||
+ RestorePanelsHiddenByMe();
|
||||
|
||||
base._Destroy();
|
||||
|
||||
@@ -221,6 +336,7 @@ namespace MFrame
|
||||
|
||||
// 清除面板数据
|
||||
_data = null;
|
||||
+ _fullScreenCover = null;
|
||||
if (_view != null)
|
||||
{
|
||||
_view.RemoveUpdateHandler(this.Update);
|
||||
diff --git a/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/iface/IPanelBase.cs b/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/iface/IPanelBase.cs
|
||||
index c0a2283c..48cd4d14 100644
|
||||
--- a/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/iface/IPanelBase.cs
|
||||
+++ b/unity/my_libs/unityLib/GameFrame/Runtime/frame/module/moduleUI/iface/IPanelBase.cs
|
||||
@@ -31,7 +31,30 @@ namespace MFrame
|
||||
/// <returns></returns>
|
||||
bool isLoading();
|
||||
|
||||
+ /// <summary>
|
||||
+ /// 面板所在 UI 层级(全屏遮挡判定用:仅隐藏低于本层面板层级的面板)
|
||||
+ /// </summary>
|
||||
+ UILayerIdx panelLayerIdx { get; }
|
||||
|
||||
+ /// <summary>
|
||||
+ /// 被上层全屏面板遮挡时是否仍保持显示(常驻特殊面板为 true)
|
||||
+ /// </summary>
|
||||
+ bool isKeepShowWhenCovered { get; }
|
||||
+
|
||||
+ /// <summary>
|
||||
+ /// 是否正被上层全屏面板隐藏
|
||||
+ /// </summary>
|
||||
+ bool isHiddenByCover { get; }
|
||||
+
|
||||
+ /// <summary>
|
||||
+ /// 全屏遮挡优化:由上层全屏面板调用,隐藏本面板,省去被盖住的 DrawCall
|
||||
+ /// </summary>
|
||||
+ void SetCoverHidden();
|
||||
+
|
||||
+ /// <summary>
|
||||
+ /// 全屏遮挡优化:由隐藏者关闭/销毁时调用,恢复本面板显示(幂等)
|
||||
+ /// </summary>
|
||||
+ void RestoreFromCover();
|
||||
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,13 @@
|
||||
diff --git a/unity/Assets/Settings/URP-Balanced-Renderer.asset b/unity/Assets/Settings/URP-Balanced-Renderer.asset
|
||||
index ef0bb8d5..b94cfcc4 100644
|
||||
--- a/unity/Assets/Settings/URP-Balanced-Renderer.asset
|
||||
+++ b/unity/Assets/Settings/URP-Balanced-Renderer.asset
|
||||
@@ -12,7 +12,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: f62c9c65cf3354c93be831c8bc075510, type: 3}
|
||||
m_Name: SSAO
|
||||
m_EditorClassIdentifier:
|
||||
- m_Active: 1
|
||||
+ m_Active: 0
|
||||
m_Settings:
|
||||
AOMethod: 1
|
||||
Downsample: 1
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,240 @@
|
||||
# UIPrefabCheck 能力清单与优化方案
|
||||
|
||||
> 源文件:`Assets/Editor/UIPrefabCheck.cs`(1434 行,本目录含只读副本,MD5 `74D4A63A33869AE560BA6B522E2DABFC`)
|
||||
> 菜单入口:`Tools/UI/Prefab 和图片优化扫描工具`、`Tools/UI/界面性能巡检`
|
||||
> 评估时间:2026-09-10 | **结论:工具本身设计扎实(只读、分页、LSH 相似图、dHash 带 alpha 包围盒),但对"降 DC"这个目标存在 4 个关键能力缺口,其中 2 个改动量很小、收益很大。**
|
||||
|
||||
---
|
||||
|
||||
## 第一章 它现在是什么(两个类,7 个标签页)
|
||||
|
||||
一个文件里两个独立部分:
|
||||
|
||||
| 类 | 行号 | 职责 | 运行时机 |
|
||||
| --- | --- | --- | --- |
|
||||
| `UIRuntimePerformanceAudit` | L17-478 | 界面性能巡检(自动逐个 Show/Close 面板 + 手动录制实际操作) | **必须 Play Mode** |
|
||||
| `UIPrefabCheck : EditorWindow` | L480-1433 | Prefab / 图片 / 图集静态扫描(6 个页)+ 承载上面那个巡检页 | 编辑模式即可 |
|
||||
|
||||
### 1.1 七个标签页逐项能力
|
||||
|
||||
| 标签页 | 绘制方法 | 扫什么 | 判定标准 / 阈值 | 能回答什么问题 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 引用明细 | `DrawReferencePage` L662 | 每个 prefab 里的 `Image.sprite` / `RawImage.texture` / `SpriteRenderer.sprite` | 图片不在 `gui/` 或 `PSD2UI/UI/` 下 → 标"非标准 UI 资源目录"(`IsStandardUiPath` L1431) | 某张图被哪个 prefab 的哪个节点用了、属于哪个图集(可点击定位图集) |
|
||||
| 多预制体共用 | `DrawSharedPage` L696 | 按 `AssetPath` 分组,`PrefabPaths.Count >= 2` | — | 哪些图被多个 prefab 共用(共用度高 = 更该进同一个图集) |
|
||||
| 疑似无用图片 | `DrawUnusedImagePage` L722 | `UnusedImageRoots`(L487-491:`gui` + `PSD2UI/UI`)下的 `t:Texture2D` | 既不在 `prefabDependencies`(仅来自 prefabRoot 下 prefab 的递归依赖,L895),也没有静态代码引用(`HasStaticCodeReference` L1148:guid / 全路径 / Resources 相对路径的**字面**子串) | 哪些图可能是老项目遗留的废图 |
|
||||
| 节点统计 | `DrawNodeStatisticsPage` L760 | 每个 prefab 的 `Transform` 总数 | 超过 `GetSuggestedNodeCount` L942 的硬编码值(UI面板 150 / UI组件 50 / 特效 80 / 角色 100 / 通用 80)→ 标"建议关注"或"建议拆分" | 哪些 prefab 层级过深/节点冗余(影响 Rebuild 与合批) |
|
||||
| 大图标记 | `DrawLargeImagePage` L796 | `UnusedImageRoots` 下的 `t:Texture2D` | 源文件 > 500 KB(L484)**或**分辨率 ≥ 1024×1024(L485) | 哪些图源文件太大 |
|
||||
| 相似图 | `DrawSimilarImagePage` L831 | `UnusedImageRoots` 下的 png/jpg/jpeg(L1282) | dHash 64bit(**带 alpha 包围盒裁剪** L1311-1320,再采 8×9 亮度网格 L1323-1333)+ 宽高比差 ≤ 0.05(L1363)+ 汉明距离 ≤ 6(L486);用 8 字节分桶 LSH 避免 O(n²)(`FindSimilarImagePairs` L1346) | 哪些图是重复/近似重复(老项目遗留的典型症状) |
|
||||
| 界面性能 | `UIRuntimePerformanceAudit.DrawEmbedded` L83 | 从 `GameInitWrapper` 源码正则提取 `AddItem<T>`(L176)发现所有注册面板 | 自动巡检:预热 10 帧 + 采样 60 帧(L19-20),输出 打开耗时 / 平均 / P95 / 最大帧耗时 / GC 峰值 / 关闭后残留对象数;手动录制:轮询可见面板,记录实际停留期间的帧耗时 | 哪个面板打开慢、哪个面板停留时拖帧、哪个面板关不干净 |
|
||||
|
||||
### 1.2 值得肯定的设计(不要在优化时破坏)
|
||||
|
||||
1. **全程只读**:`PrefabUtility.LoadPrefabContents` + `finally UnloadPrefabContents`(L908-916),不改 prefab、不改图集、不写 Undo。
|
||||
2. **dHash 前先做 alpha 包围盒裁剪**(L1311-1320)——对 UI 图至关重要:UI 图周围大量透明,不裁剪的话所有图都"相似",误报会淹没结果。这一步做得比很多商业工具细。
|
||||
3. **LSH 分桶代替两两比对**(L1346-1375):1715 张图两两比对是 147 万对,分桶后只比对同桶候选,扫描时间从分钟级降到秒级。
|
||||
4. **分页 100 条/页 + 筛选 + 可点击 Ping**(`DrawPager` L1403、`DrawAtlasLink` L974):结果多也不会卡 GUI。
|
||||
5. **CSV 导出带 UTF8 BOM**(L1426、L458):Excel 直接打开不乱码。
|
||||
6. **性能巡检有"关闭后残留对象"检查**(`FinishClosing` L416-427):能发现"面板关了但子对象还活着",这正是 T5 FullScreenCover 复活问题的同类症状。
|
||||
|
||||
---
|
||||
|
||||
## 第二章 它对"降 DC"能帮什么、不能帮什么
|
||||
|
||||
### 2.1 能帮(已在 T5 验证过的用法)
|
||||
|
||||
- **找图集分散问题**:引用明细页每行都显示图集归属,`FindAtlases` 用 `atlas.CanBindTo(sprite)`(L968)判断,比读 meta 可靠。一个面板里图的图集名越杂,批次越多。
|
||||
- **找重复图**:相似图页直接指出"这两张图其实是一张",合并后既降包体又降批次。
|
||||
- **找废图**:疑似无用图片页(但判定弱,见 §3 B1)。
|
||||
- **量化面板性能**:界面性能页给出 P95 帧耗时,可作为优化前后对比的基线。
|
||||
|
||||
### 2.2 不能帮(4 个关键缺口)
|
||||
|
||||
1. **测不出 Draw Call / 批次数**。`PanelResult`(L37-50)只有帧耗时和 GC,**降 DC 的成果无法用这个工具量化**——改完补丁只能靠 Frame Debugger 手工数,做不了"全项目面板 DC 排行"。
|
||||
2. **看不出"谁在断批"**。工具只扫 `Image` / `RawImage` / `SpriteRenderer` 三种组件(L912-914),**完全没扫 `Mask` / `RectMask2D` / `UIEffect` / `UIEffectReplica` / 嵌套 `Canvas` / TMP 字体资产**——而这些才是断批的真正元凶。
|
||||
> T5 真实案例:`LevelGiftItem.prefab` 里 1 个 `UIEffect` + 6 个 `UIEffectReplica`(参数全是默认 None、无视觉效果),运行时每条克隆出 7 个独立材质,直接把面板顶到 101 批。**这个工具当时没能报出这个问题**。
|
||||
3. **不做合批预估**。已经有"每张图属于哪个图集"的数据,却没有按 prefab 聚合出"这个面板涉及几个图集 / 几张散图 / 预估最少批次",无法批量排出"最该优化的面板 TOP N"。
|
||||
4. **不看纹理导入设置**。大图页只看**源文件大小**和分辨率,但真机成本是**运行时显存**,由 `TextureImporter` 的压缩格式 / maxSize / mipmap / isReadable 决定。一张 1024×1024 的图:RGBA32 未压缩 = 4 MB 显存,ASTC 6×6 = 约 0.5 MB——工具现在看不出这个差别,而 T5 光 png 就有 302 MB / 1715 张。
|
||||
|
||||
---
|
||||
|
||||
## 第三章 优化清单(按优先级,含代码位置与改法)
|
||||
|
||||
### A 级:能力补齐(对降 DC / 瘦身目标收益最大)
|
||||
|
||||
#### A1 给性能巡检加 DC / 批次 / SetPass 指标 ★首推
|
||||
|
||||
- **现状**:`StartRecorder` L443-448 只起了一个 `ProfilerRecorder`(`ProfilerCategory.Memory` 的 `GC Allocated In Frame`);采样在 `Tick` L384-393。
|
||||
- **改法**:照 `gcRecorder` 的同一套写法,多起 3 个 Render 类别的 recorder:
|
||||
```csharp
|
||||
drawCallRecorder = ProfilerRecorder.StartNew(ProfilerCategory.Render, "Draw Calls Count");
|
||||
batchRecorder = ProfilerRecorder.StartNew(ProfilerCategory.Render, "Batches Count");
|
||||
setPassRecorder = ProfilerRecorder.StartNew(ProfilerCategory.Render, "SetPass Calls Count");
|
||||
```
|
||||
在 `Tick` 的 Sampling 分支里按现有风格累积(`Math.Max(..., recorder.LastValue)` 取峰值;再加一个累加/计数得平均值);
|
||||
`PanelResult`(L37)加 `MaxDrawCalls` / `AvgDrawCalls` / `MaxBatches` / `MaxSetPass` 四个字段;
|
||||
`DrawEmbedded` 的结果表头(L140-149)与 `ExportCsv` 的列(L455-457)同步加列;`Finish` L440 处一并 Dispose。
|
||||
- **备选**:`UnityEditor.UnityStats.drawCalls / batches / setPassCalls` 更简单,但受编辑器自身渲染和 Scene 视图影响更大,建议以 `ProfilerRecorder` 为主。
|
||||
- **诚实提示(务必写进界面 HelpBox)**:编辑器 Play Mode 测得的 DC 含编辑器渲染开销,**只能用于同环境下的前后对比**,不能当真机绝对值;真机结论仍需 Profiler 连真机复测。这与 `05_排查方法论/DC与帧率排查经验.md` §2.1 的立场一致。
|
||||
- **收益**:降 DC 从此可量化、可排行、可回归。做完这一项,"改了补丁到底降了多少"不再靠肉眼数 Frame Debugger。
|
||||
- **工作量**:约 30-40 行,半天内含验证。
|
||||
|
||||
#### A2 新增"合批体检"页(静态分析,不需 Play Mode) ★首推
|
||||
|
||||
- **现状**:`FindAtlases` L962 已能给出每张图的图集归属,但数据只逐行展示,没有按 prefab 聚合;`ScanPrefab` L906 只遍历三种图形组件。
|
||||
- **改法**:
|
||||
1. `ScanPrefab` 里补扫:`Mask` / `RectMask2D` 数量、`UIEffect` / `UIEffectReplica` 数量(用 `GetComponentsInChildren<Coffee.UIEffects.UIEffectBase>(true)` 可一次拿到两者)、嵌套 `Canvas` 数量(`GetComponentsInChildren<Canvas>(true).Length - 1`)、TMP 文本用到的字体资产 distinct 数;
|
||||
2. 新增 `BatchHealthRecord`:`PrefabPath`、涉及图集数、散图数(`Atlases == "未加入图集"`)、Mask 数、UIEffect 数、嵌套 Canvas 数、字体种类数、**预估最少批次数**;
|
||||
3. 预估公式(标注为估算,非实测):
|
||||
`预估批次 ≈ distinct图集数 + 散图数 + distinct字体图集数 + UIEffect配置种类数 + Mask分区数 + 嵌套Canvas数`
|
||||
4. 新增第 8 个 `ViewTab`,按"预估批次"降序排列,支持导出 CSV。
|
||||
- **收益**:一次扫描就能列出"全项目最该优化的面板 TOP N"以及每个面板的**具体病因**(是图集分散、还是 UIEffect 断批、还是 Mask 太多)。这正是丢失的 `UIDCProbeTool` 的核心价值,而且**静态就能算、可批量、可导表做优化前后对比**,比原来那个只能在运行时看单个 Canvas 的版本更实用。
|
||||
- **工作量**:约 150 行,一天内。
|
||||
- **依赖注意**:引用 `Coffee.UIEffects` 命名空间会让这个 Editor 脚本依赖 UIEffect 包。若担心耦合,可改用 `GetComponentsInChildren<Component>` + `GetType().Name` 字符串判断(`"UIEffect"` / `"UIEffectReplica"`),零编译依赖。
|
||||
|
||||
#### A3 新增纹理导入设置审计页(服务"源文件瘦身")
|
||||
|
||||
- **现状**:`ScanLargeImages` L1185 只看 `GetAssetFileSize`(L1215,源文件字节数)和 `image.width/height`。
|
||||
- **改法**:对每张图取 `AssetImporter.GetAtPath(path) as TextureImporter`,输出并判定:
|
||||
| 检查项 | 判定 | 浪费量级 |
|
||||
| --- | --- | --- |
|
||||
| `mipmapEnabled` | UI 图(`textureType == Sprite`)开 mipmap = 白扔 | 约 +33% 显存 |
|
||||
| `isReadable` | 非必须读取却开着 = 内存里存两份 | ×2 显存 |
|
||||
| `maxTextureSize` | 远大于 prefab 里实际显示尺寸 | 按平方倍 |
|
||||
| 平台 override 格式 | Android/iOS 用 RGBA32 / ARGB32 未压缩 | 4-8 倍于 ASTC |
|
||||
| `npotScale` | 非 2 的幂图被放大到 2 的幂 | 最多 4 倍 |
|
||||
| `sRGBTexture` / `alphaIsTransparency` | 设置与用途不符 | 视觉问题 |
|
||||
同时给出**预估运行时显存**(宽 × 高 × 每像素字节数,按 format 查表),并与源文件大小并列显示——两者差异常常是数量级的。
|
||||
- **收益**:T5 有 302 MB png / 1715 张 + 65 MB TGA + 11 MB PSD,瘦身真正的肥肉在导入设置而不是源文件。这一页直接指向"改哪个设置省多少 MB"。
|
||||
- **工作量**:约 120 行,一天内。
|
||||
|
||||
#### A4 扩大扫描范围(配置化)
|
||||
|
||||
- **现状**:`DefaultPrefabRoot` L482 固定为 `gui/panel`;`UnusedImageRoots` L487-491 硬编码两个目录(`gui` + `PSD2UI/UI`)。T5 的 `Resources_moved` 共 739.6 MB,其中 `effects` 254.5 MB、`res3d` 202.3 MB、`font` 93.8 MB、`animation` 45 MB **全都扫不到**。
|
||||
- **改法**:把两个常量改成可编辑字段 + `EditorPrefs` 持久化(参考 `UnusedResourceAuditTool.cs` 的做法),并支持多目录(`string[]`);prefabRoot 也支持多个。
|
||||
- **收益**:从"只能体检 UI 目录"变成"能体检全工程"。
|
||||
- **工作量**:约 40 行。
|
||||
|
||||
---
|
||||
|
||||
### B 级:判定准确性(降低误报/漏报)
|
||||
|
||||
#### B1 "疑似无用图片"漏判动态引用 ★与另一工具冲突,必须处理
|
||||
|
||||
- **现状**:`HasStaticCodeReference` L1148-1158 只做三种**字面**子串匹配:guid、资源全路径、`/Resources/` 之后的相对路径(去扩展名)。`DrawUnusedImagePage` 的 HelpBox(L724-726)已诚实声明"配置表、字符串拼接、反射和运行时下载资源无法可靠识别"。
|
||||
- **漏判的典型形态**(T5 实测存在):
|
||||
- 配置表驱动:`achievement.json` 里 `"icon":"achievement_icon_1"`——代码和路径里都没有这个名字;
|
||||
- 代码拼接:`Config_Const.Config_path_ui_task + "btn_go"`(T5 有 21 处此类模式);
|
||||
- 插值前缀 + 表数字后缀:`AtlasUtils.cs` 里 `$"rarity_{rarity}"`、`$"img_card{rarity}"`、`$"phase_{phases}"`——资源实际叫 `rarity_1`,语料里永远只有 `rarity_{rarity}`;
|
||||
- Addressables 条目 / Label、场景文件引用、材质与图集 packables 引用。
|
||||
- **关键冲突**:`Assets/Editor/UnusedResourceAuditTool.cs`(菜单 `Tools/只读资源巡检/未引用资源候选分析`)**已经实现了完整的 5 层动态识别链**(代码字面量 → Resources 拼接 → 语料子串 → 图集/控制器/字体/Spine 配置名 → 插值/拼接前缀+数字后缀),还带 Addressables、场景根、打包可见性分析和三阶段备份流程。两个工具会对同一张图给出**矛盾结论**(这里说"无用",那里说"疑似动态引用")。
|
||||
- **改法(推荐第 2 种)**:
|
||||
1. 最小改动:在 `DrawUnusedImagePage` 的 HelpBox 里加一句指引——"完整分析请用 `Tools/只读资源巡检/未引用资源候选分析`",并把该页改名为"疑似无用图片(弱判定,仅供参考)";
|
||||
2. 彻底改动:把 `UnusedResourceAuditTool` 里的语料构建与前缀命中逻辑抽成一个共享静态类(如 `DynamicReferenceCorpus`),两个工具都调它,结论自然一致。
|
||||
- **工作量**:方案 1 约 5 行;方案 2 约 1 天(含抽取与回归)。
|
||||
|
||||
#### B2 依赖面太窄导致误报
|
||||
|
||||
- **现状**:`prefabDependencies` L586 只在 `Scan` L895 里由 **prefabRoot 下的 prefab** 累积(`AssetDatabase.GetDependencies(path, true)`)。默认 prefabRoot = `gui/panel`。
|
||||
- **后果**:被**场景文件**、**其他目录的 prefab**(如 `gui/item`、`effects`)、**材质**、**SpriteAtlas 的 packables** 引用的图,都会被判成"疑似无用"。
|
||||
- **改法**:根集合补上 `t:Scene`、全工程 `t:Prefab`(或至少 `gui` 全域)、`t:Material`、`t:SpriteAtlas`、Addressables 条目——即 `UnusedResourceAuditTool.CollectRoots` 的那套。或者干脆按 B1 方案 2 合并。
|
||||
|
||||
#### B3 `warningsOnly` 在 4 个页无效
|
||||
|
||||
- **现状**:只有 `Matches`(L1053,引用明细页)和 `RefreshFilteredNodeRecords`(L1174-1176,节点统计页)判了 `warningsOnly`;`RefreshFilteredSharedRecords` L1100、`RefreshFilteredUnusedImages` L1160、`RefreshFilteredLargeImages` L1227、`RefreshFilteredSimilarImagePairs` L1391 都只判 `filter`。
|
||||
- **后果**:勾上"仅显示需确认项"后,这 4 个页列表毫无变化,也没有任何提示,容易被误认为"这些页没有问题项"。
|
||||
- **改法**:要么给这些页的 record 补 `NeedsConfirm` 语义并参与过滤,要么在这些页顶部显示一行说明"本页不受'仅显示需确认项'影响"。约 15 行。
|
||||
|
||||
---
|
||||
|
||||
### C 级:工具自身性能与测量可信度
|
||||
|
||||
#### C1 手动录制每帧全量遍历所有 GameObject ★自测量污染,必须修
|
||||
|
||||
- **现状**:`TickManualRecording` L258 每帧调用 `GetVisibleRegisteredPanels` L289,后者执行 `Resources.FindObjectsOfTypeAll<GameObject>()`(L292)——遍历**全工程所有 GameObject**(含资产、prefab 内的对象,不只是场景对象)。
|
||||
- **后果**:**工具自己在拖慢被测帧**。手动录制测出来的 P95 帧耗时里包含了这次全量遍历的开销,面板越多、工程越大,污染越严重。这是"测量工具影响测量结果"的经典陷阱,会让所有手动录制数据偏高。
|
||||
- **改法(三选一,按效果排序)**:
|
||||
1. **最优**:按作者自己在 L301-306 留的 TODO 做——在 `MFrame.PanelMgr` 的 `Show` / `completeHandler` / `Close` 里加生命周期事件(`OnPanelShowRequested` / `OnPanelShown` / `OnPanelClosed`),工具订阅事件。既零轮询开销,又能拿到真实的"请求 Show → 可见"耗时,不再是一帧近似值;
|
||||
2. **次优**:轮询降频——加一个 `manualPollIntervalSeconds`(如 0.1 s),未到间隔就跳过本轮 `GetVisibleRegisteredPanels`;帧耗时采样仍每帧进行,不受影响;
|
||||
3. **最省事**:把 `Resources.FindObjectsOfTypeAll<GameObject>()` 换成 `Object.FindObjectsByType<GameObject>(FindObjectsInactive.Exclude, FindObjectsSortMode.None)`(只返回场景对象,且跳过排序),并预先把 `Entries` 的 prefab 名做成查找表。
|
||||
- **工作量**:方案 2 约 8 行;方案 3 约 5 行;方案 1 约半天(要动框架代码,需谨慎,属于"改 UI 框架"范畴,动手前应单独确认)。
|
||||
|
||||
#### C2 代码语料是 O(n×m) 全文扫描
|
||||
|
||||
- **现状**:`ReadAllProjectCode` L1136 把 `Assets` 下**所有 .cs** 拼成一个大字符串(T5 有 4659 个文件,语料是 MB 级);`HasStaticCodeReference` L1150-1157 对每张图做 **3 次** `string.IndexOf` 全文搜索。1715 张图 = 5145 次 MB 级全文扫描。
|
||||
- **改法**:一次性用正则从语料里提取所有字符串字面量和标识符 token,塞进 `HashSet<string>`;再对每个资源名/路径做 O(1) 查表 + 前缀匹配。`UnusedResourceAuditTool.BuildCodeCorpus` 已经是这个做法,可直接复用。
|
||||
- **收益**:扫描时间从"十几秒到几十秒"降到"1-2 秒"。
|
||||
|
||||
#### C3 dHash 对全尺寸图取像素
|
||||
|
||||
- **现状**:`TryGetDifferenceHash` L1310 `image.GetPixels32()` 取**整张解码图**的像素,再双重循环遍历每个像素找 alpha 包围盒(L1312-1319)。
|
||||
- **后果**:一张 4096×4096 的图 = 64 MB 托管内存 + 1677 万次循环。相似图扫描在含大图的目录上会明显卡顿甚至触发 GC 尖峰。
|
||||
- **改法**:`ImageConversion.LoadImage` 解码后,先 `Graphics.Blit` 到一张 128×128 的 `RenderTexture` 再 `ReadPixels` 回小 `Texture2D`,在小图上找包围盒并采样。dHash 最终只取 8×9 = 72 个点(L1323-1333),128² 的精度完全够,而内存从 64 MB 降到 64 KB。
|
||||
- **注意**:包围盒精度会略降(小图上 alpha≤10 的判定 L1316 会因缩放插值而边缘模糊)。稳妥做法是**双阶段**:先在 128² 上算粗略包围盒,再按该包围盒比例回到原图取那 72 个采样点(只取 72 个像素,不再全图遍历)。
|
||||
- **工作量**:约 30 行。
|
||||
|
||||
#### C4 逐 prefab 递归依赖
|
||||
|
||||
- **现状**:`Scan` L890-896 对每个 prefab 调 `AssetDatabase.GetDependencies(path, true)`(递归全依赖)。566 个 prefab = 566 次递归依赖解析。
|
||||
- **改法**:先收集全部 prefab 路径,一次 `AssetDatabase.GetDependencies(string[] pathNames, bool recursive)` 批量调用(Unity 支持数组重载);或改为 `GetDependencies(path, false)` 只取直接依赖 + 自行按 `prefabDependencies` 传递闭包。
|
||||
- **收益**:扫描时间下降(幅度取决于依赖深度)。
|
||||
|
||||
---
|
||||
|
||||
### D 级:UI / 一致性小问题
|
||||
|
||||
| 编号 | 现状(代码位置) | 问题 | 改法 |
|
||||
| --- | --- | --- | --- |
|
||||
| D1 | `ExportCsv` L1419-1428 | 导出按钮(L624)在任何标签页都可见,但**永远只导引用明细**(`records`)。站在"大图标记"页点导出,拿到的却是引用明细表 | 按 `activeTab` 分派导出内容,或把导出按钮移进各页;至少在非引用明细页禁用并提示 |
|
||||
| D2 | `GetSuggestedNodeCount` L942-952 | 节点数建议值全部硬编码(150/50/80/100/80),不同项目的 UI 复杂度差异很大 | 提到常量区并允许界面覆盖 + `EditorPrefs` 持久化;或按本工程实际分布给分位数建议(如 P90) |
|
||||
| D3 | 两个 MenuItem L599 / L607 | 两个菜单打开的是**同一个** `UIPrefabCheck` 窗口,只是 `activeTab` 不同。从"界面性能巡检"进去后点"扫描",会切到静态页,容易迷失 | 用 `EditorPrefs` 记住上次所在页;或在标题上反映当前页 |
|
||||
| D4 | `DrawUnusedImagePage` L732 | "扫描疑似无用图片"按钮每次都重跑 `ReadAllProjectCode`(见 C2),连点两次就是两次全量读盘 | 语料缓存 + 失效判断(按 .cs 文件最后修改时间) |
|
||||
| D5 | `Record.Warning` L959 | 唯一的告警是"非标准 UI 资源目录",判据是路径前缀(`IsStandardUiPath` L1431)——这是**目录约定**告警,不是性能告警 | 配合 A2 增加真正的合批告警(散图、跨图集、UIEffect 断批) |
|
||||
|
||||
---
|
||||
|
||||
## 第四章 如果只做三件事
|
||||
|
||||
按"收益 / 工作量"比排序:
|
||||
|
||||
| 优先级 | 项目 | 收益 | 工作量 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | **A1** 性能巡检加 DC / 批次 / SetPass 指标 | 降 DC 成果从"肉眼数 Frame Debugger"变成"可量化、可排行、可回归" | 30-40 行 |
|
||||
| 2 | **A2** 新增静态"合批体检"页 | 一次扫描列出全项目最该优化的面板 TOP N + 具体病因(图集分散 / UIEffect / Mask / 嵌套 Canvas) | 约 150 行 |
|
||||
| 3 | **C1** 修手动录制的每帧全量遍历(方案 2 或 3) | 消除工具对被测帧的污染,让手动录制数据可信 | 5-8 行 |
|
||||
|
||||
其次:**B1**(避免与 `UnusedResourceAuditTool` 结论打架)、**A3**(导入设置审计,服务包体瘦身)、**C2/C3**(扫描提速)。
|
||||
|
||||
---
|
||||
|
||||
## 第五章 跨项目可移植性(归档前必须知道)
|
||||
|
||||
这个工具**不是通用工具**,有框架绑定:
|
||||
|
||||
| 部分 | 依赖 | 能否直接搬到别的项目 |
|
||||
| --- | --- | --- |
|
||||
| 6 个静态扫描页(引用明细/共用/无用图/节点/大图/相似图) | 只用 `UnityEditor` + `UnityEngine.UI` + `UnityEngine.U2D`;唯一框架引用是文件头 `using MFrame;`(L10) | **可以**,删掉 `using MFrame;` 后基本即用(需按项目改 `DefaultPrefabRoot` L482 与 `UnusedImageRoots` L487) |
|
||||
| 界面性能页(自动巡检) | `PanelMgr.Instance.Show/Close`(L366、L410)+ 从 `GameInitWrapper` 源码正则解析 `AddItem<T>`(L172-176) | **不能**,必须改成目标项目的面板管理器 API 与注册方式 |
|
||||
| 界面性能页(手动录制) | `ManualPanelNameLookup` 由 `DiscoverPanels` 建立,同样依赖上面两项 | **不能**,同上 |
|
||||
|
||||
移植时的最小改动清单:
|
||||
1. 删 `using MFrame;`(L10),确认剩余部分能编译;
|
||||
2. 改 `DefaultPrefabRoot`(L482)与 `UnusedImageRoots`(L487-491)为目标项目的 UI 目录;
|
||||
3. `IsStandardUiPath`(L1431)里的两个路径前缀同步改;
|
||||
4. 界面性能页:替换 `DiscoverPanels`(L167)的面板发现方式与 `PanelMgr.Instance.Show/Close` 调用(L366 / L410 / L432),或在不需要时整块删掉 `UIRuntimePerformanceAudit`(L17-478)与 `ViewTab.界面性能`(L567)及其分派(L644-647)。
|
||||
|
||||
---
|
||||
|
||||
## 第六章 作者留下的 TODO(原文摘录,L301-306)
|
||||
|
||||
工具里已有一段明确的改进计划,做 C1 方案 1 时直接照它执行:
|
||||
|
||||
```
|
||||
// TODO(精确手动录制耗时):在 MFrame.PanelMgr 中增加 UI 生命周期事件。
|
||||
// 1. 在 Show(name, data, completeHandler) 入口记录 startTime,并触发 OnPanelShowRequested(name, startTime)。
|
||||
// 2. 在资源加载完成且 PanelBase._OnShow 执行完成后的 completeHandler 中,触发 OnPanelShown(name, startTime, elapsedMs)。
|
||||
// 3. 在 Close(name) 成功后触发 OnPanelClosed(name)。
|
||||
// 4. 本工具订阅上述事件,以 name 关联会话,便可得到真实的"请求 Show 到可见"耗时,
|
||||
// 不再依赖当前轮询 GameObject 激活状态所得的一帧近似值。
|
||||
```
|
||||
|
||||
> 注意:这一项要动 `MFrame` 框架代码(`PanelMgr` / `PanelBase`)。T5 有过教训——**改 UI 框架前必须确认现有全屏遮挡机制(FullScreenCover)不被破坏**,且改动要能被业务面板无感继承。动手前单独确认,不要顺手做。
|
||||
@@ -0,0 +1,175 @@
|
||||
# DC 与帧率排查经验
|
||||
|
||||
> 从 T5 / T11 两个工程的实际排查里沉淀下来的方法论与踩坑记录。
|
||||
> 配合 `01_UIEffect材质共享降DC/移植与验证手册.md` 使用:手册负责"怎么改",本文负责"改完怎么看、看不明白怎么办"。
|
||||
> 全屏遮挡机制的专项排查(含团结引擎加密 guid 的坑)见 `02_全屏面板遮挡省DC/FullScreenCover_使用方法与避坑.md` 第八章。
|
||||
|
||||
---
|
||||
|
||||
## 一、Frame Debugger 看图技巧(最常用)
|
||||
|
||||
`Window → Analysis → Frame Debugger` → Enable。
|
||||
|
||||
### 1.1 纹理名怎么读
|
||||
|
||||
| 纹理名形态 | 含义 |
|
||||
| --- | --- |
|
||||
| `sactx-0-1024x1024-DXT5\|BC3-atlas-<hash>` | **SpriteAtlas 运行时图集**(`sactx` = Sprite Atlas Texture)。`<hash>` 标识具体哪个图集,两个批次 hash 不同 = 不同图集 = 不可能合批 |
|
||||
| `Font Texture` / `xxx SDF` | TextMeshPro / 字体图集,跟普通 Image 天然不同纹理 |
|
||||
| 具体贴图名(如 `ui_icon_gold`) | 散图,没进图集。**大量散图 = DC 大户**,应归并进 SpriteAtlas |
|
||||
|
||||
### 1.2 "预览是黑的"不等于浪费
|
||||
|
||||
Frame Debugger 右侧预览里 mesh 显示为黑色/深色,通常是 **mesh 的 UV 落在了图集的透明区域**——
|
||||
透明像素的 RGB 通道常被填成黑色(alpha=0),预览只画 RGB 不乘 alpha,所以看起来是黑的。
|
||||
|
||||
判断方法:看这一批的 **顶点数 / 纹理名 / 所属 Canvas**,而不是看预览颜色。
|
||||
真正确认"这一批画了什么"的办法:在事件树里选中该 Draw Mesh,Scene 视图会高亮对应元素。
|
||||
|
||||
> T5 实例:通行证面板的"黑批次"实际是 `sactx-...-atlas-bf6ec7a7` 图集里的一块透明区域,不是无效绘制。
|
||||
|
||||
### 1.3 别把 Event 编号当批次序号
|
||||
|
||||
Frame Debugger 左侧的 `Event #N` 是**整个帧的事件序号**(含相机 clear、ShadowMap、UI 之外的绘制),
|
||||
不是"UI 第 N 批"。
|
||||
|
||||
> T5 实例:双相机结构下 `Event #6` 对应的其实是 UI 的**第 1 批**(偏移 +5)。
|
||||
> 换一台机器、换一个场景结构,偏移量就变了。
|
||||
|
||||
正确做法:只统计 `Canvas.BuildBatch (<CanvasName>)` 节点**下面**的 `Draw Mesh` 子项数量,那才是 UI 批次数。
|
||||
|
||||
### 1.4 定位"谁在拆批次"的标准流程
|
||||
|
||||
1. 展开 `Canvas.BuildBatch`,数子项数量(= 该 Canvas 的批次数);
|
||||
2. 逐个点子项,记下每项的 **Material + Texture**;
|
||||
3. 相邻两项若 Material 相同但 Texture 不同 → **图集问题**(合图集);
|
||||
4. 若 Material 不同 → 看是不是 UIEffect / Mask / 自定义材质导致(本补丁解决的就是 UIEffect 这一类);
|
||||
5. 若 Material、Texture 都相同却还是两批 → 中间夹了别的元素打断了层级相邻性(调 hierarchy 顺序)。
|
||||
|
||||
---
|
||||
|
||||
## 二、"帧率不稳"的排查(先分清是真问题还是假象)
|
||||
|
||||
### 2.1 编辑器里的 FPS 数字乱跳 ≠ 真机卡
|
||||
|
||||
三个必然存在的干扰因素:
|
||||
|
||||
1. **Stats 面板是滚动平均**:本身就有延迟和抖动,瞬时值无意义;
|
||||
2. **编辑器自身开销**:Scene 视图重绘、Inspector GUI repaint、资源导入、Play 模式的 EditorApplication update,都会吃掉帧时间,且**不均匀**;
|
||||
3. **vSync 量化**:开了垂直同步后帧率只能在 60 / 30 / 20 这些档位跳,看起来像"忽高忽低",其实是量化台阶。
|
||||
|
||||
**结论**:判断帧率问题必须用 Profiler 看**帧时间**,不要用编辑器 Stats 的 FPS 数字,更不要靠肉眼感觉。
|
||||
|
||||
### 2.2 Profiler 三步法
|
||||
|
||||
`Window → Analysis → Profiler`,勾 CPU Usage,连真机(或编辑器 Play):
|
||||
|
||||
**第一步:看帧时间尖峰,不看平均值**
|
||||
- 图表区找突然拉高的柱子(正常 60 帧 = 16.6ms,超过 33ms 就是明显卡顿);
|
||||
- 区分"**偶发尖峰**"(GC、资源加载、实例化)和"**持续偏高**"(每帧都慢,通常是渲染或 Update 逻辑)。
|
||||
|
||||
**第二步:展开尖峰帧,找两类元凶**
|
||||
- `GC.Alloc` / `GC.Collect`:内存分配导致的卡顿。看调用栈定位到具体函数;
|
||||
- 耗时 Top 函数:常见是 `Canvas.SendWillRenderCanvases`(UI Rebuild)、`SkeletonGraphic.Update`(Spine)、`Physics`、`Animator.Update`。
|
||||
|
||||
**第三步:对号入座**
|
||||
|
||||
| Profiler 里看到的 | 通常原因 | 处理方向 |
|
||||
| --- | --- | --- |
|
||||
| `Canvas.SendWillRenderCanvases` 高 | UI Rebuild 频繁(Text 改字、SetActive 抖动、布局组件重算) | 减少每帧改 UI;避免 LayoutGroup 嵌套;文字更新加脏标记 |
|
||||
| `Canvas.BuildBatch` 高 | 批次太多 / Canvas 太大 | 降 DC(本包主题);拆 SubCanvas 隔离动态区 |
|
||||
| `GC.Alloc` 持续出现 | 每帧 new / 装箱 / 字符串拼接 / foreach 某些集合 | 缓存对象;用 struct 枚举;避免运行时 `$"..."` 拼路径 |
|
||||
| `SkeletonGraphic` 相关高 | Spine UI 骨骼持续驱动 | 不可见时禁用组件;降低 update mode 频率 |
|
||||
| `Shader.Parse` / `Material` 创建 | 材质实例被反复创建 | 本补丁正是解决 UIEffect 这一类 |
|
||||
|
||||
### 2.3 帧率设置本身的自查(排除"根本没设 60 帧")
|
||||
|
||||
```powershell
|
||||
# 在目标工程里搜这几处,确认帧率策略
|
||||
cd <目标工程>\unity
|
||||
Select-String -Path (Get-ChildItem -Recurse -Filter *.cs -Exclude *Library*).FullName `
|
||||
-Pattern "targetFrameRate|vSyncCount|OnApplicationFocus|OnApplicationPause" |
|
||||
Select-Object Path, LineNumber, Line
|
||||
```
|
||||
|
||||
T5 的实际情况(可作为参考范式):
|
||||
- 启动即设:`GameInitWrapper.cs` → `AppUtils.Init(60, true)` → `Application.targetFrameRate = 60`;
|
||||
- 真机切后台降帧:`Engine.cs` 的 `OnApplicationFocus`,在 `#if (UNITY_ANDROID || UNITY_IOS) && !UNITY_EDITOR` 内,
|
||||
失焦 → 10 帧,回前台 → 60 帧。
|
||||
|
||||
> **重要**:这个"后台降 10 帧"经常被误读成"帧率不稳"。它只在真机切后台时生效,编辑器里不触发。
|
||||
> 排查时先确认这类分支有没有被误命中(比如某些平台宏定义写错,导致编辑器也走进去了)。
|
||||
|
||||
### 2.4 对照实验:怀疑某个组件时,禁用它再测
|
||||
|
||||
判断"是不是这个组件在拖帧"的唯一可靠方法是**对照实验**:
|
||||
|
||||
1. Profiler 开着,记录当前帧时间基线;
|
||||
2. 运行时把嫌疑组件 `enabled = false`(或在 Inspector 里取消勾选);
|
||||
3. 看帧时间是否明显下降;
|
||||
4. 恢复,确认帧时间回到基线(排除"只是这一帧刚好快了")。
|
||||
|
||||
> T5 实例:大厅 `UI_MainPanel` 挂了 `sk_hero` + `sk_pet` 两个 `SkeletonGraphic`(UI Spine),
|
||||
> 是"持续负载"最大嫌疑。验证方式就是运行时禁用这两个组件看帧时间变化。
|
||||
> **注意**:对照实验只做临时禁用,不要顺手删组件——删之前必须 grep 业务代码引用(见 §4)。
|
||||
|
||||
---
|
||||
|
||||
## 三、UI 降 DC 的通用手段清单(按性价比排序)
|
||||
|
||||
本包的 configId 补丁只解决"UIEffect 拆批次"这一项。完整的降 DC 手段:
|
||||
|
||||
| 手段 | 收益 | 成本 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| **UIEffect 共享材质(本包路线①)** | 高(同配置特效密集时) | 极低(改插件 2 文件,业务零改动) | 见 `01_UIEffect材质共享降DC/` |
|
||||
| **全屏面板遮挡(本包路线②)** | 高(全屏页多、下层界面复杂时) | 低(挂组件,业务零改动) | 见 `02_全屏面板遮挡省DC/`;**有两个生命周期陷阱,必看文档第七章** |
|
||||
| **关掉无用的 URP Feature(本包路线③)** | 中(T5 实测 SSAO 占 4 DC) | 极低(改一个开关) | 见 `03_渲染设置与资源清理/`;2D/UI 场景关 SSAO 无视觉损失 |
|
||||
| **散图归并进 SpriteAtlas** | 高 | 中(需美术/UI 配合规划图集) | 一个面板的图尽量进同一个图集 |
|
||||
| **统一 UIEffect 配置** | 中高 | 低(改 prefab 参数) | 补丁生效的前提是"配置相同",美术参数乱调会抵消收益 |
|
||||
| **调整 hierarchy 顺序让同图集相邻** | 中 | 低 | 合批要求层级相邻,穿插即断批 |
|
||||
| **减少 Mask / RectMask2D** | 中 | 中 | 每个 Mask 内部独立成批,跨 Mask 永不合批 |
|
||||
| **拆 SubCanvas 隔离高频变化区** | 中(降 Rebuild 更明显) | 中 | 动静分离;但 SubCanvas 会各自建批,别拆过头 |
|
||||
| **TMP 字体图集与 Image 分区** | 低中 | 低 | 文字与图片天然不同纹理,布局上让它们各自成组 |
|
||||
| **控制同屏 Spine(SkeletonGraphic)数量** | 中 | 中 | 每个至少独占一批且打断周围 UI 合批;不可见时禁用 |
|
||||
|
||||
---
|
||||
|
||||
## 四、血泪教训(务必遵守)
|
||||
|
||||
1. **删任何 UI 组件前,先 grep 业务代码有没有引用它。**
|
||||
T5 曾误判"某 UIEffect 没有视觉效果"就删了组件,结果业务代码 `GetComponent<UIEffect>()` 拿到 null,运行时 NRE。
|
||||
```powershell
|
||||
# 删组件前的强制动作:确认无引用
|
||||
cd <目标工程>\unity\Assets
|
||||
Select-String -Path (Get-ChildItem -Recurse -Filter *.cs).FullName `
|
||||
-Pattern "UIEffect" | Select-Object Path, LineNumber, Line
|
||||
```
|
||||
2. **不要用肉眼/主观感受下性能结论。**"我感觉有点卡"必须先变成 Profiler 里的帧时间数字,再谈优化。
|
||||
3. **改插件源码前先确认它不是 PackageCache 只读副本**,否则改完 Unity 一刷新就没了(详见手册 §1.1)。
|
||||
4. **补丁一定要提交进 git 并写清楚 commit message**,插件升级后才知道要重打什么。
|
||||
5. **优化改动要单独提交,别和无关改动混在一个 commit 里。**
|
||||
T5 的 `c61b9012 优化DC` 就混进了 7 个 prefab 的 FullScreenCover 配置改动,导致后来做归档时必须手工拆分 diff。
|
||||
|
||||
---
|
||||
|
||||
## 五、附:UI DC 体检工具设计规格(源码已丢失,可据此重建)
|
||||
|
||||
T5 曾有一个自研 Editor 工具 `UIDCProbeTool.cs`(`Assets/Editor/` 下),删除时**未提交 git**,源码不可恢复。
|
||||
如果新项目需要比 Frame Debugger 更高效的批量归因工具,按下面规格重建即可(约 800 行,一个下午的工作量):
|
||||
|
||||
**定位**:只读的 EditorWindow,不改场景、不改资源,专门回答"这个面板的 DC 被谁拆了"。
|
||||
|
||||
**两种模式**:
|
||||
1. **真实批次模式**:Play 模式下遍历目标 Canvas,按 UGUI 的合批规则(材质 + 纹理 + 层级相邻 + 同 Canvas)模拟分批,输出批次列表;**批次 index 从 1 开始计数**(不是 0,避免与 Frame Debugger 的 Event 编号混淆)。
|
||||
2. **材质分组模式**:把所有可绘制元素按 `材质.GetInstanceID() + 主纹理.GetInstanceID()` 归组,每组显示"元素数量 / 涉及对象路径 / 纹理名",直接暴露"哪种材质独占了多少批"。支持按纹理 hash 搜索(比如输入 Frame Debugger 里看到的 `bf6ec7a7` 反查是哪个图集)。
|
||||
|
||||
**输出字段(每批一行)**:批次 index、Canvas 路径、材质名、纹理名、元素数量、是否可合并到前一批、不可合并原因(图集不同 / 材质不同 / 层级被打断 / Mask 隔离)。
|
||||
|
||||
**界面**:顶部选 Canvas(下拉 + Ping 按钮定位)+ 模式切换 + 搜索框;列表分页(每页 100 条);支持导出 CSV(UTF8 BOM,Excel 直接打开不乱码);底部汇总行(总批次 / 可优化批次 / 预估可降到多少)。
|
||||
|
||||
**关键实现提示**:
|
||||
- 全程走 `AssetDatabase` / 反射读 Canvas 内部批次数据时注意团结引擎(Tuanjie)对 guid 的加密,**不要依赖 guid 字符串**,用 `GetInstanceID()` 做 key;
|
||||
- 只做只读分析,不 `SetActive`、不改材质、不 Undo 记录;
|
||||
- 工具本身放 `Assets/Editor/`,用完可以整文件删除(记得连 `.meta` 一起删,否则 Unity 报孤立 meta 警告)。
|
||||
|
||||
> 如果只是想快速定位问题,**Unity 自带 Frame Debugger + 本文第一章的方法已经够用**,不必重建工具。
|
||||
@@ -0,0 +1,265 @@
|
||||
diff --git a/unity/Assets/Resources/Resources_moved/gui/panel/UI_LevelGiftPanel.prefab b/unity/Assets/Resources/Resources_moved/gui/panel/UI_LevelGiftPanel.prefab
|
||||
index c7ab8ebc..e20954ef 100644
|
||||
--- a/unity/Assets/Resources/Resources_moved/gui/panel/UI_LevelGiftPanel.prefab
|
||||
+++ b/unity/Assets/Resources/Resources_moved/gui/panel/UI_LevelGiftPanel.prefab
|
||||
@@ -1020,7 +1020,7 @@ GameObject:
|
||||
- component: {fileID: 2933118048976270207}
|
||||
- component: {fileID: 2429120442925024079}
|
||||
- component: {fileID: 8817440891751262561}
|
||||
- - component: {fileID: 4706064949595997982}
|
||||
+ - component: {fileID: 6126304919991226096}
|
||||
m_Layer: 5
|
||||
m_HasEditorInfo: 1
|
||||
m_Name: UI_LevelGiftPanel
|
||||
@@ -1094,7 +1094,7 @@ MonoBehaviour:
|
||||
tf_item: {fileID: 736732509760957031}
|
||||
prefab_item: {fileID: 6078732611595661672, guid: 06738302f7ce6a548a9f6a4406c392d0,
|
||||
type: 3}
|
||||
---- !u!114 &4706064949595997982
|
||||
+--- !u!114 &6126304919991226096
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
@@ -1106,7 +1106,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 0a2deb478a85eef4883e8465d48c426f, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
- isKeepShowWhenCovered: 1
|
||||
+ isKeepShowWhenCovered: 0
|
||||
--- !u!1 &6984570939639282589
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
diff --git a/unity/Assets/Resources/Resources_moved/gui/panel/UI_MythHeroPanel.prefab b/unity/Assets/Resources/Resources_moved/gui/panel/UI_MythHeroPanel.prefab
|
||||
index 34e4e8eb..bc6d8229 100644
|
||||
--- a/unity/Assets/Resources/Resources_moved/gui/panel/UI_MythHeroPanel.prefab
|
||||
+++ b/unity/Assets/Resources/Resources_moved/gui/panel/UI_MythHeroPanel.prefab
|
||||
@@ -14065,7 +14065,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 0a2deb478a85eef4883e8465d48c426f, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
- isKeepShowWhenCovered: 1
|
||||
+ isKeepShowWhenCovered: 0
|
||||
--- !u!1 &6273192759332336783
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
diff --git a/unity/Assets/Resources/Resources_moved/gui/panel/UI_PassPanel.prefab b/unity/Assets/Resources/Resources_moved/gui/panel/UI_PassPanel.prefab
|
||||
index d98867ad..78966c76 100644
|
||||
--- a/unity/Assets/Resources/Resources_moved/gui/panel/UI_PassPanel.prefab
|
||||
+++ b/unity/Assets/Resources/Resources_moved/gui/panel/UI_PassPanel.prefab
|
||||
@@ -1462,7 +1462,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 0a2deb478a85eef4883e8465d48c426f, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
- isKeepShowWhenCovered: 1
|
||||
+ isKeepShowWhenCovered: 0
|
||||
--- !u!1 &6984570939639282589
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
diff --git a/unity/Assets/Resources/Resources_moved/gui/panel/UI_PetPanel.prefab b/unity/Assets/Resources/Resources_moved/gui/panel/UI_PetPanel.prefab
|
||||
index ed816a9a..472d7d2f 100644
|
||||
--- a/unity/Assets/Resources/Resources_moved/gui/panel/UI_PetPanel.prefab
|
||||
+++ b/unity/Assets/Resources/Resources_moved/gui/panel/UI_PetPanel.prefab
|
||||
@@ -4364,7 +4364,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 0a2deb478a85eef4883e8465d48c426f, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
- isKeepShowWhenCovered: 1
|
||||
+ isKeepShowWhenCovered: 0
|
||||
--- !u!1 &8696742643833394590
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
diff --git a/unity/Assets/Resources/Resources_moved/gui/panel/UI_RaidSelectPanel.prefab b/unity/Assets/Resources/Resources_moved/gui/panel/UI_RaidSelectPanel.prefab
|
||||
index 8367f81c..093e8953 100644
|
||||
--- a/unity/Assets/Resources/Resources_moved/gui/panel/UI_RaidSelectPanel.prefab
|
||||
+++ b/unity/Assets/Resources/Resources_moved/gui/panel/UI_RaidSelectPanel.prefab
|
||||
@@ -6583,7 +6583,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 0a2deb478a85eef4883e8465d48c426f, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
- isKeepShowWhenCovered: 1
|
||||
+ isKeepShowWhenCovered: 0
|
||||
--- !u!1 &9182812132619542671
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
diff --git a/unity/Assets/Resources/Resources_moved/gui/panel/UI_ShopPanel.prefab b/unity/Assets/Resources/Resources_moved/gui/panel/UI_ShopPanel.prefab
|
||||
index a6fdadf7..f5be125e 100644
|
||||
--- a/unity/Assets/Resources/Resources_moved/gui/panel/UI_ShopPanel.prefab
|
||||
+++ b/unity/Assets/Resources/Resources_moved/gui/panel/UI_ShopPanel.prefab
|
||||
@@ -3946,7 +3946,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 0a2deb478a85eef4883e8465d48c426f, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
- isKeepShowWhenCovered: 1
|
||||
+ isKeepShowWhenCovered: 0
|
||||
--- !u!1 &6483444692411636741
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
diff --git a/unity/Assets/Resources/Resources_moved/gui/panel/UI_SkillPanel.prefab b/unity/Assets/Resources/Resources_moved/gui/panel/UI_SkillPanel.prefab
|
||||
index 6269aa6a..84c6c01d 100644
|
||||
--- a/unity/Assets/Resources/Resources_moved/gui/panel/UI_SkillPanel.prefab
|
||||
+++ b/unity/Assets/Resources/Resources_moved/gui/panel/UI_SkillPanel.prefab
|
||||
@@ -167,7 +167,7 @@ MonoBehaviour:
|
||||
m_Script: {fileID: 11500000, guid: 0a2deb478a85eef4883e8465d48c426f, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
- isKeepShowWhenCovered: 1
|
||||
+ isKeepShowWhenCovered: 0
|
||||
--- !u!1 &1018902619444209224
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
diff --git a/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectBase.cs b/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectBase.cs
|
||||
index c3ceb2fd..210257d0 100644
|
||||
--- a/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectBase.cs
|
||||
+++ b/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectBase.cs
|
||||
@@ -196,8 +196,11 @@ namespace Coffee.UIEffects
|
||||
|
||||
Profiler.BeginSample("(UIE)[UIEffect] GetModifiedMaterial");
|
||||
var samplingScaleId = (uint)(Mathf.InverseLerp(0.01f, 100, actualSamplingScale) * uint.MaxValue);
|
||||
- var rootId = (uint)(transitionRoot != null ? transitionRoot.GetHashCode() : 0);
|
||||
- var hash = new Hash128((uint)baseMaterial.GetHashCode(), effectId, samplingScaleId, rootId);
|
||||
+ // 使用特效配置哈希作为材质共享 key,相同配置的特效共享同一材质,从而可以合批。
|
||||
+ // root 仅在特效需要视图矩阵(transition/gradation/detail/shiny)时才影响材质。
|
||||
+ var configId = context.configId;
|
||||
+ var rootId = context.useViewMatrix && transitionRoot ? (uint)transitionRoot.GetInstanceID() : 0u;
|
||||
+ var hash = new Hash128((uint)baseMaterial.GetInstanceID(), configId, samplingScaleId, rootId);
|
||||
if (!MaterialRepository.Valid(hash, _material))
|
||||
{
|
||||
Profiler.BeginSample("(UIE)[UIEffect] GetModifiedMaterial > Get or create material");
|
||||
diff --git a/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectContext.cs b/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectContext.cs
|
||||
index c315f4cb..48b6ae4b 100644
|
||||
--- a/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectContext.cs
|
||||
+++ b/unity/my_libs/unityLib/UIEffect-main/Packages/src/Runtime/UIEffectContext.cs
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
+using System.Reflection;
|
||||
using Coffee.UIEffectInternal;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Profiling;
|
||||
@@ -263,6 +264,126 @@ namespace Coffee.UIEffects
|
||||
|| m_DetailFilter != DetailFilter.None
|
||||
|| m_EdgeMode == EdgeMode.Shiny;
|
||||
|
||||
+ /// <summary>
|
||||
+ /// 特效配置的稳定指纹。自动遍历所有序列化配置字段生成,因此插件新增属性时无需手动维护字段清单。
|
||||
+ /// 相同配置会得到相同指纹,从而可以共享同一个材质并参与合批。
|
||||
+ /// </summary>
|
||||
+ public uint configId
|
||||
+ {
|
||||
+ get
|
||||
+ {
|
||||
+ unchecked
|
||||
+ {
|
||||
+ var h = (uint)2166136261;
|
||||
+ var fields = s_ConfigFields;
|
||||
+ for (var i = 0; i < fields.Length; i++)
|
||||
+ {
|
||||
+ var f = fields[i];
|
||||
+ h = Mix(h, (uint)f.Name.GetHashCode());
|
||||
+ h = Mix(h, HashValue(f.GetValue(this)));
|
||||
+ }
|
||||
+
|
||||
+ // Avalanche 混淆(MurmurHash3 终混),让相近配置散列到不同指纹。
|
||||
+ h ^= h >> 16;
|
||||
+ h *= 0x85ebca6b;
|
||||
+ h ^= h >> 13;
|
||||
+ h *= 0xc2b2ae35;
|
||||
+ h ^= h >> 16;
|
||||
+ return h;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ private static uint Mix(uint h, uint v)
|
||||
+ {
|
||||
+ h ^= v;
|
||||
+ h *= 16777619;
|
||||
+ return h;
|
||||
+ }
|
||||
+
|
||||
+ /// <summary>
|
||||
+ /// 对任意配置值做内容哈希。值类型会逐字段展开,嵌套结构体(Color、Vector2/3/4、MinMax01 等)
|
||||
+ /// 无需单独写类型处理即可贡献全部数据。只有 UnityEngine.Object 和 Gradient 需要显式处理,
|
||||
+ /// 因为它们的默认哈希基于引用而非内容。
|
||||
+ /// </summary>
|
||||
+ private static uint HashValue(object value)
|
||||
+ {
|
||||
+ if (value == null) return 0u;
|
||||
+
|
||||
+ if (value is UnityEngine.Object obj)
|
||||
+ {
|
||||
+ return obj ? (uint)obj.GetInstanceID() : 0u;
|
||||
+ }
|
||||
+
|
||||
+ if (value is Gradient gradient)
|
||||
+ {
|
||||
+ return GradientHash(gradient);
|
||||
+ }
|
||||
+
|
||||
+ var type = value.GetType();
|
||||
+ if (type.IsPrimitive || type.IsEnum)
|
||||
+ {
|
||||
+ return unchecked((uint)value.GetHashCode());
|
||||
+ }
|
||||
+
|
||||
+ if (type.IsValueType)
|
||||
+ {
|
||||
+ return HashFields(value, type);
|
||||
+ }
|
||||
+
|
||||
+ return unchecked((uint)value.GetHashCode());
|
||||
+ }
|
||||
+
|
||||
+ private static uint HashFields(object boxed, Type type)
|
||||
+ {
|
||||
+ unchecked
|
||||
+ {
|
||||
+ if (!s_FieldCache.TryGetValue(type, out var fields))
|
||||
+ {
|
||||
+ fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
+ s_FieldCache[type] = fields;
|
||||
+ }
|
||||
+
|
||||
+ var h = (uint)2166136261;
|
||||
+ for (var i = 0; i < fields.Length; i++)
|
||||
+ {
|
||||
+ h = Mix(h, HashValue(fields[i].GetValue(boxed)));
|
||||
+ }
|
||||
+
|
||||
+ return h;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ private static uint GradientHash(Gradient gradient)
|
||||
+ {
|
||||
+ unchecked
|
||||
+ {
|
||||
+ var h = (uint)2166136261;
|
||||
+
|
||||
+ var colorKeys = gradient.colorKeys;
|
||||
+ for (var i = 0; i < colorKeys.Length; i++)
|
||||
+ {
|
||||
+ h = Mix(h, HashValue(colorKeys[i].color));
|
||||
+ h = Mix(h, HashValue(colorKeys[i].time));
|
||||
+ }
|
||||
+
|
||||
+ var alphaKeys = gradient.alphaKeys;
|
||||
+ for (var i = 0; i < alphaKeys.Length; i++)
|
||||
+ {
|
||||
+ h = Mix(h, HashValue(alphaKeys[i].alpha));
|
||||
+ h = Mix(h, HashValue(alphaKeys[i].time));
|
||||
+ }
|
||||
+
|
||||
+ return h;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ private static readonly FieldInfo[] s_ConfigFields = typeof(UIEffectContext)
|
||||
+ .GetFields(BindingFlags.Public | BindingFlags.Instance);
|
||||
+
|
||||
+ private static readonly Dictionary<Type, FieldInfo[]> s_FieldCache =
|
||||
+ new Dictionary<Type, FieldInfo[]>();
|
||||
+
|
||||
private Texture2D _gradationRampTex;
|
||||
private Texture2D _transitionRampTex;
|
||||
private bool _isGradientDirty = true;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,181 @@
|
||||
# Unity UI 降 DC 优化包(可移植归档)
|
||||
|
||||
> 归档时间:2026-09-10 | 来源工程:T5(`d:\Project\T5`)、T11(`d:\Project\T11`)
|
||||
> 一句话:**四条互相独立的降 DrawCall 路线 + 配套巡检工具 + 排查方法论,全部来自 T5/T11 实战并已验证生效,业务代码零改动。**
|
||||
|
||||
---
|
||||
|
||||
## 一、这个包解决什么问题
|
||||
|
||||
Unity UI 的 DrawCall 高,通常是四个不同原因叠加。本包按**功能**分成四个目录,可以单独取用,互不依赖:
|
||||
|
||||
| 路线 | 病因 | 药方 | 目录 |
|
||||
| --- | --- | --- | --- |
|
||||
| **① UIEffect 材质共享** | Coffee.UIEffect 5.x 给每个组件建独立材质实例(缓存 key 用实例唯一的 `effectId`)→ 100 个同配置图标 = 100 个材质 = 完全无法合批 | 把缓存 key 换成**配置内容指纹 `configId`** → N 个同配置特效共享 1 个材质 → 可被 UGUI 动态合批 | `01_` |
|
||||
| **② 全屏面板遮挡** | 全屏界面打开时,被完全盖住的下层界面**照样提交 DC**(GPU 白画一遍) | 挂 `FullScreenCover` 组件 → 打开时把下层 `SetActive(false)`,关闭时自动恢复 | `02_` |
|
||||
| **③ 渲染设置与资源清理** | URP Renderer 里 SSAO 等 Feature 在 2D/UI 场景纯属浪费;超大散图撑包体 | 关掉 SSAO(省 4 个 DC)、Skybox 改纯色、删无用大图 | `03_` |
|
||||
| **④ 巡检工具** | 靠人眼在几百个 prefab 里找问题不现实 | Prefab/图片/图集静态扫描 + 界面性能巡检(含相似图 LSH、dHash) | `04_` |
|
||||
|
||||
**T5 实测总收益**:大厅整帧批次数 **约 100 → 35**;等级礼包面板 **~101 → 30~40** 量级(①的贡献,具体取决于同面板内同配置特效数量)。
|
||||
|
||||
另外 `05_` 是排查方法论(Frame Debugger 看图、编辑器 FPS 乱跳、Profiler 三步法),`99_` 是原始提交全量 diff(仅供对照,**不建议直接 apply**,原因见第五章)。
|
||||
|
||||
---
|
||||
|
||||
## 二、目录结构与文件清单
|
||||
|
||||
> ★ = 必读/推荐入口。每个功能目录内都是「补丁 + 成品源码 + 说明文档」三件套,可单独取用。
|
||||
|
||||
```
|
||||
DC优化归档/
|
||||
├── README.md ← 本文件(总入口)
|
||||
│
|
||||
├── 01_UIEffect材质共享降DC/ ← 路线①:同配置特效共享材质
|
||||
│ ├── 移植与验证手册.md ★必读 8 章:前提核对/安装/验证/回滚/坑
|
||||
│ ├── uieffect_configId.patch ★推荐 git 标准补丁(2 文件 +126 -2)
|
||||
│ ├── UIEffectContext.cs 打完补丁的成品源码(兜底覆盖用)
|
||||
│ ├── UIEffectBase.cs 打完补丁的成品源码(兜底覆盖用)
|
||||
│ └── UIEffect降DC改动总结.md T11 原始技术文档(方案选型 + 指纹算法)
|
||||
│
|
||||
├── 02_全屏面板遮挡省DC/ ← 路线②:盖住的界面不再出 DC
|
||||
│ ├── FullScreenCover_使用方法与避坑.md ★必读 9 章:配置/原理/移植/★2 个真实陷阱/排查
|
||||
│ ├── fullscreen_cover_mechanism.patch ★推荐 git 标准补丁(4 文件 +474)
|
||||
│ ├── FullScreenCover.cs 组件本体(17 行,挂预制体根节点)
|
||||
│ ├── IPanelBase.cs 接口(新增 5 个成员)
|
||||
│ ├── PanelBase.cs 核心实现(遮挡/恢复全部逻辑)
|
||||
│ └── FullScreenCoverTool.cs 编辑器工具(Tools/UI/全屏面板遮挡工具)
|
||||
│
|
||||
├── 03_渲染设置与资源清理/ ← 路线③:渲染管线开销
|
||||
│ └── urp_关闭SSAO.patch URP Renderer 的 SSAO m_Active 1→0(省 4 DC)
|
||||
│
|
||||
├── 04_性能与图片巡检工具/ ← 路线④:批量找问题
|
||||
│ ├── UIPrefabCheck_能力与优化清单.md ★必读 7 个标签页能力表 + 4 个缺口 + 分级优化方案
|
||||
│ └── UIPrefabCheck.cs 工具源码副本(1434 行,只读归档)
|
||||
│
|
||||
├── 05_排查方法论/
|
||||
│ └── DC与帧率排查经验.md Frame Debugger 看图/编辑器 FPS 乱跳/Profiler 三步法
|
||||
│
|
||||
└── 99_原始提交备查/ ← 仅供对照,不建议直接 apply
|
||||
├── T5_c61b9012_UIEffect补丁提交.patch 路线① 在 T5 的原始提交(混入了 prefab 改动)
|
||||
└── T5_cdbbca4b_DC战役总提交.patch 路线②③ 的总提交(905 KB,含删 30MB 大图)
|
||||
```
|
||||
|
||||
### 各路线的动手前必读
|
||||
|
||||
| 路线 | 必读文档 | 核心前提 |
|
||||
| --- | --- | --- |
|
||||
| ① UIEffect | `01_/移植与验证手册.md` | UIEffect 必须是**本地源码包**(`file:` 引用),不能是 PackageCache 只读包 |
|
||||
| ② 全屏遮挡 | `02_/FullScreenCover_使用方法与避坑.md` | 必须用 MFrame 的 `PanelBase<V>` + `PanelMgr` + `UILayerIdx` 体系;**第七章两个陷阱务必看** |
|
||||
| ③ 渲染设置 | 无(补丁即说明) | URP;2D/UI 场景才有意义 |
|
||||
| ④ 巡检工具 | `04_/UIPrefabCheck_能力与优化清单.md` | 静态扫描 6 页删 `using MFrame;` 即可搬走;性能巡检页**强绑 T5 框架需重写** |
|
||||
|
||||
---
|
||||
|
||||
## 三、快速开始
|
||||
|
||||
四条路线**互相独立**,按需取用。建议顺序:③(改一个数字,风险最低)→ ② → ① → ④(工具,最后上)。
|
||||
|
||||
### 路线③ 渲染设置(最简单,先看这个)
|
||||
|
||||
```powershell
|
||||
cd <目标工程 git 根目录>
|
||||
git apply --3way "<归档路径>\03_渲染设置与资源清理\urp_关闭SSAO.patch"
|
||||
```
|
||||
|
||||
只改一个 `m_Active: 1 → 0`。也可以在 Unity 里手动做:选中 URP Renderer 资产 → 找到 SSAO Feature → 取消勾选。2D/UI 场景关掉无视觉损失。
|
||||
|
||||
### 路线② 全屏遮挡
|
||||
|
||||
```powershell
|
||||
cd <目标工程 git 根目录>
|
||||
git apply --check "<归档路径>\02_全屏面板遮挡省DC\fullscreen_cover_mechanism.patch"
|
||||
git apply --3way "<归档路径>\02_全屏面板遮挡省DC\fullscreen_cover_mechanism.patch"
|
||||
```
|
||||
|
||||
**动手前必须先读** `02_/FullScreenCover_使用方法与避坑.md` 第六章(前提核对)与**第七章(两个真实陷阱)**。编译通过后用菜单 `Tools/UI/全屏面板遮挡工具` 逐个挂组件,**一次只挂一个并立即验证**。
|
||||
|
||||
### 路线① UIEffect(3 步)
|
||||
|
||||
前提:目标工程把 UIEffect 以**本地源码包**方式引入(`Packages/manifest.json` 里形如
|
||||
`"com.coffee.ui-effect": "file:../my_libs/unityLib/UIEffect-main/Packages/src"`)。
|
||||
如果它在 `Library/PackageCache/` 下(只读),先按手册第二章把它生成本地包,否则改了也白改。
|
||||
|
||||
```powershell
|
||||
# 1) 进目标工程的 git 根目录(T5 是 <项目>/T5_game)
|
||||
cd <目标工程 git 根目录>
|
||||
|
||||
# 2) 应用补丁(--3way 允许三方合并,冲突时会在文件里留标记)
|
||||
git apply --3way "<归档路径>\01_UIEffect材质共享降DC\uieffect_configId.patch"
|
||||
|
||||
# 3) 回 Unity 编译,打开目标面板用 Frame Debugger 看批次是否下降
|
||||
```
|
||||
|
||||
apply 失败(版本不同、路径不同)就走兜底:把 `01_UIEffect材质共享降DC/` 里的两个 `.cs` 直接覆盖到目标工程的
|
||||
`.../UIEffect-main/Packages/src/Runtime/` 下;再不行就照 `移植与验证手册.md` 第三章手工贴代码块。
|
||||
|
||||
### 路线④ 巡检工具
|
||||
|
||||
把 `04_/UIPrefabCheck.cs` 拷到目标工程 `Assets/Editor/` 下即可。静态扫描 6 页可能需删 `using MFrame;`;性能巡检页强绑 T5 框架,详见 `UIPrefabCheck_能力与优化清单.md` 第五章。
|
||||
|
||||
---
|
||||
|
||||
## 四、路线① 改动核心(速览)
|
||||
|
||||
**`UIEffectBase.cs` → `GetModifiedMaterial()`**,材质缓存 key 换掉:
|
||||
|
||||
```csharp
|
||||
// 改前:effectId = 组件实例唯一 → 永不共享
|
||||
var rootId = (uint)(transitionRoot != null ? transitionRoot.GetHashCode() : 0);
|
||||
var hash = new Hash128((uint)baseMaterial.GetHashCode(), effectId, samplingScaleId, rootId);
|
||||
|
||||
// 改后:configId = 配置内容指纹 → 同配置共享
|
||||
var configId = context.configId;
|
||||
var rootId = context.useViewMatrix && transitionRoot ? (uint)transitionRoot.GetInstanceID() : 0u;
|
||||
var hash = new Hash128((uint)baseMaterial.GetInstanceID(), configId, samplingScaleId, rootId);
|
||||
```
|
||||
|
||||
**`UIEffectContext.cs`**,新增 121 行:`configId` 属性(反射遍历所有 public 序列化字段 → FNV-1a 混合 → MurmurHash3 终混雪崩),配套 `Mix` / `HashValue` / `HashFields` / `GradientHash` 与两个静态缓存 `s_ConfigFields`、`s_FieldCache`。
|
||||
|
||||
三个设计要点(细节见 `UIEffect降DC改动总结.md`):
|
||||
1. **反射结果静态缓存**:`GetFields` 只在类型加载时跑一次,运行时无类型发现开销,避免批量刷新时的 GC 尖峰;
|
||||
2. **`Gradient` 按内容哈希**:不同实例、内容相同的渐变得到相同指纹(否则按引用哈希会白拆批次);
|
||||
3. **`rootId` 仅在需要视图矩阵时参与**(transition / gradation / detail / shiny),普通灰度不受 root 影响,**共享范围更大**。
|
||||
|
||||
---
|
||||
|
||||
## 五、来源与可追溯性
|
||||
|
||||
| 项目 | 提交 | 路线 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| T11 | `fa5f0cdb 降低DC的操作`(2026-08-26) | ① | 首次实现,产出 `UIEffect降DC改动总结.md` |
|
||||
| T5 | `cdbbca4b 提交DC优化` | ②③ | FullScreenCover 整套机制 + 关 SSAO + 删 30MB 无用大图 |
|
||||
| T5 | `c61b9012 优化DC`(2026-09-08) | ① | 从 T11 移植到 T5,验证生效(DC 确认下降) |
|
||||
| T5 | `d9fc47e0 debug` | ② | 因副本异常把 `UI_RaidSelectPanel` 的 FullScreenCover **摘掉**(详见 02_ 文档第七章) |
|
||||
|
||||
> **为何 `99_原始提交备查/` 里的补丁不建议直接 apply**:两个提交都是“搭车提交”,路线混在一起:
|
||||
> - `c61b9012` 除了路线① 的 2 个源码文件,还捎带了 7 个面板 prefab 的 `isKeepShowWhenCovered: 1 → 0`(属路线②的配置);
|
||||
> - `cdbbca4b` 除了路线②③,还含删图、prefab 改动等共 905 KB diff。
|
||||
>
|
||||
> 所以各功能目录里提供的是**拆分后的纯净补丁**(`uieffect_configId.patch`、`fullscreen_cover_mechanism.patch`、`urp_关闭SSAO.patch`),
|
||||
> 原始提交只用于事后对照“T5 当时到底改了什么”。
|
||||
|
||||
---
|
||||
|
||||
## 六、本包缺什么(诚实说明)
|
||||
|
||||
- **⚠ 路线② 有一个已知隐患未修,随包一起交付**:`PanelBase.RestorePanelsHiddenByMe()` 只看“是否被遮挡隐藏”,
|
||||
不看“是否已被业务 Close”,导致已关闭的面板可能被 `SetActive(true)` 复活。T5 副本异常就是这么来的,
|
||||
当时的处置是**把 RaidSelectPanel 的组件摘掉**(保守做法),**框架层的 4 行守卫至今未实施**(已核实:PanelBase 全文搜 `_closed`/`isClosed` 零命中)。
|
||||
归档的源码是 T5 当前真实状态(含隐患),修复代码已写在 `02_/FullScreenCover_使用方法与避坑.md` 第七章陷阱一,移植时建议一并补上。
|
||||
- **UI DC 体检工具(`UIDCProbeTool.cs`)没有归档进来**:该工具在 T5 里按需求删除时**从未提交进 git**,
|
||||
会话缓存中也没有源码副本,已不可恢复。它当时的能力是:运行时按 Canvas 采集真实批次 + 材质分组模式
|
||||
(按材质/纹理 hash 归组,定位"谁在拆批次")。如果新项目需要,可按 `05_/DC与帧率排查经验.md` 的思路重建,
|
||||
或直接用 Unity 自带的 Frame Debugger(多数场景够用)。
|
||||
**更推荐的替代路线**:`04_/UIPrefabCheck_能力与优化清单.md` 的 A1 + A2 两项——给界面性能巡检加
|
||||
DC/批次指标、再给静态扫描加一页"合批体检",即可覆盖 UIDCProbeTool 的核心价值,而且静态分析可批量、
|
||||
可导表、不需要 Play Mode,比原来那个运行时版本更好用。
|
||||
- **`ShaderVariantRegistry.cs` 的 Profiler.EndSample 配对修复没有归档**:T11 文档提到过这个改动,
|
||||
但 T5 当前插件版本(5.11.3)里该文件的采样配对本身已是正确的,未做修改,因此无补丁可归档。
|
||||
若目标工程控制台出现 `Non-matching Profiler.EndSample`,按 `01_/移植与验证手册.md` 第四章处理。
|
||||
- **路线③ 只归档了 SSAO 一项**:当时还做了 Skybox 改纯色、删三张超大图(map.png 30MB / bg.png ×2 各 ~2.5MB),
|
||||
这些属于具体工程的美术决策,不具可移植性,只在 `99_/T5_cdbbca4b` 里能看到完整 diff。
|
||||
Reference in New Issue
Block a user