Files
aibis-dream/Assets/Scripts/MiniGame/HuoShan/SalesSystem/SliderController.cs
T

632 lines
23 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using AibisDream;
using UnityEngine;
using UnityEngine.Events;
using DG.Tweening;
namespace AibisDream.MiniGame.HuoShan
{
/// <summary>
/// 滑片控制器 - 实现滑片沿圆环滑动
/// 功能:
/// - 仅通过Yarn指令控制移动,不接受玩家直接交互
/// - 等离子体发光段:从起始位置到当前位置显示等离子体进度指示
/// - 位置变化通过 onSliderPositionChanged 通知(EmotionWaveManager 等可订阅)
/// </summary>
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<float> 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 位置更新
/// <summary>
/// 根据角度应用滑片位置与旋转(顺时针移动:角度取负)
/// </summary>
/// <returns>滑片世界坐标</returns>
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<ParticleSystem>();
ConfigureParticleSystem(plasmaParticleSystem);
var renderer = go.GetComponent<ParticleSystemRenderer>();
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<ParticleSystemRenderer>();
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;
}
/// <param name="progressOverride">若不为 null,则用此值作为进度(如 OpenView 时固定 360° 满弧=1</param>
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;
}
}
/// <summary>
/// 在编辑器中一键将滑片重置到初始状态
/// </summary>
[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 公共方法
/// <summary>
/// 设置等离子体是否激活。OpenView 时调用 SetPlasmaActive(true),显示 360° 满弧。
/// </summary>
public void SetPlasmaActive(bool active)
{
isPlasmaActive = active;
}
/// <summary>
/// 设置滑片控制是否激活。进入动画完成后由 SalesManager 调用 SetControlActive(true)
/// 此后才接受 SlideTo/SlideToSegment 等控制。
/// </summary>
public void SetControlActive(bool active)
{
isControlActive = active;
}
/// <summary>
/// 控制是否已激活
/// </summary>
public bool IsControlActive => isControlActive;
/// <summary>
/// 移动到指定角度
/// </summary>
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());
}
/// <summary>
/// 移动到指定位置(0-1标准化)
/// </summary>
public void SlideToNormalized(float normalizedPosition, float duration = 0.5f)
{
float angle = normalizedPosition * slideAngleRange;
SlideTo(angle, duration);
}
/// <summary>
/// 移动到指定段
/// </summary>
public void SlideToSegment(int segmentIndex, float duration = 0.5f)
{
if (segmentIndex < 0 || segmentIndex > segmentCount) return;
float segmentAngle = slideAngleRange / segmentCount * segmentIndex;
SlideTo(segmentAngle, duration);
}
/// <summary>
/// 获取当前角度
/// </summary>
public float GetCurrentAngle()
{
return _currentAngle;
}
/// <summary>
/// 获取标准化位置(0-1
/// </summary>
public float GetNormalizedPosition()
{
return _currentAngle / slideAngleRange;
}
/// <summary>
/// 重置滑片位置
/// </summary>
public void ResetPosition()
{
if (_moveTween != null)
{
_moveTween.Kill();
_moveTween = null;
}
_targetAngle = 0f;
_currentAngle = 0f;
ResetPhaseFlags();
}
/// <summary>
/// 重置阶段标志(下次到达阈值时会再次触发事件)
/// </summary>
public void ResetPhaseFlags()
{
_phaseSemanticFired = false;
_phaseReviewFired = false;
_phaseStyleFired = false;
}
/// <summary>
/// 设置离散段数量
/// </summary>
public void SetSegmentCount(int count)
{
segmentCount = Mathf.Max(1, count);
}
/// <summary>
/// 获取当前段索引
/// </summary>
public int GetCurrentSegment()
{
float segmentAngle = slideAngleRange / segmentCount;
return Mathf.FloorToInt(_currentAngle / segmentAngle);
}
#endregion
}
}