429 lines
17 KiB
C#
429 lines
17 KiB
C#
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<MaterialConfig> particleMaterials = new List<MaterialConfig>();
|
|
[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<GameObject, Renderer> _rendererCache = new Dictionary<GameObject, Renderer>();
|
|
// 性能:记录已对某共享材质设过 renderQueue,避免重复写。
|
|
private readonly HashSet<Material> _renderQueueApplied = new HashSet<Material>();
|
|
// 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<MaterialConfig> _cachedEnabledConfigs = new List<MaterialConfig>();
|
|
private int _materialIndex = 0;
|
|
|
|
private IObjectPool<GameObject> _pool;
|
|
private Vector3 _originalStartScale;
|
|
|
|
private void Awake()
|
|
{
|
|
if (startPoint != null) _originalStartScale = startPoint.localScale;
|
|
_sharedPropBlock = new MaterialPropertyBlock();
|
|
|
|
_pool = new ObjectPool<GameObject>(
|
|
createFunc: () => {
|
|
GameObject go = Instantiate(particlePrefab);
|
|
go.SetActive(false);
|
|
// 创建时缓存 Renderer,后续 Emit/Move 复用,避免每颗粒子 GetComponent。
|
|
var r = go.GetComponent<Renderer>();
|
|
if (r != null) _rendererCache[go] = r;
|
|
return go;
|
|
},
|
|
actionOnGet: (obj) => {
|
|
obj.SetActive(true);
|
|
// 解决对象池拉线问题:如果球体带拖尾,获取时必须清除旧路径
|
|
var trail = obj.GetComponent<TrailRenderer>();
|
|
if (trail != null) trail.Clear();
|
|
},
|
|
actionOnRelease: (obj) => obj.SetActive(false),
|
|
actionOnDestroy: (obj) => Destroy(obj),
|
|
defaultCapacity: 30
|
|
);
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
// 执行实时音频 analysis(获取音量强度)
|
|
AnalyzeAudio();
|
|
|
|
// 持续发射逻辑
|
|
UpdateContinuousEmit();
|
|
|
|
|
|
|
|
// 起点缩放抖动
|
|
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;
|
|
|
|
// 批量生成
|
|
for (int i = 0; i < emitBatchSize; i++)
|
|
{
|
|
// 引入混乱度:随机延迟发射。用协程替代 Invoke(nameof)——
|
|
// 后者每次做字符串反射+分配;协程延迟无字符串开销。
|
|
if (chaosRandomDelay > 0)
|
|
{
|
|
StartCoroutine(DelayedEmitRoutine(Random.Range(0f, chaosRandomDelay)));
|
|
}
|
|
else
|
|
{
|
|
Emit();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private System.Collections.IEnumerator DelayedEmitRoutine(float delay)
|
|
{
|
|
if (delay > 0f) yield return new WaitForSeconds(delay);
|
|
Emit();
|
|
}
|
|
|
|
// 取粒子缓存的 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<Renderer>();
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 发射一个粒子
|
|
/// </summary>
|
|
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)
|
|
);
|
|
particle.transform.position = startPoint.TransformPoint(finalLocalPos);
|
|
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. 开始移动
|
|
StartCoroutine(MoveRoutine(particle, finalLocalPos, selectedConfig));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 轮询式均衡选取材质,确保每种材质数量大致相同
|
|
/// </summary>
|
|
|
|
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];
|
|
}
|
|
|
|
private System.Collections.IEnumerator MoveRoutine(GameObject particle, Vector3 initialLocalPos, MaterialConfig? config)
|
|
{
|
|
if (startPoint == null || endPoint == null) yield break;
|
|
|
|
// 【关键修复】在协程开始时,缓存起点的世界坐标
|
|
Vector3 spawnWorldPos = startPoint.TransformPoint(initialLocalPos);
|
|
|
|
Vector3 currentStartPos = startPoint.position;
|
|
Vector3 currentEndPos = endPoint.position;
|
|
Vector3 moveDir = (currentEndPos - currentStartPos).normalized;
|
|
float totalDist = Vector3.Distance(currentStartPos, currentEndPos);
|
|
float traveledDist = 0f;
|
|
|
|
particle.transform.position = spawnWorldPos;
|
|
|
|
// 复用共享属性块(不 new,避免每颗粒子 GC)与缓存的 Renderer。
|
|
MaterialPropertyBlock propBlock = _sharedPropBlock;
|
|
Renderer renderer = GetCachedRenderer(particle);
|
|
|
|
while (particle != null && particle.activeInHierarchy)
|
|
{
|
|
// 如果游戏暂停,则等待直到取消暂停
|
|
if (PauseManager.Instance != null && PauseManager.Instance.IsPaused)
|
|
{
|
|
yield return new WaitUntil(() => !PauseManager.Instance.IsPaused);
|
|
}
|
|
|
|
float currentBPM = GetCurrentBPM();
|
|
float noteSpawnerSpeedMultiplier = 1.0f;
|
|
// 粒子速度须与音符下落同速:跟随 NoteSpawner 的“有效 BPM”(开关决定用谱面 bpm 还是固定参考 bpm)。
|
|
if (beatmapManager != null && beatmapManager.noteSpawner != null)
|
|
{
|
|
noteSpawnerSpeedMultiplier = beatmapManager.noteSpawner.EffectiveSpeedMultiplier;
|
|
currentBPM = beatmapManager.noteSpawner.EffectiveBpm;
|
|
}
|
|
|
|
float effectiveBPM = Mathf.Max(currentBPM, 60f);
|
|
float noteSpeed = (totalDist * effectiveBPM * noteSpawnerSpeedMultiplier) / 240f;
|
|
|
|
float step = noteSpeed * speedMultiplier * emissionSpeedMultiplier * perFrameMoveMultiplier * Time.deltaTime;
|
|
if (step <= 0) step = 0.01f;
|
|
|
|
traveledDist += step;
|
|
particle.transform.position += moveDir * step;
|
|
|
|
// 【动态空间渐变色逻辑】
|
|
if (config != null && config.Value.useGradientOverride && renderer != null)
|
|
{
|
|
// 获取渐变的首尾颜色
|
|
Color headColor = config.Value.colorGradient.Evaluate(1f);
|
|
Color tailColor = config.Value.colorGradient.Evaluate(0f);
|
|
|
|
renderer.GetPropertyBlock(propBlock);
|
|
// 用缓存的属性 ID(避免每帧字符串查找)
|
|
propBlock.SetColor(_headColorId, headColor);
|
|
propBlock.SetColor(_tailColorId, tailColor);
|
|
propBlock.SetVector(_moveDirId, moveDir);
|
|
renderer.SetPropertyBlock(propBlock);
|
|
}
|
|
|
|
if (traveledDist >= totalDist)
|
|
{
|
|
if (particle.activeInHierarchy) _pool.Release(particle);
|
|
yield break;
|
|
}
|
|
yield return null;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|