342 lines
14 KiB
C#
342 lines
14 KiB
C#
using System;
|
||
using UnityEngine;
|
||
using GameFramework;
|
||
|
||
namespace IndianOceanAssets.Engine2_5D
|
||
{
|
||
/// <summary>
|
||
/// 回声系统 v2 —— 描边波纹版。
|
||
/// 按 E 键从玩家位置释放扩散波纹,波纹扫过的精灵(非光源物体)
|
||
/// 会显示外描边(默认白色,可自定义),维持一段时间后整体消散。
|
||
///
|
||
/// 与 v1 的区别:
|
||
/// - 不再在黑暗遮罩上画光波(那样会漏出地板)
|
||
/// - 改为通过全局 shader 参数控制精灵描边 shader (SpriteEchoOutline)
|
||
/// - 时间模型:扩散 → 维持(sustainTime) → 消散(fadeTime)
|
||
///
|
||
/// 挂载位置:玩家物体上(或跟随玩家的空物体)
|
||
/// 依赖:场景中需要有用 SpriteEchoOutline shader 的描边精灵
|
||
/// (由 EchoOutlineManager 自动生成)
|
||
/// </summary>
|
||
public class EchoSystem : MonoBehaviour
|
||
{
|
||
/// <summary>
|
||
/// 按 E 释放回声(摇铃)时触发,参数为释放时的玩家世界位置。
|
||
/// 敌人聆听系统等可订阅此事件。
|
||
/// </summary>
|
||
public static event Action<Vector3> OnEchoReleased;
|
||
|
||
[Header("按键")]
|
||
[SerializeField] private KeyCode echoKey = KeyCode.E;
|
||
|
||
[Header("扩散参数")]
|
||
[Tooltip("波纹扩散速度(世界单位/秒)")]
|
||
[SerializeField] private float expandSpeed = 15f;
|
||
|
||
[Tooltip("最大扩散半径(到达后进入维持阶段)")]
|
||
[SerializeField] private float maxRadius = 30f;
|
||
|
||
[Tooltip("波纹前沿柔和宽度(精灵被扫到时的过渡宽度,越大越柔)")]
|
||
[SerializeField] private float ringWidth = 1.5f;
|
||
|
||
[Header("时间模型")]
|
||
[Tooltip("扩散到最大半径后,描边维持的时间(秒)")]
|
||
[SerializeField] private float sustainTime = 4f;
|
||
|
||
[Tooltip("维持结束后,描边消散的时间(秒)")]
|
||
[SerializeField] private float fadeTime = 1.5f;
|
||
|
||
[Header("描边外观")]
|
||
[Tooltip("描边颜色(默认白色,可自定义)")]
|
||
[SerializeField] private Color outlineColor = Color.white;
|
||
|
||
[Header("红圈视觉")]
|
||
[Tooltip("红圈颜色")]
|
||
[SerializeField] private Color ringColor = new Color(1f, 0.15f, 0.1f, 1f);
|
||
|
||
[Tooltip("红圈宽度(世界单位)")]
|
||
[SerializeField] private float ringVisualWidth = 2.5f;
|
||
|
||
[Tooltip("红圈离地高度(避免Z-fighting)")]
|
||
[SerializeField] private float ringYOffset = 0.1f;
|
||
|
||
[Tooltip("红圈渐隐总时间(秒)。从释放瞬间开始线性渐隐,到 0 后隐藏。默认 4 秒")]
|
||
[SerializeField] private float ringFadeTime = 4f;
|
||
|
||
[Tooltip("红圈扩散速度(世界单位/秒),独立于波纹扩散速度")]
|
||
[SerializeField] private float ringExpandSpeed = 15f;
|
||
|
||
[Header("Shader 引用")]
|
||
[Tooltip("直接拖入 Assets/Light/shaders/EchoRing.shader。\n"
|
||
+ "用序列化字段引用而不是 Shader.Find,确保打包时构建管线能静态识别,"
|
||
+ "不会被 shader stripping 剔除(否则运行时会 fallback 成粉色错误着色器)。\n"
|
||
+ "留空时仍会尝试 Shader.Find 兜底,但需要把该 shader 加入 "
|
||
+ "GraphicsSettings 的 Always Included Shaders 列表。")]
|
||
[SerializeField] private Shader ringShader;
|
||
|
||
[Header("冷却")]
|
||
[Tooltip("回声冷却时间(秒)")]
|
||
[SerializeField] private float cooldown = 3f;
|
||
|
||
// 红圈视觉
|
||
private GameObject _ringVisual;
|
||
private Material _ringMaterial;
|
||
|
||
// 全局 shader 属性 ID(通过 Shader.SetGlobalX 设置,所有描边精灵共享)
|
||
private static readonly int EchoCenterID = Shader.PropertyToID("_EchoCenter");
|
||
private static readonly int EchoRadiusID = Shader.PropertyToID("_EchoRadius");
|
||
private static readonly int EchoWidthID = Shader.PropertyToID("_EchoWidth");
|
||
private static readonly int EchoOutlineIntensityID = Shader.PropertyToID("_EchoOutlineIntensity");
|
||
private static readonly int EchoOutlineColorID = Shader.PropertyToID("_EchoOutlineColor");
|
||
private static readonly int EchoRingAlphaID = Shader.PropertyToID("_EchoRingAlpha");
|
||
private static readonly int EchoRingRadiusID = Shader.PropertyToID("_EchoRingRadius");
|
||
|
||
private enum State { Idle, Expanding, Sustaining, Fading }
|
||
|
||
private State _state = State.Idle;
|
||
private float _radius;
|
||
private float _intensity;
|
||
private float _stateTimer;
|
||
private Vector2 _center;
|
||
private float _lastEchoTime = -999f;
|
||
private float _ringAlpha = 0f;
|
||
private float _ringRadius = 0f;
|
||
|
||
private void Start()
|
||
{
|
||
// 初始化:描边关闭
|
||
Shader.SetGlobalFloat(EchoOutlineIntensityID, 0f);
|
||
Shader.SetGlobalColor(EchoOutlineColorID, outlineColor);
|
||
Shader.SetGlobalFloat(EchoWidthID, ringWidth);
|
||
Shader.SetGlobalFloat(EchoRingAlphaID, 0f);
|
||
|
||
CreateRingVisual();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建红圈视觉:一个平铺在地面上的 Quad,使用 EchoRing shader。
|
||
/// shader 通过全局 _EchoCenter / _EchoRadius 自动计算圆环位置,
|
||
/// 无需每帧更新 Mesh —— 只需在释放时移动 Quad 到回声中心即可。
|
||
/// </summary>
|
||
private void CreateRingVisual()
|
||
{
|
||
_ringVisual = GameObject.CreatePrimitive(PrimitiveType.Quad);
|
||
_ringVisual.name = "EchoRing_Visual";
|
||
|
||
// 移除不需要的碰撞体
|
||
var col = _ringVisual.GetComponent<Collider>();
|
||
if (col != null) Destroy(col);
|
||
|
||
// 平铺在地面:绕 X 轴旋转 90°
|
||
_ringVisual.transform.rotation = Quaternion.Euler(90f, 0f, 0f);
|
||
|
||
// 缩放:覆盖 maxRadius * 2 的范围
|
||
float size = maxRadius * 2f;
|
||
_ringVisual.transform.localScale = new Vector3(size, size, 1f);
|
||
// 不挂载到玩家下——回声中心固定,不跟随玩家移动
|
||
|
||
// 创建材质(运行时)
|
||
// 优先使用序列化字段引用的 shader(构建管线可静态识别,不会被剔除);
|
||
// 字段为空时再尝试 Shader.Find 兜底(必须同时把 shader 加入 GraphicsSettings
|
||
// 的 Always Included Shaders,否则打包后 Find 会返回 null → 粉色错误着色器)。
|
||
var shader = ringShader != null ? ringShader : Shader.Find("IndianOcean/EchoRing");
|
||
if (shader != null)
|
||
{
|
||
_ringMaterial = new Material(shader);
|
||
_ringMaterial.SetColor("_RingColor", ringColor);
|
||
_ringMaterial.SetFloat("_RingWidth", ringVisualWidth);
|
||
// 确保渲染在黑暗遮罩之上(黑暗遮罩 sortingOrder=9999, renderQueue=3100)
|
||
_ringMaterial.renderQueue = 3250;
|
||
var renderer = _ringVisual.GetComponent<MeshRenderer>();
|
||
renderer.sortingOrder = 10001;
|
||
renderer.material = _ringMaterial;
|
||
}
|
||
else
|
||
{
|
||
Debug.LogError(
|
||
"[EchoSystem] 找不到 IndianOcean/EchoRing shader!红圈将不会渲染。\n"
|
||
+ "修复方法(二选一):\n"
|
||
+ " 1) 把 Assets/Light/shaders/EchoRing.shader 拖到本组件的 Ring Shader 字段;\n"
|
||
+ " 2) Edit → Project Settings → Graphics → Always Included Shaders "
|
||
+ "里添加该 shader,然后重新打包。");
|
||
}
|
||
|
||
_ringVisual.SetActive(false);
|
||
}
|
||
|
||
private void Update()
|
||
{
|
||
// 触发:CD 好即可释放,无需等状态机结束(StartEcho 会重置所有状态)
|
||
if (Input.GetKeyDown(echoKey) && Time.time > _lastEchoTime + cooldown)
|
||
{
|
||
StartEcho();
|
||
}
|
||
|
||
// 状态机
|
||
switch (_state)
|
||
{
|
||
case State.Expanding: UpdateExpanding(); break;
|
||
case State.Sustaining: UpdateSustaining(); break;
|
||
case State.Fading: UpdateFading(); break;
|
||
}
|
||
|
||
// 每帧推送全局参数
|
||
Shader.SetGlobalVector(EchoCenterID, new Vector4(_center.x, _center.y, 0f, 0f));
|
||
Shader.SetGlobalFloat(EchoRadiusID, _radius);
|
||
Shader.SetGlobalFloat(EchoWidthID, ringWidth);
|
||
Shader.SetGlobalFloat(EchoOutlineIntensityID, _intensity);
|
||
Shader.SetGlobalColor(EchoOutlineColorID, outlineColor);
|
||
|
||
// 红圈独立渐隐与扩散
|
||
UpdateRingAlpha();
|
||
UpdateRingRadius();
|
||
Shader.SetGlobalFloat(EchoRingAlphaID, _ringAlpha);
|
||
Shader.SetGlobalFloat(EchoRingRadiusID, _ringRadius);
|
||
}
|
||
|
||
private void StartEcho()
|
||
{
|
||
_state = State.Expanding;
|
||
_radius = 0f;
|
||
_ringRadius = 0f;
|
||
_intensity = 1f;
|
||
_stateTimer = 0f;
|
||
_lastEchoTime = Time.time;
|
||
|
||
// 播放摇铃音效
|
||
if (AudioManager.Instance != null)
|
||
AudioManager.Instance.PlaySFXFromResources("SFX/Bell", 0.9f);
|
||
|
||
// 记录释放时刻的玩家位置(回声中心固定,不跟随移动)
|
||
Vector3 p = transform.position;
|
||
_center = new Vector2(p.x, p.z);
|
||
|
||
// 显示红圈:移动到回声中心位置
|
||
if (_ringVisual != null)
|
||
{
|
||
_ringVisual.transform.position = new Vector3(p.x, ringYOffset, p.z);
|
||
_ringVisual.SetActive(true);
|
||
}
|
||
|
||
// 通知订阅者(敌人聆听等)
|
||
OnEchoReleased?.Invoke(p);
|
||
}
|
||
|
||
private void UpdateExpanding()
|
||
{
|
||
_radius += expandSpeed * Time.deltaTime;
|
||
if (_radius >= maxRadius)
|
||
{
|
||
_radius = maxRadius;
|
||
_state = State.Sustaining;
|
||
_stateTimer = 0f;
|
||
}
|
||
}
|
||
|
||
private void UpdateSustaining()
|
||
{
|
||
_stateTimer += Time.deltaTime;
|
||
_intensity = 1f; // 维持阶段满强度
|
||
if (_stateTimer >= sustainTime)
|
||
{
|
||
_state = State.Fading;
|
||
_stateTimer = 0f;
|
||
}
|
||
}
|
||
|
||
private void UpdateFading()
|
||
{
|
||
_stateTimer += Time.deltaTime;
|
||
// 消散阶段强度从 1 → 0
|
||
_intensity = Mathf.Clamp01(1f - _stateTimer / Mathf.Max(0.0001f, fadeTime));
|
||
if (_stateTimer >= fadeTime)
|
||
{
|
||
_state = State.Idle;
|
||
_intensity = 0f;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 红圈独立渐隐逻辑:
|
||
/// 从释放瞬间开始线性渐隐(1 → 0),ringFadeTime 秒后完全消失。
|
||
/// </summary>
|
||
private void UpdateRingAlpha()
|
||
{
|
||
if (_state == State.Idle)
|
||
{
|
||
_ringAlpha = 0f;
|
||
return;
|
||
}
|
||
|
||
float elapsed = Time.time - _lastEchoTime;
|
||
_ringAlpha = Mathf.Clamp01(1f - elapsed / Mathf.Max(0.001f, ringFadeTime));
|
||
|
||
// 渐隐完成后隐藏红圈物体(节省渲染开销)
|
||
if (_ringAlpha <= 0f && _ringVisual != null)
|
||
_ringVisual.SetActive(false);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 红圈独立扩散:按 ringExpandSpeed 扩散,封顶 maxRadius。
|
||
/// </summary>
|
||
private void UpdateRingRadius()
|
||
{
|
||
if (_state == State.Idle)
|
||
{
|
||
_ringRadius = 0f;
|
||
return;
|
||
}
|
||
|
||
_ringRadius += ringExpandSpeed * Time.deltaTime;
|
||
if (_ringRadius > maxRadius)
|
||
_ringRadius = maxRadius;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 外部调用:强制停止回声
|
||
/// </summary>
|
||
public void StopEcho()
|
||
{
|
||
_state = State.Idle;
|
||
_intensity = 0f;
|
||
_ringAlpha = 0f;
|
||
_ringRadius = 0f;
|
||
Shader.SetGlobalFloat(EchoOutlineIntensityID, 0f);
|
||
Shader.SetGlobalFloat(EchoRingAlphaID, 0f);
|
||
if (_ringVisual != null)
|
||
_ringVisual.SetActive(false);
|
||
}
|
||
|
||
public bool IsActive => _state != State.Idle;
|
||
|
||
// ===== 编辑器可视化 =====
|
||
private void OnDrawGizmosSelected()
|
||
{
|
||
// 绘制回声最大范围(青色线框)
|
||
Gizmos.color = new Color(0f, 1f, 1f, 0.3f);
|
||
Gizmos.DrawWireSphere(transform.position, maxRadius);
|
||
|
||
// 绘制当前波纹半径(如果在扩散中)
|
||
if (_state != State.Idle && _radius > 0)
|
||
{
|
||
Gizmos.color = new Color(1f, 1f, 0f, 0.5f);
|
||
Gizmos.DrawWireSphere(new Vector3(_center.x, transform.position.y, _center.y), _radius);
|
||
|
||
// 绘制波纹中心
|
||
Gizmos.color = Color.yellow;
|
||
Gizmos.DrawWireSphere(new Vector3(_center.x, transform.position.y, _center.y), 0.3f);
|
||
}
|
||
|
||
// 绘制冷却时间指示
|
||
if (Time.time < _lastEchoTime + cooldown)
|
||
{
|
||
float remaining = (_lastEchoTime + cooldown) - Time.time;
|
||
float cooldownRatio = remaining / cooldown;
|
||
|
||
Gizmos.color = new Color(1f, 0f, 0f, 0.3f);
|
||
Gizmos.DrawWireSphere(transform.position, maxRadius * cooldownRatio);
|
||
}
|
||
}
|
||
}
|
||
}
|