→💡 해당블로그는 "Unity엔진과 Chat gpt를 활용한 멀티플레이 환경 구성 과정"에서 학습한 내용을
3달이 지난 지금에서야 복습 겸 스파르타 유니티 과제에 적용시키는 과정을 담았습니다. 


1. 멀티플레이 환경
2. 포톤 기본 세팅 
3. 포톤 연결 / 로비와 방 생성
4. 플레이어 생성 
5. Agora를 사용한 실시간 카메라 연결 

해당 포스터는 5번: Agora를 사용한 실시간 카메라 연결부터 작성되었습니다. 전 내용이 궁금하신 분들은 이전 블로그를 확인해 주세요.

이전 글 보러 가기 >

2025.02.19 - [Unity] - [Unity][멀티플레이] #1. Photon을 유니티에 연동해 보자

 

[Unity][멀티플레이] #1. Photon을 유니티에 연동해보자

💡 해당블로그는 "Unity엔진과 Chat gpt를 활용한 멀티플레이 환경 구성 과정"에서 학습한 내용을3달이 지난 지금에서야   복습 겸 스파르타 유니티 과제에 적용시키는 과정을 담았습니다.  1.

youcheachae.tistory.com

2025.02.20 - [Unity] - [Unity][멀티플레이] #2. 플레이어 생성, 동기화

 

[Unity][멀티플레이] #2. 플레이어 생성, 동기화

💡 해당블로그는 "Unity엔진과 Chat gpt를 활용한 멀티플레이 환경 구성 과정"에서 학습한 내용을3달이 지난 지금에서야   복습 겸 스파르타 유니티 과제에 적용시키는 과정을 담았습니다. 1. 멀

youcheachae.tistory.com


🎨5. Agora를 사용한 카메라 연결

- Agora란?

  RTC(Real Time Communication)의 플랫폼

  음성통화/화상통화/실시간 방송 등 실시간 대화형 기능을 웹, 모바일, 데스크톱에서 쉽게 사용할 수 있게 해 준다

 

- 패키지 다운로드

https://docs.agora.io/en/sdks?platform=unity

 

SDKs Download | Agora Docs

Description will go into a meta tag in <head />

docs.agora.io

  해당 링크에서 Unity 선택 후 Video Sdk를 다운로드한다.

  버전은 4.4.0을 사용하였다. 

 

  압축을 푼 후 파일을 드래그해서 Asset 폴더에 넣은 후 Import 한다. 

 

 

- App Id 받아오기

  Agora 홈페이지에서 로그인 후 Projects → Create New 

  프로젝트의 App Id를 복사한다.

 

- VideoChatManger.cs

public class VideoChatManger : MonoBehaviour
{
    [Header("===Data===")]
    [SerializeField] private string _appID = "85636c045ba54ceda8cee9c505f35e5b";
    [SerializeField] private string _token = "";
    [SerializeField] private string _currentChannelName;

    [Header("===오브젝트===")]
    static public GameObject _videoChatObject;    // raw 이미지 프리팹
    static public Transform _videoChatLayout;     // 프리팹 담길 상위 부모 

    [Header("===아고라 인스턴스===")]
    static public IRtcEngine rtcEngine;
}

  복사한 Agora 프로젝트의 Id를 변수로 할당한다.

 

private void SetUpVideoRtcEngine()
{
    // 아고라 인스턴스 생성
    rtcEngine = Agora.Rtc.RtcEngine.CreateAgoraRtcEngine();

    // 세팅
    RtcEngineContext context = new RtcEngineContext();
    context.appId = _appID;
    context.channelProfile = CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_LIVE_BROADCASTING;
    context.audioScenario = AUDIO_SCENARIO_TYPE.AUDIO_SCENARIO_DEFAULT;
    context.areaCode = AREA_CODE.AREA_CODE_GLOB;

    // rtc 엔진 초기화
    rtcEngine.Initialize(context);

    Debug.Log("아고라 엔진 셋업");
}

   AppId를 통해서 RTC 엔진을 초기화 세팅한다.

 

private void SetVideoConfiguration()
{
    rtcEngine.EnableAudio();
    rtcEngine.EnableVideo();

    VideoEncoderConfiguration config = new VideoEncoderConfiguration();

    // 화면 영역 
    config.dimensions = new VideoDimensions(200, 200);
    config.frameRate = 15;
    config.bitrate = 1000;

    rtcEngine.SetVideoEncoderConfiguration(config);
    rtcEngine.SetChannelProfile(CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_COMMUNICATION);
    rtcEngine.SetClientRole(CLIENT_ROLE_TYPE.CLIENT_ROLE_BROADCASTER);

    Debug.Log("아고라 비디오 셋업");

}

□ 비디오 세팅 메서드

EnableVideo();

            비디오 모듈을 활성화시키는 함수

             채널에 가입하기 전 or 채널에 있는 동안 호출할 수 있다

 

 private void InitEventHandler()
 {
     UserEventHandler handler = new UserEventHandler(this);
     rtcEngine.InitEventHandler(handler);

     Debug.Log("아고라 유저 이벤트 핸들러 셋업");
 }

 internal class UserEventHandler : IRtcEngineEventHandler
 {
     readonly VideoChatManger _chatManger;

     internal UserEventHandler(VideoChatManger chatManger)
     {
         _chatManger = chatManger;
     }

     // 채널에 접속 성공시 출력 오버라이딩
     public override void OnJoinChannelSuccess(RtcConnection connection, int elapsed)
     {
         Debug.Log($"You Joined Channel : " + connection.channelId);
     }

     public override void OnUserOffline(RtcConnection connection, uint remoteUid, USER_OFFLINE_REASON_TYPE reason)
     {
         // 함수 추가 예정
     }
     public override void OnUserJoined(RtcConnection connection, uint remoteUid, int elapsed)
     {
         // 함수 추가 예정
     }
     // 로컬 비디오 스트림 상태 변경 콜백
     public override void OnLocalVideoStateChanged(VIDEO_SOURCE_TYPE source, LOCAL_VIDEO_STREAM_STATE state, LOCAL_VIDEO_STREAM_REASON errorCode)
     {
         Debug.Log($"로컬 비디오 상태: {state}, 에러 코드: {errorCode}");

         switch (state)
         {
             case LOCAL_VIDEO_STREAM_STATE.LOCAL_VIDEO_STREAM_STATE_CAPTURING:
                 Debug.Log("카메라 캡처 중 - 카메라가 정상적으로 활성화되었습니다.");
                 break;
             case LOCAL_VIDEO_STREAM_STATE.LOCAL_VIDEO_STREAM_STATE_STOPPED:
                 Debug.Log("카메라 캡처가 중지되었습니다.");
                 break;
             case LOCAL_VIDEO_STREAM_STATE.LOCAL_VIDEO_STREAM_STATE_FAILED:
                 // 오류 코드별 상세 처리
                 if (errorCode == LOCAL_VIDEO_STREAM_REASON.LOCAL_VIDEO_STREAM_REASON_DEVICE_BUSY)
                 {
                     Debug.LogError("카메라 장치가 이미 사용 중입니다.");
                 }
                 else if (errorCode == LOCAL_VIDEO_STREAM_REASON.LOCAL_VIDEO_STREAM_REASON_DEVICE_NO_PERMISSION)
                 {
                     Debug.LogError("카메라 접근 권한이 없습니다.");
                 }
                 else
                 {
                     Debug.LogError($"카메라 캡처 실패! 상세 오류 코드: {errorCode}");
                 }
                 break;
         }
     }
 }

이벤트핸들러 작성

IRtcEngineEventHandler를 상속받아야 한다

채널에 접속 성공 시 / 유저가 채널에서 떠날  / 유저가 채널에 입장했을 때 / 로컬 비디오 스트림 상태가 변화 콜백시 출력 

 

- Start()

private void Start()
{
    // rtc 엔진 생성, 초기화
    SetUpVideoRtcEngine();

    // 비디오 셋업
    SetVideoConfiguration();

    // 이벤트 핸들러 셋업
    InitEventHandler();
}

: 각 초기화 함수를 Start에서 실행해 준다

 

📝 채널 입장

// 해당 채널에 들어오면 
public void Join(string channelName)
{
    // 현재 채널 이름 저장 
    _currentChannelName = channelName;

    rtcEngine.JoinChannel(_token, channelName, 0, null);
    rtcEngine.EnableVideo();

    // 채널에 뷰 생성 
    string _cameraName = PlayerManager.Instnace.nowPlayer.PlayerName;
    MakeVideoView(0, channelName);

    Debug.Log($"{channelName} 의 채널에 조인 ");
}

채널이름으로 해당 채널에 입장

  카메라 뷰 생성 

 

// 뷰 생성
static public void MakeVideoView(uint uid, string channelId = "")
{
    VideoSurface videoSurface = MakeImageSurface(uid.ToString());
    // null이면 return
    if (ReferenceEquals(videoSurface, null))
        return;

    // 로컬 , 리모트 유저 뷰 설정
    if (uid != 0)
    {
        videoSurface.SetForUser(uid, channelId);
    }
    else
    {
        // 로컬일때  
        videoSurface.SetForUser(uid, channelId, VIDEO_SOURCE_TYPE.VIDEO_SOURCE_CAMERA_PRIMARY);
    }

    videoSurface.SetEnable(true);

}

// 비디오 서페이스 생성
static public VideoSurface MakeImageSurface(string name)
{
    // raw 이미지 오브젝트 생성
    GameObject videochat = Instantiate(_videoChatObject);

    if (videochat == null)
        return null;

    // 오브젝트 이름 설정 
    videochat.name = name;

    if (_videoChatLayout != null)
    {
        // 비디오 레이아웃 밑에 넣기
        videochat.transform.SetParent(_videoChatLayout, false);
    }
    else
    {
        Debug.Log("Layout is null");
    }

    videochat.transform.localPosition = Vector3.zero;
    var videoSurface = videochat.AddComponent<VideoSurface>();

    return videoSurface;
}

MakeVideoView()

            로컬 사용자일 때(uid == 0)

                       기본적으로 카메라를 사용하게 설정

MakeImageSurface()

            raw 이미지 오브젝트를 생성 후

            VideoSurface추가

 

📝 채널 퇴장 

    // 해당 채널에서 나가면 
    public void Leave()
    {
        rtcEngine.DisableVideo();
        rtcEngine.LeaveChannel();

        // 생성된 뷰 삭제 
        DestoryAll();

        Debug.Log($"{_currentChannelName} 의 채널에서 퇴장 ");
    }

  채널에서 나가면 현재 생성되어 있는 뷰를 삭제

 

static public void DestoryAll()
{
    List<uint> uids = new List<uint>();

    for (int i = 0; i < _videoChatLayout.transform.childCount; i++)
    {
        uids.Add(uint.Parse(_videoChatLayout.transform.GetChild(i).name));
    }

    for (int i = 0; i < uids.Count; i++)
    {
        DestoryVideoView(uids[i]);
    }
}

static public void DestoryVideoView(uint id) 
{
    var obj = FindChild(UiManager.Instnace._canvas.transform, id.ToString()).gameObject;

    if (!ReferenceEquals(obj, null))
        Destroy(obj);
}

static public Transform FindChild(Transform parent, string name)
{
    Transform result = parent.Find(name);

    if (result != null)
        return result;

    // 자식 오브젝트 모두 검색
    foreach(Transform child in parent) 
    { 
        result = FindChild(child, name);
        if (result != null) return result;
    }

    return null;
}

  카메라 layoutGroup하위에 있는 카메라들의 이름을 저장 한 뒤, canvas 하위에서 해당 오브젝트를 찾아 Destory()

 

📝 특정 채널에 입장했을 때

2025.02.20 - [Unity] - [Unity][멀티플레이] 게임에서 충돌 시 동기화 문제

 

[Unity][멀티플레이] 게임에서 충돌 시 동기화 문제

1. 구현 동작2. 문제점3. 해결과정    □ 어디서 문제가 발생하는가?    □ GetHashCode()로 해결가능4. 해결 방법    □ PhonView의 IsMine    □ 최종 코드5. 결론 📝 1. 구현 중인 동작 □  zep처럼 특

youcheachae.tistory.com

: 해당글의 ConferenceBox.cs

           OnTriggerEnter2 D 

                       VideoChatManger.cs의 Join() 함수 실행 

            OnTriggerExit2D 

                        VideoChatManger.cs의 Leave() 함수 실행 

 

📝 현재까지의 문제점

: Join() 함수 내부의 MakeVideo() 함수에서 uid를 항상 0으로 넘기고 있기 때문에 , 생성되는 Videosurface 오브젝트의 이름은 항상 0이다

: DestoryAll에서 오브젝트의 이름을 가지고 Destroy()하는 부분에서 오류가 생긴다.

: 다른 유저가 채널에 입장했을 때, 즉 리모트 유저가 채널에 입장했을 때는

그 유저의 고유 아이디를 가지고 VideoView를 만들어야 한다.

 

- 해결방안

: UserEventHandler 클래스의 UserEventHandler에서 오버라이딩 한 메서드에서 MakeView()를 추가해 준다.

public override void OnUserOffline(RtcConnection connection, uint remoteUid, USER_OFFLINE_REASON_TYPE reason)
{
    // 코드 추가 
    DestoryVideoView(remoteUid);
}
public override void OnUserJoined(RtcConnection connection, uint remoteUid, int elapsed)
{
    // 코드 추가
    MakeVideoView(remoteUid, _chatManger._currentChannelName);
}

: 이렇게 하게 되면 리모트 유저가 채널에 입장했을 때, 리모트 유저의 고유 아이디로 View를 만들게 된다.


📝 최종 영상 

 

: 현재 카메라는 동기화가 되지 않고 있다.

: 이 부분은 추후 추가해 보겠다.


- Agora 공식 문서

https://docs.agora.io/en/video-calling/get-started/get-started-sdk?platform=unity

 

Video Calling Quickstart | Agora Docs

Rapidly develop and easily enhance your social, work, and educational apps with face-to-face interaction.

docs.agora.io

- Agora의 메서드

https://api-ref.agora.io/en/video-sdk/unity/4.x/API/class_irtcengine.html#api_irtcengine_enablevideo

 

IRtcEngine

Preloads a channel with token, channelId, and userAccount. public abstract int PreloadChannel(string token, string channelId, string userAccount); When audience members need to switch between different channels frequently, calling the method can help short

api-ref.agora.io

 


깃허브

https://github.com/kimYouChae/Sparta_Metaverse

 

GitHub - kimYouChae/Sparta_Metaverse

Contribute to kimYouChae/Sparta_Metaverse development by creating an account on GitHub.

github.com

 

+ Recent posts