using UnityEngine; using UnityEngine.Pool; using System.Collections.Generic; public class groundParticularController : MonoBehaviour { [System.Serializable] public struct MaterialConfig { public Material material; public bool isEnabled; [Tooltip("是否为该材质启用渐变颜色覆盖")] public bool useGradientOverride; [Tooltip("粒子生命周期内的颜色渐变 (仅在 useGradientOverride 开启时有效)")] public Gradient colorGradient; } [Header("References")] [Tooltip("粒子的起点")] [SerializeField] private Transform startPoint; [Tooltip("粒子的终点")] [SerializeField] private Transform endPoint; [Tooltip("作为粒子的预制体")] [SerializeField] private GameObject particlePrefab; [Header("Visuals")] [Tooltip("粒子可用的材质配置列表")] [SerializeField] private List particleMaterials = new List(); [Tooltip("粒子最小缩放比例")] [SerializeField] private float minParticleScale = 0.8f; [Tooltip("粒子最大缩放比例")] [SerializeField] private float maxParticleScale = 1.2f; [Header("Settings")] [Tooltip("性能门控:是否启用粒子生成")] public bool enableParticles = true; [Tooltip("粒子的物理层级 (Layer Name)")] [SerializeField] private string physicsLayerName = "particles"; [Tooltip("粒子的渲染层级名称 (Sorting Layer)")] [SerializeField] private string sortingLayerName = "Default"; [Tooltip("粒子的渲染层级编号 (Order in Layer)")] [SerializeField] private int sortingOrder = 0; [Tooltip("渲染队列偏移 (Render Queue Offset),用于解决 3D 物体与 UI 遮挡问题。数值越小越先渲染(被遮挡)")] [SerializeField] private int renderQueueOffset = 0; [Header("Spawn Quantity")] [Tooltip("基础发射频率(每秒发射次数)")] [SerializeField] private float baseEmitRate = 10f; [Tooltip("每次发射生成的粒子数量 (Batch Size) - 增加此数值可显著增加粒子密度")] [SerializeField] private int emitBatchSize = 1; [Header("Spawn Randomness")] [Tooltip("混乱度:随机时间偏移范围")] [SerializeField] private float chaosRandomDelay = 0.05f; [Tooltip("出生点 X 轴随机偏移范围")] [SerializeField] private float spawnOffsetX = 0.5f; [Tooltip("出生点 Z 轴 (深度) 随机偏移范围")] [SerializeField] private float spawnOffsetY = 0.5f; [Tooltip("速率系数,用于调节基础移动速度")] [SerializeField] private float speedMultiplier = 0.1f; [Tooltip("发射速度倍增器")] [SerializeField] private float emissionSpeedMultiplier = 1f; [SerializeField] private float perFrameMoveMultiplier = 0.01f; [Header("Rotation Randomness")] [Tooltip("If enabled, each spawned particle gets a random initial rotation offset.")] [SerializeField] private bool enableRandomRotation = false; [Tooltip("Random initial rotation range in degrees. Each axis is offset from -range to +range.")] [SerializeField] private float randomRotationRangeDegrees = 0f; [Tooltip("起点缩放抖动强度 (基于音频电平)")] [SerializeField] private float shakeIntensity = 0.1f; [Header("External References")] [Tooltip("粒子生成的父物体")] [SerializeField] private Transform particleParent; [Tooltip("主音频源,用于同步音乐状态")] public AudioSource musicSource; [Tooltip("赋值对应的 BeatmapManager 以获取当前歌曲的 BPM")] public BeatmapManager beatmapManager; [Header("Audio Analysis")] public bool useAudioAnalysis = true; [Range(0, 1)] public float audioSensitivity = 0.5f; [Tooltip("音频对发射频率的影响强度")] [SerializeField] private float audioEmitBoost = 50f; private float[] _audioSamples = new float[128]; private float _currentAverageVolume = 0f; private float _emitTimer = 0f; // 性能:复用一个 MaterialPropertyBlock,避免每颗粒子 new(GC)。 private MaterialPropertyBlock _sharedPropBlock; // 性能:缓存粒子的 Renderer,避免 Emit/MoveRoutine 每次 GetComponent。 private readonly Dictionary _rendererCache = new Dictionary(); // 性能:记录已对某共享材质设过 renderQueue,避免重复写。 private readonly HashSet _renderQueueApplied = new HashSet(); // Shader 属性 ID 缓存(比字符串查找快,且避免每帧字符串)。 private static readonly int _headColorId = Shader.PropertyToID("_HeadColor"); private static readonly int _tailColorId = Shader.PropertyToID("_TailColor"); private static readonly int _moveDirId = Shader.PropertyToID("_MoveDir"); // 材质均衡分配逻辑 private List _cachedEnabledConfigs = new List(); private int _materialIndex = 0; private IObjectPool _pool; private Vector3 _originalStartScale; // 【GC 优化】去掉 per-particle 协程,改为中心化移动更新:活跃粒子列表 + 统一 Update 遍历 private struct ActiveParticle { public GameObject gameObject; public Vector3 moveDir; public float totalDist; public float traveledDist; public MaterialConfig? config; public Renderer renderer; } private readonly List _activeParticles = new List(64); // 【GC 优化】延迟发射队列(替代 per-emit 协程 + WaitForSeconds 分配) private struct DelayedEmit { public float fireTime; } private readonly List _delayedEmits = new List(32); private void Awake() { if (startPoint != null) _originalStartScale = startPoint.localScale; _sharedPropBlock = new MaterialPropertyBlock(); _pool = new ObjectPool( createFunc: () => { GameObject go = Instantiate(particlePrefab); go.SetActive(false); // 创建时缓存 Renderer,后续 Emit/Move 复用,避免每颗粒子 GetComponent。 var r = go.GetComponent(); if (r != null) _rendererCache[go] = r; return go; }, actionOnGet: (obj) => { obj.SetActive(true); // 解决对象池拉线问题:如果球体带拖尾,获取时必须清除旧路径 var trail = obj.GetComponent(); if (trail != null) trail.Clear(); }, actionOnRelease: (obj) => obj.SetActive(false), actionOnDestroy: (obj) => Destroy(obj), defaultCapacity: 30 ); } private void Update() { // 执行实时音频 analysis(获取音量强度) AnalyzeAudio(); // 持续发射逻辑 UpdateContinuousEmit(); // 【GC 优化】处理延迟发射队列(替代 per-emit 协程) ProcessDelayedEmits(); // 【GC 优化】统一更新所有活跃粒子(替代 per-particle MoveRoutine 协程) UpdateActiveParticles(); // 起点缩放抖动 if (startPoint != null) { float shakeEffect = _currentAverageVolume * shakeIntensity; startPoint.localScale = _originalStartScale * (1f + shakeEffect); } } private void UpdateContinuousEmit() { // 门控检查:如果玩家禁用了粒子,则不执行生成逻辑 if (!enableParticles || musicSource == null || !musicSource.isPlaying) return; // 计算当前瞬间的发射频率:基础频率 + 音频增益 float currentRate = baseEmitRate + (_currentAverageVolume * audioEmitBoost); if (currentRate <= 0) return; float interval = 1f / currentRate; _emitTimer += Time.deltaTime; if (_emitTimer >= interval) { _emitTimer = 0; // 批量生成:引入混乱度用延迟队列(无 GC),立即发射直接调 Emit for (int i = 0; i < emitBatchSize; i++) { if (chaosRandomDelay > 0) { // 【GC 优化】入队延迟发射,替代 StartCoroutine + WaitForSeconds 分配 _delayedEmits.Add(new DelayedEmit { fireTime = Time.time + Random.Range(0f, chaosRandomDelay) }); } else { Emit(); } } } } // 【GC 优化】处理延迟发射队列(替代 DelayedEmitRoutine 协程) private void ProcessDelayedEmits() { float now = Time.time; for (int i = _delayedEmits.Count - 1; i >= 0; i--) { if (now >= _delayedEmits[i].fireTime) { Emit(); _delayedEmits.RemoveAt(i); } } } // 取粒子缓存的 Renderer;未缓存(异常情况)则补取一次。 private Renderer GetCachedRenderer(GameObject particle) { if (particle == null) return null; if (_rendererCache.TryGetValue(particle, out Renderer r) && r != null) return r; r = particle.GetComponent(); if (r != null) _rendererCache[particle] = r; return r; } private void AnalyzeAudio() { if (musicSource == null || !musicSource.isPlaying || !useAudioAnalysis) { _currentAverageVolume = 0; return; } // 使用 GetOutputData 获取实时波形采样数据(电平) musicSource.GetOutputData(_audioSamples, 0); float sum = 0; for (int i = 0; i < _audioSamples.Length; i++) { sum += Mathf.Abs(_audioSamples[i]); } // 计算平均振幅并应用敏感度 _currentAverageVolume = (sum / _audioSamples.Length) * audioSensitivity * 2.0f; } /// /// 发射一个粒子 /// public void Emit() { if (startPoint == null || endPoint == null || particlePrefab == null) return; // 1. 获取粒子 GameObject particle = _pool.Get(); // 2. 设置层级 particle.transform.SetParent(particleParent != null ? particleParent : null); // 3. 计算缩放 float randomScale = Random.Range(minParticleScale, maxParticleScale); float audioBoost = useAudioAnalysis ? (1f + _currentAverageVolume) : 1f; particle.transform.localScale = particlePrefab.transform.localScale * randomScale * audioBoost; // 4. 计算位置 (全表面随机 - XZ 平面) // 修改为 X 和 Z 轴随机,以覆盖地面 (Ground) 的水平和深度方向 Vector3 surfaceRandomPos = new Vector3(Random.Range(-0.5f, 0.5f), 0f, Random.Range(-0.5f, 0.5f)); Vector3 finalLocalPos = surfaceRandomPos + new Vector3( Random.Range(-spawnOffsetX, spawnOffsetX), 0f, Random.Range(-spawnOffsetY, spawnOffsetY) ); Vector3 spawnWorldPos = startPoint.TransformPoint(finalLocalPos); particle.transform.position = spawnWorldPos; particle.transform.rotation = GetSpawnRotation(); // 5. 材质均衡选取逻辑 MaterialConfig? selectedConfig = GetBalancedMaterialConfig(); // 设置物理层级 int layer = LayerMask.NameToLayer(physicsLayerName); if (layer != -1) { particle.layer = layer; } Renderer renderer = GetCachedRenderer(particle); if (renderer != null) { // 设置渲染层级 renderer.sortingLayerName = sortingLayerName; renderer.sortingOrder = sortingOrder; if (selectedConfig != null) { Material mat = selectedConfig.Value.material; // 用共享材质(sharedMaterial)避免克隆实例产生 GC / 破坏合批。 // renderQueue 是全局统一配置(renderQueueOffset),只需对每个材质设一次即可, // 所有用该材质的粒子渲染队列一致,视觉与原来相同。 if (renderQueueOffset != 0 && _renderQueueApplied.Add(mat)) { mat.renderQueue = mat.renderQueue + renderQueueOffset; } if (renderer.sharedMaterial != mat) { renderer.sharedMaterial = mat; } } } // 6. 【GC 优化】注册到活跃粒子列表(替代 StartCoroutine MoveRoutine) Vector3 currentStartPos = startPoint.position; Vector3 currentEndPos = endPoint.position; Vector3 moveDir = (currentEndPos - currentStartPos).normalized; float totalDist = Vector3.Distance(currentStartPos, currentEndPos); _activeParticles.Add(new ActiveParticle { gameObject = particle, moveDir = moveDir, totalDist = totalDist, traveledDist = 0f, config = selectedConfig, renderer = renderer }); } /// /// 轮询式均衡选取材质,确保每种材质数量大致相同 /// private Quaternion GetSpawnRotation() { if (!enableRandomRotation || randomRotationRangeDegrees <= 0f || startPoint == null) { return startPoint != null ? startPoint.rotation : Quaternion.identity; } float range = Mathf.Abs(randomRotationRangeDegrees); Vector3 randomEuler = new Vector3( Random.Range(-range, range), Random.Range(-range, range), Random.Range(-range, range) ); return startPoint.rotation * Quaternion.Euler(randomEuler); } private MaterialConfig? GetBalancedMaterialConfig() { if (particleMaterials == null || particleMaterials.Count == 0) return null; // 过滤出所有启用的材质 _cachedEnabledConfigs.Clear(); foreach (var config in particleMaterials) { if (config.isEnabled && config.material != null) _cachedEnabledConfigs.Add(config); } if (_cachedEnabledConfigs.Count == 0) return null; // 轮询选取 _materialIndex = (_materialIndex + 1) % _cachedEnabledConfigs.Count; return _cachedEnabledConfigs[_materialIndex]; } // 【GC 优化】统一更新所有活跃粒子(替代 per-particle MoveRoutine 协程,消除协程开销) private void UpdateActiveParticles() { if (startPoint == null || endPoint == null) return; // 暂停检查:暂停期间不更新粒子 if (PauseManager.Instance != null && PauseManager.Instance.IsPaused) return; float currentBPM = GetCurrentBPM(); float noteSpawnerSpeedMultiplier = 1.0f; if (beatmapManager != null && beatmapManager.noteSpawner != null) { noteSpawnerSpeedMultiplier = beatmapManager.noteSpawner.EffectiveSpeedMultiplier; currentBPM = beatmapManager.noteSpawner.EffectiveBpm; } float effectiveBPM = Mathf.Max(currentBPM, 60f); MaterialPropertyBlock propBlock = _sharedPropBlock; // 倒序遍历便于移除 for (int i = _activeParticles.Count - 1; i >= 0; i--) { var p = _activeParticles[i]; if (p.gameObject == null || !p.gameObject.activeInHierarchy) { _activeParticles.RemoveAt(i); continue; } float noteSpeed = (p.totalDist * effectiveBPM * noteSpawnerSpeedMultiplier) / 240f; float step = noteSpeed * speedMultiplier * emissionSpeedMultiplier * perFrameMoveMultiplier * Time.deltaTime; if (step <= 0) step = 0.01f; p.traveledDist += step; p.gameObject.transform.position += p.moveDir * step; // 动态空间渐变色 if (p.config != null && p.config.Value.useGradientOverride && p.renderer != null) { Color headColor = p.config.Value.colorGradient.Evaluate(1f); Color tailColor = p.config.Value.colorGradient.Evaluate(0f); p.renderer.GetPropertyBlock(propBlock); propBlock.SetColor(_headColorId, headColor); propBlock.SetColor(_tailColorId, tailColor); propBlock.SetVector(_moveDirId, p.moveDir); p.renderer.SetPropertyBlock(propBlock); } // 到达终点:回收 if (p.traveledDist >= p.totalDist) { if (p.gameObject.activeInHierarchy) _pool.Release(p.gameObject); _activeParticles.RemoveAt(i); continue; } // 写回更新的 traveledDist(struct 需回写) _activeParticles[i] = p; } } private float GetCurrentBPM() { if (beatmapManager != null && beatmapManager.assignedSongData != null) { return beatmapManager.assignedSongData.bpm; } return 120f; } private void OnDrawGizmosSelected() { if (startPoint == null) return; // 绘制起点的发射区域(完全匹配 Cube 的视觉大小) Gizmos.matrix = startPoint.localToWorldMatrix; Gizmos.color = Color.cyan; Gizmos.DrawWireCube(Vector3.zero, Vector3.one); if (endPoint != null) { Gizmos.matrix = Matrix4x4.identity; // 切回世界坐标画线 Gizmos.color = Color.red; Gizmos.DrawLine(startPoint.position, endPoint.position); Gizmos.matrix = endPoint.localToWorldMatrix; Gizmos.DrawWireCube(Vector3.zero, Vector3.one); } } }