using System.Text; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using WL.Player; namespace WL.EditorTools { /// /// 카메라 충돌 회피에서 제외할 오브젝트(나무 등)를 전용 레이어 "CameraIgnore" 로 옮기고, /// FollowCamera.obstacleMask 에서 그 레이어를 뺀다 (PD #736 — 특정 지역에서 카메라가 멋대로 줌인). /// 실측: 플레이어 주변 48m 격자 255점 중 88점에서 카메라 캐스트가 Tree02 크라운 BoxCollider 에 걸려 카메라가 0.2~6m 로 당겨졌다. /// 나무는 캐릭터 물리 충돌엔 그대로 참여한다(레이어 충돌 행렬은 기본값 = 전부 충돌). /// public static class CameraObstacleLayerSetup { public const string LayerName = "CameraIgnore"; /// 이 접두어로 시작하는 오브젝트(자식 콜라이더 포함)를 카메라 통과 대상으로 본다. public static readonly string[] IgnorePrefixes = { "Tree" }; [MenuItem("WL/카메라 - 나무를 카메라 회피에서 제외 (CameraIgnore 레이어) (PD #736)")] public static void MenuApply() { Debug.Log(Apply()); } public static string Apply() { var sb = new StringBuilder(); int layer = EnsureLayer(LayerName, sb); if (layer < 0) return sb.ToString(); int moved = 0, already = 0; foreach (var col in Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None)) { if (!MatchesPrefix(col.transform)) continue; if (col.gameObject.layer == layer) { already++; continue; } Undo.RecordObject(col.gameObject, "camera ignore layer"); col.gameObject.layer = layer; EditorUtility.SetDirty(col.gameObject); moved++; } sb.AppendLine("레이어 이동: " + moved + "개 (이미 적용 " + already + "개) → " + LayerName + "(" + layer + ")"); var fc = Object.FindFirstObjectByType(); if (fc != null) { var so = new SerializedObject(fc); var prop = so.FindProperty("obstacleMask"); int mask = prop.intValue & ~(1 << layer); prop.intValue = mask; so.ApplyModifiedPropertiesWithoutUndo(); sb.AppendLine("FollowCamera.obstacleMask = " + mask + " (CameraIgnore 제외)"); EditorSceneManager.MarkSceneDirty(fc.gameObject.scene); EditorSceneManager.SaveScene(fc.gameObject.scene); sb.AppendLine("씬 저장: " + fc.gameObject.scene.name); } else sb.AppendLine("FollowCamera 없음"); AssetDatabase.SaveAssets(); return sb.ToString(); } private static bool MatchesPrefix(Transform t) { // 자기 이름 또는 부모 이름이 접두어로 시작하면 대상 (콜라이더가 자식에 붙은 프리팹 대응) for (var cur = t; cur != null; cur = cur.parent) foreach (var p in IgnorePrefixes) if (cur.name.StartsWith(p)) return true; return false; } /// TagManager 에 레이어가 없으면 빈 사용자 슬롯(8~31)에 만든다. private static int EnsureLayer(string name, StringBuilder sb) { int existing = LayerMask.NameToLayer(name); if (existing >= 0) return existing; var tagManager = new SerializedObject(AssetDatabase.LoadAllAssetsAtPath("ProjectSettings/TagManager.asset")[0]); var layers = tagManager.FindProperty("layers"); for (int i = 8; i < layers.arraySize; i++) { var el = layers.GetArrayElementAtIndex(i); if (string.IsNullOrEmpty(el.stringValue)) { el.stringValue = name; tagManager.ApplyModifiedProperties(); sb.AppendLine("레이어 생성: " + name + " = " + i); return i; } } sb.AppendLine("빈 레이어 슬롯이 없다"); return -1; } } }