using AibisDream; using UnityEngine; using UnityEngine.Events; using DG.Tweening; namespace AibisDream.MiniGame.HuoShan { /// /// 滑片控制器 - 实现滑片沿圆环滑动 /// 功能: /// - 仅通过Yarn指令控制移动,不接受玩家直接交互 /// - 等离子体发光段:从起始位置到当前位置显示等离子体进度指示 /// - 位置变化通过 onSliderPositionChanged 通知(EmotionWaveManager 等可订阅) /// public class SliderController : MonoBehaviour { [Header("圆环配置")] [Tooltip("圆环中心Transform")] [SerializeField] private Transform ringCenter; [Tooltip("外环半径")] [SerializeField] private float outerRadius = 2f; [Tooltip("内环半径(等离子体发光段)")] [SerializeField] private float innerRadius = 1.5f; [Tooltip("滑片滑动弧度范围(度)")] [SerializeField] private float slideAngleRange = 330f; [Tooltip("滑片起始角度偏移(度);270=正上方起点")] [SerializeField] private float startAngleOffset = 270f; [Header("滑片配置")] [Tooltip("滑片Transform")] [SerializeField] private Transform sliderTransform; [Tooltip("滑片电夹Transform")] [SerializeField] private Transform clampTransform; [Tooltip("离散段数量(用于slide_to_segment等Yarn命令)")] [SerializeField] private int segmentCount = 12; [Tooltip("滑片跟随目标的角度速度(度/秒),匀速移动")] [SerializeField] private float slideSpeed = 360f; [Header("等离子体弧线(Particle System)")] [Tooltip("用于等离子体效果的粒子系统,不赋值则自动创建")] [SerializeField] private ParticleSystem plasmaParticleSystem; [Tooltip("粒子数量下限(短弧时)")] [SerializeField] [Min(2)] private int minParticleCount = 20; [Tooltip("粒子数量上限(满弧时)")] [SerializeField] [Min(2)] private int maxParticleCount = 48; [Tooltip("粒子大小")] [SerializeField] private float particleSize = 0.08f; [Tooltip("粒子材质(Additive 发光,需手动引用)")] [SerializeField] private Material plasmaParticleMaterial; [Tooltip("粒子纹理(圆形中心亮边沿渐隐,不引用则自动生成软圆)")] [SerializeField] private Texture2D plasmaParticleTexture; [Tooltip("流动速度(小球沿弧线移动速度,1=每秒跑完一整圈)")] [SerializeField] private float flowSpeed = 3f; [Tooltip("发光颜色")] [SerializeField] private Color glowColor = new Color(0.3f, 0.9f, 0.55f); [CustomSortingLayer] [Tooltip("粒子渲染 Sorting Layer")] [SerializeField] private int plasmaSortingLayerId; [Tooltip("粒子渲染 Sorting Order")] [SerializeField] private int plasmaSortingOrder = 51; [Header("事件")] [Tooltip("滑片位置改变时触发(参数:标准化位置0-1)")] public UnityEvent onSliderPositionChanged; [Tooltip("滑片到达语义组织阶段时触发(进度 ≥ 1/3)")] public UnityEvent onPhaseSemanticReached; [Tooltip("滑片到达审查阶段时触发(进度 ≥ 2/3)")] public UnityEvent onPhaseReviewReached; [Tooltip("滑片到达风格阶段时触发(进度 ≥ 1)")] public UnityEvent onPhaseStyleReached; [Tooltip("滑片滑动到目标位置完成时触发")] public UnityEvent onSlideComplete; [Header("控制激活")] [Tooltip("等离子体是否激活:OpenView 时设为 true,显示 360° 满弧")] [SerializeField] private bool isPlasmaActive = false; [Tooltip("滑片控制是否激活:进入动画完成后由 SalesManager 调用 SetControlActive(true),才接受 SlideTo 等")] [SerializeField] private bool isControlActive = false; [Header("调试测试")] [Tooltip("勾选后,拖拽下方滑块可直接测试滑片与弧线(Play 模式下生效,优先于程序控制)")] [SerializeField] private bool useTestSlider; [Tooltip("测试用滑块:0=起始位置,1=终点位置")] [SerializeField] [Range(0f, 1f)] private float testSliderPosition; private float _currentAngle = 0f; private float _targetAngle = 0f; private bool _phaseSemanticFired; private bool _phaseReviewFired; private bool _phaseStyleFired; private Tweener _moveTween; private Vector3 _lastValidPosition; private ParticleSystem.Particle[] _particles; private float[] _particlePhases; private Material _plasmaMaterialInstance; private Texture2D _generatedSoftCircleTexture; #region 生命周期 private void Awake() { if (sliderTransform == null) { sliderTransform = transform; } // 初始化等离子体粒子系统 EnsurePlasmaParticleSystem(); } private void Update() { if (isControlActive) UpdateSliderPosition(); if (isPlasmaActive) UpdatePlasmaGlow(isControlActive ? null : (float?)1f); // 未接受控制时固定 360° 满弧 else if (plasmaParticleSystem != null && _particles != null && _particles.Length > 0) plasmaParticleSystem.SetParticles(_particles, 0); // 未激活时清空,避免 OpenView 前乱点 } private void OnDestroy() { if (_plasmaMaterialInstance != null) Destroy(_plasmaMaterialInstance); if (_generatedSoftCircleTexture != null) Destroy(_generatedSoftCircleTexture); } #endregion #region 位置更新 /// /// 根据角度应用滑片位置与旋转(顺时针移动:角度取负) /// /// 滑片世界坐标 private Vector3 ApplyAngleToTransform(float angle) { if (ringCenter == null || sliderTransform == null) return _lastValidPosition; // 顺时针:标准数学角度增加为逆时针,取负则为顺时针 float radian = -(angle + startAngleOffset) * Mathf.Deg2Rad; Vector3 position = ringCenter.position + new Vector3( Mathf.Cos(radian) * outerRadius, Mathf.Sin(radian) * outerRadius, sliderTransform.position.z ); sliderTransform.position = position; // 滑片沿轨道顺时针方向(切线方向) Vector3 direction = position - ringCenter.position; float radialAngle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg; float tangentAngle = radialAngle - 90f; // 顺时针切线 sliderTransform.rotation = Quaternion.Euler(0f, 0f, tangentAngle); return position; } private void UpdateSliderPosition() { #if UNITY_EDITOR // 调试模式:Inspector 滑块直接驱动滑片,勾选后始终优先(覆盖程序控制) if (useTestSlider && Application.isPlaying) { if (_moveTween != null && _moveTween.IsActive()) { _moveTween.Kill(); _moveTween = null; } _targetAngle = testSliderPosition * slideAngleRange; } #endif // 匀速移动到目标角度(避免 Lerp 导致的越接近越慢、卡顿感) float maxDelta = slideSpeed * Time.deltaTime; _currentAngle = Mathf.MoveTowards(_currentAngle, _targetAngle, maxDelta); if (ringCenter == null || sliderTransform == null) return; // 计算滑片在世界空间的位置(顺时针:角度取负) Vector3 position = ApplyAngleToTransform(_currentAngle); // 夹子不跟随滑片移动,保持原位 // 触发位置改变事件 float normalizedPosition = _currentAngle / slideAngleRange; if (Vector3.Distance(position, _lastValidPosition) > 0.01f) { onSliderPositionChanged?.Invoke(normalizedPosition); _lastValidPosition = position; // 阶段阈值事件:1/3 语义组织, 2/3 审查, 1 风格 const float t1 = 1f / 3f; const float t2 = 2f / 3f; if (normalizedPosition >= t1 && !_phaseSemanticFired) { _phaseSemanticFired = true; onPhaseSemanticReached?.Invoke(); } if (normalizedPosition >= t2 && !_phaseReviewFired) { _phaseReviewFired = true; onPhaseReviewReached?.Invoke(); } if (normalizedPosition >= 1f - 0.001f && !_phaseStyleFired) { _phaseStyleFired = true; onPhaseStyleReached?.Invoke(); } } } #endregion #region 等离子体弧线 private void EnsurePlasmaParticleSystem() { if (plasmaParticleSystem != null) { ConfigureParticleSystem(plasmaParticleSystem); return; } // 自动创建粒子系统 var go = new GameObject("PlasmaParticles"); go.transform.SetParent(transform); go.transform.localPosition = Vector3.zero; go.transform.localScale = Vector3.one; plasmaParticleSystem = go.AddComponent(); ConfigureParticleSystem(plasmaParticleSystem); var renderer = go.GetComponent(); if (renderer != null) renderer.renderMode = ParticleSystemRenderMode.Billboard; } private void ConfigureParticleSystem(ParticleSystem ps) { var main = ps.main; main.simulationSpace = ParticleSystemSimulationSpace.World; main.loop = false; main.startLifetime = 999f; main.startSize = particleSize; main.maxParticles = maxParticleCount; main.playOnAwake = false; main.startSpeed = 0f; main.startColor = glowColor; var emission = ps.emission; emission.enabled = false; var velocity = ps.velocityOverLifetime; velocity.enabled = false; var colorOverLifetime = ps.colorOverLifetime; colorOverLifetime.enabled = false; var sizeOverLifetime = ps.sizeOverLifetime; sizeOverLifetime.enabled = false; var renderer = ps.GetComponent(); if (renderer != null) { ApplyPlasmaMaterial(renderer); int layerId = SortingLayer.IsValid(plasmaSortingLayerId) ? plasmaSortingLayerId : SortingLayer.layers[0].id; renderer.sortingLayerID = layerId; renderer.sortingOrder = plasmaSortingOrder; } // 初始化粒子数组 _particles = new ParticleSystem.Particle[maxParticleCount]; _particlePhases = new float[maxParticleCount]; for (int i = 0; i < maxParticleCount; i++) { _particlePhases[i] = (float)i / maxParticleCount; } // 不在此处 Emit,避免 OpenView 前出现乱点;首次 isPlasmaActive 时由 UpdatePlasmaGlow 再 Emit } private void ApplyPlasmaMaterial(ParticleSystemRenderer renderer) { var tex = plasmaParticleTexture != null ? plasmaParticleTexture : GetOrCreateSoftCircleTexture(); if (tex == null) return; Material mat = plasmaParticleMaterial != null ? plasmaParticleMaterial : null; if (mat == null) { var shader = Shader.Find("Legacy Shaders/Particles/Additive") ?? Shader.Find("Particles/Additive"); if (shader == null) return; mat = new Material(shader); mat.SetColor("_TintColor", Color.white); } else { mat = new Material(plasmaParticleMaterial); } mat.mainTexture = tex; _plasmaMaterialInstance = mat; renderer.material = mat; } private Texture2D GetOrCreateSoftCircleTexture() { if (_generatedSoftCircleTexture != null) return _generatedSoftCircleTexture; const int size = 64; _generatedSoftCircleTexture = new Texture2D(size, size); _generatedSoftCircleTexture.wrapMode = TextureWrapMode.Clamp; _generatedSoftCircleTexture.filterMode = FilterMode.Bilinear; var pixels = new Color[size * size]; float center = (size - 1) * 0.5f; float radius = center; for (int y = 0; y < size; y++) for (int x = 0; x < size; x++) { float dx = x - center; float dy = y - center; float dist = Mathf.Sqrt(dx * dx + dy * dy); float t = 1f - Mathf.Clamp01(dist / radius); float alpha = t * t; pixels[y * size + x] = new Color(1f, 1f, 1f, alpha); } _generatedSoftCircleTexture.SetPixels(pixels); _generatedSoftCircleTexture.Apply(); return _generatedSoftCircleTexture; } /// 若不为 null,则用此值作为进度(如 OpenView 时固定 360° 满弧=1) private void UpdatePlasmaGlow(float? progressOverride = null) { if (plasmaParticleSystem == null || ringCenter == null || _particles == null) return; float progress = progressOverride ?? Mathf.Clamp01(_currentAngle / slideAngleRange); // 进度为 0 时隐藏粒子 if (progress <= 0.001f) { plasmaParticleSystem.SetParticles(_particles, 0); return; } // 弧线跨度由 progress 决定(360° 时 progress=1) float effectiveAngle = progress * slideAngleRange; float startRad = -startAngleOffset * Mathf.Deg2Rad; float endRad = -(startAngleOffset + effectiveAngle) * Mathf.Deg2Rad; float arcSpan = endRad - startRad; float z = sliderTransform != null ? sliderTransform.position.z : ringCenter.position.z; Vector3 center = ringCenter.position; int aliveCount = plasmaParticleSystem.GetParticles(_particles); if (aliveCount == 0) { plasmaParticleSystem.Play(); plasmaParticleSystem.Emit(maxParticleCount); aliveCount = plasmaParticleSystem.GetParticles(_particles); if (aliveCount == 0) return; } // 随进度在 min~max 间平滑过渡粒子数量 float countF = Mathf.Lerp(minParticleCount, maxParticleCount, progress); int actualCount = Mathf.Clamp(Mathf.RoundToInt(countF), minParticleCount, maxParticleCount); for (int i = 0; i < maxParticleCount; i++) { // 相位沿弧线流动 _particlePhases[i] += flowSpeed * Time.deltaTime; if (_particlePhases[i] >= 1f) _particlePhases[i] -= 1f; if (_particlePhases[i] < 0f) _particlePhases[i] += 1f; float t = _particlePhases[i]; float angle = startRad + arcSpan * t; Vector3 pos = center + new Vector3(Mathf.Cos(angle) * innerRadius, Mathf.Sin(angle) * innerRadius, z); _particles[i].position = pos; _particles[i].remainingLifetime = 999f; _particles[i].startLifetime = 999f; _particles[i].startSize = particleSize; _particles[i].startColor = Color.Lerp( new Color(glowColor.r, glowColor.g, glowColor.b, 0.5f), glowColor, t ); } plasmaParticleSystem.SetParticles(_particles, actualCount); } #endregion #region Gizmo 与编辑器 private void OnDrawGizmosSelected() { var center = ringCenter != null ? ringCenter.position : transform.position; float z = sliderTransform != null ? sliderTransform.position.z : center.z; // 外环 Gizmos.color = new Color(0.2f, 0.8f, 1f, 0.6f); DrawCircleGizmo(center, outerRadius, z, 64); // 内环(等离子体弧线) Gizmos.color = new Color(0.3f, 0.9f, 0.55f, 0.5f); DrawCircleGizmo(center, innerRadius, z, 64); // 起始位置标记(绿色) float startRad = -(0f + startAngleOffset) * Mathf.Deg2Rad; Vector3 startPos = center + new Vector3(Mathf.Cos(startRad) * outerRadius, Mathf.Sin(startRad) * outerRadius, 0f); Gizmos.color = Color.green; Gizmos.DrawWireSphere(startPos, outerRadius * 0.05f); // 终点位置标记(黄色) float endRad = -(slideAngleRange + startAngleOffset) * Mathf.Deg2Rad; Vector3 endPos = center + new Vector3(Mathf.Cos(endRad) * outerRadius, Mathf.Sin(endRad) * outerRadius, 0f); Gizmos.color = Color.yellow; Gizmos.DrawWireSphere(endPos, outerRadius * 0.05f); // 段分割线 Gizmos.color = new Color(1f, 1f, 0.5f, 0.3f); float segAngle = slideAngleRange / Mathf.Max(1, segmentCount); for (int i = 1; i < segmentCount; i++) { float rad = -(i * segAngle + startAngleOffset) * Mathf.Deg2Rad; Vector3 segPos = center + new Vector3(Mathf.Cos(rad) * outerRadius, Mathf.Sin(rad) * outerRadius, 0f); Gizmos.DrawLine(center, segPos); } // 当前滑片位置(红色,仅 Play 模式有值) #if UNITY_EDITOR if (Application.isPlaying) #endif { float curRad = -(_currentAngle + startAngleOffset) * Mathf.Deg2Rad; Vector3 curPos = center + new Vector3(Mathf.Cos(curRad) * outerRadius, Mathf.Sin(curRad) * outerRadius, 0f); Gizmos.color = Color.red; Gizmos.DrawWireSphere(curPos, outerRadius * 0.08f); } } private void DrawCircleGizmo(Vector3 center, float radius, float z, int segments) { Vector3 prev = center + new Vector3(radius, 0f, 0f); for (int i = 1; i <= segments; i++) { float rad = (float)i / segments * Mathf.PI * 2f; Vector3 next = center + new Vector3(Mathf.Cos(rad) * radius, Mathf.Sin(rad) * radius, 0f); Gizmos.DrawLine(prev, next); prev = next; } } /// /// 在编辑器中一键将滑片重置到初始状态 /// [ContextMenu("重置到初始状态")] private void ResetToInitialStateInEditor() { _targetAngle = 0f; _currentAngle = 0f; ResetPhaseFlags(); if (_moveTween != null && _moveTween.IsActive()) { _moveTween.Kill(); _moveTween = null; } if (ringCenter != null && sliderTransform != null) ApplyAngleToTransform(0f); #if UNITY_EDITOR UnityEditor.EditorUtility.SetDirty(this); if (gameObject.scene.IsValid()) UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(gameObject.scene); #endif } #endregion #region 公共方法 /// /// 设置等离子体是否激活。OpenView 时调用 SetPlasmaActive(true),显示 360° 满弧。 /// public void SetPlasmaActive(bool active) { isPlasmaActive = active; } /// /// 设置滑片控制是否激活。进入动画完成后由 SalesManager 调用 SetControlActive(true), /// 此后才接受 SlideTo/SlideToSegment 等控制。 /// public void SetControlActive(bool active) { isControlActive = active; } /// /// 控制是否已激活 /// public bool IsControlActive => isControlActive; /// /// 移动到指定角度 /// public void SlideTo(float angle, float duration = 0.5f) { if (!isControlActive) return; angle = Mathf.Clamp(angle, 0f, slideAngleRange); if (_moveTween != null) { _moveTween.Kill(); } _moveTween = DOTween.To( () => _targetAngle, value => _targetAngle = value, angle, duration ).SetEase(Ease.Linear).OnComplete(() => onSlideComplete?.Invoke()); } /// /// 移动到指定位置(0-1标准化) /// public void SlideToNormalized(float normalizedPosition, float duration = 0.5f) { float angle = normalizedPosition * slideAngleRange; SlideTo(angle, duration); } /// /// 移动到指定段 /// public void SlideToSegment(int segmentIndex, float duration = 0.5f) { if (segmentIndex < 0 || segmentIndex > segmentCount) return; float segmentAngle = slideAngleRange / segmentCount * segmentIndex; SlideTo(segmentAngle, duration); } /// /// 获取当前角度 /// public float GetCurrentAngle() { return _currentAngle; } /// /// 获取标准化位置(0-1) /// public float GetNormalizedPosition() { return _currentAngle / slideAngleRange; } /// /// 重置滑片位置 /// public void ResetPosition() { if (_moveTween != null) { _moveTween.Kill(); _moveTween = null; } _targetAngle = 0f; _currentAngle = 0f; ResetPhaseFlags(); } /// /// 重置阶段标志(下次到达阈值时会再次触发事件) /// public void ResetPhaseFlags() { _phaseSemanticFired = false; _phaseReviewFired = false; _phaseStyleFired = false; } /// /// 设置离散段数量 /// public void SetSegmentCount(int count) { segmentCount = Mathf.Max(1, count); } /// /// 获取当前段索引 /// public int GetCurrentSegment() { float segmentAngle = slideAngleRange / segmentCount; return Mathf.FloorToInt(_currentAngle / segmentAngle); } #endregion } }