수업시간에 구글 프로토콜 버퍼를 이용하여 프로토콜 패킷을 생성하는데, 클라이언트와 서버단에서 PacketHandler에는 함수를 직접 정의해서 생성해야합니다. 이 과정이 꽤 번거롭고 이름도 정확하게 맞춰야합니다.
그래서 자동 툴을 이용하여 작성해야 할 코드를 자동으로 정확하게 생성해주는 툴을 제작하여 배포합니다. 이 기능의 배포는 여기에서만 합니다.
이 기능은 UnityEditor Tools를 구현하여 이용하는 방식입니다.
[여기를 클릭하여 다운로드] 스크립트를 다운로드합니다. (또는 아래 스크립트를 복사하여 작성)
유니티 에디터 Assets 폴더 안에 넣습니다. 위치는 상관 없습니다.
유니티 에디터 상단 바에서 Tools/Bonnate/Socket/Generate Protocol Code From Enum을 선택합니다.
프로토콜 버퍼 에디터에서 MsgId에 프로토콜 이름을 복사합니다.
주석을 지원합니다.
/* 주석은 구분 없이 모든곳에 동일하게 생성됩니다.
// 주석은 프로토콜의 주석으로 생성됩니다.
본 글에서는 아래 이미지와 같이 복사하였습니다.
복사한 값 Enum 스니펫을 Tools 윈도우 창에 붙여넣기합니다.
우측의 변환 버튼을 클릭합니다.
생성된 값을 각 스크립트에 붙여넣기합니다.
변환된 프로토콜 코드는 프로토콜버퍼 에디터 구현부에 붙여넣으세요.
서버가 보내는 프로토콜은 클라이언트단(유니티)에서 사용합니다.
클라이언트에서 보내는 프로토콜은 서버단에서 사용합니다.
전체 코드입니다. 위에서 다운로드 받은 파일과 동일합니다.
/*
* GenerateProtocolCodeFromEnum - Enum을 이용한 프로토콜 코드 생성을 위한 유니티 에디터 창
* 작성자: [Bonnate] https://bonnate.tistory.com/
* 라이선스: 없음 (명시적인 라이선스 없이 제공됨)
*
* 설명:
* 이 스크립트는 Enum 스니펫에서 프로토콜 코드, 클라이언트 핸들러 코드, 서버 핸들러 코드를 생성하기 위한 유니티 에디터 창을 정의합니다.
* 입력한 Enum 스니펫을 자동으로 프로토콜 코드로 변환하고, 해당하는 클라이언트와 서버 핸들러 함수를 생성합니다.
* 생성된 코드는 클립보드에 복사하여 추가적인 사용이 가능합니다.
*
* 주의 사항:
* 이 스크립트는 [C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] https://www.inflearn.com/roadmaps/355
* 의 기능을 이용하기 위해 작성된 스크립트입니다.
*
* 사용 방법:
* 1. 이 스크립트를 유니티 프로젝트의 Editor 폴더에 첨부합니다.
* 2. GenerateProtocolCodeFromEnum 창을 열기 위해 Tools -> Bonnate -> Socket -> Generate Protocol Code From Enum을 선택합니다.
* 3. 입력 텍스트 영역에 Enum 코드 스니펫을 입력합니다.
* 4. "변환" 버튼을 클릭하여 프로토콜 코드, 클라이언트 핸들러 코드, 서버 핸들러 코드를 생성합니다.
* 5. 생성된 코드는 각각의 텍스트 영역에 표시됩니다.
* 6. 해당 영역의 텍스트를 클립보드에 복사하려면 옆에 있는 버튼을 클릭합니다.
* 7. 생성된 코드를 복사하여 각 코드에 붙여넣기하여 편리하게 프로토콜을 디자인합니다.
*
* 참고: 이 스크립트는 현재 상태로 제공되며 일부 제한 사항과 문제가 있을 수 있습니다.
* 개인적으로 업데이트 되나, 온라인(깃허브 리포지트리 등)에 배포하지는 않을 예정입니다.
*/
#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;
using System.Text;
using System.Text.RegularExpressions;
public class GenerateProtocolCodeFromEnum : EditorWindow
{
private string mInputText = "";
private string mProtocolText = "";
private string mClientHandlerText = "";
private string mServerHandlerText = "";
private Vector2 mScrollPosition;
[MenuItem("Tools/Bonnate/Socket/Generate Protocol Code From Enum")]
public static void ShowWindow()
{
GetWindow<GenerateProtocolCodeFromEnum>("Generate Protocol Code From Enum");
}
private void OnGUI()
{
mScrollPosition = EditorGUILayout.BeginScrollView(mScrollPosition, GUILayout.ExpandHeight(true));
// 입력 텍스트 영역
EditorGUILayout.BeginHorizontal();
GUILayout.Label("Enum 코드 스니펫 입력");
// 변환 버튼
if (GUILayout.Button("변환", GUILayout.Width(80)))
{
mInputText = RemoveCommonIndentation(mInputText);
GenerateProtocol();
EditorGUIUtility.systemCopyBuffer = mProtocolText; // 변환된 텍스트를 클립보드에 복사
}
EditorGUILayout.EndHorizontal();
mInputText = EditorGUILayout.TextArea(mInputText, GUILayout.ExpandHeight(true));
GUILayout.Space(30);
// 프로토콜 코드 영역
EditorGUILayout.BeginHorizontal();
GUILayout.Label("변환된 프로토콜 코드");
// 복사 버튼
if (GUILayout.Button("복사", GUILayout.Width(80)))
{
EditorGUIUtility.systemCopyBuffer = mProtocolText; // 프로토콜 코드를 클립보드에 복사
}
EditorGUILayout.EndHorizontal();
mProtocolText = EditorGUILayout.TextArea(mProtocolText, GUILayout.ExpandHeight(true));
GUILayout.Space(10);
// 서버 핸들러 영역
EditorGUILayout.BeginHorizontal();
GUILayout.Label("서버가 보내는 프로토콜 (클라이언트단에서 구현)");
// 버튼
if (GUILayout.Button("복사", GUILayout.Width(80)))
{
EditorGUIUtility.systemCopyBuffer = mServerHandlerText; // 서버 핸들러 코드를 클립보드에 복사
}
EditorGUILayout.EndHorizontal();
mServerHandlerText = EditorGUILayout.TextArea(mServerHandlerText, GUILayout.ExpandHeight(true));
GUILayout.Space(10);
// 클라이언트 핸들러 영역
EditorGUILayout.BeginHorizontal();
GUILayout.Label("클라이언트가 보내는 프로토콜 (서버단에서 구현)");
// 버튼
if (GUILayout.Button("복사", GUILayout.Width(80)))
{
EditorGUIUtility.systemCopyBuffer = mClientHandlerText; // 클라이언트 핸들러 코드를 클립보드에 복사
}
EditorGUILayout.EndHorizontal();
mClientHandlerText = EditorGUILayout.TextArea(mClientHandlerText, GUILayout.ExpandHeight(true));
EditorGUILayout.EndScrollView();
GUILayout.Space(20);
GUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Powered by: Bonnate");
if (GUILayout.Button("Github", GetHyperlinkLabelStyle()))
{
OpenURL("https://github.com/bonnate");
}
if (GUILayout.Button("Blog", GetHyperlinkLabelStyle()))
{
OpenURL("https://bonnate.tistory.com/");
}
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal();
EditorGUILayout.LabelField("이 기능은 인프런 강의의 기능을 편리하게 사용하기 위해 구현되었습니다.");
if (GUILayout.Button("[C#과 유니티로 만드는 MMORPG 게임 개발 시리즈]", GetHyperlinkLabelStyle()))
{
OpenURL("https://www.inflearn.com/roadmaps/355");
}
GUILayout.EndHorizontal();
}
private GUIStyle GetHyperlinkLabelStyle()
{
GUIStyle style = new GUIStyle(GUI.skin.label);
style.normal.textColor = new Color(0f, 0.5f, 1f);
style.stretchWidth = false;
style.wordWrap = false;
return style;
}
private void OpenURL(string url)
{
EditorUtility.OpenWithDefaultApp(url);
}
private string RemoveCommonIndentation(string input)
{
string[] lines = input.Split('\n');
// 찾아낼 공백 패턴
Regex indentRegex = new Regex(@"^\s+");
// 최소 공통 공백 찾기
int maxIndentLength = 0;
foreach (string line in lines)
{
if (!string.IsNullOrWhiteSpace(line))
{
Match match = indentRegex.Match(line);
if (match.Success)
{
int indentLength = match.Length;
if (indentLength > maxIndentLength)
maxIndentLength = indentLength;
}
}
}
// 최소 공통 공백 제거
string output = "";
foreach (string line in lines)
{
if (line.Trim() == "")
continue;
if (!string.IsNullOrWhiteSpace(line))
{
string trimmedLine = line.Substring(maxIndentLength);
output += trimmedLine + "\n";
}
}
return output;
}
private void GenerateProtocol()
{
// 입력 텍스트를 줄 단위로 분할
string[] lines = mInputText.Split('\n');
// 생성된 코드 문자열 초기화
string generatedProtocolCode = "";
string generatedClientHandlerCode = "";
string generatedServerHandlerCode = "";
bool isMultiLineComment = false;
bool isClientHandlerComment = false;
bool isServerHandlerComment = false;
for (int i = 0; i < lines.Length; ++i)
{
string line = lines[i];
// 다중 줄 주석 처리
if (line.Trim().StartsWith("/*"))
{
isMultiLineComment = true;
}
if (isMultiLineComment)
{
generatedProtocolCode += line + "\n";
generatedClientHandlerCode += line + "\n";
generatedServerHandlerCode += line + "\n";
// 다중 줄 주석 종료
if (line.Trim().EndsWith("*/"))
{
isMultiLineComment = false;
}
continue;
}
if (line == "\n")
{
generatedProtocolCode += "\n";
generatedClientHandlerCode += "\n";
generatedServerHandlerCode += "\n";
continue;
}
// 주석인 경우 처리하여 유지
if (line.Trim().StartsWith("//"))
{
StringBuilder funcComment = new StringBuilder();
for (; i < lines.Length; ++i)
{
line = lines[i];
if (!line.Trim().StartsWith("//"))
{
funcComment.Remove(funcComment.Length - 2, 2);
--i;
break;
}
funcComment.Append($"{line}\n");
}
// 클라이언트 핸들러 주석 확인
if (i + 1 < lines.Length && lines[i + 1].Trim().StartsWith("C_"))
{
isClientHandlerComment = true;
isServerHandlerComment = false;
}
// 서버 핸들러 주석 확인
else if (i + 1 < lines.Length && lines[i + 1].Trim().StartsWith("S_"))
{
isClientHandlerComment = false;
isServerHandlerComment = true;
}
if (isClientHandlerComment)
{
generatedClientHandlerCode += funcComment + "\n";
}
if (isServerHandlerComment)
{
generatedServerHandlerCode += funcComment + "\n";
}
generatedProtocolCode += funcComment + "\n";
continue;
}
// 빈 줄이거나 등호 (=)가 없는 줄은 건너뜀
if (string.IsNullOrEmpty(line) || !line.Contains("="))
continue;
// 줄에서 변수 이름과 값 추출
string[] parts = line.Split('=');
string variableName = parts[0].Trim();
string variableValue = parts[1].Trim();
// 접두사 추출 및 나머지 이름을 카멜 표기법으로 변환
string[] nameParts = variableName.Split('_');
string prefix = nameParts[0];
string camelCaseName = "";
for (int j = 1; j < nameParts.Length; ++j)
{
camelCaseName += char.ToUpper(nameParts[j][0]) + nameParts[j].Substring(1).ToLower();
}
// 새 프로토콜 메시지 생성
string protocolMessage = $"message {prefix}_{camelCaseName} {{\n}}";
// 생성된 프로토콜 메시지를 코드에 추가
generatedProtocolCode += protocolMessage + "\n";
// 클라이언트 핸들러 코드 생성
if (prefix.StartsWith('C'))
{
string clientHandlerCode = $"public static void {prefix}_{camelCaseName}Handler(PacketSession session, IMessage packet)\n{{\n {prefix}_{camelCaseName} {prefix.ToLower()}_{camelCaseName.ToLower()} = packet as {prefix}_{camelCaseName};\n // TODO\n}}\n";
generatedClientHandlerCode += clientHandlerCode + "\n";
}
// 서버 핸들러 코드 생성
if (prefix.StartsWith('S'))
{
string serverHandlerCode = $"public static void {prefix}_{camelCaseName}Handler(PacketSession session, IMessage packet)\n{{\n {prefix}_{camelCaseName} {prefix.ToLower()}_{camelCaseName.ToLower()} = packet as {prefix}_{camelCaseName};\n // TODO\n}}\n";
generatedServerHandlerCode += serverHandlerCode + "\n";
}
}
// 생성된 코드를 입력 필드에 할당
mProtocolText = generatedProtocolCode;
mClientHandlerText = generatedClientHandlerCode;
mServerHandlerText = generatedServerHandlerCode;
}
}
#endif