333 lines
11 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|