509 lines
15 KiB
C#
509 lines
15 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
|
||
/// <summary>
|
||
/// 全局鼠标悬停呼吸灯效果管理器
|
||
/// 只需将此脚本挂载到Camera或任意游戏对象上
|
||
/// </summary>
|
||
public class GlobalHoverBreathingManager : MonoBehaviour
|
||
{
|
||
[Header("全局呼吸效果设置")]
|
||
[Tooltip("呼吸灯颜色")]
|
||
public Color breathColor = Color.cyan;
|
||
[Tooltip("呼吸速度")]
|
||
public float breathSpeed = 0.7f;
|
||
|
||
[Tooltip("最小亮度 (0-1)")]
|
||
[Range(0f, 1f)]
|
||
public float minIntensity = 0.2f;
|
||
|
||
[Tooltip("最大亮度 (0-1)")]
|
||
[Range(0f, 1f)]
|
||
public float maxIntensity = 0.9f;
|
||
|
||
[Header("悬停触发设置")]
|
||
[Tooltip("悬停触发延时(秒)")]
|
||
public float hoverDelay = 1.2f;
|
||
|
||
[Tooltip("悬停时是否显示高亮轮廓(可选)")]
|
||
public bool useOutline = false;
|
||
|
||
[Header("模型处理设置")]
|
||
[Tooltip("模型根节点(自动递归处理其下的所有子物体)")]
|
||
public GameObject modelRoot;
|
||
|
||
[Tooltip("需要交互的层")]
|
||
public LayerMask interactableLayers = -1;
|
||
|
||
[Tooltip("是否自动为子物体添加碰撞体(如无)")]
|
||
public bool autoAddColliders = true;
|
||
|
||
[Tooltip("碰撞体类型:Box(性能好)或 Mesh(精确但耗性能)")]
|
||
public ColliderType colliderType = ColliderType.Mesh;
|
||
|
||
[Header("渲染器过滤设置")]
|
||
[Tooltip("最小渲染器尺寸(过滤过小的碎片)")]
|
||
public float minRendererSize = 0.01f;
|
||
|
||
[Tooltip("忽略名称包含以下关键词的物体(用逗号分隔)")]
|
||
public string ignoreKeywords = "ignore,skip,hidden,internal,debug";
|
||
|
||
[Tooltip("只包含名称包含以下关键词的物体(为空则包含所有)")]
|
||
public string includeKeywords = "";
|
||
|
||
[Tooltip("忽略透明材质(Alpha < 0.5)的物体")]
|
||
public bool ignoreTransparentMaterials = true;
|
||
|
||
[Header("调试设置")]
|
||
[Tooltip("是否显示调试信息")]
|
||
public bool showDebugInfo = true;
|
||
|
||
[Tooltip("是否绘制悬停物体的边框")]
|
||
public bool drawHoverBounds = true;
|
||
|
||
[Tooltip("边框颜色")]
|
||
public Color boundsColor = Color.yellow;
|
||
|
||
// 枚举:碰撞体类型
|
||
public enum ColliderType
|
||
{
|
||
Box,
|
||
Mesh
|
||
}
|
||
|
||
// 内部数据结构
|
||
private class RendererInfo
|
||
{
|
||
public Renderer renderer;
|
||
public Material originalMaterial;
|
||
public Material breathMaterial;
|
||
public Collider collider;
|
||
public float hoverTimer = 0f;
|
||
public float breathPhase = 0f;
|
||
public bool isBreathing = false;
|
||
}
|
||
|
||
private Camera mainCamera;
|
||
private List<RendererInfo> allRendererInfos = new List<RendererInfo>();
|
||
private Dictionary<Renderer, RendererInfo> rendererToInfo = new Dictionary<Renderer, RendererInfo>();
|
||
private RaycastHit[] raycastHits = new RaycastHit[20];
|
||
private Bounds? hoverBounds = null;
|
||
|
||
// 忽略关键词数组
|
||
private string[] ignoreKeywordArray = new string[0];
|
||
private string[] includeKeywordArray = new string[0];
|
||
|
||
void Start()
|
||
{
|
||
// 初始化主相机
|
||
mainCamera = GetComponent<Camera>();
|
||
if (mainCamera == null) mainCamera = Camera.main;
|
||
if (mainCamera == null)
|
||
{
|
||
Debug.LogError("未找到相机!请将此脚本挂载到相机上。", this);
|
||
enabled = false;
|
||
return;
|
||
}
|
||
|
||
// 解析关键词
|
||
ignoreKeywordArray = ignoreKeywords.ToLower().Split(new char[] { ',' }, System.StringSplitOptions.RemoveEmptyEntries);
|
||
includeKeywordArray = includeKeywords.ToLower().Split(new char[] { ',' }, System.StringSplitOptions.RemoveEmptyEntries);
|
||
|
||
// 如果指定了模型根节点,自动处理
|
||
if (modelRoot != null)
|
||
{
|
||
ProcessModelRecursively(modelRoot.transform);
|
||
}
|
||
else
|
||
{
|
||
Debug.LogWarning("未指定模型根节点,将不会自动处理任何物体。");
|
||
}
|
||
|
||
if (showDebugInfo)
|
||
{
|
||
Debug.Log($"管理器初始化完成,共处理 {allRendererInfos.Count} 个渲染器");
|
||
}
|
||
}
|
||
|
||
void Update()
|
||
{
|
||
HandleMouseHover();
|
||
UpdateAllBreathingEffects();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 递归处理模型的所有子物体
|
||
/// </summary>
|
||
private void ProcessModelRecursively(Transform parent)
|
||
{
|
||
if (parent == null) return;
|
||
|
||
// 处理当前物体的所有Renderer
|
||
ProcessRenderersOnObject(parent.gameObject);
|
||
|
||
// 递归处理所有子物体
|
||
foreach (Transform child in parent)
|
||
{
|
||
// 如果物体名称包含忽略关键词,跳过整个子树
|
||
if (ShouldIgnoreObject(child.gameObject))
|
||
{
|
||
if (showDebugInfo)
|
||
{
|
||
Debug.Log($"忽略物体及其子物体: {child.name}", child.gameObject);
|
||
}
|
||
continue;
|
||
}
|
||
|
||
ProcessModelRecursively(child);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判断是否应忽略此物体
|
||
/// </summary>
|
||
private bool ShouldIgnoreObject(GameObject obj)
|
||
{
|
||
string objName = obj.name.ToLower();
|
||
|
||
// 检查忽略关键词
|
||
foreach (string keyword in ignoreKeywordArray)
|
||
{
|
||
if (!string.IsNullOrEmpty(keyword) && objName.Contains(keyword))
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// 检查包含关键词(如果设置了包含关键词)
|
||
if (includeKeywordArray.Length > 0 && !string.IsNullOrEmpty(includeKeywords))
|
||
{
|
||
bool shouldInclude = false;
|
||
foreach (string keyword in includeKeywordArray)
|
||
{
|
||
if (!string.IsNullOrEmpty(keyword) && objName.Contains(keyword))
|
||
{
|
||
shouldInclude = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!shouldInclude) return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理单个物体上的所有Renderer
|
||
/// </summary>
|
||
private void ProcessRenderersOnObject(GameObject obj)
|
||
{
|
||
Renderer[] renderers = obj.GetComponents<Renderer>();
|
||
|
||
foreach (Renderer renderer in renderers)
|
||
{
|
||
// 检查渲染器有效性
|
||
if (renderer == null || renderer.material == null) continue;
|
||
|
||
// 检查最小尺寸
|
||
if (renderer.bounds.size.magnitude < minRendererSize)
|
||
{
|
||
if (showDebugInfo)
|
||
{
|
||
Debug.Log($"跳过过小渲染器: {renderer.name} (尺寸: {renderer.bounds.size.magnitude})", renderer.gameObject);
|
||
}
|
||
continue;
|
||
}
|
||
|
||
// 检查透明材质
|
||
if (ignoreTransparentMaterials && renderer.material.HasProperty("_Color"))
|
||
{
|
||
Color matColor = renderer.material.color;
|
||
if (matColor.a < 0.5f) continue;
|
||
}
|
||
|
||
// 检查层
|
||
if (((1 << renderer.gameObject.layer) & interactableLayers) == 0) continue;
|
||
|
||
// 创建Renderer信息
|
||
RendererInfo info = new RendererInfo
|
||
{
|
||
renderer = renderer,
|
||
originalMaterial = renderer.material
|
||
};
|
||
|
||
// 创建呼吸材质
|
||
info.breathMaterial = new Material(renderer.material);
|
||
renderer.material = info.breathMaterial;
|
||
|
||
// 确保有碰撞体
|
||
Collider existingCollider = renderer.GetComponent<Collider>();
|
||
if (existingCollider == null && autoAddColliders)
|
||
{
|
||
info.collider = AddColliderToRenderer(renderer);
|
||
}
|
||
else
|
||
{
|
||
info.collider = existingCollider;
|
||
}
|
||
|
||
// 添加到列表
|
||
allRendererInfos.Add(info);
|
||
rendererToInfo[renderer] = info;
|
||
|
||
if (showDebugInfo)
|
||
{
|
||
Debug.Log($"添加渲染器: {renderer.name} (物体: {obj.name}, 材质: {renderer.material.name})", renderer.gameObject);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 为渲染器添加碰撞体
|
||
/// </summary>
|
||
private Collider AddColliderToRenderer(Renderer renderer)
|
||
{
|
||
GameObject obj = renderer.gameObject;
|
||
|
||
switch (colliderType)
|
||
{
|
||
case ColliderType.Box:
|
||
BoxCollider boxCollider = obj.AddComponent<BoxCollider>();
|
||
boxCollider.isTrigger = true;
|
||
return boxCollider;
|
||
|
||
case ColliderType.Mesh:
|
||
if (renderer is MeshRenderer meshRenderer)
|
||
{
|
||
MeshFilter meshFilter = obj.GetComponent<MeshFilter>();
|
||
if (meshFilter != null && meshFilter.sharedMesh != null)
|
||
{
|
||
MeshCollider meshCollider = obj.AddComponent<MeshCollider>();
|
||
meshCollider.convex = true;
|
||
meshCollider.isTrigger = true;
|
||
return meshCollider;
|
||
}
|
||
}
|
||
// 回退到BoxCollider
|
||
BoxCollider fallbackBox = obj.AddComponent<BoxCollider>();
|
||
fallbackBox.isTrigger = true;
|
||
return fallbackBox;
|
||
|
||
default:
|
||
return obj.AddComponent<BoxCollider>();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理鼠标悬停
|
||
/// </summary>
|
||
private void HandleMouseHover()
|
||
{
|
||
Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition);
|
||
|
||
// 清空之前的悬停状态
|
||
ClearPreviousHover();
|
||
|
||
// 射线检测
|
||
int hitCount = Physics.RaycastNonAlloc(ray, raycastHits, Mathf.Infinity, interactableLayers);
|
||
|
||
RendererInfo closestHitInfo = null;
|
||
float closestDistance = Mathf.Infinity;
|
||
hoverBounds = null;
|
||
|
||
// 找到最近的、在管理范围内的渲染器
|
||
for (int i = 0; i < hitCount; i++)
|
||
{
|
||
RaycastHit hit = raycastHits[i];
|
||
Renderer hitRenderer = hit.collider.GetComponent<Renderer>();
|
||
|
||
if (hitRenderer != null && rendererToInfo.TryGetValue(hitRenderer, out RendererInfo hitInfo))
|
||
{
|
||
if (hit.distance < closestDistance)
|
||
{
|
||
closestDistance = hit.distance;
|
||
closestHitInfo = hitInfo;
|
||
|
||
// 记录边界框用于调试绘制
|
||
hoverBounds = hitRenderer.bounds;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 更新悬停计时器
|
||
if (closestHitInfo != null)
|
||
{
|
||
closestHitInfo.hoverTimer += Time.deltaTime;
|
||
|
||
if (closestHitInfo.hoverTimer >= hoverDelay && !closestHitInfo.isBreathing)
|
||
{
|
||
closestHitInfo.isBreathing = true;
|
||
|
||
if (showDebugInfo)
|
||
{
|
||
Debug.Log($"开始呼吸效果: {closestHitInfo.renderer.name}", closestHitInfo.renderer.gameObject);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清空之前的悬停状态
|
||
/// </summary>
|
||
private void ClearPreviousHover()
|
||
{
|
||
foreach (RendererInfo info in allRendererInfos)
|
||
{
|
||
if (info.isBreathing) continue; // 保持呼吸效果
|
||
|
||
if (info.hoverTimer > 0f)
|
||
{
|
||
info.hoverTimer = 0f;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新所有呼吸效果
|
||
/// </summary>
|
||
private void UpdateAllBreathingEffects()
|
||
{
|
||
foreach (RendererInfo info in allRendererInfos)
|
||
{
|
||
if (info.isBreathing)
|
||
{
|
||
UpdateBreathingEffect(info);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新单个渲染器的呼吸效果
|
||
/// </summary>
|
||
private void UpdateBreathingEffect(RendererInfo info)
|
||
{
|
||
info.breathPhase += breathSpeed * Time.deltaTime;
|
||
float t = (Mathf.Sin(info.breathPhase) + 1f) * 0.5f; // 0~1
|
||
float intensity = Mathf.Lerp(minIntensity, maxIntensity, t);
|
||
|
||
// 混合呼吸颜色和原始颜色
|
||
Color targetColor = Color.Lerp(info.originalMaterial.color, breathColor, intensity);
|
||
info.breathMaterial.color = targetColor;
|
||
|
||
// 可选:轮廓效果
|
||
if (useOutline)
|
||
{
|
||
// 这里可以添加轮廓Shader效果
|
||
// 需要配合自定义Shader实现
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 停止单个渲染器的呼吸效果
|
||
/// </summary>
|
||
public void StopBreathingForRenderer(Renderer renderer)
|
||
{
|
||
if (rendererToInfo.TryGetValue(renderer, out RendererInfo info))
|
||
{
|
||
info.isBreathing = false;
|
||
info.hoverTimer = 0f;
|
||
info.breathPhase = 0f;
|
||
info.breathMaterial.color = info.originalMaterial.color;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 停止所有呼吸效果
|
||
/// </summary>
|
||
public void StopAllBreathing()
|
||
{
|
||
foreach (RendererInfo info in allRendererInfos)
|
||
{
|
||
info.isBreathing = false;
|
||
info.hoverTimer = 0f;
|
||
info.breathPhase = 0f;
|
||
info.breathMaterial.color = info.originalMaterial.color;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 手动开始渲染器的呼吸效果
|
||
/// </summary>
|
||
public void StartBreathingForRenderer(Renderer renderer)
|
||
{
|
||
if (rendererToInfo.TryGetValue(renderer, out RendererInfo info))
|
||
{
|
||
info.isBreathing = true;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 动态添加单个渲染器
|
||
/// </summary>
|
||
public void AddRenderer(Renderer renderer)
|
||
{
|
||
if (renderer == null || rendererToInfo.ContainsKey(renderer)) return;
|
||
|
||
RendererInfo info = new RendererInfo
|
||
{
|
||
renderer = renderer,
|
||
originalMaterial = renderer.material,
|
||
breathMaterial = new Material(renderer.material)
|
||
};
|
||
|
||
renderer.material = info.breathMaterial;
|
||
|
||
allRendererInfos.Add(info);
|
||
rendererToInfo[renderer] = info;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 动态移除单个渲染器
|
||
/// </summary>
|
||
public void RemoveRenderer(Renderer renderer)
|
||
{
|
||
if (!rendererToInfo.TryGetValue(renderer, out RendererInfo info)) return;
|
||
|
||
// 恢复原始材质
|
||
renderer.material = info.originalMaterial;
|
||
|
||
// 从列表中移除
|
||
allRendererInfos.Remove(info);
|
||
rendererToInfo.Remove(renderer);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取所有管理的渲染器
|
||
/// </summary>
|
||
public List<Renderer> GetAllManagedRenderers()
|
||
{
|
||
List<Renderer> renderers = new List<Renderer>();
|
||
foreach (RendererInfo info in allRendererInfos)
|
||
{
|
||
renderers.Add(info.renderer);
|
||
}
|
||
return renderers;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 调试绘制
|
||
/// </summary>
|
||
void OnDrawGizmos()
|
||
{
|
||
if (!Application.isPlaying || !drawHoverBounds || !hoverBounds.HasValue) return;
|
||
|
||
Gizmos.color = boundsColor;
|
||
Gizmos.DrawWireCube(hoverBounds.Value.center, hoverBounds.Value.size);
|
||
}
|
||
|
||
void OnDestroy()
|
||
{
|
||
// 恢复所有原始材质
|
||
foreach (RendererInfo info in allRendererInfos)
|
||
{
|
||
if (info.renderer != null && info.originalMaterial != null)
|
||
{
|
||
info.renderer.material = info.originalMaterial;
|
||
}
|
||
}
|
||
}
|
||
} |