inflearn logo
강의

강의

N
챌린지

챌린지

멘토링

멘토링

N
클립

클립

로드맵

로드맵

지식공유

[C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part7: MMO 컨텐츠 구현 (Unity + C# 서버 연동 기초)

프로토콜 버퍼 자동 변환기 툴 배포합니다!

850

Bonnate
1

수업시간에 구글 프로토콜 버퍼를 이용하여 프로토콜 패킷을 생성하는데, 클라이언트와 서버단에서 PacketHandler에는 함수를 직접 정의해서 생성해야합니다. 이 과정이 꽤 번거롭고 이름도 정확하게 맞춰야합니다.

 

그래서 자동 툴을 이용하여 작성해야 할 코드를 자동으로 정확하게 생성해주는 툴을 제작하여 배포합니다. 이 기능의 배포는 여기에서만 합니다.

 

이 기능은 UnityEditor Tools를 구현하여 이용하는 방식입니다.

 

  1. [여기를 클릭하여 다운로드] 스크립트를 다운로드합니다. (또는 아래 스크립트를 복사하여 작성)

  2. 유니티 에디터 Assets 폴더 안에 넣습니다. 위치는 상관 없습니다.

  3. 유니티 에디터 상단 바에서 Tools/Bonnate/Socket/Generate Protocol Code From Enum을 선택합니다.

툴 경로.jpg

  1. 프로토콜 버퍼 에디터에서 MsgId에 프로토콜 이름을 복사합니다.

    1. 주석을 지원합니다.

    2. /* 주석은 구분 없이 모든곳에 동일하게 생성됩니다.

    3. // 주석은 프로토콜의 주석으로 생성됩니다.

    4. 본 글에서는 아래 이미지와 같이 복사하였습니다.

 

  1. 복사한 값 Enum 스니펫을 Tools 윈도우 창에 붙여넣기합니다.

    1. 우측의 변환 버튼을 클릭합니다.

윈도우창.jpg

  1. 생성된 값을 각 스크립트에 붙여넣기합니다.

    1. 변환된 프로토콜 코드는 프로토콜버퍼 에디터 구현부에 붙여넣으세요.

    2. 서버가 보내는 프로토콜은 클라이언트단(유니티)에서 사용합니다.

    3. 클라이언트에서 보내는 프로토콜은 서버단에서 사용합니다.

 

전체 코드입니다. 위에서 다운로드 받은 파일과 동일합니다.

/*
 * 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

C# unity socket packet protocol

답변 0

공부 방향성

0

205

1

Desktop용 게임 개발에도 도움이 될까요?

0

212

1

진로가 큰 걱정입니다...

0

323

1

신입 게임 클라이언트 포트폴리오 레포 평가 부탁드립니다

0

310

0

개발자 자리와 AI?

0

467

1

파이어 베이스만 사용하는데

0

349

1

강의 예제 파일을 첨부해 주세요!

0

359

0

강의를 들을 때 해당 메서드의 개념이나 어떻게 작동하는지 모르고 따라 칠때가 있습니다.

0

448

0

클라이언트 네트워크 부분 수업 질문

0

346

0

강의에 대해 질문있습니다

0

427

0

객체지향을 어디서 배워야 할까요

0

390

1

메모리 누수에 대해서 질문드립니다.

1

400

3