BackEndMatch 구현

구현 단계에서는 BackEndMatch에서 기본적인 것만을 구현했으며, 게임의 있어서 문제가 되지 않을 정도로 구현했다. 서버에 대한 문제와 에러는 고려하지 않은 상태로 구현했기에 코드가 단순할 수 있다.

1.BackEndManager

Main Scene에서 부터 BackEndManager를 생성하여, DontDestroyOnLoad() 함수를 사용하여, Scene이 넘어가도 오브젝트가 파괴되지 않고 유지된다. 아래의 모든 CODE는 BackEndManager 오브젝트의 script이다.

2.Poll 함수

서버와 게임에 있어서 제일 중요한 함수이다. Poll 함수가 없으면 서버와의 통신이 불가능하다.

    private void Update() 
    {
        Backend.Match.Poll();
    }

3.MatchMakingHandler

이벤트 핸들러를 통해 매칭 서버에 관한 것을 관리한다.

private void MatchMakingHandler()
    {
        Backend.Match.OnJoinMatchMakingServer += (args) =>
        {
            Debug.Log("OnJoinMatchMakingServer : " + args.ErrInfo);
        };
        Backend.Match.OnMatchMakingRoomCreate += (args) => 
        {
            Debug.Log("OnMatchMakingRoomCreate : " + args.ErrInfo + " : " + args.Reason);
            //매칭방 접속 이벤트
        };
        Backend.Match.OnMatchMakingResponse += (args) =>
        {
            Debug.Log("OnMatchMakingResponse : " + args.ErrInfo + " : " + args.Reason);
            ProcessMatchMakingResponse(args);
            //매칭 요청 이벤트
        };
        Backend.Match.OnLeaveMatchMakingServer += (args) => 
        {
            Debug.Log("OnLeaveMatchMakingServer : " + args.ErrInfo.Category);
            //매칭서버 종료 이벤트
        };
        Backend.Match.OnMatchMakingRoomLeave += (args) => {
            Debug.Log("OnMatchMakingRoomLeave : " + args.UserInfo);
        };
        Backend.Match.OnMatchMakingRoomDestory += (args) => {
            Debug.Log("OnMatchMakingRoomDestory : " + args.ErrInfo);
        };
    }

4.GameHandler

이벤트 핸들러를 통해 인게임 서버에 관한 것을 관리한다.

private void GameHandler()
    {
        Backend.Match.OnSessionJoinInServer += (args) =>
        {
            Debug.Log("OnSessionJoinInServer : " + args.ErrInfo);
            AccessInGameRoom(inGameRoomToken);
            ViewCard.Inst.SceneChager("GAME_SCENE");
        };
        Backend.Match.OnMatchInGameAccess += (args) =>
        {
            Debug.Log("OnMatchInGameAccess : " + args.ErrInfo);
            
        };
        Backend.Match.OnMatchInGameStart += () =>
        {
            Debug.Log("OnMatchInGameStart : ");
            if (!game)
            {
                SendMyInfo();
                game = true;
            }
            StartCoroutine(start());
        };
        Backend.Match.OnMatchResult += (args) => {
            Debug.Log(args.Reason);
        };
        Backend.Match.OnMatchRelay += (args) => {
            if (Backend.UserNickName != args.From.NickName)
            {
                if (args.BinaryUserData[0] == Convert.ToByte(101))
                {
                    MyId(Backend.Match.GetMySessionId());
                    OtherId(args.From.SessionId);
                    byte2image1(args.BinaryUserData, args.From.NickName);
                }
                else if (args.BinaryUserData[0] == Convert.ToByte(102))
                {
                    byte2image2(args.BinaryUserData);
                }
                else
                {
                    DataProcess(args.BinaryUserData);
                }
            }
        };
    }

5.매칭 서버 접속 요청 및 이벤트

매칭 서버의 접속 & 퇴장 요청 함수이다.

    //매칭 서버 접속 요청
    public void JoinMatchMakingServer()
    {
        ErrorInfo errorinfo;
        Backend.Match.JoinMatchMakingServer(out errorinfo);
        
    }
    //매칭 서버 퇴장 요청
    public void LeaveMatchMakingSetver()
    {
        Backend.Match.LeaveMatchMakingServer();
        
    }

6.대기방 생성 요청 및 이벤트

대기방 접속 & 퇴장 요청 함수이다.

    //대기방 생성
    public void CreateMatchRoom()
    {
        Backend.Match.CreateMatchRoom();
    }
    //대기방 퇴장
    public void LeaveMatchRoom()
    {
        Backend.Match.LeaveMatchRoom();
    }

7.매치 정보 가져오기

매칭 신청의 인자에는 매치 정보의 타입과 모드 타입 그리고 indate 정보가 필요하기에 매치리스트로 만들어 관리하면 된다.

    public class MatchInfo //매치 class
    {
        public string title;
        public string inDate;
        public MatchType matchType;
        public MatchModeType matchModeType;
    }
    public List<MatchInfo> matchInfos { get; private set; } = new List<MatchInfo>();

위에 생성된 Class를 이용하여 매치 정보를 채운다.

    public void GetMatchList()
    {
        matchInfos.Clear();
        var bro = Backend.Match.GetMatchList();
        foreach(LitJson.JsonData row in bro.Rows())
        {
            MatchInfo matchInfo = new MatchInfo();
            matchInfo.title = row["matchTitle"]["S"].ToString();
            matchInfo.inDate = row["inDate"]["S"].ToString();
            foreach (MatchType type in Enum.GetValues(typeof(MatchType)))
            {
                if (type.ToString().ToLower().Equals(row["matchType"]["S"].ToString().ToLower()))
                {
                    matchInfo.matchType = type;
                }
            }
            foreach (MatchModeType type in Enum.GetValues(typeof(MatchModeType)))
            {
                if (type.ToString().ToLower().Equals(row["matchModeType"]["S"].ToString().ToLower()))
                {
                    matchInfo.matchModeType = type;
                }
            }
            matchInfos.Add(matchInfo);
        }
    }

8.매칭 신청 요청 및 이벤트

매칭 신청 요청 함수

    public void RequestMatchMaking(int index) //Main Scene의 매칭 시작 버튼에 On Click() 이벤트를 통해 index 값을 0을 주고 매칭 신청(0은 친선모드)
    {
        Backend.Match.RequestMatchMaking(matchInfos[index].matchType,matchInfos[index].matchModeType,matchInfos[index].inDate);
        Debug.Log(matchInfos[index].matchType.ToString());
    }
    public void CancelMatchMaking() //매칭 취소
    {
        Backend.Match.CancelMatchMaking();
    }

매칭 시도에 대한 결과에 대한 이벤트 핸들러이다.

    private void ProcessMatchMakingResponse(MatchMakingResponseEventArgs args)
    {
        string debugLog = string.Empty;
        switch (args.ErrInfo)
        {
            case ErrorCode.Success:
                // 매칭 성공했을 때
                debugLog = string.Format("SUCCESS_MATCHMAKE", args.Reason);
                ProcessMatchSuccess(args); // -> 인게임 서버로 입장하는 함수
                break;
            case ErrorCode.Match_InProgress:
                // 매칭 신청 성공했을 때 or 매칭 중일 때 매칭 신청을 시도했을 때

                // 매칭 신청 성공했을 때
                if (args.Reason == string.Empty)
                {
                    debugLog = "SUCCESS_REGIST_MATCHMAKE";
                }
                break;
            case ErrorCode.Match_MatchMakingCanceled:
                // 매칭 신청이 취소되었을 때
                debugLog = string.Format("CANCEL_MATCHMAKE", args.Reason);
                break;
            case ErrorCode.Match_InvalidMatchType:
                // 매치 타입을 잘못 전송했을 때
                debugLog = string.Format("FAIL_REGIST_MATCHMAKE", "INVAILD_MATCHTYPE");
                break;
            case ErrorCode.Match_InvalidModeType:
                // 매치 모드를 잘못 전송했을 때
                debugLog = string.Format("FAIL_REGIST_MATCHMAKE", "INVALID_MODETYPE");
                break;
            case ErrorCode.InvalidOperation:
                // 잘못된 요청을 전송했을 때
                debugLog = string.Format("INVALID_OPERATION", args.Reason);
                break;
            case ErrorCode.Match_Making_InvalidRoom:
                // 잘못된 요청을 전송했을 때
                debugLog = string.Format("INVALID_OPERATION", args.Reason);
                break;
            case ErrorCode.Exception:
                // 매칭 되고, 서버에서 방 생성할 때 에러 발생 시 exception이 리턴됨
                // 이 경우 다시 매칭 신청해야 됨
                debugLog = string.Format("EXCEPTION_OCCUR", args.Reason);
                break;
        }
    }

9.인게임 서버 접속 요청 및 이벤트

위의 매칭신청에 성공하면 자동적으로 인게임 서버로 입장하는 함수를 호출하게 된다.

    private void ProcessMatchSuccess(MatchMakingResponseEventArgs args)
    {
        ErrorInfo errorInfo;

        if (!Backend.Match.JoinGameServer(args.RoomInfo.m_inGameServerEndPoint.m_address, args.RoomInfo.m_inGameServerEndPoint.m_port, false, out errorInfo)) 
        {
            var debugLog = string.Format("FAIL_ACCESS_INGAME", errorInfo.ToString(), string.Empty);
            Debug.Log(debugLog);
        }
        inGameRoomToken = args.RoomInfo.m_inGameRoomToken;
    }

인게임 서버에 접속에 성공하면, Game 이벤트 핸들러에서 인게임룸으로 입장을 시도한다.

        Backend.Match.OnSessionJoinInServer += (args) =>
        {
            Debug.Log("OnSessionJoinInServer : " + args.ErrInfo);
            AccessInGameRoom(inGameRoomToken);
            ViewCard.Inst.SceneChager("GAME_SCENE"); // Main Scene -> Game Scene 변경
        };
    private void AccessInGameRoom(string roomToken)
    {
        Backend.Match.JoinGameRoom(roomToken);
    }

10.데이터 송수신

데이터 송수신 단계는 2단계로 나뉘는데, 먼저 게임이 시작되기 전 유저들의 정보를 교환하게 된다. 다른 하나는 게임 중의 데이터 전송이다. 데이터 전송의 방식은 바이트 배열을 이용하게 된다. 게임이 시작되기 전에는 게임에 필요한 상대방의 덱 정보와, 플레이어의 이미지, 서버, 닉네임 정보를 교환하게 된다. 가공해야 할 데이터의 양이 많아 partial을 나누어 따로 관리하고 있다.

        Backend.Match.OnMatchRelay += (args) => { // 상대 유저가 데이터를 송신할 때 핸들러가 진행됨
            if (Backend.UserNickName != args.From.NickName) // 게임룸에 접속해 있는 모든 유저가 데이터를 확인할 수 있어, 상대방이 보낸 메시지만 가공
            {
                if (args.BinaryUserData[0] == Convert.ToByte(101)) // 상대가 보낸 바이트 배열의 첫 요소가 101(int)을 바이트로 보냈을 때 사진 정보를 업데이트한다.
                {
                    MyId(Backend.Match.GetMySessionId()); // 내정보를 저장 (SessionId, 닉네임)
                    OtherId(args.From.SessionId); // 상대 정보를 저장 (SessionId, 닉네임)
                    byte2image1(args.BinaryUserData, args.From.NickName); // 게임 화면의 상대방 플레이어 이미지 업데이트
                }
                else if (args.BinaryUserData[0] == Convert.ToByte(102)) // 상대가 보낸 바이트 배열의 첫 요소가 102(int)을 바이트로 보냈을 때 서버 정보를 업데이트한다. 
                {
                    byte2image2(args.BinaryUserData); //게임 화면의 상대방 서버 이미지 업데이트
                }
                else
                {
                    DataProcess(args.BinaryUserData); // 이외의 게임 진행에 필요한 데이터 주고 받기
                }
            }
        };

바이트 배열의 첫 요소를 확인하여, 해당하는 함수로 이동하게 된다. 먼저 101, 102는 유저의 정보를 업데이트한다.

송신

public void SendMyInfo()
    {
        byte[] playerimage = myPlayer.EncodeToPNG(); // 내 플레이어 이미지 파일 (PNG 파일 -> byte)
        byte[] send =new byte[] {Convert.ToByte(101)};
        byte[] bytedata = new byte[playerimage.Length+send.Length];
        Array.Copy(send,0,bytedata,0,send.Length);
        Array.Copy(playerimage,0 ,bytedata,send.Length,playerimage.Length);
        Backend.Match.SendDataToInGameRoom(bytedata);
        
        byte[] serverimage = myServer.EncodeToPNG(); // 내 서버 이미지 파일 (PNG 파일 -> byte)
        byte[] send1 =new byte[] {Convert.ToByte(102)};
        byte[] bytedata1 = new byte[serverimage.Length+send1.Length];
        Array.Copy(send1,0,bytedata1,0,send1.Length);
        Array.Copy(serverimage,0 ,bytedata1,send1.Length,serverimage.Length);
        Backend.Match.SendDataToInGameRoom(bytedata1);
        
        // Game Scene에서 CardManager에서 덱의 셔플이 끝나면 그 정보를 보낸다. 카드의 정보는 6자리 코드로 이루어져있다.
        // 그러므로 3번에 나뉘어 보내게 되는데, 처음에는 00xxxx , 두번째는 xx00xx, 마지막으로 xxxx00 으로 보내게 된다. 
        byte[] item1 = CardManager.Inst.aitembuffer;
        byte[] send2 =new byte[] {Convert.ToByte(103)};
        byte[] bytedata2 = new byte[item1.Length+send2.Length];
        Array.Copy(send2,0,bytedata2,0,send2.Length);
        Array.Copy(item1,0 ,bytedata2,send2.Length,item1.Length);
        Backend.Match.SendDataToInGameRoom(bytedata2);

        byte[] item2 = CardManager.Inst.bitembuffer;
        byte[] send3 =new byte[] {Convert.ToByte(104)};
        byte[] bytedata3 = new byte[item2.Length+send3.Length];
        Array.Copy(send3,0,bytedata3,0,send3.Length);
        Array.Copy(item2,0 ,bytedata3,send3.Length,item2.Length);
        Backend.Match.SendDataToInGameRoom(bytedata3);

        byte[] item3 = CardManager.Inst.citembuffer;
        byte[] send4 =new byte[] {Convert.ToByte(105)};
        byte[] bytedata4 = new byte[item3.Length+send4.Length];
        Array.Copy(send4,0,bytedata4,0,send4.Length);
        Array.Copy(item3,0 ,bytedata4,send4.Length,item3.Length);
        Backend.Match.SendDataToInGameRoom(bytedata4);
    }

수신

    public void byte2image1(byte[] array,string name) // 플레이어 이미지와 닉네임을 업데이트한다.
    {
        byte[] calc = new byte[array.Length-1];
        Array.Copy(array,1,calc,0,calc.Length);
        Texture2D tex = new Texture2D(2,2);
        tex.LoadImage(calc);
        Rect rect = new Rect(0,0,tex.width,tex.height);
        GameObject.Find("OtherArea").transform.GetChild(0).gameObject.transform.GetChild(0).GetComponent<SpriteRenderer>().sprite = Sprite.Create(tex,rect,new Vector2(0.5f,0.5f));
        GameObject.Find("OtherArea").transform.GetChild(0).gameObject.transform.GetChild(2).GetComponent<TextMeshPro>().text = name;
    }
    public void byte2image2(byte[] array) // 서버 이미지를 업데이트한다.
    {
        byte[] calc = new byte[array.Length-1];
        Array.Copy(array,1,calc,0,calc.Length);
        Texture2D tex = new Texture2D(2,2);
        tex.LoadImage(calc);
        Rect rect = new Rect(0,0,tex.width,tex.height);
        GameObject.Find("OtherArea").transform.GetChild(0).gameObject.transform.GetChild(1).GetComponent<SpriteRenderer>().sprite = Sprite.Create(tex,rect,new Vector2(0.5f,0.5f));
    }
    public void DataProcess(byte[] array)
    {
        byte[] calc = new byte[array.Length-1];
        Array.Copy(array,1,calc,0,calc.Length);
        switch(Convert.ToInt32(array[0]))
        {
            case 103:
                SetOtherItem(calc,Convert.ToInt32(array[0]));
                break;
            case 104:
                SetOtherItem(calc,Convert.ToInt32(array[0]));
                break;
            case 105:
                SetOtherItem(calc,Convert.ToInt32(array[0]));
                break;
                ...
        }
    }
    public void SetOtherItem(byte[] array,int index) //받아온 카드 code를 입력한다.
    {
        if(index == 103)
        {
            for(int i = 0;i<otherItem.Length;i++)
            {
                otherItem[i]+=Convert.ToInt32(array[i])*10000;
            }
        }
        else if(index == 104)
        {
            for(int i = 0;i<otherItem.Length;i++)
            {
                otherItem[i]+=Convert.ToInt32(array[i])*100;
            }
        }
        else if(index == 105)
        {
            for(int i = 0;i<otherItem.Length;i++)
            {
                otherItem[i]+=Convert.ToInt32(array[i]);
            }
        }
    }

받아온 카드 code를 통해 상대방의 덱을 CardManager에 저장한다. 아래의 방식은 내 덱을 만드는 법과 똑같다.

    public List<Item> LoadOtherdeck()
    {
        List<Item> otherItemBuffer = new List<Item>(50);
        string[] type = new string[3] {"monster.xml","magic.xml","equip.xml"};
        foreach(var key in otherItem)
        {
            if(key.ToString().Substring(0,1)== "1")
            {
                XDocument document = XDocument.Load(@"C:\nexon_maplestory_battle_monsters_league_s\Assets\Resources\CARD\xml\" + type[0]);
                var books = from row in document.Descendants("row")
                            where(row.Element("CODE").Value == key.ToString())
                            select new
                            {
                                name = row.Element("NAME").Value,
                                grade = row.Element("GRADE").Value,
                                cost = row.Element("COST").Value,
                                att = row.Element("ATT").Value,
                                def = row.Element("DEF").Value,
                                abl = row.Element("ABL").Value,
                                code = row.Element("CODE").Value,
                                area = row.Element("AREA").Value,
                                ablcode = row.Element("ABLCODE").Value,
                                link = row.Element("LINK").Value
                            };
                
                    foreach(var row in books)
                    {
                        Item item = new Item();
                        item.name = row.name;
                        item.grade = int.Parse(row.grade);
                        item.cost = Resources.Load<Sprite>("CARD/num/"+row.cost);
                        item.cost_num = int.Parse(row.cost);
                        item.attack = int.Parse(row.att);
                        item.health=int.Parse(row.def);
                        item.abl = row.abl;
                        item.code = int.Parse(row.code);
                        item.main = Resources.Load<Sprite>("CARD/monster/"+row.code);
                        item.background = Resources.Load<Sprite>("CARD/background/"+row.area);
                        item.cardbase = CardManager.Inst.card_base_mon[item.grade];
                        item.ablcode = row.ablcode;
                        item.link = row.link;
                        otherItemBuffer.Add(item);
                    }
            }
            if(key.ToString().Substring(0,1)== "2")
            {
                XDocument document = XDocument.Load(@"C:\nexon_maplestory_battle_monsters_league_s\Assets\Resources\CARD\xml\" + type[1]);
                var books = from row in document.Descendants("row")
                            where(row.Element("CODE").Value == key.ToString())
                            select new
                            {
                                name = row.Element("NAME").Value,
                                grade = row.Element("GRADE").Value,
                                cost = row.Element("COST").Value,
                                abl = row.Element("ABL").Value,
                                code = row.Element("CODE").Value
                            };

                foreach (var row in books)
                {
                    Item item = new Item();
                    item.name = row.name;
                    item.grade = int.Parse(row.grade);
                    item.cost = Resources.Load<Sprite>("CARD/num/" + row.cost);
                    item.cost_num = int.Parse(row.cost);
                    item.attack = -1;
                    item.health = -1;
                    item.abl = row.abl;
                    item.code = int.Parse(row.code);
                    if (int.Parse(row.code.Substring(3, 2)) <= 27)
                    {
                        item.background = Resources.Load<Sprite>("CARD/magic/" + row.code);
                        item.cardbase = CardManager.Inst.card_base[item.grade];
                        item.main = null;
                    }
                    otherItemBuffer.Add(item);

                }
            }
            if(key.ToString().Substring(0,1)== "3")
            {
                XDocument document = XDocument.Load(@"C:\nexon_maplestory_battle_monsters_league_s\Assets\Resources\CARD\xml\" + type[2]);
                var books = from row in document.Descendants("row")
                            where(row.Element("CODE").Value == key.ToString())
                            select new
                            {
                                name = row.Element("NAME").Value,
                                grade = row.Element("GRADE").Value,
                                cost = row.Element("COST").Value,
                                att = row.Element("ATT").Value,
                                def = row.Element("DEF").Value,
                                abl = row.Element("ABL").Value,
                                code = row.Element("CODE").Value
                            };
                foreach (var row in books)
                {
                    Item item = new Item();
                    item.name = row.name;
                    item.grade = int.Parse(row.grade);
                    item.cost = Resources.Load<Sprite>("CARD/num/" + row.cost);
                    item.attack = int.Parse(row.att);
                    item.health = int.Parse(row.def);
                    item.abl = row.abl;
                    item.code = int.Parse(row.code);
                    item.cardbase = CardManager.Inst.card_base_mon[item.grade];
                    item.cost_num = int.Parse(row.cost);
                    item.background = CardManager.Inst.back[item.grade];
                    item.main = Resources.Load<Sprite>("CARD/equip/" + row.code);
                    otherItemBuffer.Add(item);
                }

            }
        }
        return otherItemBuffer;
    }

게임 중의 데이터 송수신은 추후의 post에서 다루도록 하겠다.

11.게임 결과 처리

게임의 결과를 처리하기 위해 필요한 것은 이긴 사람의 SessionID와 진 사람의 SessionID이다. 게임 시작 전에 이미 SessionID를 저장해 놓았다.

    public static void EndMatch(bool isMine) // isMine이 true면 내가 이긴 것이고, false면 상대가 이긴 것이다.
    {
        MatchGameResult nowGameResult = new MatchGameResult();
        nowGameResult.m_winners = new List<SessionId>();
        nowGameResult.m_losers = new List<SessionId>();
        if(isMine)
        {
            Debug.Log("win : " +myinfo.sessionId +"lose : " +otherinfo.sessionId);
            nowGameResult.m_winners.Add(myinfo.sessionId);
            nowGameResult.m_losers.Add(otherinfo.sessionId);
        }
        else
        {
            Debug.Log("win : " +otherinfo.sessionId +"lose : " +myinfo.sessionId);
            nowGameResult.m_winners.Add(otherinfo.sessionId);
            nowGameResult.m_losers.Add(myinfo.sessionId);
        }
        Backend.Match.MatchEnd(nowGameResult);
    }

이벤트 핸들러에서는 딱히 진행할 것이 없고, 양측 유저가 모두 게임의 결과를 서버로 보내게 되면 게임은 종료되게 된다. 결과 창을 통해 다시 Main Scene으로 돌아갈 수 있다.