using System.Collections; using System.Collections.Generic; using TMPro; using UnityEngine; public class MobControlMgr : MyCoroutine { public int SpawnerID; public float MobGenRange = 10f; public TextMeshPro text_lv; MonsterAppearData m_MonsterAppearData; Dictionary> dic_actors = new Dictionary>(); int orderIndex, killmobcount; bool m_LoadComplete; // #764 — 이 맵의 스폰 규칙. 프리팹 계층에서 찾고(같은 맵 프리팹 루트), 없으면 현재 로드된 맵의 MapData 를 쓴다. MapData m_MapCfg; MapData Get_MapCfg() { if (DSUtil.CheckNull(m_MapCfg)) m_MapCfg = GetComponentInParent(true); if (DSUtil.CheckNull(m_MapCfg) && MapData.isIns) m_MapCfg = MapData.Ins; return DSUtil.CheckNull(m_MapCfg) ? null : m_MapCfg; } IEnumerator Start() { m_LoadComplete = false; text_lv.text = "Lv.???"; m_MonsterAppearData = table_monsterappear.Ins.Get_Data_orNull(SpawnerID); if (!DSUtil.CheckNull(m_MonsterAppearData)) { text_lv.text = DSUtil.Format("Lv.{0}", m_MonsterAppearData.n_AppearMonsterLv); // #764 — 맵이 요구하면 스포너 원점이 플레이어 시작 지점에서 도달 가능한지 1회 확인한다. // NavMesh 연결 성분 안에서 도달성은 대칭·추이적이므로, 원점이 도달 가능하면 // "원점에서 완전 경로가 나오는 표집점" 도 전부 도달 가능하다 → 몹마다 경로를 두 번 계산할 필요가 없다. var cfg = Get_MapCfg(); if (cfg != null && cfg.spawnRequireReachableFromPlayer) yield return StartCoroutine(Co_CheckSpawnerReachable()); var list_make_mobs = Get_Mobs(); for (int i = 0; i < list_make_mobs.Count; i++) { var rdnpos = Get_SpawnPos(); var mobData = list_make_mobs[i]; yield return StartCoroutine(Make_Mob(mobData, i, rdnpos)); } // 생성한 몬스터를 m_MonsterAppearData.n_MaxMonsterCount 만큼 활성화 for (int i = 0; i < m_MonsterAppearData.n_MaxMonsterCount; i++) dic_actors[0][i].On_Regen(); m_LoadComplete = true; } } public bool Get_LoadComplete() { return m_LoadComplete; } /// /// #764 — 몬스터 1마리의 생성 위치. 맵이 요구할 때만 엄격 표집을 쓰고, 아니면 기존 동작 그대로다. /// 반경·재시도 수치는 코드 상수가 아니라 MapData 에서 읽는다(C45). /// Vector3 Get_SpawnPos() { var cfg = Get_MapCfg(); if (cfg == null || !cfg.spawnRequireReachableFromPlayer) return DSUtil.Get_RandomPos_onNavMesh(transform.position, 0f, MobGenRange); return DSUtil.Get_RandomPos_onNavMesh_Reachable( transform.position, 0f, MobGenRange, cfg.spawnSnapRadius, cfg.spawnSampleTries > 0 ? cfg.spawnSampleTries : 50); } /// #764 — 스포너 원점이 플레이어 시작 지점(포탈)에서 도달 가능한지 확인하고, 아니면 경고를 남긴다. IEnumerator Co_CheckSpawnerReachable() { if (!LoadMapMgr.isIns) yield break; bool got = false; Vector3 startpos = Vector3.zero; LoadMapMgr.Ins.Get_PortalPos(MyValue.MyChoiceMapData, p => { startpos = p; got = true; }); // 맵 로딩이 끝날 때까지 기다린다. 안전 상한 — 못 받으면 확인을 건너뛰고 스폰은 정상 진행한다. float wait = 0f; while (!got && wait < 10f) { wait += Time.deltaTime; yield return null; } if (!got) { Debug.LogWarning($"[#764] Spawner {SpawnerID}: 시작 지점을 받지 못해 도달성 확인을 건너뛴다"); yield break; } if (!DSUtil.IsReachable_onNavMesh(startpos, transform.position)) Debug.LogWarning($"[#764] Spawner {SpawnerID} 원점이 플레이어 시작 지점에서 도달 불가 " + $"(spawner {transform.position.ToString("F1")} / start {startpos.ToString("F1")}) — 스포너 위치 재배치가 필요하다"); } List Get_Mobs() { var list_make_mobs = new List(); switch (m_MonsterAppearData.e_SpawnerType) { // 생성할 몹 정보 case eSpawnerType.Random: // s_AppearMonsterID에 지정된 몬스터가 n_MaxMonsterCount 수량만큼 Spawner 범위 내 랜덤으로 생성되는 방식 for (int i = 0; i < m_MonsterAppearData.n_MaxMonsterCount * 2; i++) list_make_mobs.Add(m_MonsterAppearData.Get_MobData_orNull()); dic_actors.Add(0, new List()); break; case eSpawnerType.Order: // s_AppearMonsterID에 지정 된 몬스터가 n_MaxMonsterCount 수량만큼 Spawner 범위 내 순서대로 생성되는 방식 (예: 1001|1002|1003 → 1001|1002|1003 → 순서를 반복해 생성) int index = 0; for (int i = 0; i < m_MonsterAppearData.n_MaxMonsterCount * 2; i++) { list_make_mobs.Add(m_MonsterAppearData.Get_MobData_orNull(index)); ++index; if (index >= m_MonsterAppearData.Get_MobTypeCount()) index = 0; } dic_actors.Add(0, new List()); orderIndex = m_MonsterAppearData.n_MaxMonsterCount - 1; break; case eSpawnerType.Phase: // s_AppearMonsterID에 지정 된 몬스터가 n_MaxMonsterCount 수량만큼 Spawner 범위 한 종류씩 순차적으로 생성되는 방식 // (예: 1001|1002|1003일 경우 1001번 몬스터가 사망하면 1002번 몬스터가 n_MaxMonsterCount 만큼 f_SpawnerDelay 후 생성) case eSpawnerType.PhaseAllKill: // s_AppearMonsterID에 지정 된 몬스터가 n_MaxMonsterCount 수량만큼 Spawner 범위 한 종류씩 순차적으로 생성되고, 모든 몬스터가 제거되면 다음 페이즈가 발생되는 방식 (※주로 컨텐츠에서 활용될 예정) // (예: 1001|1002|1003일 경우 1001번 몬스터를 전부 제거해야 1002번 몬스터가 n_MaxMonsterCount 만큼 f_SpawnerDelay 후 일괄 생성) for (int i = 0; i < m_MonsterAppearData.Get_MobTypeCount(); i++) { dic_actors.Add(i, new List()); for (int j = 0; j < m_MonsterAppearData.n_MaxMonsterCount; j++) list_make_mobs.Add(m_MonsterAppearData.Get_MobData_orNull(i)); } break; } return list_make_mobs; } IEnumerator Make_Mob(MonsterTableData mobData, int i, Vector3 rdnpos) { bool makeComplete = false; AddrResourceMgr.Ins.LoadObject(mobData.s_MonsterPrefab, handle => { var mob = DSUtil.Get_Clone(handle.Result, transform); AddrResourceMgr.Ins.Set_AddressableReleaseSelf(mob.gameObject); ActorInfo.Ins.Add_Actor(mob, eRole.Mob, SpawnerID, mobData.s_MonsterPrefab); switch (m_MonsterAppearData.e_SpawnerType) { case eSpawnerType.Random: case eSpawnerType.Order: dic_actors[0].Add(mob); break; case eSpawnerType.Phase: case eSpawnerType.PhaseAllKill: var list_ids = m_MonsterAppearData.Get_MobIds(); int findindex = list_ids.FindIndex(f => f == mobData.n_MonsterID); if (findindex >= 0 && findindex < dic_actors.Count) // 인덱스가 유효한지 체크 dic_actors[findindex].Add(mob); else Debug.LogError($"Invalid findindex: {findindex}, MonsterID: {mobData.n_MonsterID}"); break; } mob.Set(true, null, null, m_MonsterAppearData.n_SpawnerID, false); mob.Set(mobData, rdnpos, actDie, i); mob.Off(); mob.MagicID = 0; makeComplete = true; }); while (!makeComplete) yield return null; } private void actDie(int mobid) { if (m_MonsterAppearData != null) { ++killmobcount; switch (m_MonsterAppearData.e_SpawnerType) { // 사망 후 리젠 case eSpawnerType.Random: Set_Coroutine(() => { var deadmobs = dic_actors[0].FindAll(f => !f.isActiveAndEnabled); deadmobs[Random.Range(0, deadmobs.Count)].On_Regen(); }, m_MonsterAppearData.f_SpawnerDelay); break; case eSpawnerType.Order: Set_Coroutine(() => { if (dic_actors[0].Count > 0) { orderIndex = orderIndex % dic_actors[0].Count; while (true) { var mob = dic_actors[0][orderIndex++]; if (!mob.isActiveAndEnabled) { mob.On_Regen(); break; } orderIndex = orderIndex % dic_actors[0].Count; } } }, m_MonsterAppearData.f_SpawnerDelay); break; case eSpawnerType.Phase: Set_Coroutine(() => { var ids = m_MonsterAppearData.Get_MobIds(); var index = ids.FindIndex(f => f == mobid); var nextindex = index + 1; if (nextindex >= dic_actors.Count) nextindex = 0; dic_actors[nextindex].Find(f => !f.isActiveAndEnabled).On_Regen(); }, m_MonsterAppearData.f_SpawnerDelay); break; case eSpawnerType.PhaseAllKill: if (killmobcount == m_MonsterAppearData.n_MaxMonsterCount) { Set_Coroutine(() => { killmobcount = 0; ++orderIndex; if (orderIndex >= dic_actors.Count) orderIndex = 0; for (int i = 0; i < dic_actors[orderIndex].Count; i++) dic_actors[orderIndex][i].On_Regen(); }, m_MonsterAppearData.f_SpawnerDelay); } break; } } } }