Net.Like.Xue.Tokyo/Packages-Local/Com.Project.B.Unity/Npc/NpcSpawnService.cs

303 lines
11 KiB
C#

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<NpcSpawnService> _logger;
private readonly IHealthService _healthService;
private readonly IGameMapService _gameMapService;
private readonly IEntitiesService _entitiesService;
private readonly IAsyncTicker _asyncTicker;
private readonly INpcFactory _npcFactory;
private readonly ConcurrentDictionary<int, HashSet<Vector3>> _spawnNodes=new();
private readonly ConcurrentDictionary<string,int> _scriptableNpcSpawns=new();
private readonly HashSet<Transform> _spawnedNpc = new();
private readonly HashSet<Vector3> _usedPoints = new();
private readonly ConcurrentQueue<Transform> _removeNpcByDistance = new();
private readonly Queue<Transform> _pool = new();
private bool _isDisposed;
public int MaxNpcCount { get; set; } = 64;
public NpcSpawnService(IGameMapService gameMapService, IEntitiesService entitiesService, IAsyncTicker asyncTicker, INpcFactory npcFactory, ILogger<NpcSpawnService> 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<IScriptableNpcSpawn>())
{
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<IHealthComponent>();
await _healthService.AddHealth(id, health.MaxHealthPoint, this);
}
else
{
var npc = await _npcFactory.CreateAsync(npcAddress, null);
npcTransform = npc.ServiceProvider.GetService<Transform>();
npc.CancellationToken.Register(OnDispose);
var health = npc.ServiceProvider.GetRequiredService<IHealthComponent>();
health.OnHealthChanged += OnHealthChanged;
}
npcTransform.position = point;
npcTransform.rotation = Quaternion.Euler(Random.Range(0,360),0,0);
if (npcTransform.TryGetComponent<NavMeshAgent>(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<Transform>();
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<UnityNpcSpawnAreaNode>() is not {} areaNode)return;
if(obj.ServiceProvider.GetService<Transform>() 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<Vector3> GenerateSpawnPoints(Vector3 center, float radius, float minDist, int maxPoints)
{
List<Vector3> spawnPoints = new List<Vector3>();
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<Vector3> 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;
}
}
}