using UnityEditor; using UnityEditor.Animations; using UnityEditor.SceneManagement; using UnityEngine; /// /// 애니메이션 배선을 씬에 넣고, 상태 이름이 맞춰진 빈 Animator Controller 를 만들어 준다. /// /// 컨트롤러를 손으로 만들면 상태 이름을 CharacterMotionDirector 의 필드와 일일이 /// 맞춰야 하는데, 오타 하나면 조용히 아무 일도 안 일어난다. 그 자리를 없앤다. /// public static class CharacterMotionSetupMenu { const string ObjectName = "CharacterMotion"; const string ControllerPath = "Assets/99_Settings/CharacterMotion.controller"; const string OccluderShaderPath = "Assets/03_Shaders/PlatformOccluder.shader"; [MenuItem("Tools/Desktop Overlay/10. Add Character Motion To Scene")] public static void AddMotionDirector() { // 이미 있어도 그냥 돌아가지 않는다. 나중에 추가된 컴포넌트(가림, 포즈 메뉴)가 // 빠져 있을 수 있으므로, 있는 것은 두고 없는 것만 채워 넣는다. var existing = Object.FindFirstObjectByType(); GameObject go; if (existing != null) { go = existing.gameObject; } else { go = new GameObject(ObjectName); Undo.RegisterCreatedObjectUndo(go, "Add Character Motion"); go.AddComponent(); } // 발판 뒤로 숨기기용 사각형. 셰이더는 직접 참조해야 빌드에서 빠지지 않는다. if (Object.FindFirstObjectByType() == null) { var occluder = Undo.AddComponent(go); var shader = AssetDatabase.LoadAssetAtPath(OccluderShaderPath); if (shader != null) { var so = new SerializedObject(occluder); so.FindProperty("occluderShader").objectReferenceValue = shader; so.ApplyModifiedProperties(); } else { Debug.LogWarning($"[CharacterMotionSetupMenu] 셰이더를 찾지 못했습니다: {OccluderShaderPath}"); } } // 우클릭 포즈 목록. if (Object.FindFirstObjectByType() == null) { Undo.AddComponent(go); } Selection.activeGameObject = go; EditorSceneManager.MarkSceneDirty(go.scene); Debug.Log( "[CharacterMotionSetupMenu] CharacterMotion 을 추가했습니다. 씬을 저장(Ctrl+S)하세요.\n" + "Motion Controller 가 비어 있으면 지금처럼 ProceduralIdle 로 동작합니다.\n" + "메뉴 11 로 빈 컨트롤러를 만들고 클립을 채워 넣으세요."); } [MenuItem("Tools/Desktop Overlay/12. Add Frame Rate Limiter To Scene")] public static void AddFrameRateLimiter() { var existing = Object.FindFirstObjectByType(); if (existing != null) { Selection.activeGameObject = existing.gameObject; EditorGUIUtility.PingObject(existing.gameObject); Debug.Log($"[CharacterMotionSetupMenu] 이미 있습니다: {existing.gameObject.name}"); return; } // 카메라(TransparentWindow) 옆에 두는 게 자연스럽다. 없으면 새 오브젝트. var window = Object.FindFirstObjectByType(); GameObject host; if (window != null) { host = window.gameObject; Undo.AddComponent(host); } else { host = new GameObject("FrameRateLimiter"); Undo.RegisterCreatedObjectUndo(host, "Add Frame Rate Limiter"); host.AddComponent(); } Selection.activeGameObject = host; EditorSceneManager.MarkSceneDirty(host.scene); Debug.Log( "[CharacterMotionSetupMenu] FrameRateLimiter 를 추가했습니다. 씬을 저장(Ctrl+S)하세요.\n" + "이게 없으면 상시 구동 중에 낼 수 있는 최대 FPS 로 계속 렌더링합니다."); } [MenuItem("Tools/Desktop Overlay/11. Create Motion Controller")] public static void CreateController() { if (AssetDatabase.LoadAssetAtPath(ControllerPath) != null) { Debug.LogWarning($"[CharacterMotionSetupMenu] 이미 있습니다: {ControllerPath}\n" + "덮어쓰지 않습니다. 지우고 다시 실행하세요."); Selection.activeObject = AssetDatabase.LoadAssetAtPath(ControllerPath); return; } var controller = AnimatorController.CreateAnimatorControllerAtPath(ControllerPath); var layer = controller.layers[0]; var machine = layer.stateMachine; // CharacterMotionDirector 의 기본 이름과 같아야 한다. var idle = machine.AddState("Idle"); machine.AddState("Fall"); machine.AddState("Walk"); machine.AddState("Jump"); machine.AddState("Sit"); machine.AddState("LieDown"); // 전환은 코드에서 CrossFade 로 직접 하므로 트랜지션을 만들지 않는다. // 상태만 있으면 된다. machine.defaultState = idle; AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Selection.activeObject = controller; EditorGUIUtility.PingObject(controller); Debug.Log( $"[CharacterMotionSetupMenu] 컨트롤러를 만들었습니다: {ControllerPath}\n" + "각 상태(Idle / Fall / Walk / Jump / Sit / LieDown)에 Humanoid 클립을 넣으세요.\n" + "당장 없는 것은 비워둬도 됩니다 — 그 몸짓은 건너뛰고 이전 자세를 유지합니다.\n" + "FBX 는 임포트 설정에서 Rig > Animation Type = Humanoid 여야 리타게팅됩니다.\n" + "다 채운 뒤 CharacterMotionDirector 의 Motion Controller 에 연결하세요."); } }