feat: add player avatar selection UI
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
@@ -10,19 +12,34 @@ namespace XGame
|
||||
{
|
||||
private const string HudPrefabPath = "Assets/Game/Art/UI/Prefab/UI_PlayerProfileHud.prefab";
|
||||
private const string PanelPrefabPath = "Assets/Game/Art/UI/Prefab/UI_PlayerProfilePanel.prefab";
|
||||
private const string AvatarPickerPrefabPath = "Assets/Game/Art/UI/Prefab/UI_PlayerAvatarPicker.prefab";
|
||||
private const float PreviewAvatarFrameFill = 0.58f;
|
||||
|
||||
private GameObject hudView;
|
||||
private GameObject detailView;
|
||||
private GameObject avatarPickerView;
|
||||
private bool openingHud;
|
||||
private bool openingDetail;
|
||||
private bool openingAvatarPicker;
|
||||
private int openVersion;
|
||||
private int avatarPickerVersion;
|
||||
private int pendingAvatarId;
|
||||
|
||||
private TMP_Text hudNicknameText;
|
||||
private TMP_Text hudLevelText;
|
||||
private TMP_Text hudExpText;
|
||||
private Image hudAvatarImage;
|
||||
private Image detailAvatarImage;
|
||||
private RectTransform hudExpFill;
|
||||
private RectTransform hudExpTrack;
|
||||
|
||||
private Image avatarPickerPreviewImage;
|
||||
private TMP_Text avatarPickerHintText;
|
||||
private readonly Dictionary<int, Sprite> avatarSpriteCache = new Dictionary<int, Sprite>();
|
||||
private readonly HashSet<int> loadingAvatarIds = new HashSet<int>();
|
||||
private readonly Dictionary<int, Image> avatarPickerAvatarImages = new Dictionary<int, Image>();
|
||||
private readonly Dictionary<int, GameObject> avatarPickerSelectedStates = new Dictionary<int, GameObject>();
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
PlayerProfileStore.Changed += OnProfileChanged;
|
||||
@@ -73,6 +90,8 @@ namespace XGame
|
||||
openVersion++;
|
||||
openingHud = false;
|
||||
openingDetail = false;
|
||||
openingAvatarPicker = false;
|
||||
avatarPickerVersion++;
|
||||
if (hudView != null)
|
||||
{
|
||||
Destroy(hudView);
|
||||
@@ -81,6 +100,10 @@ namespace XGame
|
||||
{
|
||||
Destroy(detailView);
|
||||
}
|
||||
if (avatarPickerView != null)
|
||||
{
|
||||
Destroy(avatarPickerView);
|
||||
}
|
||||
hudView = null;
|
||||
detailView = null;
|
||||
hudNicknameText = null;
|
||||
@@ -88,6 +111,16 @@ namespace XGame
|
||||
hudExpText = null;
|
||||
hudExpFill = null;
|
||||
hudExpTrack = null;
|
||||
hudAvatarImage = null;
|
||||
detailAvatarImage = null;
|
||||
avatarPickerView = null;
|
||||
avatarPickerPreviewImage = null;
|
||||
avatarPickerHintText = null;
|
||||
pendingAvatarId = 0;
|
||||
avatarPickerAvatarImages.Clear();
|
||||
avatarPickerSelectedStates.Clear();
|
||||
loadingAvatarIds.Clear();
|
||||
avatarSpriteCache.Clear();
|
||||
}
|
||||
|
||||
private void OnProfileChanged(PlayerProfileData profile)
|
||||
@@ -110,6 +143,8 @@ namespace XGame
|
||||
hudNicknameText = FindComponent<TMP_Text>(hudView, "Txt_Nickname");
|
||||
hudLevelText = FindComponent<TMP_Text>(hudView, "Txt_Level");
|
||||
hudExpText = FindComponent<TMP_Text>(hudView, "Txt_Exp");
|
||||
hudAvatarImage = FindComponent<Image>(hudView, "Img_AvatarIcon");
|
||||
ArrangeAvatarIconBehindFrame(hudView);
|
||||
hudExpTrack = FindRect(hudView, "Img_ExpBg");
|
||||
hudExpFill = FindRect(hudView, "Img_ExpFill");
|
||||
}
|
||||
@@ -136,6 +171,7 @@ namespace XGame
|
||||
if (hudNicknameText != null) hudNicknameText.text = profile.Nickname;
|
||||
if (hudLevelText != null) hudLevelText.text = "Lv." + profile.Lv;
|
||||
if (hudExpText != null) hudExpText.text = profile.Exp + "/" + profile.RequiredExp;
|
||||
RefreshAvatarImage(hudAvatarImage, profile.AvatarId);
|
||||
|
||||
if (hudExpTrack != null && hudExpFill != null)
|
||||
{
|
||||
@@ -170,6 +206,8 @@ namespace XGame
|
||||
|
||||
detailView = Instantiate(prefab);
|
||||
UIManager.AttachTo(UILayer.Dialog, detailView);
|
||||
detailAvatarImage = FindComponent<Image>(detailView, "Img_AvatarIcon");
|
||||
ArrangeAvatarIconBehindFrame(detailView);
|
||||
BindDetailEvents();
|
||||
RefreshDetail(PlayerProfileStore.Current);
|
||||
}
|
||||
@@ -178,6 +216,12 @@ namespace XGame
|
||||
{
|
||||
BindCloseButton("Btn_Close");
|
||||
BindCloseButton("Img_Mask");
|
||||
Button avatarButton = FindComponent<Button>(detailView, "Img_AvatarFrame");
|
||||
if (avatarButton != null)
|
||||
{
|
||||
avatarButton.onClick.RemoveAllListeners();
|
||||
avatarButton.onClick.AddListener(OpenAvatarPicker);
|
||||
}
|
||||
}
|
||||
|
||||
private void BindCloseButton(string nodeName)
|
||||
@@ -193,6 +237,7 @@ namespace XGame
|
||||
|
||||
private void CloseDetail()
|
||||
{
|
||||
CloseAvatarPicker();
|
||||
if (detailView != null)
|
||||
{
|
||||
Destroy(detailView);
|
||||
@@ -208,6 +253,7 @@ namespace XGame
|
||||
}
|
||||
|
||||
profile = profile ?? PlayerProfileStore.Current;
|
||||
ArrangeAvatarIconBehindFrame(detailView);
|
||||
SetText("Txt_Nickname", profile.Nickname);
|
||||
SetText("Txt_Uid", "UID: " + profile.Uid);
|
||||
SetText("Txt_RoleType", profile.RoleType.ToString());
|
||||
@@ -221,6 +267,358 @@ namespace XGame
|
||||
SetText("Txt_Ap", profile.Ap.ToString());
|
||||
SetText("Txt_AtkSpeed", profile.AtkSpeed.ToString("0.##"));
|
||||
SetText("Txt_MoveSpeed", profile.MoveSpeed.ToString("0.##"));
|
||||
RefreshAvatarImage(detailAvatarImage, profile.AvatarId);
|
||||
}
|
||||
|
||||
private void ArrangeAvatarIconBehindFrame(GameObject root)
|
||||
{
|
||||
ArrangeGraphicBehindFrame(root, "Img_AvatarFrame", "Img_AvatarIcon");
|
||||
}
|
||||
|
||||
private void ArrangeGraphicBehindFrame(GameObject root, string frameName, string graphicName)
|
||||
{
|
||||
Transform frame = FindTransform(root, frameName);
|
||||
Transform graphic = FindTransform(root, graphicName);
|
||||
if (frame == null || graphic == null || frame.parent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RectTransform frameRect = frame as RectTransform;
|
||||
RectTransform graphicRect = graphic as RectTransform;
|
||||
Vector2 graphicSize = graphicRect == null ? Vector2.zero : graphicRect.sizeDelta;
|
||||
|
||||
if (graphic.parent != frame.parent)
|
||||
{
|
||||
graphic.SetParent(frame.parent, true);
|
||||
}
|
||||
|
||||
if (frameRect != null && graphicRect != null)
|
||||
{
|
||||
graphicRect.anchorMin = frameRect.anchorMin;
|
||||
graphicRect.anchorMax = frameRect.anchorMax;
|
||||
graphicRect.pivot = frameRect.pivot;
|
||||
graphicRect.anchoredPosition = frameRect.anchoredPosition;
|
||||
graphicRect.sizeDelta = graphicSize;
|
||||
}
|
||||
|
||||
if (graphic.GetSiblingIndex() > frame.GetSiblingIndex())
|
||||
{
|
||||
graphic.SetSiblingIndex(frame.GetSiblingIndex());
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshAvatarImage(Image image, int avatarId)
|
||||
{
|
||||
if (image == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int normalizedId = PlayerAvatarCatalog.NormalizeId(avatarId);
|
||||
if (avatarSpriteCache.TryGetValue(normalizedId, out Sprite cachedSprite))
|
||||
{
|
||||
image.sprite = cachedSprite;
|
||||
return;
|
||||
}
|
||||
|
||||
if (loadingAvatarIds.Add(normalizedId))
|
||||
{
|
||||
XWCoroutine.StartCo(CoLoadAvatarSprite(normalizedId, image));
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator CoLoadAvatarSprite(int avatarId, Image target)
|
||||
{
|
||||
Sprite sprite = null;
|
||||
if (XResLoader.m_ABMode == 1)
|
||||
{
|
||||
yield return CoCacheAvatarSpritesFromPickerPrefab();
|
||||
avatarSpriteCache.TryGetValue(avatarId, out sprite);
|
||||
}
|
||||
|
||||
if (sprite == null)
|
||||
{
|
||||
string path = PlayerAvatarCatalog.GetAssetPath(avatarId);
|
||||
UObject loaded = null;
|
||||
yield return XResLoader.coLoadRes(path, typeof(Sprite), obj => loaded = obj);
|
||||
sprite = loaded as Sprite;
|
||||
}
|
||||
|
||||
if (sprite == null)
|
||||
{
|
||||
yield return CoCacheAvatarSpritesFromPickerPrefab();
|
||||
avatarSpriteCache.TryGetValue(avatarId, out sprite);
|
||||
}
|
||||
|
||||
loadingAvatarIds.Remove(avatarId);
|
||||
|
||||
if (sprite == null)
|
||||
{
|
||||
Debug.LogError("[PlayerProfileModule] load avatar failed: " + PlayerAvatarCatalog.GetAssetPath(avatarId));
|
||||
yield break;
|
||||
}
|
||||
|
||||
avatarSpriteCache[avatarId] = sprite;
|
||||
if (PlayerAvatarCatalog.NormalizeId(PlayerProfileStore.Current.AvatarId) != avatarId)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
ApplyAvatarSprite(target, sprite);
|
||||
ApplyAvatarSprite(hudAvatarImage, sprite);
|
||||
ApplyAvatarSprite(detailAvatarImage, sprite);
|
||||
}
|
||||
|
||||
private IEnumerator CoCacheAvatarSpritesFromPickerPrefab()
|
||||
{
|
||||
if (avatarPickerView != null)
|
||||
{
|
||||
CacheAvatarSpritesFromRoot(avatarPickerView);
|
||||
yield break;
|
||||
}
|
||||
|
||||
UObject loaded = null;
|
||||
yield return XResLoader.coLoadRes(AvatarPickerPrefabPath, typeof(GameObject), obj => loaded = obj);
|
||||
CacheAvatarSpritesFromRoot(loaded as GameObject);
|
||||
}
|
||||
|
||||
private static void ApplyAvatarSprite(Image target, Sprite sprite)
|
||||
{
|
||||
if (target != null)
|
||||
{
|
||||
target.sprite = sprite;
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenAvatarPicker()
|
||||
{
|
||||
if (avatarPickerView != null || openingAvatarPicker)
|
||||
{
|
||||
RefreshAvatarPicker();
|
||||
return;
|
||||
}
|
||||
XWCoroutine.StartCo(CoOpenAvatarPicker());
|
||||
}
|
||||
|
||||
private IEnumerator CoOpenAvatarPicker()
|
||||
{
|
||||
openingAvatarPicker = true;
|
||||
int version = ++avatarPickerVersion;
|
||||
UObject loaded = null;
|
||||
yield return XResLoader.coLoadRes(AvatarPickerPrefabPath, typeof(GameObject), obj => loaded = obj);
|
||||
openingAvatarPicker = false;
|
||||
|
||||
if (version != avatarPickerVersion || detailView == null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
GameObject prefab = loaded as GameObject;
|
||||
if (prefab == null)
|
||||
{
|
||||
Debug.LogError("[PlayerProfileModule] load avatar picker prefab failed: " + AvatarPickerPrefabPath);
|
||||
yield break;
|
||||
}
|
||||
|
||||
avatarPickerView = Instantiate(prefab);
|
||||
UIManager.AttachTo(UILayer.Dialog, avatarPickerView);
|
||||
pendingAvatarId = PlayerAvatarCatalog.NormalizeId(PlayerProfileStore.Current.AvatarId);
|
||||
BindAvatarPickerEvents();
|
||||
CacheAvatarPickerControls();
|
||||
RefreshAvatarPicker();
|
||||
}
|
||||
|
||||
private void BindAvatarPickerEvents()
|
||||
{
|
||||
BindAvatarPickerButton("Img_Mask", CancelAvatarPicker);
|
||||
BindAvatarPickerButton("Btn_Close", CancelAvatarPicker);
|
||||
BindAvatarPickerButton("Btn_Cancel", CancelAvatarPicker);
|
||||
BindAvatarPickerButton("Btn_Confirm", ConfirmAvatarPicker);
|
||||
|
||||
for (int avatarId = 1; avatarId <= PlayerAvatarCatalog.Count; avatarId++)
|
||||
{
|
||||
int selectedId = avatarId;
|
||||
Button button = FindComponent<Button>(avatarPickerView, AvatarButtonName(selectedId));
|
||||
if (button == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
button.onClick.RemoveAllListeners();
|
||||
button.onClick.AddListener(() => SelectPendingAvatar(selectedId));
|
||||
}
|
||||
}
|
||||
|
||||
private void BindAvatarPickerButton(string nodeName, UnityEngine.Events.UnityAction action)
|
||||
{
|
||||
Button button = FindComponent<Button>(avatarPickerView, nodeName);
|
||||
if (button == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
button.onClick.RemoveAllListeners();
|
||||
button.onClick.AddListener(action);
|
||||
}
|
||||
|
||||
private void CacheAvatarPickerControls()
|
||||
{
|
||||
avatarPickerPreviewImage = FindComponent<Image>(avatarPickerView, "Img_PreviewAvatar");
|
||||
ArrangeGraphicBehindFrame(avatarPickerView, "Img_PreviewFrame", "Img_PreviewAvatar");
|
||||
ClampGraphicToFrameFill(avatarPickerView, "Img_PreviewFrame", "Img_PreviewAvatar", PreviewAvatarFrameFill);
|
||||
avatarPickerHintText = FindComponent<TMP_Text>(avatarPickerView, "Txt_PreviewHint");
|
||||
avatarPickerAvatarImages.Clear();
|
||||
avatarPickerSelectedStates.Clear();
|
||||
|
||||
for (int avatarId = 1; avatarId <= PlayerAvatarCatalog.Count; avatarId++)
|
||||
{
|
||||
Image avatarImage = FindComponent<Image>(avatarPickerView, AvatarImageName(avatarId));
|
||||
avatarPickerAvatarImages[avatarId] = avatarImage;
|
||||
if (avatarImage != null && avatarImage.sprite != null)
|
||||
{
|
||||
avatarSpriteCache[avatarId] = avatarImage.sprite;
|
||||
}
|
||||
|
||||
Transform selected = FindTransform(avatarPickerView, AvatarSelectedName(avatarId));
|
||||
if (selected != null)
|
||||
{
|
||||
MoveSelectedStateAboveAvatarFrame(selected);
|
||||
avatarPickerSelectedStates[avatarId] = selected.gameObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CacheAvatarSpritesFromRoot(GameObject root)
|
||||
{
|
||||
if (root == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int avatarId = 1; avatarId <= PlayerAvatarCatalog.Count; avatarId++)
|
||||
{
|
||||
Image avatarImage = FindComponent<Image>(root, AvatarImageName(avatarId));
|
||||
if (avatarImage != null && avatarImage.sprite != null)
|
||||
{
|
||||
avatarSpriteCache[avatarId] = avatarImage.sprite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void MoveSelectedStateAboveAvatarFrame(Transform selected)
|
||||
{
|
||||
if (selected == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
selected.SetAsLastSibling();
|
||||
}
|
||||
|
||||
private void ClampGraphicToFrameFill(GameObject root, string frameName, string graphicName, float fill)
|
||||
{
|
||||
Transform frame = FindTransform(root, frameName);
|
||||
Transform graphic = FindTransform(root, graphicName);
|
||||
RectTransform frameRect = frame as RectTransform;
|
||||
RectTransform graphicRect = graphic as RectTransform;
|
||||
if (frameRect == null || graphicRect == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float frameWidth = Mathf.Abs(frameRect.rect.width) > 0f ? Mathf.Abs(frameRect.rect.width) : Mathf.Abs(frameRect.sizeDelta.x);
|
||||
float frameHeight = Mathf.Abs(frameRect.rect.height) > 0f ? Mathf.Abs(frameRect.rect.height) : Mathf.Abs(frameRect.sizeDelta.y);
|
||||
float maxSide = Mathf.Min(frameWidth, frameHeight) * Mathf.Clamp01(fill);
|
||||
if (maxSide <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float currentWidth = Mathf.Abs(graphicRect.sizeDelta.x);
|
||||
float currentHeight = Mathf.Abs(graphicRect.sizeDelta.y);
|
||||
float currentSide = Mathf.Max(currentWidth, currentHeight);
|
||||
float side = currentSide > 0f ? Mathf.Min(currentSide, maxSide) : maxSide;
|
||||
graphicRect.sizeDelta = new Vector2(side, side);
|
||||
}
|
||||
|
||||
private void SelectPendingAvatar(int avatarId)
|
||||
{
|
||||
pendingAvatarId = PlayerAvatarCatalog.NormalizeId(avatarId);
|
||||
RefreshAvatarPicker();
|
||||
}
|
||||
|
||||
private void ConfirmAvatarPicker()
|
||||
{
|
||||
int selectedId = PlayerAvatarCatalog.NormalizeId(pendingAvatarId);
|
||||
if (avatarPickerAvatarImages.TryGetValue(selectedId, out Image selectedImage) &&
|
||||
selectedImage != null &&
|
||||
selectedImage.sprite != null)
|
||||
{
|
||||
avatarSpriteCache[selectedId] = selectedImage.sprite;
|
||||
}
|
||||
PlayerProfileStore.Update(profile => profile.AvatarId = selectedId);
|
||||
CloseAvatarPicker();
|
||||
}
|
||||
|
||||
private void CancelAvatarPicker()
|
||||
{
|
||||
CloseAvatarPicker();
|
||||
}
|
||||
|
||||
private void CloseAvatarPicker()
|
||||
{
|
||||
avatarPickerVersion++;
|
||||
openingAvatarPicker = false;
|
||||
if (avatarPickerView != null)
|
||||
{
|
||||
Destroy(avatarPickerView);
|
||||
}
|
||||
avatarPickerView = null;
|
||||
avatarPickerPreviewImage = null;
|
||||
avatarPickerHintText = null;
|
||||
avatarPickerAvatarImages.Clear();
|
||||
avatarPickerSelectedStates.Clear();
|
||||
}
|
||||
|
||||
private void RefreshAvatarPicker()
|
||||
{
|
||||
if (avatarPickerView == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int selectedId = PlayerAvatarCatalog.NormalizeId(pendingAvatarId);
|
||||
pendingAvatarId = selectedId;
|
||||
for (int avatarId = 1; avatarId <= PlayerAvatarCatalog.Count; avatarId++)
|
||||
{
|
||||
if (avatarPickerSelectedStates.TryGetValue(avatarId, out GameObject selectedState) && selectedState != null)
|
||||
{
|
||||
selectedState.SetActive(avatarId == selectedId);
|
||||
}
|
||||
}
|
||||
|
||||
if (avatarPickerAvatarImages.TryGetValue(selectedId, out Image selectedImage) && selectedImage != null && avatarPickerPreviewImage != null)
|
||||
{
|
||||
avatarPickerPreviewImage.sprite = selectedImage.sprite;
|
||||
}
|
||||
if (avatarPickerHintText != null)
|
||||
{
|
||||
avatarPickerHintText.text = "已选择头像 " + selectedId.ToString("00", CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
||||
private static string AvatarButtonName(int avatarId)
|
||||
{
|
||||
return "Btn_Avatar_" + avatarId.ToString("00", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static string AvatarImageName(int avatarId)
|
||||
{
|
||||
return "Img_Avatar_" + avatarId.ToString("00", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static string AvatarSelectedName(int avatarId)
|
||||
{
|
||||
return "Img_Selected_" + avatarId.ToString("00", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private void SetText(string nodeName, string value)
|
||||
@@ -243,5 +641,10 @@ namespace XGame
|
||||
Transform t = root == null ? null : root.transform.FindTransform(nodeName);
|
||||
return t == null ? null : t.GetComponent<T>();
|
||||
}
|
||||
|
||||
private static Transform FindTransform(GameObject root, string nodeName)
|
||||
{
|
||||
return root == null ? null : root.transform.FindTransform(nodeName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user