303 lines
11 KiB
C#
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;
|
|
}
|
|
|
|
|
|
}
|
|
|
|
}
|