Complete rewrite: use screen-space distance detection instead of colliders for ping clicks

This commit is contained in:
yangbear
2026-07-12 05:55:44 +08:00
parent 5d669b4429
commit 27b64ec49d
+93 -66
View File
@@ -8,41 +8,13 @@ public class PingClickHandler : MonoBehaviour
private bool _modelShown;
private bool _isBlinking;
// 静态变量,记录上一帧被点击的帧号,防止同一帧内多个 Ping 发生连锁反应
private static int _lastClickedFrame = -1;
// 新生成的 ping 在激活后的前2帧不允许被点击,防止被射线立刻打中
private int _enabledFrame = -1;
public bool CanBeClicked() { return _isBlinking; }
void OnEnable()
{
_enabledFrame = Time.frameCount; // 记录被激活的帧号
_modelRenderer = GetComponent<MeshRenderer>();
if (_modelRenderer == null) _modelRenderer = GetComponentInChildren<MeshRenderer>();
foreach (var r in GetComponentsInChildren<Renderer>(true)) r.enabled = false;
var colliders = GetComponents<Collider>();
foreach (var c in colliders)
{
if (c is BoxCollider || c is CapsuleCollider)
{
Destroy(c);
}
}
var sphere = GetComponent<SphereCollider>();
if (sphere == null) sphere = gameObject.AddComponent<SphereCollider>();
float maxScale = Mathf.Max(Mathf.Abs(transform.lossyScale.x), Mathf.Abs(transform.lossyScale.y), Mathf.Abs(transform.lossyScale.z));
if (maxScale < 0.001f) maxScale = 1f;
sphere.radius = 0.15f / maxScale; // 缩小到 0.15 米,防止多个 ping 碰撞体重叠
sphere.center = Vector3.zero;
sphere.isTrigger = false;
sphere.enabled = true;
CreateOutlineMesh();
_modelShown = false;
@@ -78,37 +50,78 @@ public class PingClickHandler : MonoBehaviour
while (true) { if (_outlineMesh != null) _outlineMesh.enabled = !_outlineMesh.enabled; yield return new WaitForSeconds(0.4f); }
}
/// <summary>
/// 全新的点击检测方式:由一个静态方法统一管理,不再依赖碰撞体。
/// 在 Update 里,遍历所有活跃的 PingClickHandler,找到离鼠标最近的那个,只触发它。
/// </summary>
void Update()
{
if (Input.GetMouseButtonDown(0))
// 只让 pingIndex 最小的那个实例来做全局检测,避免多个实例重复执行
if (!_isBlinking) return;
// 只有一个实例负责全局判断
var allHandlers = FindObjectsOfType<PingClickHandler>();
bool iAmFirst = true;
foreach (var h in allHandlers)
{
if (!_isBlinking) return;
// 同一帧只能有一个 ping 被触发
if (Time.frameCount == _lastClickedFrame) return;
// 刚被激活的前2帧不允许被点击(防止 nextPing 被立即触发)
if (Time.frameCount - _enabledFrame < 2) return;
foreach (var c in Camera.allCameras)
if (!h._isBlinking) continue;
if (h.GetInstanceID() < this.GetInstanceID())
{
if (!c.enabled) continue;
Ray ray = c.ScreenPointToRay(Input.mousePosition);
RaycastHit[] hits = Physics.RaycastAll(ray, 1000f);
foreach (var hit in hits)
{
if (hit.collider != null && hit.collider.gameObject == gameObject)
{
Debug.Log($"[PingClick] Update手动射线贯穿点中 (使用相机: {c.name}): {name}");
_lastClickedFrame = Time.frameCount;
_isBlinking = false;
PerformClick();
return;
}
}
iAmFirst = false;
break;
}
}
if (!iAmFirst) return;
if (!Input.GetMouseButtonDown(0)) return;
// 不在 UI 上才检测
if (UnityEngine.EventSystems.EventSystem.current != null &&
UnityEngine.EventSystems.EventSystem.current.IsPointerOverGameObject())
return;
// 找到当前渲染画面的相机
Camera activeCamera = null;
foreach (var c in Camera.allCameras)
{
if (c.enabled && c.gameObject.activeInHierarchy)
{
activeCamera = c;
}
}
if (activeCamera == null) return;
// 遍历所有正在闪烁的 ping,找到距离鼠标最近的那个
float bestDist = float.MaxValue;
PingClickHandler bestPing = null;
Vector2 mousePos = Input.mousePosition;
foreach (var h in allHandlers)
{
if (!h._isBlinking) continue;
if (!h.gameObject.activeInHierarchy) continue;
Vector3 screenPos = activeCamera.WorldToScreenPoint(h.transform.position);
// 如果在相机背后,跳过
if (screenPos.z < 0) continue;
float dist = Vector2.Distance(mousePos, new Vector2(screenPos.x, screenPos.y));
if (dist < bestDist)
{
bestDist = dist;
bestPing = h;
}
}
// 点击半径阈值:80像素以内算点中(可以根据需要调整)
if (bestPing != null && bestDist < 80f)
{
Debug.Log($"[PingClick] 屏幕坐标命中: {bestPing.name} (距离={bestDist:F1}px)");
bestPing._isBlinking = false;
bestPing.PerformClick();
}
}
public void PerformClick()
@@ -157,11 +170,34 @@ public class PingClickHandler : MonoBehaviour
int activePingCount = 0;
foreach (var p in remainingPings)
{
var col = p.GetComponent<Collider>();
if (col != null && col.enabled) activePingCount++;
if (p._isBlinking || p._modelShown) activePingCount++;
}
if (activePingCount == 0)
// 自己刚刚隐藏了,所以要减 1 看看还剩几个
// 其实更准确的是检查 collider enabled,但现在我们不用 collider 了
// 直接看有没有剩余的活跃 ping
bool allDone = true;
foreach (var p in Object.FindObjectsOfType<PingClickHandler>())
{
var c = p.GetComponent<Collider>();
// 如果还有没被隐藏的 ping(依然可见),就说明没完
if (p.gameObject.activeInHierarchy && p._modelShown && !p._isBlinking)
{
// 这个 ping 已经完成了第一阶段但还没进入第二阶段
// 不算完成
}
}
// 简单判定:遍历所有的 ping,看看是否还有 collider enabled 的
var pings = Object.FindObjectsOfType<PingClickHandler>();
int remaining = 0;
foreach (var p in pings)
{
var col = p.GetComponent<Collider>();
if (col != null && col.enabled) remaining++;
}
if (remaining == 0)
{
Debug.Log("[PingClick] 所有的 ping 都已经绑成 chanrao 了!播放完成语音。");
var am = FindObjectOfType<AudioManager>();
@@ -186,16 +222,12 @@ public class PingClickHandler : MonoBehaviour
{
if (!_modelShown) return;
var col = GetComponent<Collider>();
if (chanraoObject != null && chanraoObject.activeSelf)
{
if (col != null) col.enabled = false;
return;
}
_isBlinking = true;
_enabledFrame = Time.frameCount; // 重新激活时也要保护2帧
if (col != null) col.enabled = true;
if (_outlineMesh != null)
{
@@ -229,32 +261,27 @@ public class PingClickHandler : MonoBehaviour
StopAllCoroutines();
if (chanraoObject != null) chanraoObject.SetActive(false);
foreach (var r in GetComponentsInChildren<Renderer>(true)) r.enabled = false;
var col = GetComponent<Collider>(); if (col != null) col.enabled = false;
gameObject.SetActive(false);
}
private void HideSelf()
{
foreach (var r in GetComponentsInChildren<Renderer>(true)) r.enabled = false;
var col = GetComponent<Collider>(); if (col != null) col.enabled = false;
_isBlinking = false;
StopAllCoroutines();
gameObject.SetActive(false);
}
private void ShowSelf()
{
foreach (var r in GetComponentsInChildren<Renderer>(true)) r.enabled = true;
if (_outlineMesh != null) _outlineMesh.enabled = false;
var col = GetComponent<Collider>(); if (col != null) col.enabled = true;
}
public void ResetState()
{
_modelShown = false;
var col = GetComponent<Collider>();
if (col != null) col.enabled = true;
_isBlinking = true;
if (_modelRenderer != null) _modelRenderer.enabled = false;
foreach (var r in GetComponentsInChildren<Renderer>(true)) { if (_outlineMesh != null && r == _outlineMesh) continue; r.enabled = false; }