using System.Collections; using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.UI; namespace BrewMonster { public enum IconTaskType { QI_NONE = -1, QI_OUT = 0, // task tu chan (nhan) QI_IN = 1, // task tu chan (hoan thanh) QI_OUT_N = 2, // chua du dieu kien nhan task QI_IN_N = 3, // task nhan nhung chua lam (chua xong) QI_OUT_K = 4, // chua biet QI_IN_K = 5, // chua biet QI_OUT_TYPE1 = 6, // task bang hoi (nhan) QI_IN_TYPE1 = 7, // task bang hoi (hoan thanh) QI_OUT_TYPE2 = 8, // chua biet QI_IN_TYPE2 = 9, // chua biet QI_OUT_TYPE3 = 10, // task daily (nhan) QI_IN_TYPE3 = 11, // task daily (hoan thanh) QI_OUT_TYPE4 = 12, // task chinh (nhan) QI_IN_TYPE4 = 13, // task chinh (hoan thanh) } [RequireComponent(typeof(NameplateWorldAnchor))] public class UINPC : MonoBehaviour { [SerializeField] private TextMeshProUGUI _nameText; [SerializeField] private TextMeshProUGUI _healthText; [SerializeField] private Image _healthImage; [Header("List Icon Task")] [SerializeField] private GameObject _iconTaskMain; [SerializeField] private List _listIconTask; [Header("World Nameplate")] [SerializeField] private Transform _canvasRoot; private NameplateWorldAnchor _nameplateAnchor; private Image _cachedIconImage; private Coroutine _initialNameplateRoutine; private void Awake() { CacheRefs(); _nameplateAnchor = GetComponent(); if (_iconTaskMain != null) { _cachedIconImage = _iconTaskMain.GetComponent(); if (_cachedIconImage == null) { BMLogger.LogError($"[UINPC] _iconTaskMain doesn't have Image component!"); } } } private void Start() { _initialNameplateRoutine = StartCoroutine(CoApplyInitialNameplateDeferred(maxFrames: 120)); } /// /// Update nameplate position every frame so it follows skeleton animations (e.g. floating/flying NPCs or monsters). /// Only active after the initial anchor coroutine has resolved (i.e. _initialNameplateRoutine == null). /// 每帧更新名牌位置,使其跟随骨骼动画(如浮空/飞行的NPC或怪物,例如小星星)。 /// 仅在初始锚点协程完成后(_initialNameplateRoutine == null)生效。 /// private void LateUpdate() { if (_initialNameplateRoutine != null) return; if (_canvasRoot == null || _nameplateAnchor == null) return; if (_nameplateAnchor.TryGetWorldPosition(out var worldPos)) ApplyCanvasRootLocalPosition(worldPos); } /// /// Call after NPC model async load (e.g. ) so SMR bounds exist before anchoring. /// 在NPC模型异步加载完成后调用(如 ),以便SMR包围盒已存在再定位名牌。 /// public void RefreshWorldNameplatePosition() { if (_initialNameplateRoutine != null) { StopCoroutine(_initialNameplateRoutine); _initialNameplateRoutine = null; } _initialNameplateRoutine = StartCoroutine(CoApplyInitialNameplateDeferred(maxFrames: 45)); } /// /// World position for the nameplate root is applied once SMR bounds exist, or after timeout with root fallback (then may log). /// with logNpcRootFallback: false while retrying so early frames do not spam errors. /// 名牌根节点世界坐标:优先等合并SMR包围盒就绪后再设一次;超时则用根回退(此时才打LogError)。 /// private IEnumerator CoApplyInitialNameplateDeferred(int maxFrames) { if (_canvasRoot == null) { CacheRefs(); } if (_canvasRoot == null) { yield break; } if (_nameplateAnchor == null) { BMLogger.LogError( "[Cuong] [UINPC] ApplyInitialCanvasRootPosition: NameplateWorldAnchor missing. " + "Expected formula: _canvasRoot.position = anchor.TryGetWorldPosition(out worldPos) ? worldPos : unchanged."); _initialNameplateRoutine = null; yield break; } for (var i = 0; i < maxFrames; i++) { if (_nameplateAnchor.TryGetWorldPosition(out var worldPos, out var fromSmr, logNpcRootFallback: false) && fromSmr) { ApplyCanvasRootLocalPosition(worldPos); // Model + SMR ready: scan bones and cache the highest one for per-frame LateUpdate tracking. // 模型和SMR就绪:扫描骨骼并缓存最高骨骼,供LateUpdate每帧跟踪。 _nameplateAnchor.RefreshNpcTopBone(); _initialNameplateRoutine = null; yield break; } yield return null; } // Timeout: set position from whatever is available (hook / SMR / root fallback), then still cache top bone. // 超时:使用当前可用方式设置位置(挂点/SMR/根节点回退),仍然缓存最高骨骼。 if (_nameplateAnchor.TryGetWorldPosition(out var finalPos, out _, logNpcRootFallback: true)) { ApplyCanvasRootLocalPosition(finalPos); } else { BMLogger.LogError( "[Cuong] [UINPC] ApplyInitialCanvasRootPosition: TryGetWorldPosition returned false (no CECPlayer/CECNPC parent?). " + "Formula when OK: _canvasRoot.position = worldPos. " + "NPC with SMR bounds: worldPos = (combinedBounds.center.x, combinedBounds.max.y, combinedBounds.center.z) + Vector3.up * extraWorldYOffset. " + "NPC fallback: worldPos = hostNpc.transform.position + Vector3.up * (fallbackHeightAboveRoot + extraWorldYOffset)."); } _nameplateAnchor.RefreshNpcTopBone(); _initialNameplateRoutine = null; } private void ApplyCanvasRootLocalPosition(Vector3 worldPos) { var parent = _canvasRoot.parent; if (parent == null) { _canvasRoot.position = worldPos; return; } _canvasRoot.localPosition = parent.InverseTransformPoint(worldPos); } public void SetName(string name) { _nameText.text = name; } public void SetHealthImage(float health) { if(_healthImage != null) _healthImage.fillAmount = health; } public void SetHealthText(string healthText) { if(_healthText != null) _healthText.text = healthText; } public void SetTaskIcon(IconTaskType iconType) { if (_iconTaskMain == null || _cachedIconImage == null) { return; } int iconIndex = (int)iconType; if (iconIndex >= 0 && _listIconTask != null && iconIndex < _listIconTask.Count) { _cachedIconImage.sprite = _listIconTask[iconIndex]; _iconTaskMain.SetActive(true); } else { _iconTaskMain.SetActive(false); } } public void SetTaskIconMain(bool isShow) { if (_iconTaskMain != null) _iconTaskMain.SetActive(isShow); } private void CacheRefs() { if (_canvasRoot == null) { _canvasRoot = transform; } } } }