qiuqiu 1 月之前
父节点
当前提交
d9bb79d2e9

+ 7 - 1
.idea/workspace.xml

@@ -8,7 +8,12 @@
   <component name="ChangeListManager">
     <list default="true" id="fec10672-acda-4616-894b-a4b6f93aea6f" name="Default Changelist" comment="提交">
       <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
-      <change beforePath="$PROJECT_DIR$/笔记文件/2.笔记/瀑布主题替换 临时记录_第二章.md" beforeDir="false" afterPath="$PROJECT_DIR$/笔记文件/2.笔记/瀑布主题替换 临时记录_第二章.md" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/笔记文件/2.笔记/引导系统.md" beforeDir="false" afterPath="$PROJECT_DIR$/笔记文件/2.笔记/引导系统.md" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/笔记文件/2.笔记/消息推送通知.md" beforeDir="false" afterPath="$PROJECT_DIR$/笔记文件/2.笔记/消息推送通知.md" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/笔记文件/2.笔记/灵光系统 临时记录_第三章.md" beforeDir="false" afterPath="$PROJECT_DIR$/笔记文件/2.笔记/灵光系统 临时记录_第三章.md" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/笔记文件/日记/2025_10_28_星期二.md" beforeDir="false" afterPath="$PROJECT_DIR$/笔记文件/日记/2025_10_28_星期二.md" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/笔记文件/日记/2025_10_31_星期五.md" beforeDir="false" afterPath="$PROJECT_DIR$/笔记文件/日记/2025_10_31_星期五.md" afterDir="false" />
+      <change beforePath="$PROJECT_DIR$/笔记文件/日记/2025_11_03_星期一.md" beforeDir="false" afterPath="$PROJECT_DIR$/笔记文件/日记/2025_11_03_星期一.md" afterDir="false" />
     </list>
     <option name="SHOW_DIALOG" value="false" />
     <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -162,6 +167,7 @@
       <workItem from="1761181806955" duration="1293000" />
       <workItem from="1761268517174" duration="2237000" />
       <workItem from="1761612873393" duration="1743000" />
+      <workItem from="1762131895132" duration="2476000" />
     </task>
     <task id="LOCAL-00026" summary="提交">
       <created>1744803311486</created>

+ 805 - 0
笔记文件/2.笔记/unity材质污染.md

@@ -0,0 +1,805 @@
+#unity/日常积累
+
+可以看到,是材质出问题了,导致运行时,这样的显示效果;
+
+![[Pasted image 20251103160850.png]]
+
+对比修复前后的,调用逻辑:
+
+## 修复前 GuideUIHighlight.cs
+
+``` cs
+using UnityEngine;
+using UnityEngine.UI;
+using SugarFrame.Node;
+
+namespace PortableHighlight
+{
+    public enum HoleShape
+    {
+        Circle = 0,
+        Rect = 1,
+        Ellipse = 2
+    }
+    
+
+    /// <summary>
+    /// 引导高亮
+    /// </summary>
+    [RequireComponent(typeof(Image))]
+    public class GuideUIHighlight : MonoBehaviour, ICanvasRaycastFilter
+    {
+        [Header("Target")]
+        public RectTransform target;
+        public bool followTargetRuntime = true;
+
+        [Header("Appearance")]
+        public Color dimColor = new Color(0f, 0f, 0f, 0.7f);
+        public HoleShape shape = HoleShape.Rect;
+        public float radius = 200f;              // For circle
+        public Vector2 halfSize = new Vector2(200f, 120f); // For rect/ellipse
+        [Range(0f, 50f)] public float softness = 8f;
+        
+        [Header("Mask Display")]
+        public SugarFrame.Node.MaskDisplayMode maskDisplayMode = SugarFrame.Node.MaskDisplayMode.Normal;
+        [Range(0f, 1f)] public float maskAlpha = 0.7f;
+
+        private Image backgroundImage;
+        private Canvas rootCanvas;
+        private Vector3[] corners = new Vector3[4];
+
+        private readonly int idCenter = Shader.PropertyToID("_Center");
+        private readonly int idRadius = Shader.PropertyToID("_Radius");
+        private readonly int idHalfSize = Shader.PropertyToID("_HalfSize");
+        private readonly int idShape = Shader.PropertyToID("_Shape");
+        private readonly int idSoftness = Shader.PropertyToID("_Softness");
+        private readonly int idColor = Shader.PropertyToID("_Color");
+
+        // Cached values (in canvas local space) for raycast test
+        private Vector2 cachedCenter;
+        private float cachedRadius;
+        private Vector2 cachedHalfSize;
+
+        void Awake()
+        {
+            backgroundImage = GetComponent<Image>();
+            rootCanvas = GetComponentInParent<Canvas>();
+            EnsureMaterial();
+            ApplyStaticParams();
+        }
+
+        void OnValidate()
+        {
+// #if UNITY_EDITOR
+//             if (backgroundImage == null) backgroundImage = GetComponent<Image>();
+//             EnsureMaterial();
+//             ApplyStaticParams();
+//             UpdateTargetParams();
+// #endif
+        }
+
+        public void RefreshHighlight()
+        {
+            EnsureMaterial();
+            ApplyStaticParams();
+            UpdateTargetParams();
+        }
+
+        void Update()
+        {
+            if (followTargetRuntime)
+            {
+                UpdateTargetParams();
+            }
+        }
+
+        void LateUpdate()
+        {
+            // Keep highlight at the very top of its canvas hierarchy
+            if (transform != null)
+            {
+                transform.SetAsLastSibling();
+            }
+        }
+
+        public void SetTarget(RectTransform newTarget)
+        {
+            target = newTarget;
+            UpdateTargetParams();
+        }
+
+        private void EnsureMaterial()
+        {
+            if (backgroundImage.material == null || backgroundImage.material.shader == null || backgroundImage.material.shader.name != "UI/SimpleUIHole")
+            {
+                var shader = Shader.Find("UI/SimpleUIHole");
+                if (shader != null)
+                {
+                    backgroundImage.material = new Material(shader);
+                }
+            }
+            
+            // raycastTarget 的设置现在在 ApplyStaticParams 中根据遮罩模式动态设置
+        }
+
+        private void ApplyStaticParams()
+        {
+            if (backgroundImage == null || backgroundImage.material == null) return;
+            var mat = backgroundImage.material;
+            
+            // 根据遮罩显示模式设置颜色
+            Color finalColor = GetFinalMaskColor();
+            mat.SetColor(idColor, finalColor);
+            
+            mat.SetFloat(idSoftness, softness);
+            mat.SetFloat(idShape, (float)shape);
+            mat.SetFloat(idRadius, radius);
+            mat.SetVector(idHalfSize, halfSize);
+            
+            // 根据遮罩显示模式设置射线检测
+            if (maskDisplayMode == SugarFrame.Node.MaskDisplayMode.None)
+            {
+                // 无遮罩模式:禁用射线检测,允许点击穿透
+                backgroundImage.raycastTarget = false;
+            }
+            else
+            {
+                // 正常遮罩和透明遮罩模式:启用射线检测,用于阻挡点击
+                backgroundImage.raycastTarget = true;
+            }
+        }
+        
+        /// <summary>
+        /// 根据遮罩显示模式获取最终的颜色
+        /// </summary>
+        /// <returns>最终的颜色</returns>
+        private Color GetFinalMaskColor()
+        {
+            switch (maskDisplayMode)
+            {
+                case SugarFrame.Node.MaskDisplayMode.Normal:
+                    // 正常遮罩:使用配置的颜色和透明度
+                    return new Color(dimColor.r, dimColor.g, dimColor.b, maskAlpha);
+                    
+                case SugarFrame.Node.MaskDisplayMode.Transparent:
+                    // 透明遮罩:无颜色显示,但保持遮罩效果(alpha为0但材质仍存在)
+                    return new Color(0f, 0f, 0f, 0f);
+                    
+                case SugarFrame.Node.MaskDisplayMode.None:
+                    // 不显示遮罩:完全透明
+                    return new Color(0f, 0f, 0f, 0f);
+                    
+                default:
+                    return dimColor;
+            }
+        }
+
+        private void UpdateTargetParams()
+        {
+            if (backgroundImage == null || backgroundImage.material == null) return;
+
+            Vector2 center;
+            if (target == null)
+            {
+                center = Vector2.zero;
+                cachedHalfSize = halfSize;
+                cachedRadius = radius;
+            }
+            else
+            {
+                if (rootCanvas == null) rootCanvas = GetComponentInParent<Canvas>();
+                target.GetWorldCorners(corners);
+
+                // Convert world corners into canvas local space
+                for (int i = 0; i < 4; i++)
+                {
+                    Vector2 screenPoint = RectTransformUtility.WorldToScreenPoint(rootCanvas != null ? rootCanvas.worldCamera : null, corners[i]);
+                    RectTransformUtility.ScreenPointToLocalPointInRectangle(
+                        rootCanvas.GetComponent<RectTransform>(),
+                        screenPoint,
+                        rootCanvas != null ? rootCanvas.worldCamera : null,
+                        out var localPoint);
+                    corners[i] = localPoint;
+                }
+
+                // Center and half size in canvas local space
+                Vector2 min = (Vector2)corners[0];
+                Vector2 max = (Vector2)corners[2];
+                center = (min + max) * 0.5f;
+                Vector2 half = (max - min) * 0.5f;
+
+                if (shape == HoleShape.Circle)
+                {
+                    radius = Mathf.Max(half.x, half.y);
+                    backgroundImage.material.SetFloat(idRadius, radius);
+                    cachedRadius = radius;
+                }
+                else
+                {
+                    halfSize = half;
+                    backgroundImage.material.SetVector(idHalfSize, halfSize);
+                    cachedHalfSize = halfSize;
+                }
+            }
+
+            backgroundImage.material.SetVector(idCenter, center);
+            cachedCenter = center;
+            ApplyStaticParams();
+        }
+
+        // Allow raycasts to pass only through the hole
+        public bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
+        {
+            // 透明遮罩和正常遮罩都需要进行射线检测,只是显示效果不同
+            if (rootCanvas == null) rootCanvas = GetComponentInParent<Canvas>();
+            if (rootCanvas == null) return true; // fallback: block
+
+            RectTransform canvasRect = rootCanvas.GetComponent<RectTransform>();
+            RectTransformUtility.ScreenPointToLocalPointInRectangle(
+                canvasRect,
+                sp,
+                rootCanvas != null ? rootCanvas.worldCamera : null,
+                out var localPoint);
+
+            // Translate to hole-local space
+            Vector2 delta = localPoint - cachedCenter;
+
+            bool insideHole = false;
+            switch (shape)
+            {
+                case HoleShape.Circle:
+                    insideHole = (delta.sqrMagnitude <= cachedRadius * cachedRadius);
+                    break;
+                case HoleShape.Rect:
+                    insideHole = Mathf.Abs(delta.x) <= cachedHalfSize.x && Mathf.Abs(delta.y) <= cachedHalfSize.y;
+                    break;
+                case HoleShape.Ellipse:
+                    float nx = cachedHalfSize.x <= 0.0001f ? 0f : delta.x / cachedHalfSize.x;
+                    float ny = cachedHalfSize.y <= 0.0001f ? 0f : delta.y / cachedHalfSize.y;
+                    insideHole = (nx * nx + ny * ny) <= 1f;
+                    break;
+            }
+
+            // Return false to let the ray pass through when inside the hole.
+            // Return true to block clicks outside the hole.
+            // 透明遮罩和正常遮罩都遵循相同的射线阻挡逻辑
+            return !insideHole;
+        }
+    }
+}
+```
+
+## 修复后 GuideUIHighlight.cs
+
+``` cs
+using UnityEngine;
+using UnityEngine.UI;
+using SugarFrame.Node;
+
+namespace PortableHighlight
+{
+    public enum HoleShape
+    {
+        Circle = 0,
+        Rect = 1,
+        Ellipse = 2
+    }
+    
+
+    /// <summary>
+    /// 引导高亮
+    /// </summary>
+    [RequireComponent(typeof(Image))]
+    public class GuideUIHighlight : MonoBehaviour, ICanvasRaycastFilter
+    {
+        [Header("Target")]
+        public RectTransform target;
+        public bool followTargetRuntime = true;
+
+        [Header("Appearance")]
+        public Color dimColor = new Color(0f, 0f, 0f, 0.7f);
+        public HoleShape shape = HoleShape.Rect;
+        public float radius = 200f;              // For circle
+        public Vector2 halfSize = new Vector2(200f, 120f); // For rect/ellipse
+        [Range(0f, 50f)] public float softness = 8f;
+        
+        [Header("Mask Display")]
+        public SugarFrame.Node.MaskDisplayMode maskDisplayMode = SugarFrame.Node.MaskDisplayMode.Normal;
+        [Range(0f, 1f)] public float maskAlpha = 0.7f;
+
+        private Image backgroundImage;
+        private Canvas rootCanvas;
+        private Vector3[] corners = new Vector3[4];
+        
+        // 使用独立材质实例避免全局Shader属性污染
+        private Material instanceMaterial;
+
+        private readonly int idCenter = Shader.PropertyToID("_Center");
+        private readonly int idRadius = Shader.PropertyToID("_Radius");
+        private readonly int idHalfSize = Shader.PropertyToID("_HalfSize");
+        private readonly int idShape = Shader.PropertyToID("_Shape");
+        private readonly int idSoftness = Shader.PropertyToID("_Softness");
+        private readonly int idColor = Shader.PropertyToID("_Color");
+
+        // Cached values (in canvas local space) for raycast test
+        private Vector2 cachedCenter;
+        private float cachedRadius;
+        private Vector2 cachedHalfSize;
+
+        void Awake()
+        {
+            backgroundImage = GetComponent<Image>();
+            rootCanvas = GetComponentInParent<Canvas>();
+            EnsureMaterial();
+            ApplyStaticParams();
+        }
+        
+        void OnDestroy()
+        {
+            // 销毁时清理材质实例,避免内存泄漏
+            if (instanceMaterial != null)
+            {
+                if (Application.isPlaying)
+                {
+                    DestroyImmediate(instanceMaterial);
+                }
+                instanceMaterial = null;
+                Debug.Log($"[GuideUIHighlight] 销毁材质实例");
+            }
+        }
+
+        public void RefreshHighlight()
+        {
+            EnsureMaterial();
+            ApplyStaticParams();
+            UpdateTargetParams();
+        }
+
+        // void Update()
+        // {
+        //     if (followTargetRuntime)
+        //     {
+        //         UpdateTargetParams();
+        //     }
+        // }
+
+        void LateUpdate()
+        {
+            // Keep highlight at the very top of its canvas hierarchy
+            if (transform != null)
+            {
+                transform.SetAsLastSibling();
+            }
+        }
+
+        public void SetTarget(RectTransform newTarget)
+        {
+            target = newTarget;
+            UpdateTargetParams();
+        }
+
+        private void EnsureMaterial()
+        {
+            // 检查是否需要创建新材质实例
+            bool needNewMaterial = false;
+            
+            if (backgroundImage.material == null)
+            {
+                needNewMaterial = true;
+            }
+            else if (backgroundImage.material.shader == null)
+            {
+                needNewMaterial = true;
+            }
+            else if (backgroundImage.material.shader.name != "UI/SimpleUIHole")
+            {
+                needNewMaterial = true;
+            }
+            else if (backgroundImage.material == instanceMaterial)
+            {
+                // 如果已经是我们的实例材质,不需要重新创建
+                needNewMaterial = false;
+            }
+            
+            if (needNewMaterial)
+            {
+                // 首先尝试从Resources加载预制的材质
+                var prefabMaterial = Resources.Load<Material>("Materials/GuideUIHoleMaterial");
+                if (prefabMaterial != null)
+                {
+                    // 关键:创建完全独立的材质实例,避免任何共享
+                    instanceMaterial = new Material(prefabMaterial);
+                    instanceMaterial.name = $"GuideUIHoleMaterial_Instance_{GetInstanceID()}";
+                    backgroundImage.material = instanceMaterial;
+                    Debug.Log($"[GuideUIHighlight] 创建独立材质实例: {instanceMaterial.name}");
+                }
+                else
+                {
+                    // 如果预制材质不存在,尝试通过Shader.Find创建
+                    var shader = Shader.Find("UI/SimpleUIHole");
+                    if (shader != null)
+                    {
+                        instanceMaterial = new Material(shader);
+                        instanceMaterial.name = $"GuideUIHoleMaterial_Instance_{GetInstanceID()}";
+                        backgroundImage.material = instanceMaterial;
+                        Debug.Log($"[GuideUIHighlight] 通过Shader.Find创建独立材质实例: {instanceMaterial.name}");
+                    }
+                    else
+                    {
+                        // 最后的fallback:使用默认UI材质但设置为透明
+                        Debug.LogError("[GuideUIHighlight] 无法找到UI/SimpleUIHoleFixed shader和预制材质!");
+                        backgroundImage.material = null;
+                        backgroundImage.color = new Color(0, 0, 0, 0); // 设置为透明
+                    }
+                }
+            }
+            
+            // raycastTarget 的设置现在在 ApplyStaticParams 中根据遮罩模式动态设置
+        }
+
+        private void ApplyStaticParams()
+        {
+            if (backgroundImage == null || backgroundImage.material == null) return;
+            
+            // 确保使用我们的独立材质实例
+            if (backgroundImage.material != instanceMaterial)
+            {
+                backgroundImage.material = instanceMaterial;
+            }
+            
+            // 根据遮罩显示模式设置颜色
+            Color finalColor = GetFinalMaskColor();
+            
+            // 直接设置材质实例的属性(不会影响全局,因为这是独立实例)
+            instanceMaterial.SetColor(idColor, finalColor);
+            instanceMaterial.SetFloat(idSoftness, softness);
+            instanceMaterial.SetFloat(idShape, (float)shape);
+            instanceMaterial.SetFloat(idRadius, radius);
+            instanceMaterial.SetVector(idHalfSize, halfSize);
+            
+            // 根据遮罩显示模式设置射线检测
+            if (maskDisplayMode == SugarFrame.Node.MaskDisplayMode.None)
+            {
+                // 无遮罩模式:禁用射线检测,允许点击穿透
+                backgroundImage.raycastTarget = false;
+            }
+            else
+            {
+                // 正常遮罩和透明遮罩模式:启用射线检测,用于阻挡点击
+                backgroundImage.raycastTarget = true;
+            }
+            
+            Debug.Log($"[GuideUIHighlight] 使用独立材质实例设置参数,避免全局污染");
+        }
+        
+        /// <summary>
+        /// 根据遮罩显示模式获取最终的颜色
+        /// </summary>
+        /// <returns>最终的颜色</returns>
+        private Color GetFinalMaskColor()
+        {
+            switch (maskDisplayMode)
+            {
+                case SugarFrame.Node.MaskDisplayMode.Normal:
+                    // 正常遮罩:使用配置的颜色和透明度
+                    return new Color(dimColor.r, dimColor.g, dimColor.b, maskAlpha);
+                    
+                case SugarFrame.Node.MaskDisplayMode.Transparent:
+                    // 透明遮罩:无颜色显示,但保持遮罩效果(alpha为0但材质仍存在)
+                    return new Color(0f, 0f, 0f, 0f);
+                    
+                case SugarFrame.Node.MaskDisplayMode.None:
+                    // 不显示遮罩:完全透明
+                    return new Color(0f, 0f, 0f, 0f);
+                    
+                default:
+                    return dimColor;
+            }
+        }
+
+        private void UpdateTargetParams()
+        {
+            if (backgroundImage == null || backgroundImage.material == null) return;
+            
+            Vector2 center;
+            if (target == null)
+            {
+                center = Vector2.zero;
+                cachedHalfSize = halfSize;
+                cachedRadius = radius;
+            }
+            else
+            {
+                if (rootCanvas == null) rootCanvas = GetComponentInParent<Canvas>();
+                target.GetWorldCorners(corners);
+            
+                // Convert world corners into canvas local space
+                for (int i = 0; i < 4; i++)
+                {
+                    Vector2 screenPoint = RectTransformUtility.WorldToScreenPoint(rootCanvas != null ? rootCanvas.worldCamera : null, corners[i]);
+                    RectTransformUtility.ScreenPointToLocalPointInRectangle(
+                        rootCanvas.GetComponent<RectTransform>(),
+                        screenPoint,
+                        rootCanvas != null ? rootCanvas.worldCamera : null,
+                        out var localPoint);
+                    corners[i] = localPoint;
+                }
+            
+                // Center and half size in canvas local space
+                Vector2 min = (Vector2)corners[0];
+                Vector2 max = (Vector2)corners[2];
+                center = (min + max) * 0.5f;
+                Vector2 half = (max - min) * 0.5f;
+            
+                if (shape == HoleShape.Circle)
+                {
+                    radius = Mathf.Max(half.x, half.y);
+                    cachedRadius = radius;
+                }
+                else
+                {
+                    halfSize = half;
+                    cachedHalfSize = halfSize;
+                }
+            }
+            
+            // 使用独立材质实例设置中心位置,避免全局污染
+            if (instanceMaterial != null)
+            {
+                instanceMaterial.SetVector(idCenter, center);
+            }
+            cachedCenter = center;
+            ApplyStaticParams();
+        }
+
+        // Allow raycasts to pass only through the hole
+        public bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
+        {
+            // 透明遮罩和正常遮罩都需要进行射线检测,只是显示效果不同
+            if (rootCanvas == null) rootCanvas = GetComponentInParent<Canvas>();
+            if (rootCanvas == null) return true; // fallback: block
+
+            RectTransform canvasRect = rootCanvas.GetComponent<RectTransform>();
+            RectTransformUtility.ScreenPointToLocalPointInRectangle(
+                canvasRect,
+                sp,
+                rootCanvas != null ? rootCanvas.worldCamera : null,
+                out var localPoint);
+
+            // Translate to hole-local space
+            Vector2 delta = localPoint - cachedCenter;
+
+            bool insideHole = false;
+            switch (shape)
+            {
+                case HoleShape.Circle:
+                    insideHole = (delta.sqrMagnitude <= cachedRadius * cachedRadius);
+                    break;
+                case HoleShape.Rect:
+                    insideHole = Mathf.Abs(delta.x) <= cachedHalfSize.x && Mathf.Abs(delta.y) <= cachedHalfSize.y;
+                    break;
+                case HoleShape.Ellipse:
+                    float nx = cachedHalfSize.x <= 0.0001f ? 0f : delta.x / cachedHalfSize.x;
+                    float ny = cachedHalfSize.y <= 0.0001f ? 0f : delta.y / cachedHalfSize.y;
+                    insideHole = (nx * nx + ny * ny) <= 1f;
+                    break;
+            }
+
+            // Return false to let the ray pass through when inside the hole.
+            // Return true to block clicks outside the hole.
+            // 透明遮罩和正常遮罩都遵循相同的射线阻挡逻辑
+            return !insideHole;
+        }
+    }
+}
+```
+
+## 关键差异概览
+
+-   材质实例管理
+
+-   GuideSystem2:显式维护私有 instanceMaterial,创建“独立材质实例”,并在销毁时主动清理;每次用前还强制校验并回填到 Image.material。
+
+-   GuideSystem3:直接用 backgroundImage.material = new Material(shader) 生成材质后,后续通过 backgroundImage.material.SetXxx 写入;没有私有引用,也没有销毁时的释放。
+
+-   动态更新逻辑
+
+-   GuideSystem2:去掉了 Update() 的跟随(注释掉),仅 LateUpdate() 置顶;外部调用 RefreshHighlight() 或设置 target 后再推一次参数。
+
+-   GuideSystem3:保留 Update(),followTargetRuntime 为真时每帧更新洞位置与大小。
+
+-   防污染措施的严谨度
+
+-   GuideSystem2:始终对“我们自己的材质实例”写属性,且在 EnsureMaterial() 中保证当前 Image 正在用的就是该实例;销毁时 DestroyImmediate(instanceMaterial),防止泄漏与复用。
+
+-   GuideSystem3:虽然创建了 new Material(shader),理论上这也会产生实例,但后续不再追踪该实例,也不保证它不被其他地方替换/共享,更不做销毁,存在潜在“材质引用复用/泄漏”的风险点。
+
+## 代码对照(材质创建/绑定/清理)
+
+-   GuideSystem2:显式独立实例 + 命名 + 回填 + 销毁
+
+``` cs
+void OnDestroy()
+{
+    // 销毁时清理材质实例,避免内存泄漏
+    if (instanceMaterial != null)
+    {
+        if (Application.isPlaying)
+        {
+            DestroyImmediate(instanceMaterial);
+        }
+        instanceMaterial = null;
+        Debug.Log($"[GuideUIHighlight] 销毁材质实例");
+    }
+}
+```
+
+``` cs
+private void EnsureMaterial()
+{
+    // 检查是否需要创建新材质实例
+    bool needNewMaterial = false;
+    ...
+    else if (backgroundImage.material == instanceMaterial)
+    {
+        // 如果已经是我们的实例材质,不需要重新创建
+        needNewMaterial = false;
+    }
+    
+    if (needNewMaterial)
+    {
+        // 首先尝试从Resources加载预制的材质
+        var prefabMaterial = Resources.Load<Material>("Materials/GuideUIHoleMaterial");
+        if (prefabMaterial != null)
+        {
+            // 关键:创建完全独立的材质实例,避免任何共享
+            instanceMaterial = new Material(prefabMaterial);
+            instanceMaterial.name = $"GuideUIHoleMaterial_Instance_{GetInstanceID()}";
+            backgroundImage.material = instanceMaterial;
+            Debug.Log($"[GuideUIHighlight] 创建独立材质实例: {instanceMaterial.name}");
+        }
+        else
+        {
+            // 如果预制材质不存在,尝试通过Shader.Find创建
+            var shader = Shader.Find("UI/SimpleUIHole");
+            if (shader != null)
+            {
+                instanceMaterial = new Material(shader);
+                instanceMaterial.name = $"GuideUIHoleMaterial_Instance_{GetInstanceID()}";
+                backgroundImage.material = instanceMaterial;
+                Debug.Log($"[GuideUIHighlight] 通过Shader.Find创建独立材质实例: {instanceMaterial.name}");
+            }
+            else
+            {
+                // 最后的fallback:使用默认UI材质但设置为透明
+                Debug.LogError("[GuideUIHighlight] 无法找到UI/SimpleUIHoleFixed shader和预制材质!");
+                backgroundImage.material = null;
+                backgroundImage.color = new Color(0, 0, 0, 0); // 设置为透明
+            }
+        }
+    }
+    
+    // raycastTarget 的设置现在在 ApplyStaticParams 中根据遮罩模式动态设置
+}
+```
+
+``` cs
+private void ApplyStaticParams()
+{
+    if (backgroundImage == null || backgroundImage.material == null) return;
+    
+    // 确保使用我们的独立材质实例
+    if (backgroundImage.material != instanceMaterial)
+    {
+        backgroundImage.material = instanceMaterial;
+    }
+    
+    // 根据遮罩显示模式设置颜色
+    Color finalColor = GetFinalMaskColor();
+    
+    // 直接设置材质实例的属性(不会影响全局,因为这是独立实例)
+    instanceMaterial.SetColor(idColor, finalColor);
+    instanceMaterial.SetFloat(idSoftness, softness);
+    instanceMaterial.SetFloat(idShape, (float)shape);
+    instanceMaterial.SetFloat(idRadius, radius);
+    instanceMaterial.SetVector(idHalfSize, halfSize);
+    ...
+    Debug.Log($"[GuideUIHighlight] 使用独立材质实例设置参数,避免全局污染");
+}
+```
+
+``` cs
+// 使用独立材质实例设置中心位置,避免全局污染
+if (instanceMaterial != null)
+{
+    instanceMaterial.SetVector(idCenter, center);
+}
+cachedCenter = center;
+ApplyStaticParams();
+```
+
+-   GuideSystem3:简单创建 + 直接用 backgroundImage.material 写属性
+
+``` cs
+private void EnsureMaterial()
+{
+    if (backgroundImage.material == null || backgroundImage.material.shader == null || backgroundImage.material.shader.name != "UI/SimpleUIHole")
+    {
+        var shader = Shader.Find("UI/SimpleUIHole");
+        if (shader != null)
+        {
+            backgroundImage.material = new Material(shader);
+        }
+    }
+    
+    // raycastTarget 的设置现在在 ApplyStaticParams 中根据遮罩模式动态设置
+}
+```
+
+``` cs
+private void ApplyStaticParams()
+{
+    if (backgroundImage == null || backgroundImage.material == null) return;
+    var mat = backgroundImage.material;
+    
+    // 根据遮罩显示模式设置颜色
+    Color finalColor = GetFinalMaskColor();
+    mat.SetColor(idColor, finalColor);
+    
+    mat.SetFloat(idSoftness, softness);
+    mat.SetFloat(idShape, (float)shape);
+    mat.SetFloat(idRadius, radius);
+    mat.SetVector(idHalfSize, halfSize);
+    ...
+```
+
+``` cs
+if (shape == HoleShape.Circle)
+{
+    radius = Mathf.Max(half.x, half.y);
+    backgroundImage.material.SetFloat(idRadius, radius);
+    cachedRadius = radius;
+}
+else
+{
+    halfSize = half;
+    backgroundImage.material.SetVector(idHalfSize, halfSize);
+    cachedHalfSize = halfSize;
+}
+...
+backgroundImage.material.SetVector(idCenter, center);
+cachedCenter = center;
+ApplyStaticParams();
+```
+
+## “材质污染”是如何被规避的(GuideSystem2 的做法)
+
+-   明确“独占实例”:通过 new Material(prefabMaterial) 或 new Material(shader) 创建专属实例,并保存到 instanceMaterial,确保不是某个共享的 Material 资源。
+
+-   强制绑定与回填:每次 ApplyStaticParams() 都校验 Image.material 是否就是 instanceMaterial,不是就回填,避免在别处被替换成共享材质后继续写入导致“串改”。
+
+-   始终对实例写属性:所有 SetColor/SetFloat/SetVector 都写在 instanceMaterial 上,不触碰 sharedMaterial,不使用全局 Shader 参数。
+
+-   生命周期清理:OnDestroy() 主动销毁 instanceMaterial,防止实例残留被别处意外复用或造成内存泄漏。
+
+-   诊断可见性:命名实例并打印日志,方便排查是否出现跨对象复用或异常替换。
+
+对比之下,GuideSystem3 虽然使用 new Material(shader) 也会创建实例,但因为不持有独立引用、不在销毁时清理,也不校验回填,存在以下潜在风险:
+
+-   该 Image 的 material 引用可能被外部代码替换为某个共享 Material,后续 SetXxx 将污染到被复用的材质。
+
+-   未清理的材质实例会常驻,可能被误引用或引发泄漏,进一步增加“污染”概率。
+
+### 其他细微差异
+
+-   运行期跟随
+
+-   GuideSystem2 注释了 Update() 跟随,减少每帧写材质属性的频率,更利于将“写材质”收敛到必要时机。
+
+-   GuideSystem3 每帧跟随,会更频繁地对材质写属性(若材质被共享,这会放大污染面)。
+
+—
+
+-   GuideSystem2:通过“私有化材质实例 + 强制回填 + 生命周期清理”系统性地规避了材质污染问题。
+
+-   GuideSystem3:简单按需创建实例,但缺乏引用约束与清理,存在被替换/复用引起污染的风险点。

+ 2 - 0
笔记文件/2.笔记/引导系统.md

@@ -5,6 +5,8 @@
 
 需求文档:https://inspire.sg.larksuite.com/wiki/D5cWwdxRDis74AkRDYmlzwq4gPc
 
+创造中心单子:https://create.inspiregames.cn/browse/DS-138
+
 ![[Pasted image 20250903155806.png]]
 
 ![[Pasted image 20250903155816.png]]

+ 0 - 5
笔记文件/2.笔记/测试svn路径.md

@@ -1,5 +0,0 @@
-#灵感
-
-```
-C:\ProgramData\Jenkins\.jenkins\workspace\TestSVN\
-```

+ 5 - 1
笔记文件/2.笔记/消息推送通知.md

@@ -155,4 +155,8 @@ lua测试消息推送逻辑参考
 
 消息推送插件 安卓底层的call调用
 
-![[Pasted image 20250319150018.png]]
+![[Pasted image 20250319150018.png]]
+
+旧版本的消息通知崩溃,需要处理一下:
+
+![[img_v3_02rm_9424b9c0-63b8-494a-95ea-f25cec76bahu.jpg]]

+ 2 - 1
笔记文件/2.笔记/灵光系统 临时记录_第三章.md

@@ -18,4 +18,5 @@ web 标签展示位置:
 
 ![[Pasted image 20251022090513.png]]
 
-灵光的初始化,要加一个回调通知,因为是异步的
+灵光的初始化,要加一个回调通知,因为是异步的;
+

+ 0 - 1
笔记文件/日记/2024_09_09_星期一.md

@@ -7,7 +7,6 @@
 
 ---
 [[Unity之RuntimeInitializeOnLoadMethod详解]]
-[[测试svn路径]]
 
 https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask
 # Journal

+ 0 - 1
笔记文件/日记/2025_09_29_星期一.md

@@ -21,7 +21,6 @@
 
 # 今日任务
 
-- [ ] 总结一下 材质污染的 相关笔记 & 完善中台库文档
 - [ ] 开发2d地图编辑器
 - [x] 复习aseprite工作流
 - [ ] 抽象一下Luna的功能出来

+ 1 - 1
笔记文件/日记/2025_10_28_星期二.md

@@ -22,7 +22,7 @@
 # 今日任务
 
 - [ ] 看看再买一个梳毛器,一三五,梳理一下猫毛发
-- [ ] 买一下猫条
+- [x] 买一下猫条
 ---
 
 # Journal

+ 0 - 2
笔记文件/日记/2025_10_31_星期五.md

@@ -22,8 +22,6 @@
 # 今日任务
 
 - [x] 完善一下 Jenkins 命令行 注意事项
-- [ ] 开始开发 第一个引导需求
-- [ ] 同步一下 灵光系统 触达率 问题 处理时间
 ---
 [[Jenkin问题记录&解决]]
 

+ 17 - 0
笔记文件/日记/2025_11_03_星期一.md

@@ -25,6 +25,23 @@
 - [ ] 看看 换一个牙刷头
 - [ ] 下一包猫粮 问链接 买皇家
 - [ ] 记得,周三看看,去剪个头发
+- [ ] 看看买一下维生素c
+- [ ] 看看维生素b相关的,摄入是否需要
+- [x] 确认一下 震动报错log,是否真的还存在
+- [x] 确认一下 mx相关,还有野牛主题的开发时间相关
+- [x] 完善一下 灵光触达率逻辑
+- [x] 解决一下 消息通知 旧版本的 手机重启 安卓崩溃问题 & 确认一下 ios是否会有影响
+- [x] 修复一下 瀑布 svn看不到log的问题<font color="#ffff00">(无法修复,大概率是权限设置问题)</font>
+- [x] 总结一下 材质污染的 相关笔记
+- [ ] 开始开发 第一个引导需求
+- [ ] 拿一下 猫条相关的 快递
+- [ ] 记得周三 同步一下 fmod技术向优化
 ---
+![[Pasted image 20251103092102.png]]
 
+完善vben前端框架,打通日志系统&性能监控系统 go服务端后台,尝试接入性能监控帧率等数据的前端曲线展示
+
+![[Pasted image 20251103152115.png]]
+
+[[unity材质污染]]
 # Journal

二进制
笔记文件/附件/Pasted image 20251103092102.png


二进制
笔记文件/附件/Pasted image 20251103152115.png


二进制
笔记文件/附件/Pasted image 20251103160850.png


二进制
笔记文件/附件/img_v3_02rm_9424b9c0-63b8-494a-95ea-f25cec76bahu.jpg