pun2
마스터 서버는 하나 -- photonServerSettings에 id가 하나만 존재하는 마스터 서버가 있고 거기에 접속해야함.
우선 using Photon.Pun;
using Photon.Realtime;
필요하며
using Hashtable = ExitGames.Client.Photon.Hashtalbe; 로 유니티에서 제공하는게아닌 , photon 해쉬테이블을 쓰도록 해준다.
MonoBehaviourPunCallbacks를 상속받고
PhotonNetwork.ConnectUsingSettings();로 접속을하면 PhotonServerSettings에 정의된
앱 ID/ 버전/ 리전으로 Photon 마스터 서버에 접속하며, 내부적으로 비동기시도를 통해 마스터서버에 연결된다.
public override void OnConnected()가 콜백으로 호출된다.
가장 먼저 호출되는 부분이다.
그다음 바로 public override void OnConnectedToMaster()가 호출이된다.
마스터 아이디를 받은것에 접속함.
이때부터 JoinLobby(), CreateRoom(), JoinRoom() 등이 동작가능하고
PhotonNetwork.IsConnected로 연결상태 확인가능하다.
자동씬 동기화설정(PhotonNetwork.AutomaticallySyncScene) 또한 이때부터 활성화된다.
연결 끊김은
PhotonNetwork.Disconnect();을 하면
public override void OnDisconnected(DisconnectCause cause)이 호출되고 연결이끊긴다.
PhotonNetwork.JoinLobby(); 로 로비참가를 가능하게한다.
public override void OnJoinedLobby() 호출, 로비참가된다.
엔진에 Resources에 넣어놓은 PhotonServerSettings에서 Lobby Statistics 옵션을
체크하면 로비도 여러개 생성가능하다.
PhotonNetwork.LeavLobby(); 를 통해
public override void OnLeftLobby() 호출,
로비를 떠날때임, -> 차례로 OnConnectedToMater()가 호출된다.
방만들기
로비에 있거나, 마스터서버에 있거나할때 방을 만들 수 있다.
PhotonNetwork.CreateRoom("room", new RoomOptions { MaxPlayer = 2 }, null);
방이름 string으로 쓰고, 방 옵션을 세팅가능 최대인원, IsOpen(열렸는지 닫혔는지), IsVisible(보이는지안보이는지 등등)
PhotonNetwork.JoinOrCreateRoom("room", new RoomOptions {MaxPlayer = 2}, null);
이렇게 같은이름으로 만들거나 참가가 가능한 코드블럭도 존재
을 통해 public override void OnCreatedRoom() 호출
public override void OnCreateRoomFailed(short returnCode, string message)
방만들기 실패시 뜬다, 해당 CreateRoom, JoinOrCreateRoom을 쓸때 방생성이 실패하면 뜬다.
같은 네임의 방을 만들수없을때 주로 실패한다.
PhotonNetWork.JoinRoom("room"); 이라던가
PhotonNetwork.JoinRandomRoom(); 이런걸 통해 (빠른매칭)
방 참가시는 바로
public override void OnJoinedRoom()이 콜백된다.
방참가 실패시에는
public override void OnJoinRoomFailed(short returnCode, string message)가 뜨거나
랜덤방창가 실패시에는
public override void OnJoinRandomFailed(short returnCode, string message) 가 뜬다.
PhotonNetwork.LeaveRoom();을 통해 방을 나가며
public override void OnLeftRoom()이 호출된다.
이때 로비로 가는게아니라 마스터로 간다. 따라서 자동으로 로비참가를 하도록 써줘야한다.
여러 로비를 사용하고싶으면 아까도 말했지만 엔진상의 PhotonserverSettings - Lobby Statistics 활성화
하게되고 마스터서버에서 로비들의 정보를 볼수있다.
public override void OnLobbyStatisticsUpdate(List<TypedLobbyInfo> lobbyStatistics)
로비를 만들때,
PhotonNetwork.JoinLobby(new TypedLobby("dduck", LobbyType.Default));
하나 만들었다치면
마스터에서 로비들의 정보를 보고싶을때,
일단 기본으로 제공되는 첫번째 로비는 이름이없고, 방금 dduck을 만들었기때문에
이름없는 로비하나랑 dduck로비하나 이렇게 생긴상태에서 for문을 돌며 정보를 불러올수있다.
for(int i = 0; i < lobbyStatistics.Count; i ++)
{
LogText.test += lobbyStatistics[i].Name +", " + lobbyStatistics[i].PlayerCount + "\n";
}
PhotonNetwork.SetMaseterClient(PhotonNetwork.PlayerList[0]); 하면(우클릭등으로 옵션띄고
원하는플레이어를 넣어준다음 해당 함수를 쓴다) 그러면
public override void OnMasterClientSwitched(Player newMasterClient)이 호출된다.
방에 있는 모든 사람들이 해당 콜백을 받는다.
List<RoomInfo> myList = new List<RoomInfo>();
로비에 있을때, 방 리스트 업데이트시
public override void OnRoomListUpdate(List<RoomInfo> roomList)가 호출된다.
아래는 방리스트업데이트시의 공식이다. 로비에서 방목록이 바뀔떄(OnRoomListUpdate)
전달된 roomList를 바탕으로 화면(UI)에 보여줄 로컬리스트인 myList를 추가,갱신,제거하기 위한 공식패턴으로
아래와 같다.
int roomCount = roomList.Count;
for(int i = 0; i< roomCount; i++)
{
if(!roomList[i].RemovedFromList)
{
if(!myList.Contains(roomList[i])) myList.Add(roomList[i]);
else myList[myList.IndexOf(roomList[i])] = roomList[i];
}
else if (myList.IndexOf(roomList[i]) != -1) mysList.RemoveAt(myList.IndexOf(roomList[i]));
}
서버로 업데이트된 roomList를 받고 각 방(RoomInfo)마다
RemovedFromList == false 인경우(새로 생겼거나 정보가 바뀐방)
myList에 없으면 추가하고
이미 있으면 교체(업데이트)한다.
RemovedFromList == true라면 없어져야할 방이므로
myList에서 제거한다.
else myList[myList.IndexOf(roomList[i])] = roomList[i];의 구문은
myList.Contains(roomList[i])일때의 경우인데,
myList[ ] 안에 IndexOf로 해당 roomList[i]가 들어있는 위치를 찾고,(순서대로 넣어져있을텐데 그중에서
언젠가 들어갓떤 roomList[i]의 순서를 찾아준뒤에 그 순서를
myList[]안에 넣어줫으니 결국 myList[ ] 안에 해당 roomList[i]를 찾아 새로운 roomList[i]로 대체(재할당)
해주는것이다. 즉 업데이트다. 여기서 IndexOf 문법은 문자열의 IndexOf가 아니라 List<T>,Indexof(T item)을 쓴것
else if (myList.IndexOf(roomList[i]) != -1)는 두가지를 함축하는데,
RemovedFromList == true(사라진방일때), IndexOf != -1 (없으면 null처럼 나오는 기본값)이아니라면
myList에 남아있다는 뜻이고, 그때 RemoveAt(해당 인덱스)를 통해 해당 위치의 방을 삭제하라는 것이다.
방에 있을때, 방에다가 태그를 달수있다. 키와 값을 넣어주고,
PhotonNetwork.CurrentRoom.SetCustomProperties(new Hastable { {"RoomTag", "tag"}});
방 태그 변경시 아래와 같은 콜백을 모든 플레이어가 실행한다.
public override void OnRoomPropertiesUpdate(Hastable propertiesThatChanged)
비슷하게
PhotonNetwork.PlayerList[0].SetCustomProperties(new Hashtable { {"PlayerTag", "tag"}}); 하게되면
방에있을때 플레이어의 태그변경시에는
public override void OnPlayerPropertiesUpdate(Player targetPlayer, Hashtable changedProps)
로 어떤 플레이어가 어떻게 태그를 변경했는지 모두에게 콜백이간다.
방에 있을때, 새로운플레이어가 들어오면
public override void OnPlayerEnteredRoom(Player newPlayer) 가 호출
방에 있을때, 다른 플레이어가 나가면
public override void OnPlayerLeftRoom(Player otherPlayer)
가 호출된다.
그다음은 CustomProperties에 대해서 알아보자. 태그를 달아 구별하는것이다.
방의 Room형식에도 태그를, Player에도 태그를 달아줄수있다.
한사람이 태그를 달면 모든 사람의 방의 태그도 똑같이 보인다.
플레이어도 마찬가지다. 첫번째 플레이어에게 태그입력했으면 모든 사람들이 해당 플레이어의 태그를 알수있다.
방에 들어가 있을때,
using Hastable = ExitGames.Client.Photon.Hashtable;로 해두고
public class NetwrokManager : MonoBehaviourPunCallbacks
{
public override void OnJoinedRoom()
{
PhotonNetwork.CurrentRoom.SetCustomProperties(new Hastable() { {"키1", "문자열" } , {"키2", 1} });
print(PhotonNetwork.CurrentRoom.CustomProperties["키1"]);
}
...
방에 있을때 현재방(Room형식)에 누구나 태그를 달수있다.
키는 문자열이어야하고 값은 포톤이 직렬화 할 수 있는 여러 형식이면 가능하다.
물론 같은 키에 다른 값 덮어쓸 수 도 있으며, 자동형변환이 안되므로 0과 0f를 구별하자.
CustomProperties로 쓰지말고, SetCustomProperties로 쓰는것을 유의하자. 동기화가 안될수있다.
플레이어의 경우,, 마찬가지로
방에 있을때 플레이어(Player형식)에 누구나 태그를 달 수 있다.
마찬가지로
using Hastable = ExitGames.Client.Photon.Hashtable;로 해두고
public class NetwrokManager : MonoBehaviourPunCallbacks
{
public override void OnJoinedRoom()
{
PhotonNetwork.PlayerList[0].SetCustomProperties(new Hastable() { {"키1", "문자열" } , {"키2", 1} });
print(PhotonNetwork.PlayerList[0].CustomProperties["키1"]);
}
...
카트라이더 방 만들기 핵심로직.
방장은 아래와 같이 방을 만들고 방에 참여하거나 나갈때 각 태그에 액터넘버를 넣어 어떤 창이 열린지 닫힌지 알 수 있다.
if(S.master()) // PhotonNetwork.LocalPlayer.IsMasterClient // 방장 자신이라면,
{
int max = PhotonNetwork.CurrentRoom.MaxPlayers -1; //초기 방최대인원설정시, 인덱스로 찾기위해 -1
// 방을 만든사람은 0에 자기번호, 참여가능 슬롯은 0, 참여불가능 슬롯은 -1, 저장은 무조건 Set으로 해야함
PhotonNetwork.CurrentRoom.SetCustomProperties( new Hastable
{
{ "0", PhotonNetwork.LocalPlayer.ActorNumber}, {"1", 0},
{"2", 2<= max ? 0 : -1 }, { "3", 3<= max? 0: -1}, { " 4", 4<= max? 0: -1},
{"5", 5<= max ? 0 : 01}, { "6", 6<= max? 0: -1}, { "7", 7<= max? 0: -1}
});
}
PhotonNetwork.LocalPlayer.ActorNumber는 방장 본인의 ActorNumber (PUN에서 플레이어 고유 ID)이다.
예상시나리오로
MaxPlayers = 4로 설정하면 max = 3 일테고
초기 hastable 결과는
json으로 나타내면
{
"0": 1001, // 방장 ActorNumber = 1001
"1": 0, // 슬롯1 비어 있음
"2": 0, // 슬롯2 비어 있음
"3": 0, // 슬롯3 비어 있음
"4": -1, // 슬롯4 이상은 아예 존재하지 않음
"5": -1,
"6": -1,
"7": -1
}
이다.
이 프로퍼티를 설정하면 다른 클라이언트들은
var props = PhotonNetwork.CurrentRoom.CustomProperties;
int slot2 = (int)props["2"]; // 0 -> 빈자리
식으로 방 상태를 바로 읽어올 수있다.
씬전환시 동기화로직, 모든사람이 씬이 동시에 전환되는게아니다. 좋은컴퓨터는 더빨리되고
안좋은컴퓨터는 로딩이 느릴것. 따라서 동기화로직을 짜준다.
public bool AllhasTag(string key)
{
for ( int i = 0; i < PhotonNetwork.PlayerList.Length; i++)
if( PhotonNetwork.PlayerList[i].CustomProperties[key] == null) return false;
return true;
}
//PlayerList는 로비또는 룸안의 모든 플레이어의 배열을 뜻함
//각 플레이어의 커스텀프로퍼티에서 키(여기서는 "loadScene"같은)를 조회
등록전엔 null, 등록 후엔 값(1또는 true)가 들어있다.
한명이라도 CustomProperties[key] == null 이라면 아직 로딩안한 사람이 존재하니, false를 반환한다
모두 비어있지않으면 -> true를 반환한다.
IEnumerator Loading()
{
BillBoard.SetActive(true); // 로딩시 가려주는 광고판임
while( PhotonNetwork.CUrrentRoom.CustomProperties["map"] == null) yield return null;
mapIndex = (int)PhotonNetowrk.CurrentRoom.CustomProperties["map"];
BillBoardImage.sprite = S.Mapsprites[mapIndex];
//누군가 SetRoomTag("map", index)를 통해 룸 프로퍼티에 "map"키를 먼저 등록할때까지
//매프레임 체크한다.
//맵정보(mapIndex)가 룸 프로퍼티에 등록될 때까지 대기한다.
while(!S.AllhasTag("loadScene")) yield return null;
// 각 클라이언트가 내가 씬 로딩이 끝났어라는 표시용 태그(loadScene)을 방 커스텀 프로퍼티에 저장한상태에서
// 모든 클라이언트가 이 태그를 등록할때까지 기다리는것을 뜻함.
if(S.master()) Spawnsetting();
//방장이 플레이어 스폰세팅을한다.
while(!S.AllhasTag("loadPlayer")) yield return null;
//마찬가지로 모든 플레이어 스폰끝났어를 띄우는 (loadPlayer)태그까지도 또 대기하는것을 뜻함.
BillBoard.setActive(false);
}
함수를 만들어 놓고 쓰면 좋다.
public void SetRoomTag(int slotIndex, int value) //키와 값을 넣으면
{
PhotonNetwork.CurrentRoom.SetCustomProperties(new Hashtable { {slotIndex.ToString(), value} });
}
public int GetRoomTag(int slotIndex) //키만 넣으면
{
object value = PhotonNetwork.CurrentRoom.CustomProperties[slotIndex.ToString()];
return value != null? (int) value : -2;
}
싱글톤이나 매니저에 이렇게쓴다.
플레이어도 마찬가지로
public void SetTag(string key, object value, Player player = null)
{
if( player == null) player = PhotonNetwork.LocalPlayer; // player 아무것도 안넣었다면(null)이라면 자기자신이되는거고
player.SetCustomProperties(new hashtable { {key, value}}); //특정플레이어를 넣어줬다면 키와 값을 넣어준다.
}
public object GetTag(string key, Player player = null)
{
if(player == null) player = PhotonNetwork.LocalPlayer;
return player.CustomProperties[key];
}
사용예시로는 씬로딩직후 각 클라이언트는 자기 슬롯 인덱스를 알아야하니
int mySlot = PhotonNetwork.LocalPlayer.ActorNumber % Maxplayers;
StartCoroutine(Loading());
//씬로딩완료시점
SetRoomTag(myslot, 1); // 여기서는 1이 가르키는 loadScene 태그 등록
//방장이 Spawnsetting() 실행후
SetRoomTag(mySlot, 2); // 여기서는 2가 가르키는 loadPlayer 태그 등록
매니저(싱글톤)에 위함수를 두고
씬전환 시점마다 startCoroutine(Loading()) 호출,
다른스크립트에서 SetRoomTag(...), GetRoomTag(...) 사용한다.
아래는,
씬로딩 -> 스폰 두단계에서 자동으로 태그를 등록하고,모두가 완료할때까지 기다리는
SceneSyncManager 예제다.
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
using Photon.Pun;
using Photon.Realtime;
using Hashtable = ExitGames.Client.Photon.Hashtable; // 타입 별칭
public class SceneSyncManager : MonoBehaviourPunCallbacks
{
public static SceneSyncManager Instance;
// 슬롯별 키
private const string LOAD_SCENE_KEY = "loadScene";
private const string LOAD_PLAYER_KEY = "loadPlayer";
private const int SCENE_LOADED_FLAG = 1;
private const int PLAYER_READY_FLAG = 1;
private int mySlot;
void Awake()
{
// 싱글톤 패턴
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else Destroy(gameObject);
}
void Start()
{
// 내 슬롯 계산 (ActorNumber는 1부터 시작)
mySlot = PhotonNetwork.LocalPlayer.ActorNumber - 1;
// Photon의 자동 씬 동기화 비활성화 → 수동으로 제어
PhotonNetwork.AutomaticallySyncScene = false;
// 방장이면 씬 로딩 명령
if (PhotonNetwork.IsMasterClient)
PhotonNetwork.LoadLevel("GameScene");
// 로딩-스폰-끝 순서대로 진행
StartCoroutine(LoadingCoroutine());
}
void OnEnable()
{
// 씬 로딩 완료 이벤트 감지
SceneManager.sceneLoaded += OnSceneLoaded;
}
void OnDisable()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
}
// ① 씬 로딩 직후 자동 호출
void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
// 씬 로딩 완료 표시
SetRoomTag(mySlot, LOAD_SCENE_KEY, 1);
}
// ② 코루틴으로 동기화 진행
IEnumerator LoadingCoroutine()
{
// (선택) 로딩 UI 켜기
// LoadingUI.SetActive(true);
// --- 맵 프로퍼티가 등록될 때까지 대기 ---
while (PhotonNetwork.CurrentRoom.CustomProperties["map"] == null)
yield return null;
int mapIndex = (int)PhotonNetwork.CurrentRoom.CustomProperties["map"];
// (선택) 맵 이미지 교체
// MapDisplay.sprite = Mapsprites[mapIndex];
// --- 모든 클라이언트가 씬 로딩을 마칠 때까지 대기 ---
yield return new WaitUntil(() => AllHasTag(LOAD_SCENE_KEY));
// --- 방장만 플레이어 스폰 로직 실행 ---
if (PhotonNetwork.IsMasterClient)
SpawnPlayers();
// --- 자신의 스폰 완료 표시 ---
SetRoomTag(mySlot, LOAD_PLAYER_KEY, 1);
// --- 모두 플레이어 스폰을 마칠 때까지 대기 ---
yield return new WaitUntil(() => AllHasTag(LOAD_PLAYER_KEY));
// (선택) 로딩 UI 끄기
// LoadingUI.SetActive(false);
}
// 플레이어 스폰 예시
void SpawnPlayers()
{
foreach (var p in PhotonNetwork.PlayerList)
{
int slot = p.ActorNumber - 1;
Vector3 spawnPos = /* 슬롯별 위치 결정 로직 */;
PhotonNetwork.Instantiate("PlayerPrefab", spawnPos, Quaternion.identity);
}
}
// 모든 플레이어가 해당 키로 값을 등록했는지 체크
bool AllHasTag(string key)
{
foreach (var p in PhotonNetwork.PlayerList)
{
if (!p.CustomProperties.ContainsKey(key))
return false;
}
return true;
}
// 룸 프로퍼티에 키–값 등록
void SetRoomTag(int slotIndex, string tagKey, int value)
{
PhotonNetwork.CurrentRoom.SetCustomProperties(
new Hashtable { { slotIndex + "_" + tagKey, value } }
);
}
}
여기서 1 부분을
const int LOAD_SCENE_DONE = 1;
const int LOAD_PLAYER_DONE = 1;
// …
SetRoomTag(mySlot, LOAD_SCENE_DONE);
이렇게 정의해서 불러올수도있다.
-----------------
방장이 있어야하는이유
MasterClient
방을 만든사람이 방장이다.
PhotonNetwork.SetMasterClient(PhotonNetwork.PlyaerList[0]); 로 방장넘기기
방장이 방을 나가면 액터넘버가 작은 사람이 방장이 된다.
방장이 바뀌면 OnMasterClientSwitched(Player newMasterClient) 콜백
방장이 씬에 배치된 PhotonView가 있는 오브젝트의 제어를 할수있음
씬에 배치된 OnPhotonSerializeView 또한 방장이 보낸다. //변수동기화, 방장이 변수동기화를 보내고 다른사람은 받는다.
public bool master()
{
return PhotonNetwork.LocalPlayer.IsMasterClient;
}
모든 클라이언트의 이벤트 같이 복잡한 계산은 모두 방장한테 전달하고
방장 혼자 계산한 다음 다른 사람들에게 전달한다.
Photon PUN2의 RPC(원격호출) 기능을 쓰면, 네트워크에 연결된 다른 클라이언트의 메서드를
마치 로컬에서 부르듯 실행시킬 수 있다.
[PunRPC] 어트리뷰트로 감싼 메서드는 PhotonView.RPC로 원격 호출이 가능하다라는 표시다.
이 어트리뷰트를 달아야 Photon 쪽에서 PV.RPC("메서드이름", ...) 호출 시 실행대상이된다.
먼저 PV에 PhotonView 컴포넌트를 GeComponent<PhotonView>로 가져와 할당한다.
void Click()
{
PV.RPC("MasterRecieveRPC", RpcTarget.MasterClient);
}
[PunRPC]
void MasterRecieveRPC()
{
//방장이 계산
PV.RPC("OtherRecieveRPC", RpcTarget.Others);
}
[PunRPC]
void OtherRecieveRPC()
{
//방장이 보낸 결과를 바탕으로 동작 주로 화면갱신, 이펙트 재생등 클라이언트별 후처리
}
RpcTarget의 경우
MasterClient: 방장에만
Others: 나를 제외한 모두
All: 모두(나포함)
AllBuffered: All + 버퍼링 이후 들어오는 플레이어들에게도 자동실행
OthersBuffered : Others + 버퍼링
Player targetPlayer : 특정 플레이어 객체(개별 클라이언트)에게만 실행
PhotonView.Rpc(메서드명 string, 대상(RpcTarget 열거형), [인자..])로 매개변수를 집어넣으며
인자들은 없어도된다.
인자들은 object[] 형태로 들어가는데
Photon이 직렬화 할수있는 자료형들이 기본타입으로 쓰인다. 가급적 가벼운 값위주로 해야좋다.
// 인자가 없을 때
photonView.RPC("PlaySound", RpcTarget.All);
// 인자가 있을 때
photonView.RPC("DealDamage", RpcTarget.All, targetId, damageAmount);
Photon 서버가 이 호출을 받고 , 대상클라이언트의 같은 PhotonView가 붙은 오브젝트에서
PunRPC 가 붙은 메서드를 찾아 실행해준다.
그러므로 위의 예제를 보면 클릭하는 순간 MasterRecieveRPC() 가 마스터클라이언트에서만
실행되고 방장이 계산 로직을 다한뒤에 OtherRecieveRPC를 방장을 제외한 나머지들이 호출하게한다.
그리고 그 OtherRecieveRPC 함수에서 방장한테 받은 결과를 바탕으로 동작하는 로직을 실행한다.
또다른 예제로는 인자가 있을때의 예시인데,
[PunRPC]
void DealDamage(int targetId, float dmg)
{
//… 데미지 처리 로직 …
}
// 방장→모두에게 데미지 알림
if (PhotonNetwork.IsMasterClient)
{
photonView.RPC("DealDamage", RpcTarget.All, hitPlayerId, 25f);
}
로 브로드캐스트하는 함수의 파라매터 인자들을 뒤에, 넣어준다.
JSON 을 쓰는 이유.
클래스 리스트는 포톤직렬화 가능한 형식에 없기때문에
모든 이벤트는 방장에게 보내고, 방장은 클래스리스트로 실시간으로 관리와 정렬을한다음
다른사람들에게 JSON 직렬화로 보내게되고, 다른사람들은 JSON 파싱해서 같은
클래스 리스트를 갖게된다.
Photon View 컴포넌트를 보면
Owner에 Fixed, Takeover, Request 이렇게 세가지 옵션이있는데
Fixed: 생성된 게임 오브젝트의 주인은 변하지않는다.
Takeover : 누구나 주인을 쉽게 가져가거나 줄 수 있다.
Request : 현재 주인에게 소유권을 요청할 수 있으나 거절 될 수 있다.
Takeover로 옵션을 선택해놓았다면
void Update()
{
if(Input.GetKeyDown(KeyCode.Alpha1))
{
BallPV.RequestOwnership(); // 자기가 주인이된다는 함수 호출
BallPV.TranserOwnership(PhotonNetwork.PlayerListOthers[0]);
// 해당플레이어가 주인이되도록 내가 주인이아니어도 넘길수있다.
}
if(BallPv.Owner == PhotonNetwork.LocalPlayer)
{
BallPV.transform.Translate( new Vector3(Input.GetAxis("Horizontal") *3 , 0 ,0));
}
}
정리하자면 Fixed에선 생성 직후 주인이 절대 바뀌지않으니
RequestOwnership(). TransferOwnership() 모두 무시된다.
Takeover 은 누구나 TransferOwnership(targetPlayer) 로 소유권을 넘겨주거나 빼앗을 수 있음
RequestOwnereship()와 무관하게 바로 권한변경된다.
Request의 경우 RequestOwnership()를 호출하면 현재 주인에게 소유권 요청 이벤트가 간다.
주인이 자동으로 거절하기(OnOwnershipRequest 훅에서 처리)
자동으로 수락하기도 가능하다. TransferOwnership(reqeustingPlayer)
아무튼 IPunOwnershipCallbacks 인터페이스를 구현하면 요청하거나 넘겨줄때 방에 있는 모두에게 콜백이된다.
public class NetworkManager : MonoBehaviourPunCallbacks, IPunOwnershipCallbacks
{
public void OnOwnershipRequest(PhotonView targetView, Player requestingPlayer)
{
print("요쳥");
}
public void OnOwnershipTransfered(PhotonView targetView, Player previousOwner)
{
print("옮겨짐");
}
...
별개로 new Vector3를 update()에서 매번 생성하는거는 Vector3가 구조체이므로 그다지 무겁지않다.
힙영역에 할당되지않고 스택 혹은 레지스터에 바로 저장되므로 GC힙에 할당안하기때문에
메모리 할당, 해제 오버헤드가 없다.
물론 조금더 최적화가 필요하다는 전제하에 이렇게도 쓸수는 있다.
float h = Input.GetAxis("Horizontal") * speed * Time.deltaTime;
BallPV.transform.Translate(h, 0, 0);
PhotonView의 Owner가 Reqeust 옵션을 택했을때의 경우 예제다
public override void OnJoinedRoom()
{
PlayerPV = PhotonNetwork.Instantiate("Player", Vector3.zero, quaternion.identity).GetPhotonView();
}
PhotonView FindPlayer()
{
foreach(var player in GameObject.FindGameObjectsWithTag("Player"))
{
if(player.GetPhotonView().Owner != PhotonNetwork.LocalPlayer) return player.GetPhotonView();
}
// 내가 아닌 첫번째 다른플레이어의 PhotonView를 리턴하는과정이며 이걸 전체 Player태그가 달린
모든 게임오브젝트에서 실시한다.
PhotonView의 Owner가 Request라는 옵션으로 되어있다면,
다른사람이 요청하면 주인은 허용해줘야 이전된다.
여기서 GetPhotonView()의 경우 PhotonView 컴포넌트를 쉽게 꺼내주는 확장 메서드다.
PhotonView pv = gameObject. GetComponent<PhotonView>() 와 같다.
using Photon.Pun;이 제공한다.
public override void OnJoinedRoom()
{
PlayerPV = PhotonNetwork.Instantiate("Player", Vector3.zero, Quaternion.identity).GetPhotonView();
}
PhotonView FindPlayer()
{
foreach( var player in GameObject.FindGameObjectsWithTag("Player"))
{
if(player.GetPhotonView().Owner != PhotonNetwork.LocalPlayer) return player.GetPhotonView();
}
return null;
}
void Update()
{
if(Input.GetKeyDown(KeyCode.Alpha1))
FindPlayer().RequestOwnership();
}
그다음에
public Class PlayerScript : MonoBehaviourPunCallbacks, IPunOwnershipCallbacks
{
public void OnOwnershipRequest(PhotonView targetView, Player requestingPlayer)
{
targetView.TransferOwnership(requestingPlayer);
}
...
targetView의 경우 소유권 요청이 들어온 PhotonView이며 , 요청한 플레이어는 requestingPlayer다.
해당 함수를 실행하면 요청한 플레이어에게 해당 targetVIew의 소유권권한이 넘어간다.
이렇게 오브젝트에 대한 조작권한을 옮겨줘야할 상황은 아래와 같은 케이스들이있다.
1. 플레이어간 아이템(무기, 공, 도구등) 소유권 이전
예시 : 농구 공을 패스할때
공은 기본적으로 들고잇는 플레이어가 몰리연산, 애니메이션을 책임지므로
패스할때 TransferOwnership(받는플레이어)를 호출해 소유권을 넘기고
받은 플레이어가 공을 조작하도록한다.
Takeover 모드: 즉시 뺏고 줄 수 잇는 자유로운 패스
Request 모드: 패스! 요청 -> 현재 소유자가 승인후 넘겨주는 경우
2. 턴기반 게임에서의 권한제어
예시: 체스나 보드게임
내턴이 되면 이 기물을 움직일 권한을 RequestOwnership()으로 요청 -> 주인이던 서버(또는 호스트)가 승인
내턴이 끝나면 자동으로 TransferOwnership으로 권한 반납
이유: 다른 플레이어가 내턴에 오브젝트를 건드리지 못하게 막는다.
3. 물리시뮬레이션 분산처리
예시: 크래시 카트 같은 레이싱
도로 위 장애물이나 폭발물 같은 리짓모양 오브젝트는
근처 플레이어가 소유권을 가져가서 물리업데이트를 (충돌처리) 를 수행해야함
멀리떨어진 플레이어에겐 네트워크로 갱신되 상태만 동기화한다.
이점: 모든 플레이어가 같은 물체를 매 프레임 계산하는게 아니라 권한이 있는 쪽에서만 연산하여 성능절약한다.
4. 공유 인터랙티브 오브젝트
예시 : 문, 스위치, 레버조작
누군가 스위치를 당기면 REquestOwnership() -> 현재 권한 보유자가 승인 -> 그 플레이어의 입력에따라
모든 클라이언트에 애니메이션,사운드를 동기화한다.
이유: 애니메이션이나 이펙트를 발동시키는 주체를 명확하게 하기위해.
소유권이란 권한을 위임하거나 요청하는 방식을 쓸때, 네트워크 상에서 책임이 명확해져서
입력처리, 물리연산, 애니메이션 동기화가 꼬이지않고 보안 및 치팅방어에도 도움이된다.
'Unity 개발 공부' 카테고리의 다른 글
| [내배캠] 본캠 57 pun2 공부3 (0) | 2025.06.26 |
|---|---|
| [내배캠] 본캠 57 pun2 공부2 (3) | 2025.06.26 |
| [내배캠] 본캠 51 시네마신, UniTask (1) | 2025.06.18 |
| [내배캠] 본캠 45 문법 복습(컴포넌트찾기) , 코드 최적화 (2) | 2025.06.10 |
| [내배캠] 본캠 41일차. 물건집기, 로테이션 (0) | 2025.06.04 |