Delegate

  • C#에서 메서드를 대신 참조할 수 있는 형식으로, 메서드를 변수처럼 사용할 수 있게 해 준다
  • 사용자가 직접 정의하여 반환 형식과 매개변수를 지정 가능
  • Delegate는 Action과 Func의 기본이 되는 개념

Action

  • 반환 값이 없는(void) 메서드를 참조하는 제네릭 대리자
  • 최대 16개까지 입력 매개변수를 가질 수 있음
  • Action<T1, T2, ...>와 같은 형태로 사용됨

Func

  • 반환 값이 있는 메서드를 참조하는 제네릭 대리자
  • 마지막 타입 매개변수가 항상 반환 타입을 나타냄
  • Func<T1, T2, ..., TResult>와 같은 형태로 사용되며, 최대 16개의 입력 매개변수를 가질 수 있음

(+) microsoft c# 공식문서에 가면 16개의 매개변수를 가지는 Action과 Func 예시를 볼 수 있다.


델리게이트 설명과 예시

2025.02.10 - [c#] - [c#]delegate 이해하기

 

[c#]delegate 이해하기

1. delegate    □ 개념    □ 예시📝 delegate (대리자)1. 개념□ 메서드에 대한 참조를 나타내는 형식 □ C++의 함수 포인터와 비슷하게 동작함!           ■함수포인터?           : 함수의 시

youcheachae.tistory.com

 

📝함수호출                                                                               

: 함수가 인수 (매개변수)를 처리하는 방식에는 두 가지가 있다.

: 첫 번째 값에 의한 호출 (call by value) , 두 번째로 참조에 의한 호출 (call by reference)

 

📝call by value                                                                       

값에 의한 호출

  • 함수에 변수를 전달할 때 값을 복사해서 선달함
  • 함수 내부에서 값을 변경해도 원래의 변수에 영향을 미치지 않는다
  • int, float, string, bool 등의 원시 타입은 값 복사

원시타입을 매개변수로 가지는 예시를 보겠다

static void Main(string[] args)
{
    Console.WriteLine("Hello, World!");

    int a = 10;
    int b = 11;
    CallbyValue(a, b);
}

public static void CallbyValue(int tempA, int tempB) 
{
    tempA++;
    tempB++;
}

  함수에서 매개변수로 들어온 int값이 바뀌었지만, 원래 값은 변경되지 않는다.

 

  main함수가 실행될 때 스택 메모리에 a와 b 변수가 선언된다

CallByValue 메서드가 호출되면 스택에 tempA와 tempB가 할당되고 , a와 b의 값을 복사한다

함수 실행이 종료되면 tempA와 tempB는 스택에서 해제된다

즉, 함수 내에서 복사한 값을 변경하였기 때문에 원본 값은 변경되지 않는다

 

📝call by reference                                                                

참조에 의한 호출

  • 함수에 변수를 전달할 때 값을 참조, 즉 메모리 주소를 전달
  • 원래의 변수는 함수 외부에서도 변경된 값을 유지한다
  • 객체, 배열 같은 참조타입은 참조값(주소)이 복사되어 함수에 전달

클래스를 참조값으로 가지는 예시를 보겠다

 static void Main(string[] args)
 {
     Console.WriteLine("Hello, World!");
     
     Apple test = new Apple();
     CallbyReference(test);
 } 
 
 public static void CallbyReference(Apple test) 
 {
     test.count += 10;
 }
 
 public class Apple
{
    public int count = 1;
}

  함수 내에서 Apple클래스의 count값이 변경되었고, 원래 객체에도 변경이 반영된다

 

  Apple클래스를 인스턴스와 하면 힙 메모리에 할당된다

  apple 변수는 Apple 인스턴스의 찹조값(주소)을 가진다.

CallbyReference 메서드에 참조값(주소)을 전달하면, 해당 주소값이 복사되어 스택에 저장된다.

함수 내에서는 참조값(주소)에 해당하는 객체에 접근하여 값을 변경시킨다

즉, 참조된 객체의 속성을 변경하였기 때문에 원래 객체에도 변경이 반영된다

 

📝공통점 / 차이점                                                                       

  공통점

  • 모두 매개변수로 전달된 값 (값 자체 또는 참조값)을 복사한다
  • 복사된 값은 스택에 저장되고 함수 종료 시 해제된다 (스택프레임과 연관 있음)

  차이점

  • Call by Value : 원시타입 (int 등)의 값 자체가 복사된다
  • Call by Reference : 객체의 참조 주소값이 복사된다

 

(+) 스택프레임

함수가 호출될 때, 스택 영역에 차례대로 저장되는 함수의 호출정보이다.

함수가 호출될 때, 매개변수를 스택에 할당하기 때문에 두 방식 다 매개변수로 전달된 값을 복사한다.

( 참조타입의 경우 객체 자체는 힙에 있고, 참조만 스택에 복사된다! )

또한 함수의 호출이 끝나면 스택메모리에서 해제되기 때문에 복사된 값은 스택에서 해제된다.

 


스택프레임에 대한 자세한 설명은 아래 링크를 참고

 

2025.02.13 - [용어정리] - [용어정리]스택 프레임 (stack frame)

 

[용어정리]스택 프레임 (stack frame)

🔖 메모리 구조에 대해서 잘 모르시는 분들은 이전 글을 참고해 주세요!  2025.02.12 - [c#] - [c#]힙메모리와 스택 메모리 [c#]힙메모리와 스택 메모리메모리의 공간 1. 코드 (code) 영역2. 데이터 (data)

youcheachae.tistory.com

 

1. Audio 컴포넌트
2. Audio 플레이
3. 오디오 볼륨 조절 슬라이더

📝 1. Audio컴포넌트                                                                                                                                                       

🔖 Audio Clip

  • 재생할 수 있는 오디오 데이터
  • 유니티는. aif,. wav,. mp3, 및. ogg 형식을 지원함

 

🔖 Audio Souce

  • 씬에서 오디오를 재생할 수 있는 컴포넌트
  • 2D/3D 사운드 설정 가능 

 

🔖 AudioMixer 

  • 오디오 소스에 대한 제어, 균형 및 동적 조정을 제공
  • 그룹화를 통한 카테고리별 사운드 관리 가능 (BGM, SFX, Voice 등)

 

📝2. Audio 플레이                                                                                                                                   

 

0. 오디오를 "듣기"위해서 Main Camera에 AudioListener 컴포넌트를 달아준다

 

1. 오브젝트에 AudioSource 컴포넌트를 추가한다

AudioSource 필드에 오브젝트의 AudioSource  컴포넌트를 할당한다

AudioClip필드에 실행할 오디오 클립을 할당한다

 

2. BGMPlayer.cs

public class BGMPlayer : MonoBehaviour
{
    public AudioSource source;
    public AudioClip clip;

    void Start()
    {
        source = GetComponent<AudioSource>();
        
        // 1. Play 함수
        source.Play();

        // 2. PlayOnShot 함수
        source.PlayOneShot(clip);
    }

}

AudioSource.Play()

              해당 소스에 할당된 AudioClip을 처음부터 재생

              매개변수가 없음 (AudioSource 컴포넌트에 AudioClip이 미리 할당되어야 함)

AudioSource.PlayOneShot(AudioClip)

              한 번에 여러 오디오를 겹쳐 재생할 수 있음

              재생시킬 오디오 clip을 매개변수로 받음

              같은 소스에서 여러 효과음을 동시에 재생할 때 유용함

 

3. 실행

실행하면 오디오가 1회 실행되는 것을 확인할 수 있다

혹시 들리지 않는다면 게임화면의 소리가 음소거되어있는지 확인하자 

 

📝3. 오디오 볼륨 조절 슬라이더                                                                                                                                     

1. Create →  Audio Mixer를 클릭하여 오디오믹서를 만든다

 

2. 우측 Groups에서 (+) 버튼을 눌러 그룹을 만든 뒤 이름을 BGM과 SFX로 지정한다

 

3. Asset폴더에 생성되어 있는 AudioMixer하위의 Master을 클릭한 후 

인스펙터 창의 Volume 위에서 우클릭

→ Expose 'Volume (of Master) to script"를 클릭

 

🔖 스크립트로 Audio Mixer의 Master / BGM / SFX 그룹에 접근하기 위해서 Expose를 해줘야 한다.

  볼륨 옆에 → 화살표가 뜨면 된 것이다

 

4. AudioSource가 할당되어 있는 오브젝트의 AudioSource → Output에 만든 AudioMixer의 그룹을 할당해 준다

BGM을 실행하는 오브젝트의 AudioSource뿐만 아니라 SFX를 실행하는 AudioSource에도 Output을 설정해 준다 

 

5. AudioMixer의 파라미터 설정

해당 그룹에 맞는 파라미터로 이름을 수정해 주면 된다

왼쪽에 흐릿한 글씨로 Volume(of BGM)를 확인한다. BGM이면 AudioSource의 Output이 "BGM"인 경우에 해당한다. 

 

6. AudioSlider.cs

using UnityEngine.Audio;
using UnityEngine.UI;

public class AudioSlider : MonoBehaviour
{
    public AudioMixer audioMixer;

    public Slider MasterSlider;
    public Slider BGMSlier;
    public Slider SFXSlider;

    private float minVolume = -60f;
    private float maxVolume = 10f;
    
    
    void Start()
    {
        MasterSlider.onValueChanged.AddListener(SetMasterVolume);
        BGMSlier.onValueChanged.AddListener(SetBGMVolume);
        SFXSlider.onValueChanged.AddListener(SetSFXVolume);

    }
    public void SetMasterVolume(float volume)
    {
        audioMixer.SetFloat("Master", Mathf.Clamp(volume * 100 - 80 , minVolume , maxVolume));
    }

    public void SetBGMVolume(float volume)
    {
        audioMixer.SetFloat("BGM", Mathf.Clamp(volume * 100 - 80 , minVolume , maxVolume));
    }

    public void SetSFXVolume(float volume)
    {
        audioMixer.SetFloat("SFX", Mathf.Clamp(volume * 100 - 80 , minVolume , maxVolume));
    }

}

Slider.onValueChanged.AddListener()

               슬라이더에서 값 수치가 변경될 경우에 실행됨

             □ UI 슬라이더와 오디오 볼륨을 연결할 때 사용

AudioMixer.SetFloat(string, float)

               첫 번째 매개변수: 믹서에서 정의한 파라미터 이름

               두 번째 매개변수: 설정할 값 (일반적으로 dB 단위)

              AudioMixer은 -80부터 +20까지 소리를 지원한다.

              너무 작거나, 너무 크면 소리가 깨지는 현상이 발생하기 때문에 min과 max값 사이에서 볼륨의 크기를 정한다.

 

AudioMixer.SetFloat를 지정하는 일반적인 방법

audioMixer.SetFloat("Master", Mathf.Log10(Mathf.Max(0.001f, volume)) * 20);

: 데시벨(dB)은 로그 스케일이므로 슬라이더(0~1) 변환 시 로그 함수를 권장함

 

 

7. 실행

소리는 안 들린다

  Matser 슬라이더를 조절하면 AudioMixer에 Master 볼륨이 조절된다.

  BGM 슬라이더를 조절하면 AudioMixer에 BGM 볼륨이 조절된다.

  SFX 슬라이더를 조절하면 AudioMixer에 SFX  볼륨이 조절된다.

내일배움캠프-레벨업세션에서 엑셀 파일을 Json으로 변환하는 기능을 봤는데 
이번 프로젝트에서 그대로 사용하기에는 맞지않다고 느껴서
유니티 내부에서 csv파일을 Json으로 저장하는 기능을 만들어보았다. 

 

📝 CsvToJsonConverter.cs

- 전체코드

public class CsvToJsonConverter : MonoBehaviour
{
    /// <summary>
    /// ***Converter 사용 전 주의사항***
    /// 1. 입력한 string, 클래스명, Recources하위의 csv 데이터 이름이 동일해야합니다.
    /// 2. Recources파일 하위에 csv데이터가 존재해야합니다.
    /// 3. csv 데이터를 파싱해서 사용할 클래스 조건
    ///     (1) 클래스여야합니다.
    ///     (2) ICsvParsable 인터페이스를 구현해야합니다.
    ///     (3) 매개변수가 없는 생성자를 가지고 있어야 합니다.
    ///     : ICsvParsable 인터페이스와 사용예시는 Stage 스크립트를 참고해주세요
    /// </summary>

    [Header("===클래스 이름을 작성해주세요===")]
    public string[] className;

    public void CsvConverByName()
    {
        Debug.Log("CsvConverter메서드입니다");

        for (int i = 0; i < className.Length; i++)
        {
            // 여기서 type은 ? 클래스라고 생각하면 편함 
            // string에 맞는 타입 생성 
            Type type = Type.GetType(className[i]);

            if (type == null)
            {
                Debug.LogError($"해당 클래스({className[i]})를 찾을 수 없습니다.");
                continue;
            }

            if (!typeof(ICsvParsable).IsAssignableFrom(type))
            {
                Debug.LogError($"클래스({className[i]})는 ICsvParsable을 구현해야 합니다.");
                continue;
            }

            // CsvDataParsing<> 클래스의 타입을
            // MakeGeneritType : type으로 제네릭 지정
            // convertype : 즉 CsvDataParsing<클래스명>이 된다
            Type converterType = typeof(CsvDataParsing<>).MakeGenericType(type);

            // CsvDataParsing 인스턴스화 
            // 매개변수는 className[i]
            object converterInstance = Activator.CreateInstance(converterType, className[i]);

            // CsvDataParsing<>의 GetDataArray() 메서드 가져오기 
            MethodInfo method = converterType.GetMethod("GetDataArray");

            if (method != null)
            {
                // GetDataArray 메서드 Invoke
                // return 값은 List<T>이지만 object타입으로 박싱(boxing) 일어남 
                // 원본 데이터 배열 가져오기 (List<T> 타입)
                // 컴파일 타임에는 object, 런타임때는 List<T>
                object dataArray = method.Invoke(converterInstance, null);

                // 원본 타입을 유지한 채 JSON으로 변환
                string json = JsonSerialized.ConvertOriginalListToJson(dataArray, type);

                // 결과 저장
                jsonResults[className[i]] = json;

                // 파일로 저장
                JsonSerialized.SaveJsonToFile(json, className[i]);

                Debug.Log($"{className[i]} 데이터를 성공적으로 변환했습니다.");
            }

        }
    }
}

 

 

- 상세코드

Type type = Type.GetType(className[i]);

클래스 이름을 통해서 클래스 타입을 type변수에 저장한다.

Type converterType = typeof(CsvDataParsing<>).MakeGenericType(type);

CsvDataParsing 클래스의 제네릭 타입을 위에서 지정해준 type(현재 클래스의 타입)으로 설정한다. 

object converterInstance = Activator.CreateInstance(converterType, className[i]);

Reflection의 Activator.CreateInstance를 사용해서 제네릭 클래스인 CsvDataParsing를 인스턴스화 한다.

생성자의 매개변수는 현재 클래스이름 

MethodInfo method = converterType.GetMethod("GetDataArray");

  CsvDataParsing 클래스의 GetDataArray()함수를 MethodInfo에 저장한다 

 

if (method != null)
{
    object dataArray = method.Invoke(converterInstance, null);

  GetDataArray메서드를 실행 

  리턴값은 컴파일 타임에는 object 타입으로 저장되고, 런타임때는 List<T> 타입으로 저장된다

 

    // 원본 타입을 유지한 채 JSON으로 변환
    string json = JsonSerialized.ConvertOriginalListToJson(dataArray, type);

    // 파일로 저장
    JsonSerialized.SaveJsonToFile(json, className[i]);

    Debug.Log($"{className[i]} 데이터를 성공적으로 변환했습니다.");
}

■ JsonSerialized 클래스의 ConvertOriginalListToJson() 메서드를 실행 후 json 문자열을 return받는다

JsonSerialized 클래스의 SaveJsonToFile() 메서드를 실행 후 json 파일을 저장한다. 

 

📝 JsonSerialized.cs

- 전체코드

[SerializeField]
public class ListWrapper<T>
{
    public List<T> values;
}
public static class JsonSerialized
{
    // C:\Users\[user name]\AppData\LocalLow\[company name]\[product name]
    static string savePath = Application.persistentDataPath;

    // 원본 타입의 리스트를 변환하는 메서드 (리플렉션 사용)
    public static string ConvertOriginalListToJson(object dataArray, Type elementType)
    {
        // dataArray는 현재 List<T>
        // 1. 적절한 ListWrapper<tyoe> 타입 생성
        Type wrapperType = typeof(ListWrapper<>).MakeGenericType(elementType);

        // 2. 래퍼 인스턴스 생성
        object wrapper = Activator.CreateInstance(wrapperType);

        // 3. values 필드 가져오기
        FieldInfo valuesField = wrapperType.GetField("values");

        // 4. dataArray를 values 필드에 할당
        // dataArray는 object타입이지만 실제로는 List<T> (매개변수로 List<T>를 넘겼기 때문)
        valuesField.SetValue(wrapper, dataArray);

        // 5. JsonUtility로 직렬화
        return JsonUtility.ToJson(wrapper);
    }

    public static void SaveJsonToFile(string json, string saveFileName)
    {
        try
        {
            string path = Path.Combine(savePath, saveFileName);
            File.WriteAllText(path, json);
            Debug.Log($"{saveFileName}이 저장되었습니다. 경로: {path}");
        }
        catch (Exception ex)
        {
            Debug.LogError($"파일 저장 중 오류 발생: {ex.Message}");
        }
    }
}

 

 

- 상세코드

🔖 ConvertOriginalListToJson() 메서드

    // dataArray는 현재 List<T>
    // 1. 적절한 ListWrapper<tyoe> 타입 생성
    Type wrapperType = typeof(ListWrapper<>).MakeGenericType(elementType);

ListWrapper클래스의 제네릭 타입을 위에서 매개변수의 type(현재 클래스의 타입)으로 설정한다. 

 

    // 2. 래퍼 인스턴스 생성
    object wrapper = Activator.CreateInstance(wrapperType);

 Reflection의 Activator.CreateInstance를 사용해서 제네릭 클래스인 ListWrapper를 인스턴스화 한다.

 

    // 3. values 필드 가져오기
    FieldInfo valuesField = wrapperType.GetField("values");

ListWrapper 클래스의 List<T> 타입의 values변수를 FieldInfo 에 저장한다 

 

    // 4. dataArray를 values 필드에 할당
    // dataArray는 object타입이지만 실제로는 List<T> (매개변수로 List<T>를 넘겼기 때문)
    valuesField.SetValue(wrapper, dataArray);

FieldInfo의 SetValue를 통해서 dataArray를 List<T>에 할당한다

 

    // 5. JsonUtility로 직렬화
    return JsonUtility.ToJson(wrapper);

Json으로 직렬화 후 string을 return 한다 

 

🔖 SaveJsonToFile() 메서드

    string path = Path.Combine(savePath, saveFileName);
    File.WriteAllText(path, json);

json 문자열을 지정한 경로에 , 매개변수로 넘어온 파일이름으로 저장한다 

 

📝 ICsvParsable 인터페이스

public interface ICsvParsable
{
    void Parse(string[] values);
}

 

📝 Stage.cs - 테스트용 클래스 

[System.Serializable]
public class Stage : ICsvParsable
{
    [SerializeField] private int hp;
    [SerializeField] private string name;
    [SerializeField] private List<string> animal;

    public void Parse(string[] values)
    {
        hp = int.Parse(values[0]);
        name = values[1];

        animal = new List<string>();
        string[] temp = values[3].Split('-');
        for (int i = 0; i < temp.Length; i++)
        {
            animal.Add(temp[i]);
        }
    }
}

  [System.Serializable]

             □ 직렬화 하기 위해서 클래스 위에 추가한다

csv를 파싱한 데이터를 필드에 넣기위해서 ICsvParsable 인터페이스를 구현한다

Parse(string[] values)

              필드에 맞게 변수를 형변환해서 필드를 설정한다 

 

📝 CsvJsonButton.cs

[CustomEditor(typeof(CsvToJsonConverter))]
public class CsvJsonButton : Editor
{
    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();

        CsvToJsonConverter mana = (CsvToJsonConverter)target;

        if (GUILayout.Button("Cvs to Json"))
        {
            mana.CsvConverByName();
        }
    }
}

에디터에서 실행하기 위해서 커스텀에디터를 만들어준다

 


사전 준비

🔖 csv에서 파싱할 클래스를 작성한다

🔖 해당 클래스는 반드시 CsvParsable인터페이스를 구현해야한다

🔖 csv 파일 이름 , 클래스 이름, 인스펙터 창에 입력한 클래스 이름이 동일해야한다

🔖 Stage클래스 참고 ! 

 

사용방법 ( 테스트환경 )

1. Resources 폴더 내부에 csv 파일 생성

Stage.csv
0.00MB

 

2. Converter 오브젝트의 리스트에 "Stage"입력 후 "Cvs to Json"버튼 클릭 

3. C:\Users\사용자이름\AppData\LocalLow\DefaultCompany\프로젝트이름

의 경로에 Json파일이 생성되었는지 확인한다 

 


📝 깃허브

https://github.com/kimYouChae/UnityCsvToJsonConverter

 

GitHub - kimYouChae/UnityCsvToJsonConverter

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

github.com

 

static 키워드를 사용하는 객체 처음 사용될 때 한번 할당되고, 프로그램이 끝날 때까지 메모리를 유지한다. 사용할 때 같은 메모리를 참조한다. 즉 정적이다.

반대로 동적인 객체는 사용할 때 마다 메모리를 사용하고, 사용이 끝나면 메모리를 해제한다. 

 

📝 정적 변수

static void Main(string[] args)
{
    Console.WriteLine("Hello, World!");

    // 정적변수
    // 클래스명.변수 < 로 접근
    Cinema.person += 1;

    Cinema cinema = new Cinema();
    
    // 오류 : 인스턴스화 하면 정적 변수가 포함되지 않는다
    // 정적 멤버는 클래스에 속하는 것이지 인스턴스에 속하는 것이 아님
    cinema.person
}

public class Cinema
{
    // 정적 변수
    public static int person = 0;

    public Cinema() { }
}

□ 정적으로 선언된 변수는 클래스명.변수명 으로 접근할 수 있다.

정적 변수가 포함된 클래스를 인스턴스화 하면 정적 변수는 포함되지 않는다.

 

📝 정적 클래스

static void Main(string[] args)
{
    Console.WriteLine("Hello, World!");
    
    // 정적 클래스
    Animal.animalAge += 1;
    Animal.Setting();
}

public static class Animal 
{
    // 오류 : 정적 클래스에 인스턴스 변수를 선언할 수 없다
    public int age = 0;

    // 오류 : 정적 클래스에 인스턴스 메서드를 선언할 수 없다
    public void Func() { }

    // 정적 변수 
    public static int animalAge = 5;
    
    // 정적 메서드
    public static void Setting() { }
}

정적으로 선언한 클래스의 필드와 메서드는 클래스명.변수명/ 클래스명. 함수명으로 접근할 수 있다.

 

🔖 정적선언을 할 때 주의해야할 점

1. 정적 클래스는 인스턴스화 할 수 없다.

2. 정적인 클래스는 정적인 변수와 메서드만 가질 수 있다.

 

📝 정적 메서드 

static void Main(string[] args)
{
    Console.WriteLine("Hello, World!");

    // 정적 메서드
    Food.Eat();
    Food temp = new Food();
    
    // 오류 : 인스턴스화 하면 정적 메서드는 포함되지 않는다
    temp.Eat()
}


public class Food 
{
    static public void Eat() { }
}

 

□ 정적으로 선언된 함수는 클래스명.변수명 으로 접근할 수 있다.

 정적 메서드가 포함된 클래스를 인스턴스화 하면 정적 변수는 포함되지 않는다.

→💡 해당블로그는 "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

 

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


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

해당 포스터는 3번:포톤 연결 / 로비와 방 생성부터 작성되었습니다. 전 내용이 궁금하신 분들은 이전 블로그를 확인해 주세요.

이전 글 보러 가기 >

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

 

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

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

youcheachae.tistory.com


🎨 3. 포톤 연결 / 로비와 방 생성 

- 전체적인 흐름

 

- PhotonManger 스크립트 생성

using Photon.Pun;

public class PhotonManager : MonoBehaviourPunCallbacks
{
    // 싱글톤 
    private static PhotonManager instance = null;
    public static PhotonManager Instnace { get => instance; }

    [Header("===Info===")]
    [SerializeField] string _roomVersion = "1.0.0";
    [SerializeField] string _roomName = "BlackVillage";

    [Header("===동기화 Player===")]
    [SerializeField] private GameObject _player;                    // 생성할 플레이어 오브젝트
    
    public GameObject photonPlayer => _player; 

    private void Awake()
    {
        if(instance == null)
            instance = this;    
        else if( instance != this)
            Destroy(instance);

        DontDestroyOnLoad(instance);
    }
}

□ MonoBehaviourPunCallbacks를 상속받아야 함.

□ 포톤은 Lobby - Room으로 이루어져 있음

           포톤 연결 후 Lobby에 연결

           룸생성 후 접속

 

- 포톤 서버 연결 함수

private void F_MasterClient() 
{

    PhotonNetwork.AutomaticallySyncScene = true;

    // 게임버전 할당 
    PhotonNetwork.GameVersion = _roomVersion;
    Debug.Log("포톤 서버 초당 데이터 통신 수 : " + PhotonNetwork.SendRate);

    // ** 포톤 사용자들을 포톤 서버에 연결
    if (!PhotonNetwork.IsConnected)
        PhotonNetwork.ConnectUsingSettings();           // ** 포톤 서버 세팅 정보로 

}

  PhotonNetwork.ConnectUsingSettings()

            포톤의 서버 세팅 정보로 포톤에 연결

   PhotonNetwork.AutomaticallySyncScene = true;

             마스터 클라이어언트가 LoadLevel 메서드 실행 시 , 즉 다른 씬으로 이동 시 
             다른 멤버들도 마스터클라이언트가 있는 씬으로 자동 이동

             false로 세팅하면 이동하지 않는다

 

- 서버의 마스터(나)가 서버에 접속 성공 시 콜백함수

public override void OnConnectedToMaster() 
{
    // MonoBehaviourPunCallbacks 의 OnConnected~함수를 오버라이딩
    // 서버 마스터가 접속하면 메서드 실행 
    Debug.Log("포톤에 접속 성공 유무 : " + PhotonNetwork.IsConnected);

    // 로비 접속 추가 
    PhotonNetwork.JoinLobby();
}

  서버 연결에 성공하면 OnConnectedToMater() 함수 실행 

   PhotonNetwork.JoinLobby();          

           로비 입장 함수 실행 

 

- 로비 접속 시 콜백함수

 public override void OnJoinedLobby()
 {
     // 로비에 접속하면 실행

     // Debug.Log("로비접속 "  + PhotonNetwork.InLobby);
     JointRoom();
 }

  로비에 입장하면 OnJoinedLobby() 함수 실행

  방(Room)에 입장 

 

- 방(Room) 생성 후 입장 

 private void JointRoom() 
 {
     // 방 정보 생성
     RoomOptions roomOptions = new RoomOptions();
     roomOptions.MaxPlayers = 3;
     roomOptions.IsOpen = true;
     roomOptions.IsVisible = true;

     // 생성 후 바로 입장
     PhotonNetwork.JoinOrCreateRoom(_roomName , roomOptions , TypedLobby.Default);
     // -> 방 이름 기준으로 작동함
     // 즉 마스터클라이언트가 방을 만들면, 다음 접속자는 이름에 해당하는 방이 이미 있으니 거기로 접속 
 }

  RoomOptions

           방 (room)을 만들 때 필요한 설정값들을 가지고 있음

  PhotonNetwork.JoinOrCreateRoom()

          ■ 해당 이름 (_roomName)의 방이 없으면 → 새 방을 생성하고 입장

           해당 이름의 방이 있으면  → 기존 방에 입장 

 public override void OnCreatedRoom()
 {
     // 방 생성시 호출 (오버라이딩)
     Debug.Log("방 생성 완료 : " + PhotonNetwork.CurrentRoom.Name );
 }

 public override void OnJoinedRoom()
 {
     // 방 입장 시 호출 (오버라이딩)
     Debug.Log($"{PhotonNetwork.CurrentRoom.Name} 에 입장 / 방에 있는지 여부 : {PhotonNetwork.InRoom}" +
         $" 인원 수 : {PhotonNetwork.CurrentRoom.PlayerCount}" );
 }
 public override void OnJoinRandomFailed(short returnCode, string message)
 {
     // 방 접속 실패 시 호출 (오버라이딩)
     Debug.Log($"JoinRoom Failed {returnCode} {message} / 방 없음 -> 생성시도중....");
     JointRoom();
 }

  OnCreateedRoom()

           마스터클라이언트가 최초 접속했을 때 실행

  OnJoinedRoom()

           방에 입장하면 호출

  OnJoinRamdonFailed

           방에 입장 실패 시 호출

           JointRoom() 다시 방 만들기 시도 

 

- 실행

    private void Awake()
    {
        if(instance == null)
            instance = this;    
        else if( instance != this)
            Destroy(instance);

        DontDestroyOnLoad(instance);

        // 게임시작 시 포톤 세팅 
        // 1. 포톤서버에 연결, 로비 생성 후 방 생성
        F_MasterClient();

    }

: PhotonManager의 Awake에서 실행한다.

🎨 4. 플레이어 생성

- 플레이어 생성

IEnumerator CreatePlayer() 
{
    // 3초 기다리기
    yield return new WaitForSeconds(3);

    // 입력받은 닉네임 가져오기 
    PhotonNetwork.NickName = UiManager.Instnace.F_InputName();

    // 플레이어 생성 
    // PhotonNetwork의 Instantiate 사용 
    _player = PhotonNetwork.Instantiate("Player", new Vector3(0,0,0), Quaternion.identity);

    if (_player == null)
    {
        Debug.Log("생성한 플레이어가 null");
        yield break;
    }
}

□ yield return new WaitforSeconde()

            서버 연결 + 로비생성 + 방 생성 후 바로 생성하는 것보다 혹시 모를 문제를 방지하기 위해서 3초 대기 

PhotonNetwork.NickName

            InputField에서 입력받은 닉네임으로 네트워크에 설정

  PhotonNetwork.Instantiate

            네트워크상 동기화되는 오브젝트 (player) 생성

            생성 시 모든 네트워크 참가자에게 자동으로 생성됨

            각 오브젝트에 고유한 네트워크 ID가 부여됨 

 

🔖 생성할 프리팹은 Resources폴더 하위에 있어야 한다

🔖 문자열로 검사하기 때문에 프리팹의 이름을 일치시켜 줘야 한다

🔖 프리팹에는 포톤뷰 컴포넌트, 트랜스폼 컴포넌트, 애니메이션 컴포넌트를 추가한다

 

- 로컬과 리모트

: 멀티플레이 게임에서는 내가 제어하는 플레이어 오브젝트만 제어해야 함!

게임을 플레이하는 주체에 따라 '로컬'과 '리모트'로 나뉨

🔖로컬(Local)

: 내가 직접 조작하는 캐릭터나 오브젝트

🔖리모트(Remote)

: 다른 사람이 조작하는 캐릭터

: 멀티플레이어에서, 나를 제외한 다른 사람이 조작하는 캐릭터는 모두 리모트 플레이어

 

PlayerMovement.cs

[Header("===Photon View")]
private PhotonView playerView;

private IEnumerator IE_PlayerMovenet() 
{
    while (true)
    {
        // 로컬체크 - 로컬이 아니면 움직임 x
        if (!playerView.IsMine)
            continue;

        // 플레이어 움직임 코드
        playerMove();

        // 매프레임마다 
        yield return null;
    }
}

  플레이어의 PhotonView의 IsMine 사용 

            로컬이 아니면 플레이어 움직임 코드 실행 x 

 

- 실행

    private void Awake()
    {
        if(instance == null)
            instance = this;    
        else if( instance != this)
            Destroy(instance);

        DontDestroyOnLoad(instance);

        // 게임시작 시 포톤 세팅 
        // 1. 포톤서버에 연결, 로비 생성 후 방 생성
        F_MasterClient();

        // 2. 플레이어 생성 
        StartCoroutine(CreatePlayer());
    }

: PhotonManager의 Awake에서 실행한다.

 


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

2025.02.21 - [Unity] - [Unity][멀티플레이] #3. Agora를 사용한 카메라 인식

 

[Unity][멀티플레이] #3. Agora를 사용한 카메라 인식

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

youcheachae.tistory.com

 

 


깃허브

https://github.com/kimYouChae/Sparta_Metaverse

 

GitHub - kimYouChae/Sparta_Metaverse

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

github.com

 

 

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

 

📝 1. 구현 중인 동작

  zep처럼 특정 구역에 들어가면, 오른쪽에 카메라가 켜고 꺼지는 동작을 구현중.

  내가 플레이하는 캐릭터 기준으로, 특정구역 들어갔을 때 사람이 있으면 사람 수 대로 오른쪽에 카메라가 생성되고, 

특정구역에 나갔을 때 카메라가 전부 삭제되어야 한다.

 

📝 2. 문제

  특정 오브젝트에서 모든 플레이어의 충돌을 감지하여 , 내가 조종하는 캐릭터가 아닌 다른 사람의 캐릭터가 오브젝트에 충돌할 시, 내 화면에도 UI가 켜지고 꺼지는 것을 확인했다. 

 

📝 3. 해결과정 

1. 해당문제가 발생하는 코드

  현재 "특정구역"에 Conference라는 스크립트를 넣어 충돌을 감지하고 있다.

public class ConferenceBox : MonoBehaviour
{
    [SerializeField]
    private string _chanelName = "ConferenceRoom";

    private void OnTriggerEnter2D(Collider2D collision)
    {
        // 플레이어가 들어오면 
        if (collision.gameObject.layer == LayerManger.Instnace.PlayerLayerNum) 
        {
            // Ui 켜기
            UiManager.Instnace._conferenceCameraPanel.SetActive(true);

            // Agora - 조인
            VideoChatManger.Instnace.Join(_chanelName);        
        }
    }

    private void OnTriggerExit2D(Collider2D collision)
    {

        // 플레이어가 들어오면 
        if (collision.gameObject.layer == LayerManger.Instnace.PlayerLayerNum)
        {
            // Ui 끄기
            UiManager.Instnace._conferenceCameraPanel.SetActive(false);

            // Agora - 떠나기
            VideoChatManger.Instnace.Leave();
        }
    }
}

 

  여기서 하나 생각 못한 부분이 있다.

포톤에서 플레이어를 인스턴스화할 때 같은 프리팹으로 한다. 즉 생성되는 모든 플레이어는 같은 Player 레이어를 가지고 있다.

 

 

  같은 프리팹을 사용해서 인스턴스화하는 중.......

 

  즉 다른 사람(LayerMask가 Player)이 '특정구역'의 충돌 조건에 만족하는 것이다. 

 

🔖 내가 원했던 동작

  충돌을 할 때, 내가 플레이하는 캐릭터가 충돌 했을 때를 검사하고싶다.

내가 플레이 하는 캐릭터가 '특정구역'에 들어갔을 때, 나갔을 때에 조건을 검사해야 한다.

 

2. GetInstance()로 검사하면 되지 않을까?

  PhotonInstance로 생성한 오브젝트의 GetHashCode와, 충돌 시 매개변수로 들어온 충돌체의 GetHashCode를 검사해서 같으면, 내가 플레이하는 캐릭터를 알 수 있지 않을까?

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ConferenceBox : MonoBehaviour
{
    [SerializeField]
    private string _chanelName = "ConferenceRoom";

    private void OnTriggerEnter2D(Collider2D collision)
    {
        var hash = PhotonManager.Instnace.photonPlayer.GetHashCode();

        // 플레이어가 들어오면 
        if (collision.gameObject.layer == LayerManger.Instnace.PlayerLayerNum
            && hash == collision.gameObject.GetHashCode() ) 
        {
            // Ui 켜기
            UiManager.Instnace._conferenceCameraPanel.SetActive(true);

            // Agora - 조인
            VideoChatManger.Instnace.Join(_chanelName);        
        }
    }

    private void OnTriggerExit2D(Collider2D collision)
    {
        var hash = PhotonManager.Instnace.photonPlayer.GetHashCode();

        // 플레이어가 들어오면 
        if (collision.gameObject.layer == LayerManger.Instnace.PlayerLayerNum
            && hash == collision.gameObject.GetHashCode() )
        {
            // Ui 끄기
            UiManager.Instnace._conferenceCameraPanel.SetActive(false);

            // Agora - 떠나기
            VideoChatManger.Instnace.Leave();
        }
    }
}

  원하는 대로 잘 동작한다.

PotonManger의 player는 항상 로컬 플레이어 오브젝트를 가지고 있기 때문에, 충돌체가 내가 플레이하고 있는 오브젝트인지 
(로컬 인지) , 다른 사람의 오브젝트인지 (리모트 인지) 검사할 수 있다. 

 

📝 4. 해결방법

  GetHashCode()를 사용하지 않고도 해결할 수 있다.

  내가 원하는 동작은 즉 "충돌체가 로컬 오브젝트"인지 검사해야 한다.

→ 로컬 오브젝트 인지 알 수 있는 방법은 PhotonView의 IsMine 사용하면 된다.

  플레이어 오브젝트는 동기화를 위해서 
PhotonView, Photon Trasform View, Photon Animator View의 컴포넌트를 추가해 놓은 상태이다 

  PhotonNetwork.Instantiate() 로 오브젝트를 생성하면, 로컬인지 리모트인지에 관한 정보가 Photon View에 저장된다.

(포톤이 알아서 정보를 저장해 준다 고마워 따봉포톤아!)

           로컬이면 IsMine이 true로 설정

           리모트이면 IsMine이 false로 설정 

 

PhotonView의 IsMine이 true일 때만 충돌한다! 

 

- 최종 코드

using Photon.Pun;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ConferenceBox : MonoBehaviour
{
    [SerializeField]
    private string _chanelName = "ConferenceRoom";

    // 플레이어의 포톤 정보 가져오기 
    [SerializeField]
    private PhotonView _playerView;

    private void OnTriggerEnter2D(Collider2D collision)
    {
        _playerView = collision.GetComponent<PhotonView>();

        // 플레이어가 들어오면 + 로컬 플레이어면 
        if (collision.gameObject.layer == LayerManger.Instnace.PlayerLayerNum
            && _playerView.IsMine ) 
        {
            // Ui 켜기
            UiManager.Instnace._conferenceCameraPanel.SetActive(true);

            // Agora - 조인
            VideoChatManger.Instnace.Join(_chanelName);        
        }
    }

    private void OnTriggerExit2D(Collider2D collision)
    {
        _playerView = collision.GetComponent<PhotonView>();

        // 플레이어가 들어오면 + 로컬플레이어면
        if (collision.gameObject.layer == LayerManger.Instnace.PlayerLayerNum
            && _playerView.IsMine)
        {
            // Ui 끄기
            UiManager.Instnace._conferenceCameraPanel.SetActive(false);

            // Agora - 떠나기
            VideoChatManger.Instnace.Leave();
        }
    }
}

 

 

- 로컬오브젝트의 충돌만 검사해서 Ui를 끄고켬!

 

 

 

📝 5. 결론

멀티플레이 게임을 만들 때 충돌검사를 할 시 로컬인지 리모트인지 검사하는 것이 중요하다는 것을 깨달았다.

또한 충돌처리가 있는 부분마다 검사해줘야 한다 생각하니 멀티게임이 왜 만들기 까다로운지 알게 되었다

 


스파르타-유니티 개인과제 깃허브

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