Files
VFSUnity/Assets/Scripts/Interaction/SplintObjectClickHandler.cs
2026-07-17 05:42:52 +08:00

333 lines
11 KiB
C#

using System.Collections.Generic;
using UnityEngine;
public class SplintObjectClickHandler : MonoBehaviour
{
private const float MinColliderSize = 0.35f;
private const float ScreenHitPadding = 120f;
private const float ScreenCenterFallbackRadius = 160f;
private MeshRenderer _outlineMesh;
private bool _clicked;
private float _lastClickTime;
private static int _lastHandledFrame = -1;
private static readonly List<SplintObjectClickHandler> ActiveHandlers = new List<SplintObjectClickHandler>();
public bool CanBeClicked()
{
return !_clicked && gameObject.activeInHierarchy;
}
private void OnEnable()
{
if (!ActiveHandlers.Contains(this)) ActiveHandlers.Add(this);
EnsureCollider();
CreateOutlineMesh();
ResetState(true);
}
private void OnDisable()
{
ActiveHandlers.Remove(this);
StopAllCoroutines();
if (_outlineMesh != null) _outlineMesh.enabled = false;
}
private void OnMouseDown()
{
PerformClick();
}
private void Update()
{
if (!CanBeClicked()) return;
if (!Input.GetMouseButtonDown(0)) return;
if (_lastHandledFrame == Time.frameCount) return;
if (UnityEngine.EventSystems.EventSystem.current != null &&
UnityEngine.EventSystems.EventSystem.current.IsPointerOverGameObject())
return;
Camera activeCamera = GetActiveCamera();
if (activeCamera == null) return;
Vector2 mousePos = Input.mousePosition;
float myDistance;
if (!TryGetPointerDistance(activeCamera, mousePos, out myDistance)) return;
if (myDistance > ScreenCenterFallbackRadius && !IsPointerInsideExpandedBounds(activeCamera, mousePos)) return;
if (!IsNearestClickable(activeCamera, mousePos, myDistance)) return;
PerformClick();
}
public void PerformClick()
{
if (_clicked) return;
if (_lastHandledFrame == Time.frameCount) return;
if (Time.time - _lastClickTime < 0.3f) return;
_lastClickTime = Time.time;
_lastHandledFrame = Time.frameCount;
InteractionClickGuard.MarkHandled();
_clicked = true;
StopAllCoroutines();
if (_outlineMesh != null) _outlineMesh.enabled = false;
SetRenderersVisible(true);
ActionHistory.Push(ActionHistory.ActionType.SplintPlank, this);
SplintPracticeFlowManager flow = FindObjectOfType<SplintPracticeFlowManager>();
if (flow != null) flow.OnPlankClicked(this);
ExamFlowManager examFlow = FindObjectOfType<ExamFlowManager>();
if (examFlow != null) examFlow.OnObjectClicked(this);
}
public void ResetState(bool startBlink)
{
EnsureCollider();
_clicked = false;
SetRenderersVisible(false);
StopAllCoroutines();
if (_outlineMesh != null) _outlineMesh.enabled = startBlink;
if (startBlink && isActiveAndEnabled) StartCoroutine(BlinkOutline());
}
public void ForceHideOutline()
{
StopAllCoroutines();
if (_outlineMesh != null) _outlineMesh.enabled = false;
}
private void EnsureCollider()
{
BoxCollider box = GetComponent<BoxCollider>();
if (box == null) box = gameObject.AddComponent<BoxCollider>();
Bounds bounds;
if (TryGetRendererBounds(out bounds))
{
Vector3 localCenter = transform.InverseTransformPoint(bounds.center);
Vector3 localSize = transform.InverseTransformVector(bounds.size);
localSize = new Vector3(Mathf.Abs(localSize.x), Mathf.Abs(localSize.y), Mathf.Abs(localSize.z));
localSize = new Vector3(
Mathf.Max(localSize.x * 1.8f, MinColliderSize),
Mathf.Max(localSize.y * 1.8f, MinColliderSize),
Mathf.Max(localSize.z * 1.8f, MinColliderSize));
box.center = localCenter;
box.size = localSize;
}
box.isTrigger = false;
box.enabled = true;
}
private void CreateOutlineMesh()
{
if (_outlineMesh != null) return;
Mesh mesh = GetMesh();
if (mesh == null) return;
Shader shader = Shader.Find("Custom/OutlineUnlit");
if (shader == null) return;
Material mat = new Material(shader);
mat.SetColor("_OutlineColor", new Color(0.2f, 0.5f, 1f, 1f));
mat.SetColor("_MainColor", new Color(0f, 0f, 0f, 0f));
mat.SetFloat("_OutlineWidth", OutlineConfig.Width);
GameObject outline = new GameObject("Outline");
outline.transform.SetParent(transform, false);
outline.AddComponent<MeshFilter>().sharedMesh = mesh;
_outlineMesh = outline.AddComponent<MeshRenderer>();
_outlineMesh.material = mat;
}
private Mesh GetMesh()
{
MeshFilter mf = GetComponent<MeshFilter>();
if (mf == null) mf = GetComponentInChildren<MeshFilter>(true);
if (mf != null && mf.sharedMesh != null) return mf.sharedMesh;
SkinnedMeshRenderer smr = GetComponent<SkinnedMeshRenderer>();
if (smr == null) smr = GetComponentInChildren<SkinnedMeshRenderer>(true);
return smr != null ? smr.sharedMesh : null;
}
private bool TryGetRendererBounds(out Bounds bounds)
{
Renderer[] renderers = GetComponentsInChildren<Renderer>(true);
bool found = false;
bounds = new Bounds(transform.position, Vector3.zero);
for (int i = 0; i < renderers.Length; i++)
{
Renderer renderer = renderers[i];
if (renderer == null) continue;
if (_outlineMesh != null && renderer == _outlineMesh) continue;
if (!found)
{
bounds = renderer.bounds;
found = true;
}
else
{
bounds.Encapsulate(renderer.bounds);
}
}
return found;
}
private Camera GetActiveCamera()
{
Camera activeCamera = Camera.main;
foreach (Camera camera in Camera.allCameras)
{
if (camera != null && camera.enabled && camera.gameObject.activeInHierarchy)
{
activeCamera = camera;
}
}
return activeCamera;
}
private bool TryGetPointerDistance(Camera activeCamera, Vector2 pointer, out float distance)
{
Rect screenRect;
Vector2 center;
if (TryGetScreenRect(activeCamera, out screenRect, out center))
{
float rectDistance = DistanceToRect(pointer, screenRect);
float centerDistance = Vector2.Distance(pointer, center);
distance = rectDistance + centerDistance * 0.001f;
return true;
}
Vector3 screenPosition = activeCamera.WorldToScreenPoint(transform.position);
if (screenPosition.z < 0f)
{
distance = float.MaxValue;
return false;
}
center = new Vector2(screenPosition.x, screenPosition.y);
distance = Vector2.Distance(pointer, center);
return true;
}
private bool IsPointerInsideExpandedBounds(Camera activeCamera, Vector2 pointer)
{
Rect screenRect;
Vector2 center;
if (!TryGetScreenRect(activeCamera, out screenRect, out center)) return false;
return screenRect.Contains(pointer);
}
private bool TryGetScreenRect(Camera activeCamera, out Rect screenRect, out Vector2 center)
{
screenRect = new Rect();
center = Vector2.zero;
Bounds bounds;
if (!TryGetRendererBounds(out bounds)) return false;
Vector3 extents = bounds.extents;
Vector3 boundsCenter = bounds.center;
Vector3[] corners =
{
boundsCenter + new Vector3(-extents.x, -extents.y, -extents.z),
boundsCenter + new Vector3(-extents.x, -extents.y, extents.z),
boundsCenter + new Vector3(-extents.x, extents.y, -extents.z),
boundsCenter + new Vector3(-extents.x, extents.y, extents.z),
boundsCenter + new Vector3(extents.x, -extents.y, -extents.z),
boundsCenter + new Vector3(extents.x, -extents.y, extents.z),
boundsCenter + new Vector3(extents.x, extents.y, -extents.z),
boundsCenter + new Vector3(extents.x, extents.y, extents.z)
};
float minX = float.MaxValue;
float minY = float.MaxValue;
float maxX = float.MinValue;
float maxY = float.MinValue;
bool found = false;
for (int i = 0; i < corners.Length; i++)
{
Vector3 screenPoint = activeCamera.WorldToScreenPoint(corners[i]);
if (screenPoint.z < 0f) continue;
minX = Mathf.Min(minX, screenPoint.x);
minY = Mathf.Min(minY, screenPoint.y);
maxX = Mathf.Max(maxX, screenPoint.x);
maxY = Mathf.Max(maxY, screenPoint.y);
found = true;
}
if (!found) return false;
minX -= ScreenHitPadding;
minY -= ScreenHitPadding;
maxX += ScreenHitPadding;
maxY += ScreenHitPadding;
screenRect = Rect.MinMaxRect(minX, minY, maxX, maxY);
center = screenRect.center;
return true;
}
private bool IsNearestClickable(Camera activeCamera, Vector2 pointer, float myDistance)
{
int myId = GetInstanceID();
for (int i = 0; i < ActiveHandlers.Count; i++)
{
SplintObjectClickHandler other = ActiveHandlers[i];
if (other == null || other == this) continue;
if (!other.CanBeClicked()) continue;
float otherDistance;
if (!other.TryGetPointerDistance(activeCamera, pointer, out otherDistance)) continue;
if (otherDistance < myDistance) return false;
if (Mathf.Approximately(otherDistance, myDistance) && other.GetInstanceID() < myId) return false;
}
return true;
}
private float DistanceToRect(Vector2 point, Rect rect)
{
if (rect.Contains(point)) return 0f;
float closestX = Mathf.Clamp(point.x, rect.xMin, rect.xMax);
float closestY = Mathf.Clamp(point.y, rect.yMin, rect.yMax);
return Vector2.Distance(point, new Vector2(closestX, closestY));
}
private void SetRenderersVisible(bool visible)
{
Renderer[] renderers = GetComponentsInChildren<Renderer>(true);
for (int i = 0; i < renderers.Length; i++)
{
if (_outlineMesh != null && renderers[i] == _outlineMesh) continue;
renderers[i].enabled = visible;
}
}
private System.Collections.IEnumerator BlinkOutline()
{
yield return new WaitForSeconds(0.4f);
while (true)
{
if (_outlineMesh != null) _outlineMesh.enabled = !_outlineMesh.enabled;
yield return new WaitForSeconds(0.4f);
}
}
}