티스토리 뷰
# 프로잭트를 진행하며 코루틴을 제어하는게 아주 어려웠다. 더욱 더 문제가 되는 점은 내가 만든 스크립트에서 동작하는 코루틴이 아닌 팀원 이 만든 스크립트에서 동작하고 있는 코루틴을 확인 하는 것이 큰 문제였다.
에디터 툴을 만들어서 해당 코루틴이 어느 스크립트에서, 무슨 이름으로 동작하고있는지 확인 해볼 수 있다면 협업에 도움이 되겠다는 생각이 들었다.
생각한 툴의 기능은 이렇다.
1.현재 씬에서 동작하고 있는 모든 코루틴을 에디터 툴에서 보여주기 , 동작하고 있지 않은 코루틴이라면 보여줄 필요 없음
2.에디터 툴에 보여지는 내용은 '스크립트명 - 코루틴 이름' 으로 보여주기
3.버튼을 클릭하면 해당 스크립트가 열린다
4.해당 스크립트가 열리며 해당 코루틴이 동작하고 있는 라인으로 이동한다(디버그창에서 더블클릭으로 이동하는 원리로 동작해야한다)
5.주석처리가되어있는 코루틴은 조회하지 않고, 동작 중이지 않은 코루틴은 조회하지 않는다
# 먼저 어떤 방법으로 코루틴을 찾아낼지 고민해봤다. 처음 생각은 프로잭트 안에서 리플랙션을 이용해 코루틴 타입을 찾아내려했으나 이렇게 하게 된다면 실행중이지 않은 코루틴도 모두 검색이 되는 어려움이 있었다.
더 간단한 방법이 생각났다. StartCoroutine('실행시킬 코루틴 이름') 으로 실행을 하고 있으니 StartCoroutine만 검색을 하는 방향으로 생각해봤다. 검색할 코루틴에서 조회하고 싶은 정보를 구조체에 정의한 후 맴버변수로 코루틴의 정보를 담을 List둔다 . 그 후 실행중인 코루틴을 검색하는 방법으로 생각해봤다.
코루틴을 검색하는 순서는 이렇게 된다.
1. 씬에서 모든 MonoBehaviour 인스턴스를 찾기
2. foreach문에서 정의된 모든 메서드를 가져온 후 정의된 모든 메서드를 가져온다
=>MethodInfo[] methods = monoBehaviour.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
3.그 중 "StartCoroutine 으로 정의된 메서드를 찾고 메서드의 매개변수를 가져온다.
4. 메서드의 매개변수 타입 IEnumerator 인 것들을 검색한다
=>if (parameters.Length == 1 && parameters[0].ParameterType == typeof(IEnumerator))
5. 메서드의 모든 특성을 가져온다
=>object[] attributes = method.GetCustomAttributes(true);
6. 각 특성에 대해 반복하며 특성이 System.ObsoleteAttribute 인지 확인한다.
7.ObsoleteAttribute가 정의되어있지 않은 경우 실행하는 부분(직접적으로 코루틴 정보를 가져오는 부분)
*나머지 메서드들은 '주어진 코드 라인에서 코루틴 이름을 찾고 수정하여 반환' or '스크립트파일 경로를 가져와서 반환'
#전체 코드
using UnityEditor;
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
public class CoroutineTrackerWindow : EditorWindow
{
private Vector2 scrollPosition;
private List<CoroutineInfo> runningCoroutines;
private class CoroutineInfo
{
public string scriptName;
public string methodName;
public MonoBehaviour scriptInstance;
public int lineNumber;
}
[MenuItem("Window/Coroutine Viewer")]
public static void ShowWindow()
{
GetWindow<CoroutineTrackerWindow>("Coroutine Viewer");
}
private void OnEnable()
{
runningCoroutines = new List<CoroutineInfo>();
EditorApplication.update += UpdateRunningCoroutines;
}
private void OnDisable()
{
EditorApplication.update -= UpdateRunningCoroutines;
}
/// <summary>
/// 에디터 창에서 실행되는 GUI를 처리하는 메서드입니다.
/// 실행 중인 코루틴 목록을 스크롤 뷰로 표시하고, 각 코루틴을 선택하면 해당 스크립트를 열고 코루틴이 위치한 라인으로 이동합니다.
/// </summary>
private void OnGUI()
{
GUILayout.Label("Running Coroutines", EditorStyles.boldLabel);
scrollPosition = GUILayout.BeginScrollView(scrollPosition);
foreach (CoroutineInfo coroutineInfo in runningCoroutines)
{
string coroutineName = $"{coroutineInfo.scriptName} - {coroutineInfo.methodName}";
if (GUILayout.Button(coroutineName))
{
EditorUtility.FocusProjectWindow();
Selection.activeObject = coroutineInfo.scriptInstance;
// Open the script and move to the line of the coroutine
string scriptPath = AssetDatabase.GetAssetPath(MonoScript.FromMonoBehaviour(coroutineInfo.scriptInstance));
UnityEditorInternal.InternalEditorUtility.OpenFileAtLineExternal(scriptPath, coroutineInfo.lineNumber);
}
}
GUILayout.EndScrollView();
}
/// <summary>
/// 실행 중인 코루틴을 업데이트하여 runningCoroutines 리스트를 갱신합니다.
/// </summary>
private void UpdateRunningCoroutines()
{
runningCoroutines.Clear(); // 리스트 초기
MonoBehaviour[] monoBehaviours = FindObjectsOfType<MonoBehaviour>(); // 씬에서 모든 MonoBehaviour 인스턴스 찾기
foreach (MonoBehaviour monoBehaviour in monoBehaviours)
{
// 정의된 모든 메서드를 가져오기
MethodInfo[] methods = monoBehaviour.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
foreach (MethodInfo method in methods)
{
if (method.Name == "StartCoroutine") //StartCoroutine 로 정의된 메서드 찾기
{
ParameterInfo[] parameters = method.GetParameters(); // 메서드의 매개변수를 가져오기
if (parameters.Length == 1 && parameters[0].ParameterType == typeof(IEnumerator)) //메서드의 매개변수 타입이 IEnumerator(코루틴)인지 확인
{
bool isObsolete = false;
object[] attributes = method.GetCustomAttributes(true); //메서드의 모든 특성(Attribute)을 가져오기
foreach (object attribute in attributes) // 각 특성에 대해 반복
{
//메서드에 ObsoleteAttribute 가 적용되어 있다면 반복문 종료. ObsoleteAttribute가 적용되어있는 메서드는 코루틴을 간주하지 않고 제외하기
if (attribute is System.ObsoleteAttribute) //특성이 ObsoleteAttribute인지 확인합니다.
{
isObsolete = true;
break;
}
}
//ObsoleteAttribute 가 적용되어 있지 않은 경우실행
if (!isObsolete) //메서드가 표시되지 않은 경우에만 실행
{
string scriptName = monoBehaviour.GetType().Name;
string[] lines = File.ReadAllLines(GetScriptFilePath(monoBehaviour)); //MonoBehaviour 인스턴스에 해당하는 스크립트 파일의 모든 라인을 읽기
for (int i = 0; i < lines.Length; i++)
{
if (lines[i].Contains(method.Name))
{
string coroutineName = FindCoroutineName(lines[i]); //해당 라인에서 코루틴 이름을 찾기
if (!string.IsNullOrEmpty(coroutineName))
{
CoroutineInfo coroutineInfo = new CoroutineInfo(); //코루틴 정보 인스턴스 생성
coroutineInfo.scriptName = scriptName; //스크립트 이름 저장
coroutineInfo.methodName = coroutineName; //코루틴 이름 저장
coroutineInfo.scriptInstance = monoBehaviour; //MonoBehaviour 인스턴스 저장
coroutineInfo.lineNumber = i + 1; // 현재 라인의 번호를 저장 라인 번호는 1부터 시작
runningCoroutines.Add(coroutineInfo); // 실행 중인 코루틴 목록에 코루틴 정보 추가
}
}
}
}
}
}
}
}
}
/// <summary>
/// 주어진 코드 라인에서 코루틴 이름을 찾습니다.
/// </summary>
/// <param name="line">분석할 코드 라인</param>
/// <returns>찾은 코루틴 이름</returns>
private string FindCoroutineName(string line)
{
int startIndex = line.IndexOf("(");
int endIndex = line.IndexOf(")");
if (startIndex >= 0 && endIndex >= 0 && endIndex > startIndex)
{
string coroutineName = line.Substring(startIndex + 1, endIndex - startIndex).Trim();
if (line.Contains("//")) //주석 처리된 부분인지 확인
{
//주석 처리된 부분은 처리하지 않고 null 반환
return null;
}
return coroutineName; //코루틴 이름 반환
}
return null; //이름 못찾는다면 null 반환
}
/// <summary>
/// 주어진 MonoBehaviour의 스크립트 파일 경로를 가져옵니다.
/// </summary>
/// <param name="monoBehaviour">대상 MonoBehaviour</param>
/// <returns>스크립트 파일 경로</returns>
private string GetScriptFilePath(MonoBehaviour monoBehaviour)
{
MonoScript monoScript = MonoScript.FromMonoBehaviour(monoBehaviour); //MonoBehaviour에 연결된 MonoScript 인스턴스를 가져오기
string scriptFilePath = AssetDatabase.GetAssetPath(monoScript); //MonoScript에 대한 스크립트 파일 경로를 가져오기
return scriptFilePath; //스크립트 파일경로 반환하기
}
}
#Test 1
#처음 만들어보는 에디터 툴이라 많이 해맸다.
공부를 해보니 툴을 만드는 방법보다 어떻게 코루틴을 찾아낼지가 문제였다.
프로잭트가 동작 중 메서드의 정보(속성)를 가져와야함으로 C#의 리플랙션을 공부했다.
리플랙션은 프로그램이 실행 중에 코드 자체를 조사하고 조작하는 기능이다. 하지만 성능상의 문제를 불러올 수 있음으로
보통은 조작하는 기능은 쓰지않는다고 한다. 위 스크립트 중 UpdateRunningCoroutines 메서드 안에 내용들이 리플랙션에 해당하는 부분들이다 이를 통해 객체의 타입, 메서드, 속서 등의 정보를 동적으로 알아낼 수 있다(GetType() 이나 GetMethods() ).
#참고 자료
리플렉션을 사용하여 특성 액세스
GetCustomAttributes 메서드를 사용하여 C#에서 사용자 지정 특성으로 정의된 정보를 가져오려면 리플렉션을 사용합니다.
learn.microsoft.com
https://www.youtube.com/watch?v=a52b_ej0Bjg
#Test 후 문제점
프로잭트에서 테스트를 해보니 문제가 있었다. 코루틴을 조회는 UpdateRunningCoroutines메서드에서 이루어 지는데
File.ReadAllLines를 사용하여 모든 스크립트 파일을 읽고 파일 입출력은 디스크 I/O 작업임으로 성능에 큰 영향을 주고있었다 씬에는 무수히 많은 MonoBehaviour 인스턴스가 있음으로 모든 파일을 읽는다면 성능 저하를 불러오는 것이었다.
해결 방안이 필요하다. 당장 만들어 놓은 스크립트를 사용하려면 프로잭트를 일시정지 시키고 조회하는 방법 밖에 없다.
UpdateRunningCoroutines 메서드에서 코루틴을 조회하는 방법을 수정해야한다.
에디터에 Refresh 버튼을 만들어서 버튼이 눌린다면 코루틴을 조회하는 방식으로 바꿔야겠다. 매 프레임 마다 찾을 필요는 없었다.
# Refresh를 적용한 수정 버전
using UnityEditor;
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
public class CoroutineTrackerWindow : EditorWindow
{
private Vector2 scrollPosition;
private List<CoroutineInfo> runningCoroutines;
private bool coroutinesFound = false;
private bool refreshButtonClicked = false;
private class CoroutineInfo
{
public string scriptName;
public string methodName;
public MonoBehaviour scriptInstance;
public int lineNumber;
}
[MenuItem("Window/Coroutine Viewer")]
public static void ShowWindow()
{
GetWindow<CoroutineTrackerWindow>("Coroutine Viewer");
}
private void OnEnable()
{
runningCoroutines = new List<CoroutineInfo>();
EditorApplication.update += UpdateRunningCoroutines;
}
private void OnDisable()
{
EditorApplication.update -= UpdateRunningCoroutines;
}
/// <summary>
/// 에디터 창에서 실행되는 GUI를 처리하는 메서드입니다.
/// 실행 중인 코루틴 목록을 스크롤 뷰로 표시하고, 각 코루틴을 선택하면 해당 스크립트를 열고 코루틴이 위치한 라인으로 이동합니다.
/// </summary>
private void OnGUI()
{
GUILayout.Label("Running Coroutines", EditorStyles.boldLabel);
scrollPosition = GUILayout.BeginScrollView(scrollPosition);
if (GUILayout.Button("Refresh"))
{
refreshButtonClicked = true;
coroutinesFound = false;
}
foreach (CoroutineInfo coroutineInfo in runningCoroutines)
{
string coroutineName = $"{coroutineInfo.scriptName} - {coroutineInfo.methodName}";
if (GUILayout.Button(coroutineName))
{
EditorUtility.FocusProjectWindow();
Selection.activeObject = coroutineInfo.scriptInstance;
string scriptPath = AssetDatabase.GetAssetPath(MonoScript.FromMonoBehaviour(coroutineInfo.scriptInstance));
UnityEditorInternal.InternalEditorUtility.OpenFileAtLineExternal(scriptPath, coroutineInfo.lineNumber);
}
}
GUILayout.EndScrollView();
}
/// <summary>
/// 실행 중인 코루틴을 업데이트하여 runningCoroutines 리스트를 갱신합니다.
/// </summary>
private void UpdateRunningCoroutines()
{
if (!coroutinesFound || refreshButtonClicked)
{
runningCoroutines.Clear();
MonoBehaviour[] monoBehaviours = FindObjectsOfType<MonoBehaviour>();
foreach (MonoBehaviour monoBehaviour in monoBehaviours)
{
MethodInfo[] methods = monoBehaviour.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
foreach (MethodInfo method in methods)
{
if (method.Name == "StartCoroutine")
{
ParameterInfo[] parameters = method.GetParameters();
if (parameters.Length == 1 && parameters[0].ParameterType == typeof(IEnumerator))
{
bool isObsolete = false;
object[] attributes = method.GetCustomAttributes(true);
foreach (object attribute in attributes)
{
if (attribute is System.ObsoleteAttribute)
{
isObsolete = true;
break;
}
}
if (!isObsolete)
{
string scriptName = monoBehaviour.GetType().Name;
string[] lines = File.ReadAllLines(GetScriptFilePath(monoBehaviour));
for (int i = 0; i < lines.Length; i++)
{
if (lines[i].Contains(method.Name))
{
string coroutineName = FindCoroutineName(lines[i]);
if (!string.IsNullOrEmpty(coroutineName))
{
CoroutineInfo coroutineInfo = new CoroutineInfo();
coroutineInfo.scriptName = scriptName;
coroutineInfo.methodName = coroutineName;
coroutineInfo.scriptInstance = monoBehaviour;
coroutineInfo.lineNumber = i + 1;
runningCoroutines.Add(coroutineInfo);
}
}
}
}
}
}
}
}
coroutinesFound = true;
refreshButtonClicked = false;
}
}
/// <summary>
/// 주어진 코드 라인에서 코루틴 이름을 찾습니다.
/// </summary>
/// <param name="line">분석할 코드 라인</param>
/// <returns>찾은 코루틴 이름</returns>
private string FindCoroutineName(string line)
{
int startIndex = line.IndexOf("(");
int endIndex = line.IndexOf(")");
if (startIndex >= 0 && endIndex >= 0 && endIndex > startIndex)
{
string coroutineName = line.Substring(startIndex + 1, endIndex - startIndex).Trim();
if (line.Contains("//")) //주석 처리된 부분인지 확인
{
//주석 처리된 부분은 처리하지 않고 null 반환
return null;
}
return coroutineName; //코루틴 이름 반환
}
return null; //이름 못찾는다면 null 반환
}
/// <summary>
/// 주어진 MonoBehaviour의 스크립트 파일 경로를 가져옵니다.
/// </summary>
/// <param name="monoBehaviour">대상 MonoBehaviour</param>
/// <returns>스크립트 파일 경로</returns>
private string GetScriptFilePath(MonoBehaviour monoBehaviour)
{
MonoScript monoScript = MonoScript.FromMonoBehaviour(monoBehaviour); //MonoBehaviour에 연결된 MonoScript 인스턴스를 가져오기
string scriptFilePath = AssetDatabase.GetAssetPath(monoScript); //MonoScript에 대한 스크립트 파일 경로를 가져오기
return scriptFilePath; //스크립트 파일경로 반환하기
}
}
#README
자세한 사용법과 동작 원리는 마크다운업으로 README 파일 로 작성했다.
# Coroutine Tracker
Coroutine Tracker는 Unity 에디터에서 실행 중인 코루틴을 실시간으로 감지하고 표시해주는 도구입니다.
## 사용법
1. 이 프로젝트를 다운로드하고 Unity 에디터를 열어 프로젝트를 로드합니다.
2. 에디터 상단 메뉴에서 "Window"를 선택하고 "Coroutine Viewer"를 클릭합니다.
3. Coroutine Viewer 창이 열리면 실행 중인 코루틴 목록을 확인할 수 있습니다.
4. 코루틴을 선택하면 해당 스크립트 파일이 열리고 코루틴이 위치한 라인으로 이동합니다.
## 작동 원리
Coroutine Tracker는 EditorWindow를 상속받은 CoroutineTrackerWindow 클래스를 사용하여 구현되었습니다. 다음은 주요 메서드들의 기능에 대한 설명입니다:
- `OnEnable()`: 에디터 윈도우가 활성화될 때 실행되는 메서드입니다. 실행 중인 코루틴을 업데이트하기 위해 `EditorApplication.update` 이벤트에 `UpdateRunningCoroutines` 메서드를 등록합니다.
- `OnDisable()`: 에디터 윈도우가 비활성화될 때 실행되는 메서드입니다. `EditorApplication.update` 이벤트에서 `UpdateRunningCoroutines` 메서드를 제거합니다.
- `OnGUI()`: 에디터 윈도우에서 실행되는 GUI를 처리하는 메서드입니다. 실행 중인 코루틴 목록을 스크롤 뷰로 표시하고, 각 코루틴을 선택하면 해당 스크립트를 열고 코루틴이 위치한 라인으로 이동합니다.
- `UpdateRunningCoroutines()`: 실행 중인 코루틴을 업데이트하여 `runningCoroutines` 리스트를 갱신하는 메서드입니다. 현재 Scene에 존재하는 모든 MonoBehaviour를 확인하고, 그 중에서 StartCoroutine 메서드를 사용하는 경우 해당 코루틴의 정보를 저장합니다.
- `FindCoroutineName(string line)`: 주어진 코드 라인에서 코루틴 이름을 찾는 메서드입니다. 코루틴 이름은 StartCorotine 메서드의 인자로 전달된 함수 이름입니다.
- `GetScriptFilePath(MonoBehaviour monoBehaviour)`: 주어진 MonoBehaviour의 스크립트 파일 경로를 가져오는 메서드입니다. 해당 MonoBehaviour에 연결된 MonoScript 인스턴스를 사용하여 스크립트 파일 경로를 찾습니다.
위의 설명은 Coroutine Tracker의 사용법과 작동 원리를 간단히 소개한 것입니다. 더 자세한 내용이 필요하다면 코드의 주석과 해당 스크립트 파일을 참고하실 수 있습니다.
### 기여
이 프로젝트는 오픈 소스로 개발되었으며, 기여는 언제든 환영합니다. 버그를 발견하거나 개선 아이디어가 있다면 이슈를 등록하거나 풀 리퀘스트를 남겨주세요.
#Test2
# 좀 더 개선하고싶은 부분들
더 디테일하게 sorting 하는 기능이 있어야겠다. 예를 들어 Dungeon씬에서
내가 만든 보스 스크립트들의 코루틴만 조회를 하는 기능들을 개선하고싶다
툴에서 스크립트 또는 오브잭트 명 검색 기능을 만들어 좀 더 좁은 범위 안에서 검색하는 기능을 만들어봐야겠다
git hub에 올려봐야겠다.
https://github.com/dlghksrnr/CoroutineTrackerWindow
'게임클라이언트 프로그래밍 > 윈도우 에디터' 카테고리의 다른 글
실행중인 모든 코루틴 정보 확인하는 툴만들기(윈도우 에디터) (0) | 2023.05.02 |
---|