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 = 340.8f;
[Tooltip("滑片起始角度偏移(度);必须与进入步进模式动画结束时滑片停的位置一致,否则接管控制时会跳位")]
[SerializeField] private float startAngleOffset = 259.6f;
[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
}
}