Files
2026-07-06 20:19:52 +08:00

342 lines
14 KiB
C#
Raw Permalink 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 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);
}
}
}
}