EnergyEfficiencyManagement/Assets/Zion/Scripts/ChildWidgetUIRenderer.cs

473 lines
17 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 UnityEngine;
using UnityEngine.UI;
using System.Collections;
/// <summary>
/// 子部件UI渲染与观察器
/// 功能:与 EnhancedModelViewerOrbitCamera 配合,当主脚本进入子物体观察模式时,
/// 将克隆出的小部件渲染到指定UI上并允许在UI内进行旋转和缩放观察。
/// 配置步骤:
/// 1. 将本脚本挂载到场景中。
/// 2. 在Inspector中将主观察脚本拖入 `Main Viewer Script` 字段。
/// 3. 配置好UI相机 (UIRender Camera)、渲染纹理 (Render Texture) 和UI显示图片 (Widget Display Image)。
/// 4. 运行场景点击大模型的子部件即可在UI中查看和操作。
/// </summary>
public class ChildWidgetUIRenderer : MonoBehaviour
{
[Header("【主观察脚本引用】")]
[Tooltip("关联的 EnhancedModelViewerOrbitCamera 脚本。")]
[SerializeField] private EnhancedModelViewerOrbitCamera mainViewerScript;
[Header("【UI渲染核心组件】")]
[Tooltip("用于显示部件模型的UI面板通常包含一个RawImage。")]
[SerializeField] public GameObject uiPanel;
[Tooltip("用于显示模型的RawImage组件其Texture将设置为下方的Render Texture。")]
[SerializeField] public RawImage widgetDisplayImage;
[Tooltip("专用于渲染UI中部件模型的相机。")]
[SerializeField] private Camera uiRenderCamera;
[Tooltip("渲染纹理UI相机将画面渲染到此再由RawImage显示。")]
[SerializeField] private RenderTexture renderTexture;
[Header("【UI内模型控制设置】")]
[Tooltip("在UI区域内拖拽时模型的旋转速度。")]
[SerializeField] private float uiRotationSpeed = 2.0f;
[Tooltip("在UI区域内使用滚轮时模型的缩放速度。")]
[SerializeField] private float uiZoomSpeed = 0.5f;
[Tooltip("模型在UI中的最小缩放比例。")]
[SerializeField] private float uiMinZoom = 0.5f;
[Tooltip("模型在UI中的最大缩放比例。")]
[SerializeField] private float uiMaxZoom = 3.0f;
[Tooltip("模型在UI中的默认缩放比例。")]
[SerializeField] private float uiDefaultZoom = 1.5f;
[Header("【UI相机设置】")]
[Tooltip("UI相机相对于模型初始位置的偏移量。Z值影响初始观察距离。")]
[SerializeField] private Vector3 uiCameraOffset = new Vector3(0, 0.5f, 2.5f);
[Tooltip("UI相机的视野角度(FOV),影响模型在画面中的大小。")]
[SerializeField] private float uiCameraFieldOfView = 45f;
[Header("【UI操作提示】(可选)")]
[Tooltip("用于显示操作提示的UI文本组件如“拖拽旋转滚轮缩放”。")]
[SerializeField] private Text instructionText;
// --- 私有变量 ---
private GameObject currentClonedWidgetForUI = null; // 当前由本脚本管理用于UI显示的克隆部件
private bool isUIActive = false; // 标识UI面板是否处于激活状态
private bool isDraggingInUI = false; // 标识鼠标是否在UI区域内拖拽
// UI内模型的变换状态
private Vector2 uiWidgetRotation = Vector2.zero; // 累计旋转角度 (x: 绕世界Y轴, y: 绕自身X轴)
private float uiWidgetCurrentZoom = 1.5f; // 当前缩放值
private float uiWidgetTargetZoom = 1.5f; // 目标缩放值(用于平滑)
private Vector2 lastMousePosition; // 上一帧鼠标位置,用于计算拖拽增量
/// <summary>
/// 初始化设置UI和相机的初始状态并订阅主脚本的事件。
/// </summary>
void Start()
{
InitializeUIAndCamera();
SubscribeToMainViewerEvents();
}
/// <summary>
/// 初始化UI面板和渲染相机。
/// </summary>
private void InitializeUIAndCamera()
{
// 初始时隐藏UI面板
if (uiPanel != null)
{
uiPanel.SetActive(false);
}
// 配置UI相机
if (uiRenderCamera != null)
{
uiRenderCamera.enabled = false; // 初始禁用
if (renderTexture != null)
{
uiRenderCamera.targetTexture = renderTexture;
}
uiRenderCamera.fieldOfView = uiCameraFieldOfView;
}
// 将渲染纹理赋给UI的RawImage
if (widgetDisplayImage != null && renderTexture != null)
{
widgetDisplayImage.texture = renderTexture;
}
// 设置初始提示文本
UpdateInstructionText("点击大模型的子部件将在UI面板中显示。");
}
/// <summary>
/// 订阅主观察脚本的事件。
/// </summary>
private void SubscribeToMainViewerEvents()
{
if (mainViewerScript == null)
{
Debug.LogWarning("ChildWidgetUIRenderer: 未指定主观察脚本,功能将不可用。", this);
return;
}
// 订阅开始观察事件
mainViewerScript.OnChildObservationStarted += HandleChildObservationStarted;
// 订阅结束观察事件
mainViewerScript.OnChildObservationEnded += HandleChildObservationEnded;
}
/// <summary>
/// 取消订阅主观察脚本的事件(防止内存泄漏)。
/// </summary>
private void OnDestroy()
{
if (mainViewerScript != null)
{
mainViewerScript.OnChildObservationStarted -= HandleChildObservationStarted;
mainViewerScript.OnChildObservationEnded -= HandleChildObservationEnded;
}
CleanupResources(); // 清理可能残留的资源
}
/// <summary>
/// 主脚本开始观察子物体时的处理函数。
/// </summary>
/// <param name="clonedWidget">主脚本创建的克隆部件。</param>
/// <param name="originalWidget">被点击的原始部件。</param>
private void HandleChildObservationStarted(GameObject clonedWidget, GameObject originalWidget)
{
if (clonedWidget == null)
{
Debug.LogError("收到观察开始事件但克隆部件为Null。");
return;
}
Debug.Log($"UI观察器开始处理部件 '{originalWidget.name}' 的UI显示。");
// 1. 显示UI面板
ShowUIPanel();
// 2. 设置UI显示用的克隆部件
SetupWidgetForUIDisplay(clonedWidget);
// 3. 更新UI提示
UpdateInstructionText("在蓝色区域内拖拽旋转,滚轮缩放。\n按ESC或Shift键退出。");
}
/// <summary>
/// 主脚本结束观察子物体时的处理函数。
/// </summary>
/// <param name="clonedWidget">主脚本创建的克隆部件(即将被销毁)。</param>
/// <param name="originalWidget">被观察的原始部件。</param>
private void HandleChildObservationEnded(GameObject clonedWidget, GameObject originalWidget)
{
Debug.Log($"UI观察器结束观察部件 '{originalWidget?.name}'。");
// 关闭UI面板清理资源
CloseUIPanel();
}
/// <summary>
/// 为UI显示准备克隆部件。
/// 注意我们会创建这个部件的另一个副本专供UI相机渲染以避免干扰主场景的观察。
/// </summary>
/// <param name="sourceWidget">主脚本创建的克隆部件,作为复制源。</param>
private void SetupWidgetForUIDisplay(GameObject sourceWidget)
{
// 清理可能存在的旧UI部件
if (currentClonedWidgetForUI != null)
{
Destroy(currentClonedWidgetForUI);
}
// 创建专用于UI显示的副本
//currentClonedWidgetForUI = Instantiate(sourceWidget, Vector3.zero, Quaternion.identity);
//currentClonedWidgetForUI.name = sourceWidget.name + "_UI_View";
currentClonedWidgetForUI = this.transform.GetChild(0).gameObject;
currentClonedWidgetForUI.layer = LayerMask.NameToLayer("CloneModel");
// 移除可能干扰的组件(如碰撞体、刚体、主脚本可能添加的临时组件等)
CleanupWidgetComponents(currentClonedWidgetForUI);
// 将部件放置在UI相机前
//PositionWidgetInFrontOfUICamera(currentClonedWidgetForUI);
// 重置UI控制参数
ResetUIControlParameters();
// 启用UI相机
if (uiRenderCamera != null)
{
uiRenderCamera.enabled = true;
}
}
/// <summary>
/// 清理部件上不需要的组件确保其在UI中正常显示且不干扰物理系统。
/// </summary>
private void CleanupWidgetComponents(GameObject widget)
{
// 移除所有脚本避免在UI模式下执行不必要的逻辑
MonoBehaviour[] scripts = widget.GetComponentsInChildren<MonoBehaviour>();
foreach (var script in scripts)
{
Destroy(script);
}
// 移除碰撞体,避免意外的射线检测
Collider[] colliders = widget.GetComponentsInChildren<Collider>();
foreach (var collider in colliders)
{
Destroy(collider);
}
// 移除刚体,避免物理模拟
Rigidbody[] rigidbodies = widget.GetComponentsInChildren<Rigidbody>();
foreach (var rb in rigidbodies)
{
Destroy(rb);
}
}
/// <summary>
/// 将部件定位到UI相机前方合适的位置。
/// </summary>
private void PositionWidgetInFrontOfUICamera(GameObject widget)
{
if (uiRenderCamera == null || widget == null) return;
// 将部件设为UI相机的子物体便于管理
widget.transform.SetParent(uiRenderCamera.transform);
// 设置局部位置和旋转
widget.transform.localPosition = uiCameraOffset;
widget.transform.localRotation = Quaternion.identity;
// 可选:根据部件大小自动调整初始缩放,确保其在视野内
Bounds bounds = CalculateRenderBounds(widget);
float maxSize = Mathf.Max(bounds.size.x, bounds.size.y, bounds.size.z);
if (maxSize > 0)
{
// 一个简单的自适应缩放可以根据uiCameraOffset.z和FOV进一步优化
float adaptiveScale = 2.0f / maxSize;
widget.transform.localScale = Vector3.one * adaptiveScale;
}
}
/// <summary>
/// 计算包含所有Renderer的包围盒。
/// </summary>
private Bounds CalculateRenderBounds(GameObject go)
{
Renderer[] renderers = go.GetComponentsInChildren<Renderer>();
if (renderers.Length == 0) return new Bounds(go.transform.position, Vector3.one);
Bounds bounds = renderers[0].bounds;
for (int i = 1; i < renderers.Length; i++)
{
bounds.Encapsulate(renderers[i].bounds);
}
return bounds;
}
/// <summary>
/// 重置UI内的旋转和缩放控制参数。
/// </summary>
private void ResetUIControlParameters()
{
uiWidgetRotation = Vector2.zero;
uiWidgetCurrentZoom = uiDefaultZoom;
uiWidgetTargetZoom = uiDefaultZoom;
if (currentClonedWidgetForUI != null)
{
currentClonedWidgetForUI.transform.localRotation = Quaternion.identity;
currentClonedWidgetForUI.transform.localScale = Vector3.one;
}
}
/// <summary>
/// 显示UI面板。
/// </summary>
private void ShowUIPanel()
{
if (uiPanel != null && !uiPanel.activeSelf)
{
uiPanel.SetActive(true);
isUIActive = true;
}
}
/// <summary>
/// 关闭UI面板并清理相关资源。
/// </summary>
public void CloseUIPanel()
{
if (!isUIActive) return;
// 隐藏面板
if (uiPanel != null)
{
uiPanel.SetActive(false);
}
isUIActive = false;
isDraggingInUI = false;
// 禁用UI相机
if (uiRenderCamera != null)
{
uiRenderCamera.enabled = false;
}
// 销毁专用于UI显示的克隆部件
if (currentClonedWidgetForUI != null)
{
Destroy(currentClonedWidgetForUI);
currentClonedWidgetForUI = null;
}
// 更新提示文本
UpdateInstructionText("UI观察模式已关闭。");
}
/// <summary>
/// 每帧更新处理UI内的输入旋转和缩放
/// </summary>
void Update()
{
if (!isUIActive || currentClonedWidgetForUI == null) return;
HandleUIInput();
ApplyUITransforms();
}
/// <summary>
/// 处理UI显示区域内的鼠标输入。
/// </summary>
private void HandleUIInput()
{
// 检查鼠标是否在显示部件的UI区域内
bool isMouseOverUI = IsMouseOverDisplayImage();
// --- 旋转控制(鼠标拖拽)---
if (isMouseOverUI && Input.GetMouseButtonDown(0))
{
isDraggingInUI = true;
lastMousePosition = Input.mousePosition;
}
if (Input.GetMouseButton(0) && isDraggingInUI)
{
Vector2 currentMousePos = Input.mousePosition;
Vector2 delta = currentMousePos - lastMousePosition;
// 根据鼠标移动增量更新旋转角度
uiWidgetRotation.x += delta.x * uiRotationSpeed * 0.1f; // 绕世界Y轴旋转
uiWidgetRotation.y += delta.y * uiRotationSpeed * 0.1f; // 绕自身X轴旋转
uiWidgetRotation.y = Mathf.Clamp(uiWidgetRotation.y, -80f, 80f); // 限制上下翻转角度
lastMousePosition = currentMousePos;
}
if (Input.GetMouseButtonUp(0))
{
isDraggingInUI = false;
}
// --- 缩放控制(鼠标滚轮)---
if (isMouseOverUI)
{
float scroll = Input.GetAxis("Mouse ScrollWheel");
if (Mathf.Abs(scroll) > 0.01f)
{
// 根据滚轮方向调整目标缩放值
uiWidgetTargetZoom += scroll * uiZoomSpeed;
uiWidgetTargetZoom = Mathf.Clamp(uiWidgetTargetZoom, uiMinZoom, uiMaxZoom);
}
}
// --- 退出控制ESC键---
if (Input.GetKeyDown(KeyCode.Escape))
{
// 通知主脚本退出子物体观察模式这将触发我们订阅的End事件进而关闭UI。
if (mainViewerScript != null && mainViewerScript.IsObservingChild())
{
// 假设主脚本有公共的退出方法。如果没有,您可能需要通过其他方式触发退出。
// 例如,可以调用 mainViewerScript.ExitChildObservationMode(); 如果它是public的。
// 这里作为一个安全调用,实际情况取决于您的主脚本。
Debug.Log("UI观察器按ESC键尝试退出观察模式。");
// 注意更佳实践是让UI的关闭逻辑与主脚本解耦通过事件驱动。
// 这里我们直接关闭自己的UI主脚本的退出由用户按Shift键或其他方式触发。
CloseUIPanel();
}
}
}
/// <summary>
/// 检测鼠标位置是否在显示模型的RawImage矩形区域内。
/// </summary>
private bool IsMouseOverDisplayImage()
{
if (widgetDisplayImage == null) return false;
RectTransform rect = widgetDisplayImage.rectTransform;
Vector2 localPoint;
// 将屏幕鼠标坐标转换为UI矩形内的本地坐标
return RectTransformUtility.ScreenPointToLocalPointInRectangle(rect, Input.mousePosition, null, out localPoint)
&& rect.rect.Contains(localPoint);
}
/// <summary>
/// 将UI控制参数旋转、缩放应用到模型上。
/// </summary>
private void ApplyUITransforms()
{
if (currentClonedWidgetForUI == null) return;
// 应用旋转
Quaternion targetRotation = Quaternion.Euler(uiWidgetRotation.y, -uiWidgetRotation.x, 0);
currentClonedWidgetForUI.transform.localRotation = Quaternion.Slerp(
currentClonedWidgetForUI.transform.localRotation,
targetRotation,
10f * Time.deltaTime
);
// 应用缩放
uiWidgetCurrentZoom = Mathf.Lerp(uiWidgetCurrentZoom, uiWidgetTargetZoom, 5f * Time.deltaTime);
currentClonedWidgetForUI.transform.localScale = Vector3.one * uiWidgetCurrentZoom;
}
/// <summary>
/// 更新UI上的操作提示文本。
/// </summary>
private void UpdateInstructionText(string text)
{
if (instructionText != null)
{
instructionText.text = text;
}
}
/// <summary>
/// 清理脚本持有的资源。
/// </summary>
private void CleanupResources()
{
if (currentClonedWidgetForUI != null)
{
Destroy(currentClonedWidgetForUI);
}
}
/// <summary>
/// 在编辑器模式下绘制UI相机的视野范围等调试信息可选
/// </summary>
void OnDrawGizmosSelected()
{
if (uiRenderCamera != null)
{
Gizmos.color = Color.cyan;
Gizmos.matrix = uiRenderCamera.transform.localToWorldMatrix;
// 简单绘制一个相机视椎体
float distance = uiCameraOffset.z;
Gizmos.DrawFrustum(Vector3.zero, uiCameraFieldOfView, distance, 0.1f, 1.0f);
}
}
}