Improve exam flow and WebGL build
This commit is contained in:
@@ -0,0 +1,332 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user