Project_WL/Assets/WL/Editor/CameraObstacleLayerSetup.cs

92 lines
4.3 KiB
C#

using System.Text;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using WL.Player;
namespace WL.EditorTools
{
/// <summary>
/// 카메라 충돌 회피에서 제외할 오브젝트(나무 등)를 전용 레이어 "CameraIgnore" 로 옮기고,
/// FollowCamera.obstacleMask 에서 그 레이어를 뺀다 (PD #736 — 특정 지역에서 카메라가 멋대로 줌인).
/// 실측: 플레이어 주변 48m 격자 255점 중 88점에서 카메라 캐스트가 Tree02 크라운 BoxCollider 에 걸려 카메라가 0.2~6m 로 당겨졌다.
/// 나무는 캐릭터 물리 충돌엔 그대로 참여한다(레이어 충돌 행렬은 기본값 = 전부 충돌).
/// </summary>
public static class CameraObstacleLayerSetup
{
public const string LayerName = "CameraIgnore";
/// <summary>이 접두어로 시작하는 오브젝트(자식 콜라이더 포함)를 카메라 통과 대상으로 본다.</summary>
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<Collider>(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<FollowCamera>();
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;
}
/// <summary>TagManager 에 레이어가 없으면 빈 사용자 슬롯(8~31)에 만든다.</summary>
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;
}
}
}