using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using BITKit; using BITKit.Entities; using Cysharp.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Net.Project.B.Health; using Net.Project.B.WorldNode; using Project.B.Entities; using Project.B.Map; using UnityEngine; using UnityEngine.AI; using Random = UnityEngine.Random; namespace Net.Project.B.AI { public class NpcSpawnService:INpcSpawnService { private readonly ILogger _logger; private readonly IHealthService _healthService; private readonly IGameMapService _gameMapService; private readonly IEntitiesService _entitiesService; private readonly IAsyncTicker _asyncTicker; private readonly INpcFactory _npcFactory; private readonly ConcurrentDictionary> _spawnNodes=new(); private readonly ConcurrentDictionary _scriptableNpcSpawns=new(); private readonly HashSet _spawnedNpc = new(); private readonly HashSet _usedPoints = new(); private readonly ConcurrentQueue _removeNpcByDistance = new(); private readonly Queue _pool = new(); private bool _isDisposed; public int MaxNpcCount { get; set; } = 64; public NpcSpawnService(IGameMapService gameMapService, IEntitiesService entitiesService, IAsyncTicker asyncTicker, INpcFactory npcFactory, ILogger logger, IHealthService healthService) { _gameMapService = gameMapService; _entitiesService = entitiesService; _asyncTicker = asyncTicker; _npcFactory = npcFactory; _logger = logger; _healthService = healthService; _gameMapService.OnMapChanged += OnMapChanged; _entitiesService.OnAdd += OnAddEntity; _entitiesService.OnRemove += OnRemoveEntity; _asyncTicker.OnTickAsync += OnTickAsync; foreach (var scriptableNpcSpawn in entitiesService.QueryComponents()) { foreach (var (address,weight) in scriptableNpcSpawn.NpcWeights) { if (_scriptableNpcSpawns.TryGetValue(address,out var currentWeight) is false) { _scriptableNpcSpawns[address] = (currentWeight + weight) / 2; } else { _scriptableNpcSpawns.TryAdd(address, weight); } } } } private async UniTask OnTickAsync(float arg) { var mainCamera = Camera.main; if(!mainCamera)return; var cameraPosition = mainCamera.transform.position; foreach (var npc in _spawnedNpc) { if (!(Vector3.Distance(npc.position, cameraPosition) > 100)) continue; _removeNpcByDistance.Enqueue(npc); break; } while (_removeNpcByDistance.TryDequeue(out var npc)) { npc.gameObject.SetActive(false); _pool.Enqueue(npc); _spawnedNpc.Remove(npc); } if(_usedPoints.Count > _spawnNodes.Values.Sum(x => x.Count) / 3) { _usedPoints.Clear(); } foreach (var hashset in _spawnNodes.Values) { foreach (var point in hashset) { if(_spawnedNpc.Count>MaxNpcCount)return; //if(_usedPoints.Contains(point))continue; //if (NavMesh.CalculatePath(point, cameraPosition, NavMesh.AllAreas,_path) is false)continue; var distance = Vector3.Distance(point, cameraPosition); if( distance is < 32 or > 64)continue; var npcWeight = _scriptableNpcSpawns.Values.Sum(); npcWeight = Math.Max(100, npcWeight); var npcAddress = string.Empty; foreach (var (address, weight) in _scriptableNpcSpawns) { if(Random.Range(0,npcWeight) < weight)continue; npcAddress = address; break; } if(string.IsNullOrEmpty(npcAddress))continue; if (_pool.TryDequeue(out var npcTransform)) { npcTransform.gameObject.SetActive(true); var id = npcTransform.gameObject.GetInstanceID(); var health = _entitiesService.Entities[id].ServiceProvider .GetRequiredService(); await _healthService.AddHealth(id, health.MaxHealthPoint, this); } else { var npc = await _npcFactory.CreateAsync(npcAddress, null); npcTransform = npc.ServiceProvider.GetService(); npc.CancellationToken.Register(OnDispose); var health = npc.ServiceProvider.GetRequiredService(); health.OnHealthChanged += OnHealthChanged; } npcTransform.position = point; npcTransform.rotation = Quaternion.Euler(Random.Range(0,360),0,0); if (npcTransform.TryGetComponent(out var agent)) { agent.SetDestination(agent.nextPosition = point); agent.Warp(point); } _spawnedNpc.Add(npcTransform); _usedPoints.Add(point); continue; void OnHealthChanged(int arg2, int arg3) { if (arg3 >= 0) return; _pool.Enqueue(npcTransform); _spawnedNpc.Remove(npcTransform); } void OnDispose() { if(_isDisposed)return; _logger.LogWarning("NPC Disposed,It's shouldn't be happen,now have to requeue"); //requeue var tempQueue = new Queue(); while (_pool.TryDequeue(out var transform)) { if(transform==npcTransform)continue; tempQueue.Enqueue(transform); } while (tempQueue.TryDequeue(out var entity)) { _pool.Enqueue(entity); } _spawnedNpc.Remove(npcTransform); } } await UniTask.NextFrame(); } } private void OnRemoveEntity(IEntity obj) { _spawnNodes.TryRemove(obj.Id); } private void OnAddEntity(IEntity obj) { if(obj.ServiceProvider.GetService() is not {} areaNode)return; if(obj.ServiceProvider.GetService() is not {} transform)return; var points = _spawnNodes.GetOrCreate(obj.Id); foreach (var point in GenerateSpawnPoints( transform.position, areaNode.spawnRadius, areaNode.minDistanceBetweenSpawns, areaNode.maxSpawnPoints)) { points.Add(point); } return; List GenerateSpawnPoints(Vector3 center, float radius, float minDist, int maxPoints) { List spawnPoints = new List(); int attempts = 0; while (spawnPoints.Count < maxPoints && attempts < maxPoints * 10) { Vector3 randomPoint = center + Random.insideUnitSphere * radius; randomPoint.y = center.y; // 保持 Y 轴位置 if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, 2f, NavMesh.AllAreas)) { if (IsClearArea(hit.position, minDist, spawnPoints) && IsSpacious(hit.position)) { spawnPoints.Add(hit.position); } } attempts++; } return spawnPoints; } // 确保刷怪点不会过近 bool IsClearArea(Vector3 pos, float minDist, List points) { foreach (var p in points) { if (Vector3.Distance(p, pos) < minDist) return false; } return true; } // 确保周围没有障碍物,区域足够空旷 bool IsSpacious(Vector3 pos) { for (int i = 0; i < 8; i++) { Vector3 dir = Quaternion.Euler(0, i * 45, 0) * Vector3.forward * areaNode.clearanceCheckDistance; if (NavMesh.Raycast(pos, pos + dir, out _, NavMesh.AllAreas)) { return false; // 遇到障碍,位置不合适 } } return true; } } private void OnMapChanged(Guid arg1, string arg2) { if(string.IsNullOrEmpty(arg2))return; _spawnedNpc.Clear(); _pool.Clear(); } public void Dispose() { _isDisposed = true; _asyncTicker.OnTickAsync -= OnTickAsync; _entitiesService.OnAdd -= OnAddEntity; _entitiesService.OnRemove -= OnRemoveEntity; _gameMapService.OnMapChanged -= OnMapChanged; } } }