243 lines
8.5 KiB
C#
243 lines
8.5 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.AddressableAssets;
|
|
using UnityEngine.ResourceManagement.AsyncOperations;
|
|
using TMPro;
|
|
using DG.Tweening; // cần DOTween
|
|
using BrewMonster.Scripts.UI.GamePlay;
|
|
public enum ImageResType
|
|
{
|
|
IMG_POPUPNUM = 0,
|
|
IMG_HITMISSED,
|
|
IMG_FACTION,
|
|
IMG_PATEQUEST,
|
|
IMG_LEVELUP,
|
|
IMG_GOTEXP,
|
|
IMG_GOTMONEY,
|
|
IMG_DEADLYSTRIKE,
|
|
IMG_GOTSP,
|
|
IMG_INVALIDHIT,
|
|
IMG_TEAMLEADER,
|
|
IMG_BOOTHBAR,
|
|
IMG_HPWARN,
|
|
IMG_MPWARN,
|
|
IMG_RETORT,
|
|
IMG_IMMUNE,
|
|
IMG_TEAMMATE,
|
|
IMG_PKSTATE,
|
|
IMG_GMFLAG,
|
|
IMG_ATTACKLOSE,
|
|
IMG_SUCCESS,
|
|
IMG_REBOUND,
|
|
IMG_BEAT_BACK,
|
|
IMG_ADD,
|
|
IMG_DODGE_DEBUFF,
|
|
IMG_KING,
|
|
NUM_IMAGE,
|
|
}
|
|
public class FLoatingTextManager : MonoBehaviour
|
|
{
|
|
public static FLoatingTextManager Instance { get; private set; }
|
|
|
|
[Header("Prefab")]
|
|
[SerializeField] private AUIFloatTextIcon floatTextIconPrefab;
|
|
|
|
[Header("Settings")]
|
|
[SerializeField] private int poolSize = 20;
|
|
[SerializeField] private Vector3 offset = new Vector3(0, 2f, 0);
|
|
[SerializeField] private float riseDistance = 1.5f;
|
|
[SerializeField] private float riseDuration = 0.8f;
|
|
/// <summary>
|
|
/// Minimum spacing between floating texts for the same source object (seconds).
|
|
/// 同一来源物体连续飘字的最小间隔(秒)。
|
|
/// </summary>
|
|
[SerializeField] private float staggerIntervalSeconds = 0.3f;
|
|
|
|
private readonly Queue<AUIFloatTextIcon> pool = new();
|
|
/// <summary>
|
|
/// Per-source next allowed show time (Unity instance ID → Time.time).
|
|
/// </summary>
|
|
private readonly Dictionary<int, float> _nextFloatingTextTimeBySourceId = new();
|
|
|
|
[Header("Sprite List")]
|
|
[SerializeField] private Dictionary<ImageResType, Sprite> imageDic = new Dictionary<ImageResType, Sprite>();
|
|
|
|
/// <summary>
|
|
/// Keeps Addressables handles alive so sprites in imageDic are not released.
|
|
/// 保留 Addressables 句柄,避免已加载的 Sprite 被卸载。
|
|
/// </summary>
|
|
private readonly List<AsyncOperationHandle<Sprite>> _spriteLoadHandles = new List<AsyncOperationHandle<Sprite>>();
|
|
|
|
private void Awake()
|
|
{
|
|
// Singleton
|
|
if (Instance != null && Instance != this)
|
|
{
|
|
Destroy(gameObject);
|
|
return;
|
|
}
|
|
Instance = this;
|
|
DontDestroyOnLoad(gameObject);
|
|
|
|
// Tạo sẵn pool
|
|
for (int i = 0; i < poolSize; i++)
|
|
{
|
|
var textObj = Instantiate(floatTextIconPrefab, transform);
|
|
textObj.gameObject.SetActive(false);
|
|
pool.Enqueue(textObj);
|
|
}
|
|
LoadAllImages();
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
foreach (var h in _spriteLoadHandles)
|
|
{
|
|
if (h.IsValid())
|
|
Addressables.Release(h);
|
|
}
|
|
_spriteLoadHandles.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gọi để spawn text damage
|
|
/// </summary>
|
|
/// <param name="sourceForStagger">If set, multiple calls for this object are spaced by <see cref="staggerIntervalSeconds"/>.</param>
|
|
public void ShowText(Vector3 worldPos, int damage, Color color = default, float scale = 1f, ImageResType imageResType = ImageResType.NUM_IMAGE, UnityEngine.Object sourceForStagger = null)
|
|
{
|
|
float delay = 0f;
|
|
UnityEngine.Object staggerSource = sourceForStagger;
|
|
if (staggerSource != null)
|
|
{
|
|
int key = staggerSource.GetInstanceID();
|
|
float now = Time.time;
|
|
if (!_nextFloatingTextTimeBySourceId.TryGetValue(key, out float nextSlot))
|
|
nextSlot = now;
|
|
float showAt = Mathf.Max(now, nextSlot);
|
|
delay = showAt - now;
|
|
_nextFloatingTextTimeBySourceId[key] = showAt + staggerIntervalSeconds;
|
|
}
|
|
|
|
void DoShow()
|
|
{
|
|
if (staggerSource != null && !staggerSource)
|
|
return;
|
|
|
|
var text = GetFromPool();
|
|
var imageShow = imageResType == ImageResType.NUM_IMAGE ? null : imageDic[imageResType];
|
|
if (damage > 0)
|
|
text.Show(worldPos, damage.ToString(), color, scale, riseDistance, riseDuration, imageShow, () => ReturnToPool(text));
|
|
else
|
|
text.Show(worldPos, "", color, scale, riseDistance, riseDuration, imageShow, () => ReturnToPool(text));
|
|
}
|
|
|
|
if (delay <= 0f)
|
|
DoShow();
|
|
else
|
|
DOVirtual.DelayedCall(delay, DoShow, false).SetTarget(this);
|
|
}
|
|
|
|
private AUIFloatTextIcon GetFromPool()
|
|
{
|
|
if (pool.Count > 0)
|
|
{
|
|
return pool.Dequeue();
|
|
}
|
|
|
|
// Nếu hết pool, tạo thêm
|
|
var text = Instantiate(floatTextIconPrefab, transform);
|
|
text.gameObject.SetActive(false);
|
|
return text;
|
|
}
|
|
|
|
private void ReturnToPool(AUIFloatTextIcon text)
|
|
{
|
|
pool.Enqueue(text);
|
|
}
|
|
public bool LoadAllImages()
|
|
{
|
|
LoadImage(ImageResType.IMG_HITMISSED, "InGame/未命中.tga");
|
|
LoadImage(ImageResType.IMG_LEVELUP, "InGame/升级了.tga");
|
|
LoadImage(ImageResType.IMG_GOTEXP, "InGame/经验.tga");
|
|
LoadImage(ImageResType.IMG_GOTMONEY, "InGame/金钱.tga");
|
|
LoadImage(ImageResType.IMG_DEADLYSTRIKE, "InGame/爆击.tga");
|
|
LoadImage(ImageResType.IMG_GOTSP, "InGame/元神.tga");
|
|
LoadImage(ImageResType.IMG_INVALIDHIT, "InGame/无效.tga");
|
|
//LoadImage(ImageResType.IMG_TEAMLEADER, "Window/LeaderMark.tga");
|
|
LoadImage(ImageResType.IMG_HPWARN, "InGame/hp_warn.tga");
|
|
LoadImage(ImageResType.IMG_MPWARN, "InGame/mp_warn.tga");
|
|
LoadImage(ImageResType.IMG_RETORT, "InGame/反震.tga");
|
|
LoadImage(ImageResType.IMG_IMMUNE, "InGame/免疫.tga");
|
|
//LoadImage(ImageResType.IMG_TEAMMATE, "Window/Teammate.tga");
|
|
LoadImage(ImageResType.IMG_PKSTATE, "InGame/PK状态标记.tga");
|
|
LoadImage(ImageResType.IMG_GMFLAG, "InGame/GM标志.dds");
|
|
LoadImage(ImageResType.IMG_ATTACKLOSE, "InGame/失败.tga");
|
|
LoadImage(ImageResType.IMG_SUCCESS, "InGame/成功.tga");
|
|
LoadImage(ImageResType.IMG_REBOUND, "InGame/复仇惩戒.tga");
|
|
LoadImage(ImageResType.IMG_BEAT_BACK, "InGame/复仇镜像.tga");
|
|
LoadImage(ImageResType.IMG_ADD, "InGame/吸血.tga");
|
|
LoadImage(ImageResType.IMG_DODGE_DEBUFF, "InGame/状态闪避.tga");
|
|
//LoadImage(ImageResType.IMG_KING, "King/皇冠图标.tga");
|
|
return false;
|
|
}
|
|
private void LoadImage(ImageResType type, string path)
|
|
{
|
|
if (string.IsNullOrEmpty(path))
|
|
return;
|
|
|
|
// Same normalization as skill/gfx paths (PC backslashes → Addressables-style slashes).
|
|
// 与技能 gfx 路径一致:反斜杠转为斜杠,便于与 Addressables 地址对齐。
|
|
string normalized = path.Replace('\\', '/');
|
|
IReadOnlyList<string> candidates = BuildSpriteAddressCandidates(normalized);
|
|
|
|
foreach (string address in candidates)
|
|
{
|
|
var handle = Addressables.LoadAssetAsync<Sprite>(address);
|
|
handle.WaitForCompletion();
|
|
|
|
if (handle.Status == AsyncOperationStatus.Succeeded && handle.Result != null)
|
|
{
|
|
imageDic[type] = handle.Result;
|
|
_spriteLoadHandles.Add(handle);
|
|
return;
|
|
}
|
|
|
|
if (handle.IsValid())
|
|
Addressables.Release(handle);
|
|
|
|
}
|
|
Debug.Log($"[FLoatingTextManager] Sprite load failed for {type}. Addressables keys must match the catalog exactly; " +
|
|
$"tried: {string.Join("; ", candidates)}. " +
|
|
$"Similar file names are not auto-resolved (unlike fuzzy file search).");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Addressables uses exact string addresses. Imported UI art may drop .tga/.dds in the key; try those variants.
|
|
/// Addressables 为精确字符串地址;导入后地址可能没有 .tga/.dds 等后缀,依次尝试这些候选。
|
|
/// </summary>
|
|
private static List<string> BuildSpriteAddressCandidates(string path)
|
|
{
|
|
var list = new List<string> { path };
|
|
|
|
void addUnique(string s)
|
|
{
|
|
if (!string.IsNullOrEmpty(s) && !list.Contains(s))
|
|
list.Add(s);
|
|
}
|
|
|
|
string[] sourceExtensions = { ".tga", ".dds", ".bmp", ".png", ".jpg", ".jpeg" };
|
|
foreach (string ext in sourceExtensions)
|
|
{
|
|
if (path.EndsWith(ext, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
addUnique(path.Substring(0, path.Length - ext.Length));
|
|
break;
|
|
}
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
}
|