diff --git a/Assets/PerfectWorld/Prefab/UI/DlgMap.prefab b/Assets/PerfectWorld/Prefab/UI/DlgMap.prefab index 6c137804e0..16ec363ad8 100644 --- a/Assets/PerfectWorld/Prefab/UI/DlgMap.prefab +++ b/Assets/PerfectWorld/Prefab/UI/DlgMap.prefab @@ -241,6 +241,7 @@ MonoBehaviour: m_EditorClassIdentifier: skillNameText: {fileID: 0} imageProgress: {fileID: 0} + btnCancel: {fileID: 0} _debugHostPlayerPos: {x: 0, y: 209.1, z: 0} _hostPlayerIcon: {fileID: 7794785241219751657} nRow: 3 @@ -249,6 +250,9 @@ MonoBehaviour: _imageMiniMapPrefab: {fileID: 561411731206337256} _listImageMiniMap: [] _transformMiniMapParent: {fileID: 4768066627267225838} + _imageNPCMiniMapPrefab: {fileID: 5617331893333014101} + _npcMiniMapSprite: {fileID: 21300000, guid: 410207327d87441459b814da357c9b54, type: 3} + _npcMiniMapIconSize: {x: 16, y: 16} _worldMapButton: {fileID: 9145966839271910414} _spriteAtlas: {fileID: 100100200, guid: b764c7c6d08a20e41a8ebfb3435954db, type: 3} --- !u!1 &5886515903784990361 @@ -421,6 +425,7 @@ RectTransform: - {fileID: 4768066627267225838} - {fileID: 670009488162588346} - {fileID: 7794785241219751657} + - {fileID: 4868528813881887473} m_Father: {fileID: 75608124384920614} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0.5, y: 0.5} @@ -615,6 +620,102 @@ MonoBehaviour: m_hasFontAssetChanged: 0 m_baseMaterial: {fileID: 0} m_maskOffset: {x: 0, y: 0, z: 0, w: 0} +--- !u!1 &6750487080347129537 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4868528813881887473} + - component: {fileID: 5425040792438544429} + - component: {fileID: 5617331893333014101} + - component: {fileID: 4760304024276327800} + m_Layer: 0 + m_Name: npc + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 0 +--- !u!224 &4868528813881887473 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6750487080347129537} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 5306706511345298923} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5711758, y: 0.4164777} + m_AnchorMax: {x: 0.5711758, y: 0.4164777} + m_AnchoredPosition: {x: -14.804569, y: 17.372637} + m_SizeDelta: {x: 7, y: 7} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &5425040792438544429 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6750487080347129537} + m_CullTransparentMesh: 1 +--- !u!114 &5617331893333014101 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6750487080347129537} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 21300000, guid: 410207327d87441459b814da357c9b54, type: 3} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!114 &4760304024276327800 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6750487080347129537} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreLayout: 1 + m_MinWidth: -1 + m_MinHeight: -1 + m_PreferredWidth: -1 + m_PreferredHeight: -1 + m_FlexibleWidth: -1 + m_FlexibleHeight: -1 + m_LayoutPriority: 1 --- !u!1 &7056095192285021313 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/PerfectWorld/Scripts/Managers/CECNPCMan.cs b/Assets/PerfectWorld/Scripts/Managers/CECNPCMan.cs index db367ff587..dc9a2776a4 100644 --- a/Assets/PerfectWorld/Scripts/Managers/CECNPCMan.cs +++ b/Assets/PerfectWorld/Scripts/Managers/CECNPCMan.cs @@ -13,8 +13,30 @@ using UnityEngine; public class CECNPCMan : IMsgHandler { + public struct NPCMiniMapData + { + public int NPCID; + public int TemplateID; + public string Name; + public A3DVECTOR3 Position; + public int MapID; + public DATA_TYPE DataType; // what type of NPC is this? + + public NPCMiniMapData(int npcID, int templateID, string name, A3DVECTOR3 position, int mapID, DATA_TYPE dataType) + { + NPCID = npcID; + TemplateID = templateID; + Name = name; + Position = position; + MapID = mapID; + DataType = dataType; + } + } + private Dictionary m_NPCTab ; private Dictionary m_UkNPCTab ; + private readonly object m_NPCTabLock = new object(); + private readonly List m_NPCMiniMapSnapshot = new (); List m_aDisappearNPCs ; public int HandlerId => (int)MANAGER_INDEX.MAN_NPC; @@ -94,8 +116,13 @@ public class CECNPCMan : IMsgHandler } // Add to dictionary - m_NPCTab[nid] = npc; - int countAfter = m_NPCTab.Count; + int countAfter; + lock (m_NPCTabLock) + { + m_NPCTab[nid] = npc; + UpsertNPCMiniMapSnapshot(nid, npc); + countAfter = m_NPCTab.Count; + } // Verify the value was set correctly bool verifySuccess = m_NPCTab.TryGetValue(nid, out var verifyNPC); @@ -162,8 +189,14 @@ public class CECNPCMan : IMsgHandler // Remove from dictionary - bool removed = m_NPCTab.Remove(nid); - int countAfter = m_NPCTab.Count; + bool removed; + int countAfter; + lock (m_NPCTabLock) + { + removed = m_NPCTab.Remove(nid); + RemoveNPCMiniMapSnapshot(nid); + countAfter = m_NPCTab.Count; + } // Verify removal bool keyStillExists = m_NPCTab.ContainsKey(nid); @@ -729,6 +762,9 @@ public class CECNPCMan : IMsgHandler AddNPCToTable(Info.nid, npc, $"NPCEnter - nid={Info.nid}, tid={Info.tid}, bBornInSight={bBornInSight}"); return true; } + + + // Get NPC by id and optional bornStamp public CECNPC GetNPC(int nid, uint bornStamp = 0) { @@ -762,6 +798,75 @@ public class CECNPCMan : IMsgHandler return npc; } + /// + /// Returns a copy of all active NPC objects. Use this on the main thread only. + /// + public List GetAllNPCs() + { + lock (m_NPCTabLock) + { + return new List(m_NPCTab.Values); + } + } + + /// + /// Returns a plain-data snapshot for worker-thread consumers such as the minimap. + /// + public List GetAllNPCMiniMapData() + { + lock (m_NPCTabLock) + { + return m_NPCMiniMapSnapshot; + } + } + + private void UpsertNPCMiniMapSnapshot(int nid, CECNPC npc) + { + NPCMiniMapData data = CreateNPCMiniMapData(nid, npc); + for (int i = 0; i < m_NPCMiniMapSnapshot.Count; i++) + { + if (m_NPCMiniMapSnapshot[i].NPCID == nid) + { + m_NPCMiniMapSnapshot[i] = data; + return; + } + } + + m_NPCMiniMapSnapshot.Add(data); + } + + private void RemoveNPCMiniMapSnapshot(int nid) + { + for (int i = m_NPCMiniMapSnapshot.Count - 1; i >= 0; i--) + { + if (m_NPCMiniMapSnapshot[i].NPCID == nid) + { + m_NPCMiniMapSnapshot.RemoveAt(i); + return; + } + } + } + + private NPCMiniMapData CreateNPCMiniMapData(int nid, CECNPC npc) + { + var info = npc.GetNPCInfo(); + int mapID = CECGameRun.Instance?.GetWorld()?.GetInstanceID() ?? 0; + DATA_TYPE dataType = DATA_TYPE.DT_INVALID; + if (npc is CECMonster) + { + dataType = DATA_TYPE.DT_MONSTER_ESSENCE; + } + else if (npc is CECPet) + { + dataType = DATA_TYPE.DT_PET_ESSENCE; + } + else if (npc is CECNPCServer) + { + dataType = DATA_TYPE.DT_NPC_ESSENCE; + } + return new NPCMiniMapData(nid, info.tid, npc.GetName() ?? string.Empty, npc.GetServerPos(), mapID, dataType); + } + // Find first NPC/Monster by template id (tid). Used by UI auto-move coordinate resolving. // 通过模板ID(tid)查找第一个NPC/怪物。用于UI自动寻路坐标解析。 public CECNPC FindNPCByTemplateID(int tid) @@ -952,7 +1057,11 @@ public class CECNPCMan : IMsgHandler ReleaseNPC(pNPC); } - m_NPCTab.Clear(); + lock (m_NPCTabLock) + { + m_NPCTab.Clear(); + m_NPCMiniMapSnapshot.Clear(); + } // Release all NPCs in disappear table int i; diff --git a/Assets/PerfectWorld/Scripts/UI/MiniMap/CDlgMiniMap.cs b/Assets/PerfectWorld/Scripts/UI/MiniMap/CDlgMiniMap.cs index 90eccde575..e280a14d1b 100644 --- a/Assets/PerfectWorld/Scripts/UI/MiniMap/CDlgMiniMap.cs +++ b/Assets/PerfectWorld/Scripts/UI/MiniMap/CDlgMiniMap.cs @@ -1,10 +1,14 @@ 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; @@ -39,6 +43,9 @@ namespace PerfectWorld.UI.MiniMap [SerializeField] private Image _imageMiniMapPrefab; [SerializeField] private List _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 @@ -48,6 +55,12 @@ namespace PerfectWorld.UI.MiniMap private List m_vecNPCMark = new(); private Dictionary m_TexMap = new(); private List _texToDelete = new(); // list of texture to delete from m_TexMap, use in update functions. + private readonly Dictionary _npcMiniMapImages = new(); + private readonly object _npcMiniMapRenderLock = new object(); + private List _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; @@ -69,9 +82,24 @@ namespace PerfectWorld.UI.MiniMap _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(); + } } /// @@ -104,6 +132,156 @@ namespace PerfectWorld.UI.MiniMap { 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 WatchNPCMiniMapData(npcMan, token); + }, false, cancellationToken: token).Forget(); + } + + private async UniTask WatchNPCMiniMapData(CECNPCMan npcMan, CancellationToken token) + { + List lastNPCData = new(); + + try + { + while (!token.IsCancellationRequested) + { + List currentNPCData = npcMan.GetAllNPCMiniMapData(); + if (!IsSameNPCMiniMapData(lastNPCData, currentNPCData)) + { + lock (_npcMiniMapRenderLock) + { + _pendingNPCMiniMapData = currentNPCData; + } + + lastNPCData = currentNPCData; + _needRenderNPCMiniMap = true; + } + + await UniTask.Delay(1000, cancellationToken: token); + } + } + catch (OperationCanceledException) + { + // Expected when the minimap is destroyed. + } + } + + private static bool IsSameNPCMiniMapData(List previous, List 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; + } + + private void RenderNPCMiniMap() + { + List npcData; + lock (_npcMiniMapRenderLock) + { + BMLogger.Log($"RenderNPCMiniMap: _pendingNPCMiniMapData.Count={_pendingNPCMiniMapData.Count}"); + npcData = new List(_pendingNPCMiniMapData); + } + + m_vecNPCMark.Clear(); + HashSet 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(); + } + + 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 activeNPCIds) + { + List npcIdsToRemove = new(); + foreach (KeyValuePair 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; } @@ -163,6 +341,10 @@ namespace PerfectWorld.UI.MiniMap Destroy(child.gameObject); } + _npcMiniMapImages.Clear(); + m_vecNPCMark.Clear(); + _needRenderNPCMiniMap = true; + Sprite pSprite = null; for(int r = 0; r < nRow + 3; r++) { diff --git a/Assets/PerfectWorld/UI/surfaces/ingame/radar_ftasknpc.png.meta b/Assets/PerfectWorld/UI/surfaces/ingame/radar_ftasknpc.png.meta index 18cb208f8e..2e97a9d225 100644 --- a/Assets/PerfectWorld/UI/surfaces/ingame/radar_ftasknpc.png.meta +++ b/Assets/PerfectWorld/UI/surfaces/ingame/radar_ftasknpc.png.meta @@ -6,7 +6,7 @@ TextureImporter: serializedVersion: 13 mipmaps: mipMapMode: 0 - enableMipMap: 1 + enableMipMap: 0 sRGBTexture: 1 linearTexture: 0 fadeOut: 0 @@ -37,13 +37,13 @@ TextureImporter: filterMode: 1 aniso: 1 mipBias: 0 - wrapU: 0 - wrapV: 0 + wrapU: 1 + wrapV: 1 wrapW: 0 - nPOTScale: 1 + nPOTScale: 0 lightmap: 0 compressionQuality: 50 - spriteMode: 0 + spriteMode: 1 spriteExtrude: 1 spriteMeshType: 1 alignment: 0 @@ -52,9 +52,9 @@ TextureImporter: spriteBorder: {x: 0, y: 0, z: 0, w: 0} spriteGenerateFallbackPhysicsShape: 1 alphaUsage: 1 - alphaIsTransparency: 0 + alphaIsTransparency: 1 spriteTessellationDetail: -1 - textureType: 0 + textureType: 8 textureShape: 1 singleChannelComponent: 0 flipbookRows: 1 @@ -126,7 +126,7 @@ TextureImporter: customData: physicsShape: [] bones: [] - spriteID: + spriteID: 5e97eb03825dee720800000000000000 internalID: 0 vertices: [] indices: diff --git a/Assets/PerfectWorld/UI/surfaces/ingame/radar_npc.png.meta b/Assets/PerfectWorld/UI/surfaces/ingame/radar_npc.png.meta index 04a2f38421..65a7d9bb53 100644 --- a/Assets/PerfectWorld/UI/surfaces/ingame/radar_npc.png.meta +++ b/Assets/PerfectWorld/UI/surfaces/ingame/radar_npc.png.meta @@ -6,7 +6,7 @@ TextureImporter: serializedVersion: 13 mipmaps: mipMapMode: 0 - enableMipMap: 1 + enableMipMap: 0 sRGBTexture: 1 linearTexture: 0 fadeOut: 0 @@ -37,13 +37,13 @@ TextureImporter: filterMode: 1 aniso: 1 mipBias: 0 - wrapU: 0 - wrapV: 0 + wrapU: 1 + wrapV: 1 wrapW: 0 - nPOTScale: 1 + nPOTScale: 0 lightmap: 0 compressionQuality: 50 - spriteMode: 0 + spriteMode: 1 spriteExtrude: 1 spriteMeshType: 1 alignment: 0 @@ -52,9 +52,9 @@ TextureImporter: spriteBorder: {x: 0, y: 0, z: 0, w: 0} spriteGenerateFallbackPhysicsShape: 1 alphaUsage: 1 - alphaIsTransparency: 0 + alphaIsTransparency: 1 spriteTessellationDetail: -1 - textureType: 0 + textureType: 8 textureShape: 1 singleChannelComponent: 0 flipbookRows: 1 @@ -126,7 +126,7 @@ TextureImporter: customData: physicsShape: [] bones: [] - spriteID: + spriteID: 5e97eb03825dee720800000000000000 internalID: 0 vertices: [] indices: diff --git a/Assets/PerfectWorld/UI/surfaces/ingame/radar_tasknpc.png.meta b/Assets/PerfectWorld/UI/surfaces/ingame/radar_tasknpc.png.meta index c1a19f1036..01c5bac6ee 100644 --- a/Assets/PerfectWorld/UI/surfaces/ingame/radar_tasknpc.png.meta +++ b/Assets/PerfectWorld/UI/surfaces/ingame/radar_tasknpc.png.meta @@ -6,7 +6,7 @@ TextureImporter: serializedVersion: 13 mipmaps: mipMapMode: 0 - enableMipMap: 1 + enableMipMap: 0 sRGBTexture: 1 linearTexture: 0 fadeOut: 0 @@ -37,13 +37,13 @@ TextureImporter: filterMode: 1 aniso: 1 mipBias: 0 - wrapU: 0 - wrapV: 0 + wrapU: 1 + wrapV: 1 wrapW: 0 - nPOTScale: 1 + nPOTScale: 0 lightmap: 0 compressionQuality: 50 - spriteMode: 0 + spriteMode: 1 spriteExtrude: 1 spriteMeshType: 1 alignment: 0 @@ -52,9 +52,9 @@ TextureImporter: spriteBorder: {x: 0, y: 0, z: 0, w: 0} spriteGenerateFallbackPhysicsShape: 1 alphaUsage: 1 - alphaIsTransparency: 0 + alphaIsTransparency: 1 spriteTessellationDetail: -1 - textureType: 0 + textureType: 8 textureShape: 1 singleChannelComponent: 0 flipbookRows: 1 @@ -126,7 +126,7 @@ TextureImporter: customData: physicsShape: [] bones: [] - spriteID: + spriteID: 5e97eb03825dee720800000000000000 internalID: 0 vertices: [] indices: