티스토리 뷰

최근 Unity를 이용한 팀 프로젝트로 2D 멀티플레이 기반 게임을 개발하게 되었다. 

 

여기서 내가 맡은 일은 접속 부분이었다. 

- Photon PUN2를 사용한 실시간 네트워크

- 방 생성 / 방 참여 접속씬 구조

 

초기에는 접속씬의 ConnectSceneManager.cs 스크립트가 네트워크 연결부터 UI 처리까지 모두 구현하도록 코드를 짜려고 했지만, 암만 생각해도 단일책임의 원칙을 어긴 것이고, 유지보수에 악영향을 끼칠 것 같아 NetworkManager.cs 에 네트워크 관련 코드를 넣고, Init씬(일종의 까만 로딩화면)을 만들어서 거기다가 NetworkManager.cs 를 갖고 있는 오브젝트를 놓기로 했다.

 

구조 정리 :

- InitScene : NetworkManager를 배치해 최초 1회 생성

- NetworkManager : 네트워크 흐름 전부 컨트롤. 포톤 서버 연결 + 로비 입장 + RoomList 관리 + 방 생성/입장 + 씬 이동

- ConnectSceneManager : UI 버튼 눌렀을 때 NetworkManager 호출하도록 연결해주는 역할 담당

 

0. 씬 흐름은 다음과 같다. 

InitScene
   ↓ (네트워크매니저 생성 후 씬 자동 넘어감)
TitleScene
   ↓ (시작하기 버튼)
ConnectScene
   ↓ (방 생성 / 참여)
Scene_Lobby

InitScene.
TitleScene. DontDestroyOnLoad가 작동한 걸 볼 수 있다.
ConnectScene.

 

1. NetworkManager 

싱글톤 + DontDestroyOnLoad 설계

- 전체 게임에서 네트워크 매니저는 하나여야 하므로 싱글톤으로 만들었고,

- 씬 이동 시 이 네트워크 매니저가 Destroy되면 안되므로 DontDestroyOnLoad(gameObject); 를 붙였다.

- 또 Awake() 메소드에 적었다. 스크립트 객체 생성 시 가장 먼저 실행되기 때문이다.

- 이걸 Start() 메소드에 넣으면 다른 스크립트 Start 함수가 NetworkManager.Instance 를 참조하려 할 때 순서가 어긋나버려 Null이 될 수도 있다. 

Awake() Start()
1순위 2순위(Awake 이후)
오브젝트 켜져 있으면 실행됨 스크립트 켜져 있어야 실행됨
자기 자신 초기화(싱글톤 설정 등) 타 객체 참조, 초기화 완료 후 로직 실행

 

//싱글톤 유지시키는 코드
private void Awake()
{
	if (Instance == null)
	{
		Instance = this;
		DontDestroyOnLoad(gameObject); // 씬 전환 시 파괴 방지
	}
	else
	{
		Destroy(gameObject);
		return;
	}
}

 

2. 연결 흐름 : ConnectUsingSettings -> 마스터 서버 접속 -> 로비 입장

(1) ConnectUsingSettings

InitScene의 NetworkManager가 작동되면, 자동으로 연결을 시도한다.

private void Start()
    {
        if (!PhotonNetwork.IsConnected)
        {
            Debug.Log("[NM] Auto ConnectUsingSettings");
            PhotonNetwork.ConnectUsingSettings();
        }
    }

 

(2) 마스터 서버 접속 콜백(OnConnectedToMaster)

- 마스터 서버 : 게임의 로비와 방 목록을 관리하는 서버이다. 

- 위 연결 시도가 성공하면 이 OnConnectedToMaster 콜백이 실행된다.

- 참고로 포톤 라이브러리 함수들을 overriding해서 사용한다. (참고 : base.함수명() 하게 되면 부모 클래스 기본 로직 유지 + 커스텀 덧붙이기)

public override void OnConnectedToMaster()
    {
        Debug.Log("[Photon] OnConnectedToMaster -> JoinLobby");
        //로비 입장(룸 리스트 받기 위해 필수)(ConnectScene이 포톤에선 Lobby)
        PhotonNetwork.JoinLobby();
    }

 

(3) OnRoomListUpdate(List<RoomInfo> roomList)

- Photon의 이 함수는 변경된 방 정보들을 불러오는 함수이다. 근데 여기서 인자 roomList는 전체 방 목록이 아니라 변경분만을 나타내므로 전체 방 목록들을 UI로 보여주기가 어렵게 된다. 전체 상태를 유지해주는 변수가 필요하므로 cachedRoomList 라는 변수를 선언하게 됐다.

// 방 목록 캐시 (어느 씬에서든 접근 가능)
    public Dictionary<string, RoomInfo> cachedRoomList = new Dictionary<string, RoomInfo>();

    //"룸 리스트가 바뀌었다"는 신호(이벤트)
    // UI가 이 이벤트를 구독하므로 네트워크가 UI를 직접 호출하지 않아도 됨
    private System.Action OnRoomListChanged;
    
//룸 리스트 받고 UI 갱신
    public override void OnRoomListUpdate(List<RoomInfo> roomList)
    {
        Debug.Log($"[Photon] OnRoomListUpdate count={roomList.Count}");
        //포톤이 준 roomList 캐시에 반영
        foreach (var room in roomList)
        {
            Debug.Log($"[RoomDelta] name={room.Name} removed={room.RemovedFromList} " +
                  $"players={room.PlayerCount}/{room.MaxPlayers} open={room.IsOpen} visible={room.IsVisible}");
            //사라진 방이면 캐시에서 제거
            if (room.RemovedFromList) cachedRoomList.Remove(room.Name);
            //이외는 새로 업데이트
            else cachedRoomList[room.Name] = room;
        }

        Debug.Log($"[Cache] cachedRoomList.Count={cachedRoomList.Count}");

        //룸리스트가 바뀌었다고 "구독 중인 UI"에게 알림
        //?. 는 구독자가 없으면(null이면) 그냥 아무것도 안 하고 넘어감
        OnRoomListChanged?.Invoke();
    }

 

(4) 네트워크 <-> UI 끼리 소통 : 이벤트로만 신호를 준다. 

위에서 RoomList를 캐시에 잘 반영했으므로, 방 목록이 잘 뜨도록 UI 갱신을 해줘야 한다.

룸리스트가 바뀔 때마다 네트워크쪽에서 UI한테로 "바뀌었어!" 신호만 이벤트로 보내도록 했다. 

`OnRoomListChanged?.Invoke();` 가 그 역할을 해준다. 이렇게 하면 좋은 점은, UI와의 결합도가 낮아진다는 점이다. NetworkManager는 UI의 존재를 몰라도 되기 때문이다.

 

아래 코드처럼 UI 쪽에서 이벤트를 구독한다. 

public class ConnectSceneManager : MonoBehaviour
{
    private void OnEnable()
    {
        // 1. 이벤트 구독: 방 목록 바뀌면 알 수 있도록
        if (NetworkManager.Instance != null)
            NetworkManager.Instance.OnRoomListChanged += RefreshUI;
    }

    private void OnDisable()
    {
        // 2. 구독 해제: 메모리 누수 방지 및 에러 방지
        if (NetworkManager.Instance != null)
            NetworkManager.Instance.OnRoomListChanged -= RefreshUI;
    }

    private void RefreshUI()
    {
        // 실제 UI를 갱신하는 로직
        Debug.Log("NetworkManager로부터 신호를 받아 UI를 갱신합니다.");
        
        foreach(var room in NetworkManager.Instance.cachedRoomList.Values)
        {
            // 방 정보 표시...
        }
    }
}

- OnEnable : 객체가 켜져 있을 때만 신호 받고(구독)

- OnDisable : 객체 꺼지면 신호 끊는다. NetworkManager가 이벤트를 발생시키고 -> 사라진 룸리스트의 함수를 실행시키려 하면 null 에러가 뜰 수 있으므로 구독 해제를 꼭 해줘야 한다. 

아래의 내용들을 정리해봤다.
1. Photon 연결 흐름
2. 싱글톤 + DontDestroyOnLoad 이해
3. RoomList 처리 
4. 이벤트 기반 네트워크-UI 분리 구조

결국 단일 책임 원칙을 잘 지키도록 클래스를 나누고 책임을 분산시키는 것이 중요하다는 것을 또 깨달았다. 

+
유니티로 개발하는 건 처음이었는데도 역할 분배를 명확히 나눠주신 팀장님 덕분에 이 접속씬에만 집중할 수 있어서 꽤나 수월했다. 또한 팀장님이 깃크라켄을 강력추천해주셔서 이번에 처음 써보는데 너무 편리해서 깜짝 놀랐다. 깃허브 데스크탑을 쓸 때는 원격과 로컬의 구분이 잘 가지 않아서 애를 많이 먹었었는데 깃크라켄 UI에는 명확히 분리돼있어서 헷갈릴 일이 없었다. 
이번 프로젝트 덕분에 많은 걸 배워가는 것 같다. 
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2026/03   »
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
글 보관함