Instantiate, PhotonNetwork.Instantiate
일반적으로 유니티는 게임 오브젝트의 생명주기를 관리해 주기 위해 Instantiate와 Destroy 함수를 사용한다.
public GameObject obj;
void Start()
{
//Instantiate(생성할 오브젝트, position, rotiation)
//Quaternion.identity = rotation이 (0,0,0)임을 의미
Instantiate(obj, new Vector3(0,0,0), Quaternion.identity);
}
생성하려는 오브젝트는 Scene에 오브젝트로 생성되어 있어야 선택이 가능하다.
게임 실행 후 Hierarchy창을 보면, 선택한 게임오브젝트이름(clone) 이라는 명칭으로 복사 생성된 게임오브젝트를 볼 수 있다.
하지만 이렇게 생성한 게임 오브젝트는 네트워크 동기화를 따로 해 주어야 한기에, Photon Pun에서는 게임 오브젝트를 생성하고 네트워크 동기화 작업을 수행할 수 있는 아주 편리한 방법을 제공해 주고 있다.
PhotonNetwork.Instantiate("MyPrefabName", new Vector3(0, 0, 0), Quaternion.identity, 0);
같은 룸에 있는 모든 클라이언트들은 제어를 하게 될 객체 생성을 위해 이 호출을 받게 된다.
- 첫 번째 파라미터는 인스턴스를 생성하기 위한 "프리팹"이다. PUN 은 내부적으로 PhotonNetwork.PrefabPool에서 GameObject를 가져와, 네트워크용으로 설정하고 사용할 수 있도록 한다.
- 객체가 생성될 위치와 회전은 반드시 사전에 설정되어야 한다.
- 기본값으로 PUN은 Resources 폴더에서 프리팹을 로드하고 나중에 GameObject를 파괴하는 DefaultPool을 사용한다.
- 생성할 객체에는 PhotonView 컴포넌트가 붙어 있어야 한다.
더 자세한 내용은 Photon PUN공식 문서를 확인할 것.
https://doc.photonengine.com/ko-kr/pun/current/gameplay/instantiation
인스턴스생성 | Photon Engine
대부분의 멀티플레이어 게임에서는 일부 게임 오브젝트들을 생성하고 동기화 해야 할 필요가 있습니다. 게임 오브젝트는 룸에 있는 모든 클라이언트들이 가지고 있어야 할 캐릭터가 될 수 있으
doc.photonengine.com
객체 생성 텔레포테이션 동기화 버그
Photon PUN 을 활용해서 멀티 플레이 게임을 개발하던 중 문제를 마주했다.
게임이 진행 중인 방에 다른 플레이어(클라이언트)가 중간 접속(난입) 할 수 있게 게임을 구성하여 개발 중이었는데
룸을 구성한 후에
PhotonNetwork.Instantiate 를 사용해서 네트워크 객체 생성을 활용하고,
룸에 다른 네트워크 플레이어가 입장하게 되면 이런 문제가 생긴다.
다른 pc에서 기존에 있던 오브젝트들(플레이어, 몬스터 등)의 위치/회전값이 순간이동-보간해버리는 문제를 발견했다.
(캐릭터 오브젝트 위치에 대한 보간 처리를 했기 때문에, 처음 위치가 지정되어 버리면 그 후로 오브젝트가 쭉 끌리면서 점멸하는 문제, 점멸하며 중간 건물에 끼이는 문제까지 발생)
// 룸에 입장한 후 호출되는 콜백 함수
public override void OnJoinedRoom()
{
base.OnJoinedRoom();
....코드 생략....
// 네트워크 상에 캐릭터 생성
PhotonNetwork.Instantiate("Player", point, Quaternion.identity, 0);
}
동기화 할 객체에 붙어있는 OnPhotonSerializeView 함수를 살펴보면, 이 함수에서 객체의 position과 rotation을
동기화 시켜주는 것을 확인할 수 있다.
public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
{
// stream - 데이터를 주고 받는 통로
// 내가 데이터를 보내는 중이라면
if (stream.IsWriting)
{
// 이 방안에 있는 모든 사용자에게 브로드캐스트
// - 내 포지션 값을 보내보자
stream.SendNext(transform.position);
stream.SendNext(transform.rotation);
}
// 내가 데이터를 받는 중이라면
else
{
// 순서대로 보내면 순서대로 들어옴. 근데 타입캐스팅 해주어야 함
transform.position = (Vector3)stream.ReceiveNext();
transform.rotation = (Quaternion)stream.ReceiveNext();
}
}
다음은 Update()를 통해 움직임을 보간 처리해준 부분이다.
void Update()
{
//로컬이 아닌 네트워크 객체들 위치, 회전 보간처리 후 수신
InterpolateNetworkTransform();
}
void InterpolateNetworkTransform()
{
//수신된 좌표로 보간한 이동 처리
transform.position = Vector3.Lerp(transform.position, receivePos, Time.deltaTime * damping);
//수신된 회전값으로 보간한 이동 처리
transform.rotation = Quaternion.Slerp(transform.rotation, receiveRot, Time.deltaTime * damping);
}
로그를 찍어 보면 이렇게 나온다.
순서는 OnJoinedRoom() -> PhotonNetworkInstantiate() -> OnPhotonSerializedView() -> ReceiveNext 호출 순이다.
분석해 보면 이와 같은 의도치 않은 객체의 텔레포트 버그는
중간에 다른 PC가 생성된 룸에 접속 했을 때, 유니티의 생성 데이터를 먼저 받아오기 때문에,
PhotonNetwork.Instantiate를 통해 객체를 생성했을 때,
먼저 접속해 있는 PC에서 접속한 PC 객체를 먼저 생성하고
(이 때 포톤 네트워크를 통해 위치값을 전달받지 못한 상태이므로, 임의의 위치 또는 디폴트 위치로 정하게 된다!!)
그 다음에 Update 문일 실행되어 위치가 동기화가 되는 상황인 것이다.
(따라서 오브젝트가 점멸하며 있어야 할 위치로 가게 된다)
즉, 포톤 네트워크 객체 생성이 처음에 받아와야 하는 위치값(유저 데이터)를 받아오지 못하고, 유니티가 우선 객체를 씬 어딘가의 디폴트로 정해진 위치에 생성한 후 포톤 네트워크가 받은 패킷을 통해 위치 정보를 받아오기 때문에 발생하는 문제라고 판단된다.
(포톤이 우선적으로 유니티 내부 유저 데이터에 저장하지 못하는 것으로 보인다. 이 문제를 분명 알고 있을 텐데 그대로 내버려 둔 것으로 보아 유니티의 구조적인 한계 때문일 가능성이 농후하다)
샘플 프로젝트를 만들어 보고 다시 한번 확인해본 객체 생성 텔레포트 이슈
유니티 객체 생성 디폴트 위치(추정)
그렇다면 이것을 해결하는 방법은?
해결 방법
1. Awake 이용하기
미리 Awake로 객체의 위치를 직접 지정해 주게 되면, Awake 위치에 생성이 된 이후에 네트워크 패킷을 받게 된다.
즉 유니티의 디폴트 위치가 아닌, 원하는 위치에 미리 객체를 생성할 수 있게 되는 것이다.
void Awake()
{
this.transform.position = Vector3.zero;
}
그 후 Update() 의 InterpolateNetworkTransform() 함수를 통해 호출해 주면
밑의 그림과 같이 객체의 위치를 Awake 함수에서 zero로 둔 후에 샘플 프로젝트를 실행해 보았다.
조그마한 네모 오브젝트를 놓은 곳이 zero 위치다.
게임을 개발할 때에는 이 위치를 보이지 않는 곳에 지정해 놓으면 문제는 해결될 것이다.
물론 Update 함수가 보간처리 부분을 부르지 않도록 객체의 현재 (네트워크상)위치를 수신한 후에 보간처리 하도록 bool 변수를 두어 막아 주어야만 한다!
void Update()
{
if (isFirstReceived)
{
//로컬이 아닌 네트워크 객체들 위치, 회전 보간처리 후 수신
InterpolateNetworkTransform();
}
}
public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
{
//자신이 로컬 캐릭터인 경우 자신의 데이터를 다른 네트워크 유저에게 송신
if(stream.IsWriting)
{
//We own this player: send the others our data
stream.SendNext(transform.position);
stream.SendNext(transform.rotation);
}
else
{
// Network player, receive data
receivePos = (Vector3)stream.ReceiveNext();
receiveRot = (Quaternion)stream.ReceiveNext();
if (false == isFirstReceived)
{
isFirstReceived = true;
this.gameObject.transform.position = receivePos;
}
}
}
2. 커스텀 함수 이용하기
Awake 함수에서 위치를 지정해 주는 것이 아니라, 메쉬 렌더러를 꺼주는 커스텀 함수를 만들고, 이후 OnJoinedRoom에서 메쉬 렌더러를 다시 켜주는 커스텀 함수를 호출하게 되면 문제가 해결된다.
객체의 하위 child 렌더러들을 모두 검색해서 off 시켜주고, MeshRendere를 꺼 주자.
private MeshRenderer meshRenderer;
void Awake()
{
Debug.Log($"in Awake(), {this.name}, position: {this.transform.position}");
if (!PhotonNetwork.IsMasterClient)
{
for (int i = 0; i < this.transform.childCount; i++)
{
Transform tfChild = this.transform.GetChild(i);
tfChild.gameObject.SetActive(false);
}
meshRenderer = this.GetComponent<MeshRenderer>();
meshRenderer.enabled = false;
//this.transform.position = Vector3.zero;
}
}
public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
{
// stream - 데이터를 주고 받는 통로
// 내가 데이터를 보내는 중이라면
if (stream.IsWriting)
{
// 이 방안에 있는 모든 사용자에게 브로드캐스트
// - 내 포지션 값을 보내보자
stream.SendNext(transform.position);
stream.SendNext(transform.rotation);
}
// 내가 데이터를 받는 중이라면
else
{
// 순서대로 보내면 순서대로 들어옴. 근데 타입캐스팅 해주어야 함
transform.position = (Vector3)stream.ReceiveNext();
transform.rotation = (Quaternion)stream.ReceiveNext();
Debug.Log($"{transform.name}, transform.position: {transform.position.ToString()}");
//this.gameObject.SetActive(true);
for (int i = 0; i < this.transform.childCount; i++)
{
Transform tfChild = this.transform.GetChild(i);
tfChild.gameObject.SetActive(true);
}
meshRenderer = this.GetComponent<MeshRenderer>();
meshRenderer.enabled = true;
}
}
MasterClient는 제외 처리 해주고,
객체의 렌더러와 자식 오브젝트를 비활성화 시켜준 후 네트워크 정보를 받아올 때(동기화 될 때) 활성화 시켜준다.
더 이상 유니티 디폴트 위치에 객체가 생성된 것이 보이지 않는다.
즉 렌더러를 끄고 하위의 오브젝트도 꺼 주어 시작시 생성된 객체가 화면에 표시하지 않게 만들어 준 것이다. (숨겨버린 셈)
문제 해결!
3. 카메라 구도나 로딩 화면 등 연출로 가리기 -> Not recommended
이런 문제가 생기지 않도록 카메라 구도나 로딩 화면을 활용해 처음 생성 위치를 보이지 않게 만들어 주는 방법도 있다.
하지만 이런 경우 예상치 못한 버그가 추가로 발생할 수 있으므로 좋은 방법은 아니라고 판단된다.
4. 게임 방식을 동시 접속 방식으로 만들기 (난입 없는 구조)
이 이슈는 미리 생성되어 게임이 진행되고 있는 룸에 (이미 다른 네트워크 PC들의 활동이 버퍼로 쌓여 있는) 새로운 플레이어가 접속하는 네트워크 게임의 구조이기 때문에 생기는 문제이다. 따라서 게임 첫 시작 시에 모든 플레이어가 동시 접속하도록 만들면 문제는 생기지 않는다. (대부분의 멀티 플레이 게임이 채택하고 있는 방식)