본문 바로가기

유니티 개발 정보/네트워크

유니티를 이용하여 온라인 게임 만들기 - (1/2)

원본 보기

유니티를 이용하여 온라인 게임 만들기 - 1


이번 튜토리얼에서는 유니티 네트워크 기능을 사용하여, 어떻게 다중 플레이어 게임을 구현하는지 알아볼 것이다. 우리는 이전에 온라인 게임을 만들어 본 적은 없지만, 현재 작업중인 LFG: The Fork of Truth이 어떤 방식으로 구현되어있는지 보여줄 것이다. 이 게임은 4명이 함께 게임을 할 수 있는데, 각 플레이어는 LFG 만화에 등장하는 캐릭터중 하나를 선택해서 플레이 할 수 있다. 적을 무찌르고, 퀘스트를 완료하기 위해서는, 각 플레이어의 능력을 잘 조합해야 한다.

프로젝트를 시작할 때 했던 중요한 결정은, 네트워크를 먼저 구현하고, 다른 나머지를 만드는 것이였다. 새로운 것을 추가할 때 마다, 우리는 네트워크 상태에서도 잘 되는지 테스트 했다. 이러한 방법으로 많은 시간을 아낄 수 있었는데, 왜냐하면 네트워크를 나중에 구현하게 되면, 코드를 많이 수정해야 할 수도 있기 때문이다. 이번 튜토리얼을 따라하기 위해서는, 기본적인 유니티와 C#을 알고 있어야 한다.


우리는 다음과 같은 사항들을 다뤄볼 것이다.

  1. 서버 구현 및 존재하는 호스트에 접속하기.
  2. 네트워크 상에서, 플레이어와 객체가 생성되는 방법.
  3. 상태 동기화와 RPC(Remote Procedure Calls)를 이용한 네트워크 통신.
  4. 데이터 패킷간의 값을 보간하고 예측하기.

 네트워크는 우리에게 새로운 주제였고, 나는 이 튜토리얼 비디오를 찾았는데, 처음 시작할 때 매우 유용했다. 자바 스크립트로 설명이 되어있지만, 똑같은 내용을 첫 세번째 단락에서 다루고 있다.



서버 만들기

자 시작해보자! 새로운 유니티 프로젝트에서, "NetworkManager"라는 이름으로 C# 스크립트를 생성하자. 이 빈 스크립트를 씬에 있는 카메라 혹은 텅 빈 객체에 붙이자. 이는 서버를 호스팅하고, 존재하는 서버에 접속하는 일을 담당할 것이다.

서버를 만들기 위해서, 우리는 네트워크 상에서 이를 초기화 해야하고, 마스터 서버에 등록해야 한다.
초기화를 하기 위해서는 최대 플레이어 수(우리 같은 경우 4명) 와 포트 넘버(25000)를 요구한다. 서버 등록을 위해서, 게임타입의 이름은 반드시 고유한 이름이여야 하며 그렇지 않으면, 이와 똑같은 이름을 사용하는 다른 프로젝트와 문제를 발생할 것이다. 게임 이름은 어떤 이름도 올 수 있는데, 우리의 경우는 Player의 이름을 사용했다. 밑에 있는 코드를 NetworkManager 스크립트에 추가하자.

1
2
3
4
5
6
7
8
private const string typeName = "UniqueGameName";
private const string gameName = "RoomName";
 
private void StartServer()
{
    Network.InitializeServer(4, 25000, !Network.HavePublicAddress());
    MasterServer.RegisterHost(typeName, gameName);
}

서버가 성공적으로 초기화되었다면, OnServerInitialized()가 호출 될 것이다. 지금은, 서버가 실제 초기화되었다는 메시지만을 받을 수 있을 것이다.

1
2
3
4
void OnServerInitialized()
{
    Debug.Log("Server Initializied");
}

다음으로 해야 할 일은 우리가 원할 때 실제로 서버를 시작할 수 있게 간단한 입력 버튼을 만드는 것이다. 테스트를 하기 위해서 Unity GUI를 사용하여 버튼을 만들 것이다. 우리는 오직, 서버가 시작되지 않았을 경우나, 서버에 접속하지 않았을 때만 보이기를 원한다. 그래서 버튼은 유저가 클라이언트와 서버가 아닌 상태에서만 보일 것이다.

1
2
3
4
5
6
7
8
void OnGUI()
{
    if (!Network.isClient && !Network.isServer)
    {
        if (GUI.Button(new Rect(100, 100, 250, 100), "Start Server"))
            StartServer();
    }
}

이제 지금까지 개발했던 것들을 테스트 해 볼 시간이다. 프로젝트를 시작하면, "Start Sever" 버튼을 볼 수 있을 것이다.(1A 그림). 버튼을 누르면, 콘솔창에 서버가 초기화 되었다는 메시지를 볼 수 있을 것이다. 그 후 버튼은 사라진다.(1B 그림)

1A
1B



서버에 접속하기

이제 우리는 서버를 만드는 방법을 알게 되었다. 그러나 아직, 존재하는 서버를 찾거나, 서버에 접속할 수는 없다. 이를 하기 위해서는, HostData의 리스트를 얻기위해 마스터 서버에 요청을 해야 한다. 이 리스트는 서버에 접속하기위한 모든 데이터를 가지고 있다. 한번 Host 리스트를 받으면, 메시지가OnMasterServerEvent()에 의해서 게임에 전달 된다. 이 함수는 많은 이벤트에서 호출되기때문에, 이 메시지가 MasterServerEvent.HostListReceived와 같은지 검사를 해야 한다. 만약 같다면, Host 리스트를 저장할 수 있다.


1
2
3
4
5
6
7
8
9
10
11
12
private HostData[] hostList;
 
private void RefreshHostList()
{
    MasterServer.RequestHostList(typeName);
}
 
void OnMasterServerEvent(MasterServerEvent msEvent)
{
    if (msEvent == MasterServerEvent.HostListReceived)
        hostList = MasterServer.PollHostList();
}

서버에 접속하기 위해서, 호스트 리스트의 한 항목이 필요하다. OnConnectedToServer()은 실제 서버에 접속했을 때 호출된다. 나중에 우리는 이 함수를 확장할 것이다.

1
2
3
4
5
6
7
8
9
private void JoinServer(HostData hostData)
{
    Network.Connect(hostData);
}
 
void OnConnectedToServer()
{
    Debug.Log("Server Joined");
}

추가적인 버튼을 생성함으로써, 이제 막 만든 함수를 호출할 수 있을 것이다. 이제 시작을 하면 2개의 버튼이 있을 것이다. 하나는 서버를 시작하는 버튼, 그리고 나머지 하나는 Host 리스트를 갱신하는 버튼이다. 새로운 버튼은 모든 서버에 대해 생성된다.  그리고 새로 추가된 버튼을 누르면, 유저는 그와 일치하는 방에 접속할 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void OnGUI()
{
    if (!Network.isClient && !Network.isServer)
    {
        if (GUI.Button(new Rect(100, 100, 250, 100), "Start Server"))
            StartServer();
 
        if (GUI.Button(new Rect(100, 250, 250, 100), "Refresh Hosts"))
            RefreshHostList();
 
        if (hostList != null)
        {
            for (int i = 0; i < hostList.Length; i++)
            {
                if (GUI.Button(new Rect(400, 100 + (110 * i), 300, 100), hostList[i].gameName))
                    JoinServer(hostList[i]);
            }
        }
    }
}

이는 또 다른 테스트를 할 수 있는 좋은 시점이다(2A 그림).  멀티 플레이를 테스트할 때 주의해야할 점은 약간 오래 걸린다는 것이다. 왜냐하면 항상 2개의 게임 인스턴스가 필요하기 때문이다. 2개를 실행시키기 위해서, 새로 빌드를 하라(테스트를 위해 PC로). 그리고 게임을 실행시킨 다음 "Start Server"을 눌러라. 이제 우리는 유니티 편집기에서 새로운 기능을 테스트 할 수 있게 되었다. 리스트를 갱신한 하면, 또 다른 버튼이 나타날 것이다.(2B 그림) 이는 게임 내에서 2개의 인스턴스를 연결시켜 준다.(2C 그림)


2A
2B
2C




플레이어 생성하기

이제 우리는 여러명의 플레이어를 하나의 서버에 연결시킬수 있게 되었고, 우리는 이제 게임과 관련된 코드를 작성할 것이다. 바닥 평면, 플레이어, 그리고 라이트로 간단하게 씬을 만들어 보자. 플레이어에 리지드바디를 붙이고, 회전을 하지 못하게 Constraints->Freeze Rotation에서 X, Y, Z를 모두 체크하자.
이제 플레이어(큐브)가 움직이면서 회전과 같은 이상한 행동을 하지 못할 것이다.


Player 스크립트를 만들고, 이를 플레이어 객체에 붙인 다음, 스크립트에 다음과 같은 코드를 넣어 보자:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Player : MonoBehaviour
{
    public float speed = 10f;
 
    void Update()
    {
        InputMovement();
    }
 
    void InputMovement()
    {
        if (Input.GetKey(KeyCode.W))
            rigidbody.MovePosition(rigidbody.position + Vector3.forward * speed * Time.deltaTime);
 
        if (Input.GetKey(KeyCode.S))
            rigidbody.MovePosition(rigidbody.position - Vector3.forward * speed * Time.deltaTime);
 
        if (Input.GetKey(KeyCode.D))
            rigidbody.MovePosition(rigidbody.position + Vector3.right * speed * Time.deltaTime);
 
        if (Input.GetKey(KeyCode.A))
            rigidbody.MovePosition(rigidbody.position - Vector3.right * speed * Time.deltaTime);
    }
}

다음, 네트워크 뷰 컴포넌트를 Player 객체(Component > Miscellaneous > Network View)에 붙이자. 
이는 플레이어를 동기화 시키기위해서, 우리가 네트워크를 통해 데이터 패킷을  보낼 수 있게 해준다.
state synchronization 필드는 자동적으로 "reliable delta compressed"로 설정된다. 이는 동기화된 데이터는 자동으로 보내진다는 것을 의미하는데, 그러나 값이 변경되었을 경우에만 해당된다. 그래서 예를 들면, 만약 Player를 움직일 경우, Player의 위치는 서버에 업데이트 될 것이다. "off"로 설정하면, 더 이상 자동으로 동기화 되지 않고, 수동으로 해줘야 한다. 지금은 이 설정을 "reliable"로 설정하자.  이 튜토리얼에서, 후에 이 옵션에 대해서 더 자세하게 다뤄볼 것이다. 프리팹을 만들기 위해서 player 객체를 프로젝트 뷰에 추가하자. 이제 우리는 네트워크에서 player를 생성할 수 있다.
 
NetworkManager 스크립트에, player 프리팹을 위한 public game object 변수를 추가하자. 새로운 함수 SpawnPlayer()에서, 프리팹은 네트워크상에서 생성될 것이다. 그래서 모든 클라이언트들은 게임 안에서 Player 객체를 볼 수 있을 것이다. Network.Instantiate()는 프리팹뿐만 아니라, position, rotation, group를 매개변수로 가지는데, 그래서 나는 Spawn Point를 만드는 것을 추천한다.

OnServerInitialized()와 ONConnectedToServer()에 플레이어를 생성하는 SpawnPlayer()를 넣자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public GameObject playerPrefab;
 
void OnServerInitialized()
{
    SpawnPlayer();
}
 
void OnConnectedToServer()
{
    SpawnPlayer();
}
 
private void SpawnPlayer()
{
    Network.Instantiate(playerPrefab, new Vector3(0f, 5f, 0f), Quaternion.identity, 0);
}

자 이제 또 다시 테스트를 할 시간이다. 아까 전 처럼 2개의 인스턴스를 만들어야 한다. 게임을 실행해보면, 자기자신의 Player뿐만 아니라 모든 Player를 컨트롤 할 수 있다는 것을 알 게 될 것이다. 이는 멀티 플레이어 게임을 만드는데 신경써야하는 중요한 사항을 보여준다 : 누가 무엇을 컨트롤 하는가?



이 문제를 고치는 한가지 방법은, player 코드를 확인하는 부분을 만드는 것이다. 그러면 오직 객체를 생성시킨 유저의 입력만 받게 된다. 왜냐하면 우리는 Network View에서 "reliable synchronizion"으로 설정을 했기 때문에, 데이터는 자동으로 네트워크를 통해 전달 되고, 다른 정보는 필요하지 않기 때문이다.

이를 구현하기 위해, player 스크립트에서 네트워크 상에 객체가 "나의 것(isMine)"인지 확인을 해야 한다. Update()에 다음의 if문을 추가하자.

1
2
3
4
5
6
7
void Update()
{
    if (networkView.isMine)
    {
        InputMovement();
    }
}
테스트를 해보면, 자기 자신의 Player 객체만 움직이는 것을 볼 수 있을 것이다.

다른 방법은 모든 입력을 서버에 보내는 것이다. 이는 당신이 전달한 데이터를 실제 움직임으로 변환시키고, 네트워크 상에 연결된 모든 사람이에게 당신의 새로운 위치 값을 전달한다. 이 방법의 장점은, 서버에서 모두 동기화 된다는 것이다. 이는 유저가 자신의 로컬 클라이언트에서 속임수를 쓰는 것을 막을 수 있다. 하지만 이 방법은 클라이언트와 서버간의 호출시간을 발생시키는데, 유저가 입력한 행동을 눈으로 보는데 얼마 동안 기다려야 한다는 단점이 있다.


(다음 글에서 계속)