Files
test/Assets/PerfectWorld/Scripts/UI/GamePlay/UINPC.cs
T
2026-05-20 14:01:38 +07:00

216 lines
8.2 KiB
C#

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<Sprite> _listIconTask;
[Header("World Nameplate")]
[SerializeField] private Transform _canvasRoot;
private NameplateWorldAnchor _nameplateAnchor;
private Image _cachedIconImage;
private Coroutine _initialNameplateRoutine;
private void Awake()
{
CacheRefs();
_nameplateAnchor = GetComponent<NameplateWorldAnchor>();
if (_iconTaskMain != null)
{
_cachedIconImage = _iconTaskMain.GetComponent<Image>();
if (_cachedIconImage == null)
{
BMLogger.LogError($"[UINPC] _iconTaskMain doesn't have Image component!");
}
}
}
private void Start()
{
_initialNameplateRoutine = StartCoroutine(CoApplyInitialNameplateDeferred(maxFrames: 120));
}
/// <summary>
/// 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)生效。
/// </summary>
private void LateUpdate()
{
if (_initialNameplateRoutine != null)
return;
if (_canvasRoot == null || _nameplateAnchor == null)
return;
if (_nameplateAnchor.TryGetWorldPosition(out var worldPos))
ApplyCanvasRootLocalPosition(worldPos);
}
/// <summary>
/// Call after NPC model async load (e.g. <see cref="CECNPC.QueueLoadNPCModel"/>) so SMR bounds exist before anchoring.
/// 在NPC模型异步加载完成后调用(如 <see cref="CECNPC.QueueLoadNPCModel"/>),以便SMR包围盒已存在再定位名牌。
/// </summary>
public void RefreshWorldNameplatePosition()
{
if (_initialNameplateRoutine != null)
{
StopCoroutine(_initialNameplateRoutine);
_initialNameplateRoutine = null;
}
_initialNameplateRoutine = StartCoroutine(CoApplyInitialNameplateDeferred(maxFrames: 45));
}
/// <summary>
/// World position for the nameplate root is applied once SMR bounds exist, or after timeout with root fallback (then may log).
/// <see cref="NameplateWorldAnchor.TryGetWorldPosition"/> with <c>logNpcRootFallback: false</c> while retrying so early frames do not spam errors.
/// 名牌根节点世界坐标:优先等合并SMR包围盒就绪后再设一次;超时则用根回退(此时才打LogError)。
/// </summary>
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;
}
}
}
}