티스토리 뷰
최근 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



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에는 명확히 분리돼있어서 헷갈릴 일이 없었다.
이번 프로젝트 덕분에 많은 걸 배워가는 것 같다.
'개인 프로젝트 > BlackOut' 카테고리의 다른 글
| [Unity] 게임 프로젝트 작업 기록 : 접속씬에서 비공개방 만들기 (0) | 2026.02.26 |
|---|---|
| [Unity] 3주차 작업 로그 : 시야 연출 (Sight System) (0) | 2026.02.25 |
| [Unity] 2주차 작업 로그 : 상호작용 시스템 (Interaction) (0) | 2026.02.25 |
| [Unity] 1주차 작업 로그 : 타이틀 & 접속 씬 구현 (0) | 2026.02.25 |
