修改位置

This commit is contained in:
JA
2026-06-27 19:07:00 +08:00
parent 241ff83a70
commit bb46fdb81f
10 changed files with 45 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
using UnityEngine;
namespace IndianOceanAssets.Engine2_5D
{
/// <summary>
/// 回声描边颜色覆盖 —— 挂到精灵上(或其父级),让该精灵的回声描边
/// 使用自定义颜色/宽度,而非全局默认值。
///
/// 为什么需要它:
/// EchoOutlineManager 在运行时自动创建描边子物体EchoOutline_xxx
/// 这些子物体在编辑器里不存在、无法直接修改。把此组件挂到【父精灵】上,
/// 运行时代码会读取并应用。
///
/// 用法:
/// - 想让某个精灵描边变红挂上此组件outlineColor 设为红色
/// - 想让某个精灵描边更粗outlineWidth 设为 4
/// - 想让某个精灵完全不描边(即使不是光源):勾选 disableOutline
/// - 挂到父物体上可一次性影响所有子精灵
/// </summary>
public class EchoOutlineColorOverride : MonoBehaviour
{
[Tooltip("勾选后此精灵不生成描边(即使不是光源物体)")]
public bool disableOutline = false;
[Tooltip("此精灵的描边颜色(与全局回声颜色相乘;全局默认白色,所以这里设什么就是什么色)")]
public Color outlineColor = Color.white;
[Tooltip("此精灵的描边宽度(-1 = 用材质默认值≥0 = 自定义像素宽度)")]
public float outlineWidth = -1f;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: Xikftnz+AnvxcaAFnpMk8pUJC/3IJIm75WvNeiPxgeM45oPoxYTIjyE=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,185 @@
using UnityEngine;
namespace IndianOceanAssets.Engine2_5D
{
/// <summary>
/// 描边管理器 —— 自动为场景中所有 SpriteRenderer排除挂载 LightSource 的物体)
/// 创建一个使用 SpriteEchoOutline shader 的子物体,用于回声描边效果。
///
/// 为什么需要子物体:
/// - 精灵本体用原始材质(如 SpriteShader.shadergraph渲染在黑暗遮罩之下 → 黑暗中不可见
/// - 描边用 SpriteEchoOutline 材质渲染队列在黑暗遮罩之上Transparent+200→ 黑暗中可见
/// - 同一个 SpriteRenderer 只能有一个材质,所以用子物体承载描边材质
///
/// 使用方式:
/// 1. 场景中创建空物体,挂上此脚本
/// 2. 把使用 SpriteEchoOutline shader 的材质拖到 outlineMaterial 字段
/// 3. 运行后自动为每个非光源精灵生成描边子物体
/// 4. 场景中新增精灵后可右键组件 > Refresh Outlines 重新扫描
/// </summary>
public class EchoOutlineManager : MonoBehaviour
{
[Header("描边材质(使用 SpriteEchoOutline shader")]
[SerializeField] private Material outlineMaterial;
[Header("过滤")]
[Tooltip("只处理这些层的精灵(默认 Everything")]
[SerializeField] private LayerMask includeLayers = ~0;
[Tooltip("勾选后,挂载 LightSource 的物体(含父级)不生成描边")]
[SerializeField] private bool skipLightSources = true;
private static readonly int EchoIntensityID = Shader.PropertyToID("_EchoOutlineIntensity");
private static readonly int OutlineColorID = Shader.PropertyToID("_OutlineColor");
private static readonly int OutlineWidthID = Shader.PropertyToID("_OutlineWidth");
private static readonly int MainTexID = Shader.PropertyToID("_MainTex");
private readonly System.Collections.Generic.List<SpriteRenderer> _parentRenderers
= new System.Collections.Generic.List<SpriteRenderer>();
private readonly System.Collections.Generic.List<SpriteRenderer> _outlineRenderers
= new System.Collections.Generic.List<SpriteRenderer>();
private bool _renderersEnabled = false;
private void Start()
{
RefreshList();
}
/// <summary>
/// 重新扫描场景,为符合条件的精灵创建描边子物体。
/// 场景中新增精灵后可调用(也有右键菜单)。
/// </summary>
[ContextMenu("Refresh Outlines")]
public void RefreshList()
{
ClearOutlines();
var all = FindObjectsByType<SpriteRenderer>(FindObjectsSortMode.None);
foreach (var sr in all)
{
if (sr == null) continue;
if ((includeLayers.value & (1 << sr.gameObject.layer)) == 0) continue;
if (skipLightSources && sr.GetComponentInParent<LightSource>() != null) continue;
// 跳过已经是描边子物体的(避免重复)。用 sharedMaterial 避免实例化父级材质
if (sr.sharedMaterial != null && sr.sharedMaterial.shader != null
&& sr.sharedMaterial.shader.name == "IndianOcean/SpriteEchoOutline")
continue;
// 读取自定义描边覆盖(颜色/宽度/禁用)
var colorOverride = sr.GetComponentInParent<EchoOutlineColorOverride>();
if (colorOverride != null && colorOverride.disableOutline) continue;
CreateOutlineChild(sr, colorOverride);
}
_renderersEnabled = false;
UpdateRendererEnabled();
}
private void CreateOutlineChild(SpriteRenderer parent, EchoOutlineColorOverride colorOverride)
{
var child = new GameObject($"EchoOutline_{parent.gameObject.name}");
child.transform.SetParent(parent.transform, false);
child.transform.localPosition = Vector3.zero;
child.transform.localRotation = Quaternion.identity;
child.transform.localScale = Vector3.one;
var cr = child.AddComponent<SpriteRenderer>();
cr.sprite = parent.sprite;
cr.color = Color.white;
cr.flipX = parent.flipX;
cr.flipY = parent.flipY;
// 有自定义颜色/宽度时创建材质实例;否则共享材质
if (colorOverride != null)
{
// 用材质实例而非 MPBMPB 会干扰 SpriteRenderer 的 _MainTex 自动绑定,
// 导致 shader 采样到白色贴图 → alpha=1 → 描边公式算出 0 → 描边消失
var mat = new Material(outlineMaterial);
mat.SetColor(OutlineColorID, colorOverride.outlineColor);
if (colorOverride.outlineWidth >= 0f)
mat.SetFloat(OutlineWidthID, colorOverride.outlineWidth);
if (parent.sprite != null)
mat.SetTexture(MainTexID, parent.sprite.texture);
cr.sharedMaterial = mat;
}
else
{
cr.sharedMaterial = outlineMaterial;
}
cr.sortingLayerID = parent.sortingLayerID;
cr.sortingOrder = parent.sortingOrder + 1;
cr.enabled = false; // 默认关闭,回声激活时再开
_parentRenderers.Add(parent);
_outlineRenderers.Add(cr);
}
private void Update()
{
// 回声不活跃时关闭所有描边渲染器(省 draw call
UpdateRendererEnabled();
// 同步精灵帧(动画)、翻转、排序
if (!_renderersEnabled) return;
for (int i = 0; i < _outlineRenderers.Count; i++)
{
var cr = _outlineRenderers[i];
var pr = _parentRenderers[i];
if (pr == null || cr == null) continue;
if (cr.sprite != pr.sprite)
{
cr.sprite = pr.sprite;
// 有自定义材质实例时同步纹理(共享材质由 SpriteRenderer 自动绑定)
var mat = cr.sharedMaterial;
if (mat != null && mat != outlineMaterial && pr.sprite != null)
mat.SetTexture(MainTexID, pr.sprite.texture);
}
if (cr.flipX != pr.flipX) cr.flipX = pr.flipX;
if (cr.flipY != pr.flipY) cr.flipY = pr.flipY;
if (cr.sortingLayerID != pr.sortingLayerID) cr.sortingLayerID = pr.sortingLayerID;
if (cr.sortingOrder != pr.sortingOrder + 1) cr.sortingOrder = pr.sortingOrder + 1;
}
}
private void UpdateRendererEnabled()
{
float intensity = Shader.GetGlobalFloat(EchoIntensityID);
bool shouldEnable = intensity > 0.01f;
if (shouldEnable == _renderersEnabled) return;
_renderersEnabled = shouldEnable;
for (int i = 0; i < _outlineRenderers.Count; i++)
{
if (_outlineRenderers[i] != null)
_outlineRenderers[i].enabled = shouldEnable;
}
}
private void ClearOutlines()
{
for (int i = 0; i < _outlineRenderers.Count; i++)
{
if (_outlineRenderers[i] != null)
{
// 清理自定义材质实例(共享材质 outlineMaterial 不销毁)
var mat = _outlineRenderers[i].sharedMaterial;
if (mat != null && mat != outlineMaterial)
Destroy(mat);
Destroy(_outlineRenderers[i].gameObject);
}
}
_parentRenderers.Clear();
_outlineRenderers.Clear();
}
private void OnDestroy()
{
ClearOutlines();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: C3tKt3itVn3XlGgq7XSiErSLgRwUk/WCk8NYkXazVa0z1Nc71d7kP4g=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,156 @@
using UnityEngine;
namespace IndianOceanAssets.Engine2_5D
{
/// <summary>
/// 回声系统 v2 —— 描边波纹版。
/// 按 E 键从玩家位置释放扩散波纹,波纹扫过的精灵(非光源物体)
/// 会显示外描边(默认白色,可自定义),维持一段时间后整体消散。
///
/// 与 v1 的区别:
/// - 不再在黑暗遮罩上画光波(那样会漏出地板)
/// - 改为通过全局 shader 参数控制精灵描边 shader (SpriteEchoOutline)
/// - 时间模型:扩散 → 维持(sustainTime) → 消散(fadeTime)
///
/// 挂载位置:玩家物体上(或跟随玩家的空物体)
/// 依赖:场景中需要有用 SpriteEchoOutline shader 的描边精灵
/// (由 EchoOutlineManager 自动生成)
/// </summary>
public class EchoSystem : MonoBehaviour
{
[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 float cooldown = 2f;
// 全局 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 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 void Start()
{
// 初始化:描边关闭
Shader.SetGlobalFloat(EchoOutlineIntensityID, 0f);
Shader.SetGlobalColor(EchoOutlineColorID, outlineColor);
Shader.SetGlobalFloat(EchoWidthID, ringWidth);
}
private void Update()
{
// 触发
if (Input.GetKeyDown(echoKey) && _state == State.Idle && 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);
}
private void StartEcho()
{
_state = State.Expanding;
_radius = 0f;
_intensity = 1f;
_stateTimer = 0f;
_lastEchoTime = Time.time;
// 记录释放时刻的玩家位置(回声中心固定,不跟随移动)
Vector3 p = transform.position;
_center = new Vector2(p.x, p.z);
}
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>
/// 外部调用:强制停止回声
/// </summary>
public void StopEcho()
{
_state = State.Idle;
_intensity = 0f;
Shader.SetGlobalFloat(EchoOutlineIntensityID, 0f);
}
public bool IsActive => _state != State.Idle;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: DysbsSupUS13hUH/r+5Ob+rgd023D8PFN2FdDz1PS3gDF3NLpTJab8c=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: