Files
test/Assets/PerfectWorld/Scripts/UI/MiniMap/CDlgMiniMap.cs
T
2026-05-15 11:26:43 +07:00

379 lines
14 KiB
C#

using System.Collections.Generic;
using System;
using System.Threading;
using BrewMonster;
using BrewMonster.Managers;
using BrewMonster.Network;
using BrewMonster.Scripts;
using BrewMonster.Scripts.Extensions;
using BrewMonster.UI;
using CSNetwork.GPDataType;
using Cysharp.Threading.Tasks;
using TMPro;
using UnityEngine;
using UnityEngine.U2D;
using UnityEngine.UI;
namespace PerfectWorld.UI.MiniMap
{
public class CDlgMiniMap : AUIDialog
{
public struct MARK
{
public int nNPC;
public string strName;
public A3DVECTOR3 vecPos;
public int mapID; // 地图ID // map ID (exposed for USER_LAYOUT save)
public MARK(int nNPC, string strName, A3DVECTOR3 vecPos, int mapID)
{
this.nNPC = nNPC;
this.strName = strName;
this.vecPos = vecPos;
this.mapID = mapID;
}
}
[SerializeField] private Vector3 _debugHostPlayerPos;
[SerializeField] private RectTransform _hostPlayerIcon;
[SerializeField] private byte nRow, nCol; // number of rows and cols in the current map instances.txt
[SerializeField] private TMP_Text txtHostPos;
[SerializeField] private Image _imageMiniMapPrefab;
[SerializeField] private List<Image> _listImageMiniMap = new();
[SerializeField] private RectTransform _transformMiniMapParent;
[SerializeField] private Image _imageNPCMiniMapPrefab;
[SerializeField] private Sprite _npcMiniMapSprite;
[SerializeField] private Vector2 _npcMiniMapIconSize = new Vector2(16f, 16f);
[SerializeField] private Button _worldMapButton;
// reference to unity sprite atlas
[SerializeField] private SpriteAtlas _spriteAtlas;
private List<MARK> m_vecMark = new();
private List<MARK> m_vecNPCMark = new();
private Dictionary<string, Sprite> m_TexMap = new();
private List<string> _texToDelete = new(); // list of texture to delete from m_TexMap, use in update functions.
private readonly Dictionary<int, Image> _npcMiniMapImages = new();
private readonly object _npcMiniMapRenderLock = new object();
private List<CECNPCMan.NPCMiniMapData> _pendingNPCMiniMapData = new();
private volatile bool _needRenderNPCMiniMap;
private CancellationTokenSource _npcMiniMapCancellationTokenSource;
private bool _npcMiniMapWatcherStarted;
private float m_fZoom = 1.0f;
private bool m_bShowMark = true;
private bool m_bShowTargetArrow = true;
// map state
private bool isShowMiniMap = true;
CECHostPlayer m_pHostPlayer;
private float coordinateFactor = 0.5f; // the factor to convert the world coordinates to the mini map coordinates.
Vector3Int _lastIntHostPos = Vector3Int.zero;
private int m_nMode; // TODO: currently, there is only get logic, not set logic
private void Awake()
{
// LoadAllMiniMapTextures();
_worldMapButton.onClick.AddListener(OnMiniMapClicked);
}
private void OnDestroy()
{
_worldMapButton.onClick.RemoveListener(OnMiniMapClicked);
_npcMiniMapCancellationTokenSource?.Cancel();
_npcMiniMapCancellationTokenSource?.Dispose();
_npcMiniMapCancellationTokenSource = null;
}
void Update()
{
UpdateMiniMap();
EnsureNPCMiniMapWatcher();
if (_needRenderNPCMiniMap)
{
_needRenderNPCMiniMap = false;
RenderNPCMiniMap();
}
}
/// <summary>
/// We keep the player icon at the center of the minimap. Then we update the position of the map itself.
/// TODO: We have to keep track of the NPC icons on the map also.
/// </summary>
private void UpdateMiniMap()
{
m_pHostPlayer = GetHostPlayer();
if (m_pHostPlayer == null) return;
Transform hostTransform = m_pHostPlayer.transform;
Vector3 vecPosHost = hostTransform.position;
Vector3Int currentIntHostPos = new Vector3Int(Mathf.RoundToInt(vecPosHost.x) / 10 + 400, Mathf.RoundToInt(vecPosHost.y) / 10, Mathf.RoundToInt(vecPosHost.z) / 10 + 550);
if (currentIntHostPos != _lastIntHostPos)
{
txtHostPos.text = $"{currentIntHostPos.x}, {currentIntHostPos.z}, ↑{currentIntHostPos.y}";
_lastIntHostPos = currentIntHostPos;
}
Vector2 hostPlayerPos = new Vector2(vecPosHost.x * coordinateFactor, vecPosHost.z * coordinateFactor);
_transformMiniMapParent.anchoredPosition = -hostPlayerPos;
_hostPlayerIcon.localRotation = Quaternion.Euler(0, 0, -hostTransform.localRotation.eulerAngles.y);
}
/// <summary>
/// Get the Host Player instance.
/// </summary>
/// <returns></returns>
private CECHostPlayer GetHostPlayer()
{
return CECGameRun.Instance.GetHostPlayer();
}
private void EnsureNPCMiniMapWatcher()
{
if (_npcMiniMapWatcherStarted)
return;
CECNPCMan npcMan = EC_ManMessageMono.Instance?.CECNPCMan;
if (npcMan == null)
return;
_npcMiniMapWatcherStarted = true;
_npcMiniMapCancellationTokenSource = new CancellationTokenSource();
CancellationToken token = _npcMiniMapCancellationTokenSource.Token;
UniTask.RunOnThreadPool(async () =>
{
await UpdateNPCMiniMapDataTask(npcMan, token);
}, false, cancellationToken: token).Forget();
}
private async UniTask UpdateNPCMiniMapDataTask(CECNPCMan npcMan, CancellationToken token)
{
List<CECNPCMan.NPCMiniMapData> lastNPCData = new();
try
{
while (!token.IsCancellationRequested)
{
List<CECNPCMan.NPCMiniMapData> currentNPCData = npcMan.GetAllNPCMiniMapData();
if (!IsSameNPCMiniMapData(lastNPCData, currentNPCData))
{
lock (_npcMiniMapRenderLock)
{
_pendingNPCMiniMapData = currentNPCData;
}
lastNPCData.Clear();
lastNPCData.AddRange(currentNPCData);
_needRenderNPCMiniMap = true;
}
await UniTask.Delay(1000, cancellationToken: token);
}
}
catch (OperationCanceledException)
{
// Expected when the minimap is destroyed.
}
}
private static bool IsSameNPCMiniMapData(List<CECNPCMan.NPCMiniMapData> previous, List<CECNPCMan.NPCMiniMapData> current)
{
if (previous.Count != current.Count)
return false;
CECNPCMan.NPCMiniMapData oldData;
CECNPCMan.NPCMiniMapData newData;
for (int i = 0; i < current.Count; i++)
{
oldData = previous[i];
newData = current[i];
if (oldData.NPCID != newData.NPCID ||
oldData.TemplateID != newData.TemplateID ||
oldData.MapID != newData.MapID ||
oldData.Name != newData.Name ||
oldData.Position.x != newData.Position.x ||
oldData.Position.y != newData.Position.y ||
oldData.Position.z != newData.Position.z)
{
return false;
}
}
return true;
}
/// <summary>
/// Render the following objects on the minimap: <br/>
/// - NPC_ESSENCE <br/>
/// </summary>
private void RenderNPCMiniMap()
{
List<CECNPCMan.NPCMiniMapData> npcData;
lock (_npcMiniMapRenderLock)
{
BMLogger.Log($"RenderNPCMiniMap: _pendingNPCMiniMapData.Count={_pendingNPCMiniMapData.Count}");
npcData = new List<CECNPCMan.NPCMiniMapData>(_pendingNPCMiniMapData);
}
m_vecNPCMark.Clear();
HashSet<int> activeNPCIds = new();
for (int i = 0; i < npcData.Count; i++)
{
CECNPCMan.NPCMiniMapData data = npcData[i];
// only show NPC_ESSENCE on the minimap
if (data.DataType != DATA_TYPE.DT_NPC_ESSENCE)
continue;
activeNPCIds.Add(data.NPCID);
m_vecNPCMark.Add(new MARK(data.NPCID, data.Name, data.Position, data.MapID));
Image npcImage = GetOrCreateNPCMiniMapImage(data.NPCID);
npcImage.rectTransform.anchoredPosition = new Vector2(data.Position.x * coordinateFactor, data.Position.z * coordinateFactor);
npcImage.gameObject.SetActive(true);
}
RemoveInactiveNPCMiniMapImages(activeNPCIds);
}
private Image GetOrCreateNPCMiniMapImage(int npcID)
{
if (_npcMiniMapImages.TryGetValue(npcID, out Image npcImage) && npcImage != null)
return npcImage;
if (_imageNPCMiniMapPrefab != null)
{
npcImage = Instantiate(_imageNPCMiniMapPrefab, _transformMiniMapParent);
}
else
{
GameObject npcIcon = new GameObject($"NPC_{npcID}", typeof(RectTransform), typeof(Image));
npcIcon.transform.SetParent(_transformMiniMapParent, false);
npcImage = npcIcon.GetComponent<Image>();
}
npcImage.name = $"NPC_{npcID}";
if (_npcMiniMapSprite != null)
npcImage.sprite = _npcMiniMapSprite;
npcImage.raycastTarget = false;
npcImage.rectTransform.sizeDelta = _npcMiniMapIconSize;
_npcMiniMapImages[npcID] = npcImage;
return npcImage;
}
private void RemoveInactiveNPCMiniMapImages(HashSet<int> activeNPCIds)
{
List<int> npcIdsToRemove = new();
foreach (KeyValuePair<int, Image> kvp in _npcMiniMapImages)
{
if (!activeNPCIds.Contains(kvp.Key))
{
if (kvp.Value != null)
Destroy(kvp.Value.gameObject);
npcIdsToRemove.Add(kvp.Key);
}
}
for (int i = 0; i < npcIdsToRemove.Count; i++)
{
_npcMiniMapImages.Remove(npcIdsToRemove[i]);
}
}
// change radar mode
public int GetMode() { return m_nMode; }
/// <summary>Returns the list of user-placed marks on the minimap (for layout save/load).</summary>
public List<MARK> GetMarks() => m_vecMark;
/// <summary>User click on the minimap, we'll show the world map.</summary>
public void OnMiniMapClicked()
{
var dlg = CECUIManager.Instance.ShowUI("Win_WorldMap") as DlgWorldMap;
// dlg?.Show(true);
// dlg?.OnInitDialog();
}
/// <summary>
/// Call this function when user enter the game world (after select role or when use GOTO to jump to a new instance).
/// This function will get the world instance data and setup the mini map.
/// </summary>
public async void InitializeMiniMap()
{
// get current world instance
var idInstance = CECGameRun.Instance?.GetWorld()?.GetInstanceID() ?? 161;
var worldInstance = EC_Game.GetGameRun()?.GetInstance(idInstance);
if (worldInstance == null)
{
BMLogger.LogError("InitializeMiniMap: worldInstance is null");
return;
}
// set the number of rows and columns of the mini map
nRow = (byte)worldInstance.GetRowNum();
nCol = (byte)worldInstance.GetColNum();
// use Addressable to load all the textures of the mini map
_spriteAtlas = await AddressableManager.Instance.LoadSpriteAtlasAsync($"minimaps/{idInstance}");
if (_spriteAtlas == null)
{
BMLogger.LogError("InitializeMiniMap: sprite atlas is null");
return;
}
LoadAllMiniMapTextures();
}
// keep this so we can load all textures of other map also.
[ContextMenu("LoadAllMiniMapTextures")]
public void LoadAllMiniMapTextures()
{
// delete all images in parent
foreach(Transform child in _transformMiniMapParent)
{
Destroy(child.gameObject);
}
_npcMiniMapImages.Clear();
m_vecNPCMark.Clear();
_needRenderNPCMiniMap = true;
Sprite pSprite = null;
for(int r = 0; r < nRow + 3; r++)
{
for(int c = 0; c < nCol + 3; c++)
{
string strIndex = $"{r:D2}{c:D2}";
pSprite = _spriteAtlas.GetSprite(strIndex);
var image = Instantiate(_imageMiniMapPrefab, _transformMiniMapParent);
image.sprite = pSprite;
image.name = strIndex;
image.gameObject.SetActive(true);
}
}
}
#if UNITY_EDITOR
// this is for debuging/testing while this feature was in development
[ContextMenu("MoveHostPlayerIconToPos")]
public void MoveHostPlayerIconToPos()
{
Vector2 hostPlayerPos = new Vector2(_debugHostPlayerPos.x / 2, _debugHostPlayerPos.z / 2);
_transformMiniMapParent.anchoredPosition = -hostPlayerPos;
}
#endif
}
}