본문 바로가기

- Programming/- C#

★ 10. c# 네트워크 개발 p9

반응형

==================================================================

모든 출처는

- 유니티 개발자를 위한 C#으로 온라인 게임 서버 만들기 - 저자 이석현, 출판사 한빛미디어

그리고 URL :

http://lab.gamecodi.com/board/zboard.php?id=GAMECODILAB_Lecture_series&page=1&sn1=&divpage=1&sn=off&ss=on&sc=on&select_arrange=hit&desc=asc&no=78

==================================================================


C#으로 게임 서버 만들기 - 8. 온라인 세균전 게임 만들기(유저 매칭, 클라이언트 연동 - I)

이 강좌부터 파란 글씨로 적었던 소제목의 순서 번호는 생략하겠습니다.

유저 매칭

매칭 요청과 게임방 입장

클라이언트의 매칭 요청은 CGameUser 클래스의 process_user_operation 메소드에서 처리합니다.
process_user_operation 메소드는 클라이언트가 보내온 모든 메시지들을 처리하는 메소드입니다.
서버에 접속한 이후 클라이언트는 서버와 수많은 메시지들을 주고 받는데 모든 메시지 처리의 시작 부분이 바로
process_user_operation 메소드라고 할 수 있죠.

매칭 요청은 클라이언트가 PROTOCOL.ENTER_GAME_ROOM_REQ 프로토콜을 보내오면서 시작됩니다.
이 메소드에서 메시지를 핸들링하여 CGameServer 클래스의 matching_req 메소드를 호출합니다.

1
2
3
4
5
6
7
8
9
10
11
void IPeer.process_user_operation(CPacket msg)
{
    PROTOCOL protocol = (PROTOCOL)msg.pop_protocol_id();
    Console.WriteLine("protocol id " + protocol);
    switch (protocol)
    {
        case PROTOCOL.ENTER_GAME_ROOM_REQ:
            Program.game_main.matching_req(this);
            break;
    }
}
cs

일단 매칭 요청이 오면 대기 리스트에 해당 유저를 추가합니다.
세균전 게임은 1:1로 진행되기 때문에 두 명의 유저가 매칭 요청을 해야 하나의 게임 방을
생성할 수 있습니다. 따라서 대기 리스트에 두 명이 채워지기 전까지는 일단 대기 상태로 있습니다.
matching_req 메소드의 소스 코드를 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// <summary>
/// 유저로부터 매칭 요청이 왔을 때 호출됨.
/// </summary>
/// <param name="user">매칭을 신청한 유저 객체</param>
public void matching_req(CGameUser user)
{
    // 매칭 대기 리스트에 추가.
    this.matching_waiting_users.Add(user);
 
    // 2명이 모이면 매칭 성공.
    if (this.matching_waiting_users.Count == 2)
    {
        // 게임 방 생성.
        this.room_manager.create_room(this.matching_waiting_users[0], this.matching_waiting_users[1]);
 
        // 매칭 대기 리스트 삭제.
        this.matching_waiting_users.Clear();
    }
}
cs

두 명이 매칭 요청을 하면 게임 방을 생성하고 대기 리스트에서 삭제합니다.
이후 또 다른 매칭 요청이 오면 같은 방식으로 게임 방을 생성하여 유저들을 차례대로 입장시킵니다.

room_manager.create_room 메소드를 호출할 때 현재 대기 리스트에 들어있는 유저 두 명을
파라미터로 넘겨주게 되는데 이 순간이 바로 하나의 방으로 유저들을 입장시키는 순간이 됩니다.
아래는 room_manager에서 두 명의 유저들을 받아 하나의 게임 방을 생성하는 코드입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class CGameRoomManager
{
    ...
 
    /// <summary>
    /// 매칭을 요청한 유저들을 넘겨 받아 게임 방을 생성한다.
    /// </summary>
    /// <param name="user1"></param>
    /// <param name="user2"></param>
    public void create_room(CGameUser user1, CGameUser user2)
    {
        // 게임 방을 생성하여 입장 시킴.
        CGameRoom battleroom = new CGameRoom();
        battleroom.enter_gameroom(user1, user2);
 
        // 방 리스트에 추가하여 관리한다.
        this.rooms.Add(battleroom);
    }
 
    ...
}
cs

new 연산자를 통해서 논리적인 게임 방 객체 하나를 만든 뒤 유저들을 플레이어로 참가시켜 게임 방 안으로 입장시킵니다.
이 작업이 이루어진 후 부터 각 유저들은 상대방의 존재를 알게 되고 서로 통신을 하며 게임을 진행할 수 있게 됩니다.
여기까지의 호출 과정을 다시 한번 정리해보겠습니다.

- 게임 서버에 접속

- 매칭 요청 (PROTOCOL.ENTER_GAME_ROOM_REQ)

- CGameUser 클래스에서 프로토콜 핸들링

- CGameServer 클래스의 matching_req 메소드 호출

- 대기 리스트에 추가

- CRoomManager 클래스의 create_room 메소드 호출

- new CGameRoom() 으로 게임방 객체 생성

- CGameRoom 클래스의 enter_gameroom 메소드 호출
[클라이언트가 게임방에 입장하는 과정]

이러한 과정을 거치면 멀리 떨어진 클라이언트들이 하나의 방 안에서 동시에 게임을 진행할 수 있게 됩니다.
단순히 생각해보면 클라이언트는 여러 소켓들 중 하나일 뿐이지만 CGameRoom 이라는 논리적인 공간을 통해서
그룹화 되고 같은 그룹 내에 존재하는 유저들끼리 패킷을 송, 수신 하도록 제어해주면 마치
하나의 공간 안에 있는 듯한 느낌을 받게 되는 것입니다.

클라이언트와의 연동

이제부터는 클라이언트측의 소스 코드를 작성해보면서 서버에서 작업했던 내용들과 연동해보도록 하겠습니다.
서버 작업은 데이터 위주로 이루어졌지만 클라이언트는 화면에 보여지는 부분이 대부분을 차지합니다.

먼저 유저 인터페이스 구현을 해본 뒤, 이를 통해서 서버에 요청하는 부분의 코드를 작성해볼 것입니다.
그리고 서버의 응답에 따라 클라이언트의 화면을 갱신하여 실시간으로 서버와 통신하는 게임의 모습을 만들어 나가도록 하겠습니다.
비록 게임의 크기는 작지만 실시간 온라인 게임 개발의 원리를 이해하는데에는 충분하리라 생각됩니다.

유니티 프로젝트 구성
유니티 엔진을 실행한 뒤 VirusWarClient라는 이름으로 프로젝트를 생성합니다.



위 그림대로 씬을 구성합니다.
GameObject -> Create Empty 메뉴를 이용하여 씬을 구성하는 오브젝트들을 생성합니다.
이름은 각각 NetworkManager, MainTitle, BattleRoom으로 하며 transform 값은 기본 값으로 설정합니다.
지금 생성하는 오브젝트들은 화면에 보이지 않고 스크립트 수행을 위한 용도로만 사용되기 때문에
위치 값, 회전 값 등은 아무 의미가 없으니 기본 설정으로 둡니다.
다음으로 FreeNet 라이브러리 파일을 연동해보는 과정을 살펴보겠습니다.
유니티 버전의 에코 클라이언트 프로젝트를 만들었을 때 했던 과정과 비슷합니다.


FreeNet 라이브러리 파일 구성

Assets 폴더 밑에 FreeNet 폴더를 생성한 뒤 FreeNet.dll 파일을 복사해옵니다.
나머지 스크립트 파일들은 이름에 맞게 생성해줍니다. FreeNet 폴더에서 마우스 오른쪽 버튼을 눌러
Create -> C# Script 메뉴를 이용하여 스크립트를 생성해줍니다. 아직 내용은 작성하지 않아도 됩니다.

다음으로 Resources/images 폴더를 생성하여 아래 그림과 같이 이미지 파일들을 복사해옵니다.


이미지 폴더 구성

Resources/scripts 폴더를 생성하여 아래 그림과 같이 스크립트 파일들을 만듭니다.


스크립트 폴더 구성

마지막으로 씬에 있는 게임 오브젝트에 스크립트 파일을 연결시킵니다.
NetworkManager 오브젝트에는 CNetworkManager.cs
MainTitle 오브젝트에는 MainTitle.cs
BattleRoom 오브젝트에는 CBattleRoom.cs 파일을 각각 연결합니다.
연결 방법은 scripts 폴더에서 cs파일을 마우스로 드래그한 뒤 Hierachy 창의
게임 오브젝트 이름 위에 올려 놓으면 됩니다.




게임 오브젝트에 스크립트들이 정상적으로 연결된 모습입니다.

서버 접속
유니티 프로젝트의 구성이 완료 되었으며 이제 소스 코드 작성에 들어가보겠습니다.
제일 먼저 NetworkManager 오브젝트에 연결되어 있는 CNetworkManager.cs 스크립트의 소스 코드입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
using UnityEngine;
using System;
using System.Collections;
using FreeNet;
using FreeNetUnity;
using VirusWarGameServer;
 
public class CNetworkManager : MonoBehaviour {
 
    CFreeNetUnityService gameserver;
    string received_msg;
 
    public MonoBehaviour message_receiver;
 
    void Awake()
    {
        this.received_msg = "";
 
        // 네트워크 통신을 위해 CFreeNetUnityService 객체를 추가합니다.
        this.gameserver = gameObject.AddComponent<CFreeNetUnityService>();
 
        // 상태 변화 (접속, 끊김 등)를 통보 받을 델리게이트 설정.
        this.gameserver.appcallback_on_status_changed += on_status_changed;
 
        // 패킷 수신 델리게이트 설정
        this.gameserver.appcallback_on_message += on_message;
    }
 
    void Start()
    {
        connect();
    }
 
    public void connect()
    {
        this.gameserver.connect("127.0.0.1"7979);
    }
 
    /// <summary>
    /// 네트워크 상태 변경시 호출될 콜백 메소드
    /// </summary>
    /// <param name="status"></param>
    void on_status_changed(NETWORK_EVENT status)
    {
        switch (status)
        {
            // 접속 성공
            case NETWORK_EVENT.connected:
                {
                    CLogManager.log("on connected");
                    this.received_msg += "on connected\n";
 
                    GameObject.Find("MainTitle").GetComponent<CMainTitle>().on_connected();
                }
                break;
 
            // 연결 끊김
            case NETWORK_EVENT.disconnected:
                {
                    CLogManager.log("disconnected");
                    this.received_msg += "disconnected\n";
                }
                break;
        }
    }
 
    void on_message(CPacket msg)
    {
        this.message_receiver.SendMessage("on_recv", msg);
    }
 
    public void send(CPacket msg)
    {
        this.gameserver.send(msg);
    }
}
cs

NetworkManager는 게임이 시작되면 제일 먼저 서버에 접속하는 일을 담당합니다.
정상적으로 접속이 완료되면 MainTitle 오브젝트의 on_connected 메소드를 호출하여 접속 사실을 통보합니다.
또한 서버로부터 메시지를 수신하면 현재 설정되어 있는 MessageReceiver 객체의 on_recv 메소드를 호출합니다.
수신된 패킬 객체를 파라미터로 넘겨 주어 해당 오브젝트에서 패킷을 처리할 수 있도록 만들어줍니다.
NetworkManager는 유니티 엔진에서 수행되는 스크립트들과 게임 서버 사이의
중간 역할을 담당하는 스크립트라고 생각하면 됩니다.
그렇기 때문에 게임 로직에 대한 코드는 들어가지 않고 이벤트를 통보 해주는 등의 일반적인 일들을 수행합니다.
이렇게 설계해 놓으면 추후 다른 게임을 개발할 때도 NetworkManager는 크게 수정하지 않고 재사용 할 수 있게 됩니다.

접속 이후 화면 처리
서버에 접속이 완료되면 MainTitle 오브젝트에 연결되어 있는 CMainTitle.cs 스크립트의 on_connected 메소드가 수행됩니다.
on_connected 메소드에는 접속 완료를 뜻하는 is_connected 플래그를 true로 설정한 뒤
접속 이후의 로직을 처리할 after_connected 코루틴을 수행시킵니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/// <summary>
/// 서버에 접속이 완료되면 호출됨
/// </summary>
public void on_connected()
{
    //this.user_state = USER_STATE.CONNECTED;
    this.is_connected = true;
 
    StartCoroutine("after_connected");
}
 
/// <summary>
/// 서버에 접속된 이후에 처리할 루프
/// 마우스 입력이 들어오면 ENTER_GAME_ROOM_REQ 프로토콜을 요청하고
/// 중복 요청을 방지하기 위해서 현재 코루틴을 중지시킨다.
/// </summary>
/// <returns></returns>
IEnumerator after_connected()
{
    while (true)
    {
        if (this.is_connected)
        {
            if (Input.GetMouseButtonDown(0))
            {
                CPacket msg = CPacket.create((short)PROTOCOL.ENTER_GAME_ROOM_REQ);
                this.network_manager.send(msg);
 
                StopCoroutine("after_connected");
            }
        }
 
        yield return 0;
    }
}
 
void OnGUI()
{
    if (this.is_connected)
    {
        GUI.DrawTexture(new Rect(00, Screen.width, Screen.height), this.bg);
    }
}
cs

전송이 완료되면 is_connected 플래그가 true가 되며 OnGUI에서 is_connected 플래그의 상태를 체크하여
true일 경우 화면에 배경 이미지를 출력합니다. 이 이미지가 정상적으로 출력된다면 서버에 접속이
성공했다는 뜻입니다. 만약 이미지가 출력되지 않고 까만 화면만 나온다면 서버와의 연결에 문제가 있다는 뜻입니다.

그 뒤 StartCoroutine("after_connected"); 를 통해서 after_connected라는 이름의 코루틴을 수행시킵니다.
이 코루틴에서는 마우스 입력을 받아 서버에 게임 방 입장을 요청하는 ENTER_GAME_ROOM_REQ라는 패킷을 보내게 됩니다.
패킷 요청을 한 뒤에는 중복해서 요청이 전송되는 것을 막기 위하여 현재 코루틴을 종료하여
더이상 코루틴 내의 로직이 수행되지 않도록 처리합니다.
같은 일을 수행하는 코드를 Update 메소드에서 처리할 수도 있지만 코루틴을 사용하면 시작과 중지 등의 제어를 편하게
할 수 있기 때문에 코루틴을 사용하여 구현한 것입니다.
만약 Update 메소드를 사용하여 코딩한다면 별도의 플래그 변수를 또 둬야 하는 등의 번거로움이 생기겠죠.

다음 강좌에서 클라이언트 연동 - II 가 계속됩니다.


반응형

'- Programming > - C#' 카테고리의 다른 글

★ 12. c# 네트워크 개발 p11  (0) 2017.02.16
★ 11. c# 네트워크 개발 p10  (0) 2017.02.15
★ 9. c# 네트워크 개발 p8  (1) 2017.02.13
★ 8. c# 네트워크 개발 p7  (0) 2017.02.13
★ 7. c# 네트워크 개발 p6  (0) 2017.02.12