270 lines
11 KiB
C#
270 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Cielonos.MainGame.Rewards;
|
|
using Sirenix.OdinInspector;
|
|
using SLSUtilities.General;
|
|
using UnityEngine;
|
|
using UnityEngine.AI;
|
|
|
|
namespace Cielonos.MainGame.Map
|
|
{
|
|
public partial class ZoneManager : Singleton<ZoneManager>
|
|
{
|
|
[Title("Editor Tools")]
|
|
[Button("Rebuild Spawn Points"), PropertyOrder(-100)]
|
|
public void RebuildMapData()
|
|
{
|
|
if(spawnPointContainer == null)
|
|
{
|
|
Debug.LogWarning("[ZoneManager] Spawn Point Container is not assigned.");
|
|
return;
|
|
}
|
|
|
|
spawnPoints.Clear();
|
|
|
|
// 1. 查找场景中所有生成点
|
|
SpawnPoint[] allPoints = spawnPointContainer.GetComponentsInChildren<SpawnPoint>(true);
|
|
|
|
// 2. 按 groupName 分组,并按 Hierarchy 顺序排序 (保证 Index 确定性)
|
|
var grouped = allPoints
|
|
.GroupBy(p => p.groupName)
|
|
.OrderBy(g => g.Key);
|
|
|
|
foreach (var group in grouped)
|
|
{
|
|
// 按在 Hierarchy 中的顺序排序列表
|
|
var list = group.OrderBy(p => p.transform.GetSiblingIndex()).ToList();
|
|
spawnPoints[group.Key] = list;
|
|
|
|
// 3. 将计算好的 Index 写入到具体物体上(持久化)
|
|
for (int i = 0; i < list.Count; i++)
|
|
{
|
|
list[i].SetData(i); // 调用子物体的方法设置数据
|
|
}
|
|
}
|
|
|
|
//Debug.Log($"[ZoneManager] Rebuilt map: Found {allPoints.Length} points in {spawnPoints.Count} groups.");
|
|
}
|
|
}
|
|
|
|
// 继承 Singleton 保持运行时单例特性
|
|
public partial class ZoneManager
|
|
{
|
|
// 运行时查找用的字典
|
|
[Title("Runtime Data")]
|
|
[Required]
|
|
public GameObject spawnPointContainer;
|
|
public Dictionary<string, List<SpawnPoint>> spawnPoints = new Dictionary<string, List<SpawnPoint>>();
|
|
|
|
protected override void Awake()
|
|
{
|
|
base.Awake();
|
|
RebuildMapData();
|
|
}
|
|
|
|
public void SetupZone(ZoneData data)
|
|
{
|
|
// 生成敌人
|
|
if (data.enemySpawns != null)
|
|
{
|
|
foreach (KeyValuePair<ZoneData.SpawnPointKey, string> enemySpawn in data.enemySpawns)
|
|
{
|
|
string group = enemySpawn.Key.group;
|
|
int index = enemySpawn.Key.index;
|
|
|
|
if (spawnPoints.TryGetValue(group, out List<SpawnPoint> points))
|
|
{
|
|
SpawnPoint point = points[index];
|
|
point.GetTransform(out Vector3 position, out Quaternion rotation);
|
|
GameObject enemyPrefab = MapBaseCollection.Enemies[enemySpawn.Value];
|
|
Instantiate(enemyPrefab, position, rotation);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 生成交互物体
|
|
SpawnInteractables(data);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 仅生成交互物体,跳过敌人生成。用于重入已完成的战斗节点。
|
|
/// </summary>
|
|
public void SetupZoneWithoutEnemies(ZoneData data)
|
|
{
|
|
SpawnInteractables(data);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 根据 ZoneData 中配置的 interactableSpawns 动态生成所有配置的交互物体。
|
|
/// </summary>
|
|
private void SpawnInteractables(ZoneData data)
|
|
{
|
|
if (data.interactableSpawns == null) return;
|
|
|
|
// 遍历关卡数据中配置的每一种交互物定义
|
|
foreach (var def in data.interactableSpawns)
|
|
{
|
|
// 从全局配置合集查找对应的交互物预制体
|
|
if (!MapBaseCollection.Interactables.TryGetValue(def.interactableId, out GameObject prefab))
|
|
{
|
|
Debug.LogWarning($"[ZoneManager] 交互物体 '{def.interactableId}' 在 MapBaseCollection.Interactables 中未找到。");
|
|
continue;
|
|
}
|
|
|
|
// 遍历该类型交互物在场景里的所有生成点,逐个进行实例化
|
|
foreach (ZoneData.SpawnPointKey key in def.spawnPoints)
|
|
{
|
|
if (spawnPoints.TryGetValue(key.group, out List<SpawnPoint> points) && key.index < points.Count)
|
|
{
|
|
SpawnPoint point = points[key.index];
|
|
point.GetTransform(out Vector3 position, out Quaternion rotation);
|
|
Instantiate(prefab, position, rotation);
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning($"[ZoneManager] 生成点 '{key.group}_{key.index}' 不存在,跳过 '{def.interactableId}' 的生成。");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool TrySpawnInteractable(string interactableId, out GameObject instance)
|
|
{
|
|
return TrySpawnInteractable(interactableId, interactableId, out instance);
|
|
}
|
|
|
|
public bool TrySpawnInteractable(string interactableId, string spawnGroup, out GameObject instance)
|
|
{
|
|
instance = null;
|
|
if (string.IsNullOrWhiteSpace(interactableId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(spawnGroup))
|
|
{
|
|
spawnGroup = interactableId;
|
|
}
|
|
|
|
if (!MapBaseCollection.Interactables.TryGetValue(interactableId, out GameObject prefab) || prefab == null)
|
|
{
|
|
Debug.LogWarning($"[ZoneManager] 交互物体 '{interactableId}' 在 MapBaseCollection.Interactables 中未找到。");
|
|
return false;
|
|
}
|
|
|
|
if (!spawnPoints.TryGetValue(spawnGroup, out List<SpawnPoint> points) || points == null || points.Count == 0)
|
|
{
|
|
Debug.LogWarning($"[ZoneManager] 交互物体 '{interactableId}' 没有可用生成点 group '{spawnGroup}'。");
|
|
return false;
|
|
}
|
|
|
|
SpawnPoint point = points[0];
|
|
point.GetTransform(out Vector3 position, out Quaternion rotation);
|
|
instance = Instantiate(prefab, position, rotation);
|
|
return instance != null;
|
|
}
|
|
|
|
public bool TrySpawnInteractable(string interactableId, CombatRewardSpawnRequest request,
|
|
out GameObject instance)
|
|
{
|
|
instance = null;
|
|
if (string.IsNullOrWhiteSpace(interactableId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!MapBaseCollection.Interactables.TryGetValue(interactableId, out GameObject prefab) || prefab == null)
|
|
{
|
|
Debug.LogWarning($"[ZoneManager] 交互物体 '{interactableId}' 在 MapBaseCollection.Interactables 中未找到。");
|
|
return false;
|
|
}
|
|
|
|
ResolveRewardSpawnPose(request, out Vector3 position, out Quaternion rotation);
|
|
instance = Instantiate(prefab, position, rotation);
|
|
return instance != null;
|
|
}
|
|
|
|
private void ResolveRewardSpawnPose(CombatRewardSpawnRequest request, out Vector3 position,
|
|
out Quaternion rotation)
|
|
{
|
|
string spawnGroup = string.IsNullOrWhiteSpace(request.spawnGroup) ? "CombatReward" : request.spawnGroup;
|
|
float spacing = Mathf.Max(0.25f, request.spacing);
|
|
IReadOnlyList<Vector3> occupiedPositions = request.occupiedPositions;
|
|
|
|
if (spawnPoints.TryGetValue(spawnGroup, out List<SpawnPoint> points) && points != null && points.Count > 0)
|
|
{
|
|
int clampedIndex = Mathf.Clamp(request.spawnIndex, 0, points.Count - 1);
|
|
SpawnPoint point = points[clampedIndex];
|
|
point.GetTransform(out position, out rotation);
|
|
|
|
if (request.spawnIndex < points.Count && !IsNearOccupiedPosition(position, occupiedPositions, spacing))
|
|
{
|
|
position = SampleNavMesh(position);
|
|
return;
|
|
}
|
|
|
|
point = points[0];
|
|
point.GetTransform(out Vector3 center, out rotation);
|
|
position = FindOffsetRewardPosition(center, request.spawnIndex - points.Count + 1,
|
|
spacing, occupiedPositions);
|
|
return;
|
|
}
|
|
|
|
Transform playerTransform = MainGameManager.Player != null ? MainGameManager.Player.transform : transform;
|
|
position = request.spawnIndex <= 0
|
|
? playerTransform.position
|
|
: FindOffsetRewardPosition(playerTransform.position, request.spawnIndex, spacing, occupiedPositions);
|
|
rotation = playerTransform.rotation;
|
|
Debug.LogWarning($"[ZoneManager] 未找到奖励生成点 group '{spawnGroup}',使用玩家坐标作为 Combat 奖励兜底生成位置。");
|
|
}
|
|
|
|
private static Vector3 FindOffsetRewardPosition(Vector3 center, int offsetIndex, float spacing,
|
|
IReadOnlyList<Vector3> occupiedPositions)
|
|
{
|
|
int startIndex = Mathf.Max(0, offsetIndex);
|
|
for (int attempt = 0; attempt < 24; attempt++)
|
|
{
|
|
int candidateIndex = startIndex + attempt;
|
|
int ring = candidateIndex / 8 + 1;
|
|
float angle = candidateIndex * 137.50777f * Mathf.Deg2Rad;
|
|
Vector3 offset = new Vector3(Mathf.Cos(angle), 0f, Mathf.Sin(angle)) * spacing * ring;
|
|
Vector3 candidate = SampleNavMesh(center + offset);
|
|
if (!IsNearOccupiedPosition(candidate, occupiedPositions, spacing))
|
|
{
|
|
return candidate;
|
|
}
|
|
}
|
|
|
|
return SampleNavMesh(center + Vector3.right * spacing * (startIndex + 1));
|
|
}
|
|
|
|
private static bool IsNearOccupiedPosition(Vector3 position, IReadOnlyList<Vector3> occupiedPositions,
|
|
float spacing)
|
|
{
|
|
if (occupiedPositions == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
float minSqrDistance = spacing * spacing;
|
|
for (int i = 0; i < occupiedPositions.Count; i++)
|
|
{
|
|
if ((occupiedPositions[i] - position).sqrMagnitude < minSqrDistance)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static Vector3 SampleNavMesh(Vector3 position)
|
|
{
|
|
return NavMesh.SamplePosition(position, out NavMeshHit hit, 1.5f, NavMesh.AllAreas)
|
|
? hit.position
|
|
: position;
|
|
}
|
|
}
|
|
}
|