Unity呼吸效果
让空物体下的多个图片模型做周期性"呼吸"(正弦缩放,可选上下浮动 + 透明度明暗)。

代码挂载到空物体下面就能用,模型是空物体的子物体
代码如下:
using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; /// <summary> /// 呼吸效果:让空物体下的多个图片模型做周期性"呼吸"(正弦缩放,可选上下浮动 + 透明度明暗)。 /// 两种模式: /// Parent = 整体呼吸,缩放空物体自身,所有图片作为整体一起胀缩(布局跟着呼吸) /// Children = 独立呼吸,每个直接子物体围绕自身缩放,布局不动,可用相位差做出依次起伏的波浪感 /// 透明度支持 3D Renderer(材质属性块,不破坏共享材质)、SpriteRenderer、UGUI Graphic(Image/Text)。 /// 不依赖 DOTween,纯 Update 实现,支持随时暂停/继续/停止复位。 /// 用法:挂到空物体上;Play() 开始;Pause()/Resume()/TogglePause() 控制;ResetToBase() 恢复初始状态。 /// 注意:只收集"直接子物体";若运行时增删子物体或修改相位差,调用 RefreshCache() 重新缓存。 /// 测试按键:B = 开始/停止切换,空格 = 暂停/继续,R = 复位(接入项目后可删除 Update 里的按键段)。 /// </summary> public class BreathEffect : MonoBehaviour { public enum BreathMode { Parent, Children } [Header("呼吸模式")] [Tooltip("Parent = 缩放空物体自身(整体呼吸);Children = 每个图片模型独立呼吸(布局不动)")] public BreathMode mode = BreathMode.Children; [Tooltip("独立呼吸时相邻子物体的相位差(秒),0 = 全部同步呼吸;整体呼吸无效")] public float phaseStagger = 0.15f; [Header("缩放呼吸")] [Tooltip("一次完整呼吸时长(秒),越小越快")] public float duration = 2.4f; [Tooltip("缩放幅度,0.08 = 在初始大小基础上 ±8%(以各自缓存到的初始缩放为基准)")] public float scaleAmplitude = 0.08f; [Header("上下浮动(可选,沿世界 Y 轴)")] public bool enableFloat = true; [Tooltip("浮动幅度(米)。整体呼吸时浮动空物体自身,独立呼吸时各图片各自浮动")] public float floatAmplitude = 0.04f; [Header("透明度呼吸(可选,变暗再亮回)")] public bool enableFade = false; [Tooltip("最暗时透明度 = 初始透明度 × 该值")] [Range(0f, 1f)] public float minAlphaScale = 0.5f; [Header("其他")] [Tooltip("Start 时自动开始呼吸")] public bool playOnStart = true; // 每个直接子物体的呼吸数据(初始状态缓存,改参数不需要重新拖场景) private class BreathItem { public Transform Tr; public Vector3 BaseScale; // 初始本地缩放 public Vector3 BaseLocalPos; // 初始本地位置 public float Phase; // 相位偏移(秒) public SpriteRenderer Sprite; // 2D 图片(直接改 color 最干净) public Renderer Renderer; // 3D 图片模型(用材质属性块,不实例化材质) public int ColorProp; // 材质颜色属性 ID,-1 = 材质不支持改色 public Graphic Graphic; // UGUI Image/Text public Color BaseColor; // 初始颜色(含透明度) } private static readonly int BaseColorId = Shader.PropertyToID("_BaseColor"); // URP/Lit private static readonly int ColorId = Shader.PropertyToID("_Color"); // Built-in private readonly List<BreathItem> _items = new List<BreathItem>(); private Vector3 _baseSelfScale; private Vector3 _baseSelfPos; private MaterialPropertyBlock _mpb; private float _elapsed; private bool _isPlaying; private void Awake() { _mpb = new MaterialPropertyBlock(); _baseSelfScale = transform.localScale; _baseSelfPos = transform.localPosition; CacheChildren(); } private void OnEnable() { if (playOnStart) Play(); } private void Update() { if (!_isPlaying) return; _elapsed += Time.deltaTime; // 暂停时不累计,恢复后从当前进度接着呼吸 if (mode == BreathMode.Parent) { float s = Mathf.Sin(BreathT(0f)); transform.localScale = _baseSelfScale * (1f + scaleAmplitude * s); if (enableFloat) { transform.localPosition = _baseSelfPos + Vector3.up * (floatAmplitude * s); } if (enableFade) { for (int i = 0; i < _items.Count; i++) { ApplyAlpha(_items[i], 0.5f + 0.5f * s); // 整体明暗同步 } } } else { for (int i = 0; i < _items.Count; i++) { var item = _items[i]; float s = Mathf.Sin(BreathT(item.Phase)); item.Tr.localScale = item.BaseScale * (1f + scaleAmplitude * s); if (enableFloat) { item.Tr.localPosition = item.BaseLocalPos + Vector3.up * (floatAmplitude * s); } if (enableFade) { ApplyAlpha(item, 0.5f + 0.5f * s); } } } } /// <summary>开始/重新开始呼吸(时间归零,从初始状态进入)</summary> public void Play() { _elapsed = 0f; _isPlaying = true; } /// <summary>暂停(冻结在当前呼吸进度)</summary> public void Pause() { _isPlaying = false; } /// <summary>从暂停处继续(不会从头开始)</summary> public void Resume() { _isPlaying = true; } /// <summary>暂停/继续一键切换</summary> public void TogglePause() { _isPlaying = !_isPlaying; } /// <summary>停止并恢复到缓存时的初始缩放/位置/颜色</summary> public void StopAndReset() { _isPlaying = false; transform.localScale = _baseSelfScale; transform.localPosition = _baseSelfPos; for (int i = 0; i < _items.Count; i++) { var item = _items[i]; item.Tr.localScale = item.BaseScale; item.Tr.localPosition = item.BaseLocalPos; if (item.Sprite != null) item.Sprite.color = item.BaseColor; else if (item.Graphic != null) item.Graphic.color = item.BaseColor; else if (item.Renderer != null && item.ColorProp >= 0) item.Renderer.SetPropertyBlock(null); // 清除属性块覆盖,还原共享材质本色 } } /// <summary>重新收集直接子物体并缓存初始状态(运行时增删子物体/改相位差后调用)</summary> public void RefreshCache() { StopAndReset(); _baseSelfScale = transform.localScale; _baseSelfPos = transform.localPosition; CacheChildren(); } // 按索引缓存所有直接子物体的初始状态与颜色通道 private void CacheChildren() { _items.Clear(); for (int i = 0; i < transform.childCount; i++) { var child = transform.GetChild(i); var item = new BreathItem { Tr = child, BaseScale = child.localScale, BaseLocalPos = child.localPosition, Phase = i * phaseStagger, // 依次错峰,产生波浪式呼吸 Sprite = child.GetComponent<SpriteRenderer>(), Graphic = child.GetComponent<Graphic>() }; if (item.Sprite != null) { item.BaseColor = item.Sprite.color; } else if (item.Graphic != null) { item.BaseColor = item.Graphic.color; } else { item.Renderer = child.GetComponent<Renderer>(); if (item.Renderer != null) { var mat = item.Renderer.sharedMaterial; if (mat.HasProperty(BaseColorId)) item.ColorProp = BaseColorId; else if (mat.HasProperty(ColorId)) item.ColorProp = ColorId; else item.ColorProp = -1; if (item.ColorProp >= 0) item.BaseColor = mat.GetColor(item.ColorProp); } } _items.Add(item); } } // 带相位偏移的呼吸正弦,-1 ~ 1(吸气=变大变亮,呼气=变小变暗) private float BreathT(float phase) { return (Mathf.PI * 2f / duration) * (_elapsed + phase); } // t01:0 = 最暗,1 = 正常;只压暗到初始透明度的一定比例,不会比初始更亮 private void ApplyAlpha(BreathItem item, float t01) { Color c = item.BaseColor; c.a = Mathf.Lerp(c.a * minAlphaScale, c.a, t01); if (item.Sprite != null) { item.Sprite.color = c; } else if (item.Graphic != null) { item.Graphic.color = c; } else if (item.Renderer != null && item.ColorProp >= 0) { // 材质属性块:改色不实例化材质,多个图片共用同一材质也不会被逐个拷贝 item.Renderer.GetPropertyBlock(_mpb); _mpb.SetColor(item.ColorProp, c); item.Renderer.SetPropertyBlock(_mpb); } } }

浙公网安备 33010602011771号