: 첫 번째 값에 의한 호출 (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 : 객체의 참조 주소값이 복사된다
(+) 스택프레임
함수가 호출될 때, 스택 영역에 차례대로 저장되는 함수의 호출정보이다.
함수가 호출될 때, 매개변수를 스택에 할당하기 때문에 두 방식 다 매개변수로 전달된 값을 복사한다.
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값 사이에서 볼륨의 크기를 정한다.
// 원본 타입을 유지한 채 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를 인스턴스화 한다.
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();
}
}
}
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() { }
}
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()
: 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를 만들게 된다.
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());
}