实现点击屏幕任意位置播放点击特效。
屏幕点击特效
需求
现有一个需求,点击屏幕任意位置,播放一个点击特效。
美术已经做好了特效,效果如图:

特效容器
首先,画布是 Camera 模式,画布底下有一个 UIClickEffect 界面。
界面底下有一个 Effect 空节点,它带有 RectTransform 组件,后续会通过这个空节点设置特效的位置。
Effect 空节点底下则是美术制作好的特效。


创建脚本
检测输入
创建 ScreenClickEffect.cs
脚本,添加特效设置相关的字段。
在 Start
的时候把特效预制体默认隐藏,防止初始化时会播放一次特效。
在 Update
中检测鼠标和触摸输入,把点击的位置 clickPosition
传入 PlayClickEffect
方法中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| using UnityEngine;
public class ScreenClickEffect : MonoBehaviour { [Header("特效设置")] public GameObject clickEffectPrefab; public float effectDuration = 1f; void Start() { if (clickEffectPrefab != null) clickEffectPrefab.SetActive(false); } void Update() { HandleInput(); } void HandleInput() { Vector3 clickPosition = Vector3.zero; bool hasInput = false; if (Input.GetMouseButtonDown(0)) { clickPosition = Input.mousePosition; hasInput = true; } if (Input.touchCount > 0) { Touch touch = Input.GetTouch(0); if (touch.phase == TouchPhase.Began) { clickPosition = touch.position; hasInput = true; } } if (hasInput) { PlayClickEffect(clickPosition); } } void PlayClickEffect(Vector3 screenPosition) { if (clickEffectPrefab == null) { Debug.LogWarning("点击特效预制体未设置!"); return; } } }
|
实例化特效
添加节点组件字段,获得画布和界面的变换组件。
继续完善 PlayClickEffect
方法,使用 RectTransformUtility.ScreenPointToLocalPointInRectangle
把屏幕坐标转换成当前界面内的本地坐标,把转换后的 localPosition
赋值给实例化出来的特效对象。
最后,延迟 effectDuration
秒后销毁特效。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| using UnityEngine;
public class ScreenClickEffect : MonoBehaviour { [Header("节点组件")] public Canvas targetCanvas; public RectTransform rectTransform; void Start() {
if (targetCanvas == null) targetCanvas = FindObjectOfType<Canvas>();
if (rectTransform == null) rectTransform = GetComponent<RectTransform>(); } void PlayClickEffect(Vector3 screenPosition) { if (clickEffectPrefab == null) { Debug.LogWarning("点击特效预制体未设置!"); return; } if (targetCanvas == null) { Debug.LogWarning("目标Canvas未设置!"); return; } if (RectTransformUtility.ScreenPointToLocalPointInRectangle( rectTransform, screenPosition, targetCanvas.worldCamera, out Vector2 localPosition)) { GameObject effectInstance; effectInstance = Instantiate(clickEffectPrefab, transform); effectInstance.SetActive(true); if (effectInstance.TryGetComponent<RectTransform>(out var effectRect)) { effectRect.localPosition = localPosition; } else { effectInstance.transform.localPosition = (Vector3)localPosition; } Destroy(effectInstance, effectDuration); } } }
|
节点组件的引用如图:

运行游戏效果:

对象池优化
因为屏幕点击特效会比较频繁地创建和销毁,可以使用 Unity 内置的对象池进行优化。
这里定义了一个 useObjectPool
字段,可以自行选择是否启用对象池,如果不用的话,就还是以创建销毁的方式播放点击特效。
关于对象池的容量设置,可参考以下建议:
- 休闲游戏,容量4-8,启用对象池,平衡性能和内存
- 音游/快节奏,容量10-20,启用对象池,优先保证流畅度
- 低端设备,容量2-4,可关闭对象池,减少内存占用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
| using System.Collections; using UnityEngine; using UnityEngine.Pool;
public class ScreenClickEffect : MonoBehaviour {
[Header("对象池设置")] public bool useObjectPool = true; public int defaultCapacity = 4; public int maxSize = 8; private ObjectPool<GameObject> effectPool; void Start() {
if (useObjectPool) SetupObjectPool(); } void SetupObjectPool() { if (clickEffectPrefab == null) { Debug.LogWarning("无法设置对象池:特效预制体未设置!"); return; } effectPool = new ObjectPool<GameObject>( createFunc: CreateEffect, actionOnGet: OnGetEffect, actionOnRelease: OnReleaseEffect, actionOnDestroy: OnDestroyEffect, collectionCheck: true, defaultCapacity: defaultCapacity, maxSize: maxSize ); } GameObject CreateEffect() { GameObject effect = Instantiate(clickEffectPrefab, transform); effect.SetActive(false); return effect; } void OnGetEffect(GameObject effect) { effect.SetActive(true); if (effect.TryGetComponent<ParticleSystem>(out var particles)) { particles.Clear(); particles.Play(); } } void OnReleaseEffect(GameObject effect) { if (effect != null) { effect.SetActive(false); if (effect.TryGetComponent<ParticleSystem>(out var particles)) { particles.Stop(); particles.Clear(); } } } void OnDestroyEffect(GameObject effect) { if (effect != null) { Destroy(effect); } } void OnDestroy() { effectPool?.Dispose(); } void PlayClickEffect(Vector3 screenPosition) { if (RectTransformUtility.ScreenPointToLocalPointInRectangle( rectTransform, screenPosition, targetCanvas.worldCamera, out Vector2 localPosition)) { GameObject effectInstance; if (useObjectPool && effectPool != null) { effectInstance = effectPool.Get(); } else { effectInstance = Instantiate(clickEffectPrefab, transform); effectInstance.SetActive(true); } if (effectInstance.TryGetComponent<RectTransform>(out var effectRect)) { effectRect.localPosition = localPosition; } else { effectInstance.transform.localPosition = (Vector3)localPosition; } if (useObjectPool && effectPool != null) { StartCoroutine(ReleaseEffectAfterDelay(effectInstance, effectDuration)); } else { Destroy(effectInstance, effectDuration); } } } IEnumerator ReleaseEffectAfterDelay(GameObject effect, float delay) { yield return new WaitForSeconds(delay); if (effect != null && effectPool != null) { effectPool.Release(effect); } } }
|
完整代码
ScreenClickEffect.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
| using System.Collections; using UnityEngine; using UnityEngine.Pool;
public class ScreenClickEffect : MonoBehaviour { [Header("特效设置")] public GameObject clickEffectPrefab; public float effectDuration = 1f;
[Header("节点组件")] public Canvas targetCanvas; public RectTransform rectTransform;
[Header("对象池设置")] public bool useObjectPool = true; public int defaultCapacity = 4; public int maxSize = 8;
private ObjectPool<GameObject> effectPool;
void Start() { if (clickEffectPrefab != null) clickEffectPrefab.SetActive(false);
if (targetCanvas == null) targetCanvas = FindObjectOfType<Canvas>();
if (rectTransform == null) rectTransform = GetComponent<RectTransform>();
if (useObjectPool) SetupObjectPool(); }
void SetupObjectPool() { if (clickEffectPrefab == null) { Debug.LogWarning("无法设置对象池:特效预制体未设置!"); return; } effectPool = new ObjectPool<GameObject>( createFunc: CreateEffect, actionOnGet: OnGetEffect, actionOnRelease: OnReleaseEffect, actionOnDestroy: OnDestroyEffect, collectionCheck: true, defaultCapacity: defaultCapacity, maxSize: maxSize ); }
GameObject CreateEffect() { GameObject effect = Instantiate(clickEffectPrefab, transform); effect.SetActive(false); return effect; }
void OnGetEffect(GameObject effect) { effect.SetActive(true); if (effect.TryGetComponent<ParticleSystem>(out var particles)) { particles.Clear(); particles.Play(); } }
void OnReleaseEffect(GameObject effect) { if (effect != null) { effect.SetActive(false); if (effect.TryGetComponent<ParticleSystem>(out var particles)) { particles.Stop(); particles.Clear(); } } }
void OnDestroyEffect(GameObject effect) { if (effect != null) { Destroy(effect); } }
void Update() { HandleInput(); }
void HandleInput() { Vector3 clickPosition = Vector3.zero; bool hasInput = false; if (Input.GetMouseButtonDown(0)) { clickPosition = Input.mousePosition; hasInput = true; } if (Input.touchCount > 0) { Touch touch = Input.GetTouch(0); if (touch.phase == TouchPhase.Began) { clickPosition = touch.position; hasInput = true; } } if (hasInput) { PlayClickEffect(clickPosition); } }
void PlayClickEffect(Vector3 screenPosition) { if (clickEffectPrefab == null) { Debug.LogWarning("点击特效预制体未设置!"); return; } if (targetCanvas == null) { Debug.LogWarning("目标Canvas未设置!"); return; }
if (RectTransformUtility.ScreenPointToLocalPointInRectangle( rectTransform, screenPosition, targetCanvas.worldCamera, out Vector2 localPosition)) { GameObject effectInstance; if (useObjectPool && effectPool != null) { effectInstance = effectPool.Get(); } else { effectInstance = Instantiate(clickEffectPrefab, transform); effectInstance.SetActive(true); } if (effectInstance.TryGetComponent<RectTransform>(out var effectRect)) { effectRect.localPosition = localPosition; } else { effectInstance.transform.localPosition = (Vector3)localPosition; } if (useObjectPool && effectPool != null) { StartCoroutine(ReleaseEffectAfterDelay(effectInstance, effectDuration)); } else { Destroy(effectInstance, effectDuration); } } }
IEnumerator ReleaseEffectAfterDelay(GameObject effect, float delay) { yield return new WaitForSeconds(delay); if (effect != null && effectPool != null) { effectPool.Release(effect); } }
void OnDestroy() { effectPool?.Dispose(); } }
|