메시지
누구나 보낼 수 있는 형식 있는 이벤트, 누가 처리하는지, 오브젝트 채널, 그리고 실제 통신량.
메시지 선언하기
메시지는 필드와, 그것들이 어떻게 실릴지 정하는 두 메서드를 가진 클래스입니다.
public class OpenDoor : NetworkEvent
{
public int doorId;
public override void Serialize(ByteWriter writer)
{
writer.WriteInt32(doorId);
}
public override void Deserialize(ByteReader reader)
{
doorId = reader.ReadInt32();
}
}
담는 방식을 직접 쓰는 이유는, 알아서 추측하게 하는 편이 더 비싸기 때문입니다. Udon 에는 리플렉션이 없어서, 대신 직렬화해 주는 장치에도 결국 모든 필드 설명을 넘겨야 합니다. 필드 없는 신호라면 두 메서드 모두 재정의하지 않습니다.
바람과 사실을 나누세요. OpenDoorRequest 와 DoorOpened 라는 두 형식은 한 형식을 두 번 보내는 것보다 거의 언제나 낫습니다. 핸들러가 형식만 보고 어느 쪽인지 알 수 있고, 누군가의 요청을 이미 정해진 일처럼 처리할 수 없습니다.
메시지가 갈 수 있는 두 곳
메시지는 룸 전체로 가거나, 어떤 오브젝트 자신의 채널로 갑니다. 선택지는 이것뿐이고, 어떻게 쓰느냐가 어느 쪽인지 말해 줍니다.
Room.Send(new OpenDoor { doorId = 3 }); // 룸. 모든 클라이언트로
this.Send(new Touched()); // 이 오브젝트 자신의 채널
channel.Send(new Touched()); // 참조한 다른 오브젝트의 채널
this.Send 는 Room.Send 가 아닙니다. 비헤이비어가 올라가 있는 오브젝트 자신의 채널로 보내므로, 어느 오브젝트 이야기인지 알리는 id 를 실을 필요가 없습니다. Room.Send 가 룸 전체로 가는 쪽이고, static 이라 this 가 없는 평범한 클래스나 추상 클래스에서도 부를 수 있습니다.
구독도 그대로 대응합니다.
Room.Subscribe<OpenDoor>(OnOpenDoor); // 룸에서 그 형식을 받기
this.Subscribe<Touched>(OnTouched); // 이 오브젝트의 채널에서 받기
channel.Subscribe<Touched>(OnTouched); // 다른 오브젝트의 채널에서 받기
| 이렇게 쓰면 | 어디로 가고 | 누가 받는가 |
|---|---|---|
Room.Send(evt) |
룸 | 모든 클라이언트에서 그 형식을 구독한 모든 것 |
this.Send(evt) |
이 오브젝트 | 이 오브젝트에서 그 형식을 구독한 모든 것 |
channel.Send(evt) |
그 오브젝트 | 그 채널에서 그 형식을 구독한 모든 것 |
누구나 보낼 수 있게 하려면 룸입니다. 오브젝트 자신의 채널은 소유자가 하나라, this.Send 에는 그 오브젝트의 소유가 필요합니다.
누가 처리하는지
주소가 정하는 것은 누가 처리하는지이지, 누가 보내도 되는지가 아닙니다.
Room.Send(message, NetworkTarget.All); // 모두. 기본값
Room.Send(message, NetworkTarget.Master); // 조율하는 클라이언트만
Room.Send(message, NetworkTarget.Others); // 보낸 쪽 빼고 모두
Room.Send(message, player); // 한 사람에게
어느 클라이언트든 어떤 주소를 붙여서든 보낼 수 있고, 바이트는 어차피 모두에게 갑니다. NetworkTarget.Master 는 거르개이지 허가가 아니므로, 진짜 권한이 필요한 월드는 보낸 사람을 확인합니다.
private void OnOpenDoorRequest(VRCPlayerApi sender, OpenDoorRequest message)
{
if (!Room.IsMaster || !Allowed(from))
return;
Room.Send(new DoorOpened { doorId = message.doorId });
}
이것이 마스터를 서버처럼 두는 모양입니다. 클라이언트가 묻고, 조율하는 쪽이 정하고, 그쪽에서 모두에게 보낸 메시지만이 실제로 무언가를 바꿉니다.
보장되는 것
순서대로, 한 번씩, 보낸 쪽까지. 보낸 클라이언트는 곧바로 자기 것을 처리합니다. 자기가 쓴 것은 자기에게 돌아오지 않으므로, 그러지 않으면 무슨 일이 있었는지 아는 유일한 클라이언트만 듣지 못하게 됩니다.
같은 묶음이 두 번 와도 한 번만 처리합니다. 메시지마다 일련번호가 있고 읽는 쪽은 어디까지 처리했는지 기억합니다. VRChat 이 한 번 거절해서 다시 보낸 것도 아무것도 바꾸지 않습니다.
늦게 들어온 사람은 놓친 것을 받지 않습니다. 각 묶음은 언제 만들어졌는지를 지니고 있고, 나중에 들어온 클라이언트는 자기가 오기 전 것을 놓친 싸움의 재생이 아니라 버릴 것으로 다룹니다. 동기화 값에 바라는 것과는 정반대이고, 이벤트에 바라는 것 그대로입니다. 늦게 온 사람이 현재 상태를 알아야 한다면 동기화 값을 쓰세요.
거절된 전송에도 잃지 않습니다. VRChat 은 한 비헤이비어가 한 번에 보낼 양을 제한하고, 넘는 묶음은 잘리는 대신 거절됩니다. 거절된 묶음은 그대로 두었다가 다시 보냅니다. Room.Bus.Pending 이 계속 0 보다 크다면 회선이 받아주는 것보다 빠르게 보내고 있는 것입니다.
오브젝트 자신의 채널
특정 오브젝트에 관한 메시지는 룸 대신 그 오브젝트로 보낼 수 있고, 그러면 어느 오브젝트인지 알리는 id 를 실을 필요가 없습니다. 넣을 컴포넌트도 참조도 없이, 같은 두 동사를 비헤이비어 자신 위에 씁니다.
public override void Start()
{
this.Subscribe<Touched>(OnTouched);
}
public override void Interact()
{
this.Send(new Touched());
}
핸들러가 플레이어까지 받으면 누가 보냈는지도 알려줍니다.
this.Subscribe<Touched>((sender, message) => Log.Info(sender.displayName + " 가 만졌다"));
보내려면 그 오브젝트를 소유해야 합니다. 복제되는 것은 소유자의 쓰기뿐이라, 소유자가 아닌 쪽은 메시지가 나가지 않았다는 것을 나중에 아는 대신 지금 듣습니다. 아무 클라이언트나 보낼 수 있게 하려면 먼저 Ownership.Claim(gameObject) 으로 소유를 가져오세요. 픽업이나 interact 라면 대개 이미 가져와 있습니다.
다른 오브젝트의 채널을 밖에서 읽으려면 그 오브젝트에 NetworkBus 가 필요하고, 참조에 대해 같은 동사가 그대로 동작합니다.
public NetworkBus channel;
channel.Subscribe<Touched>(OnTouched);
channel.Send(new Touched());
어느 쪽이든 오브젝트 채널은 소유자가 하나라, 두 사람이 같은 채널로 보내면 서로 다툽니다. 오브젝트의 소유자가 보낼 때는 오브젝트 채널을, 누구나 보낼 때는 룸을 쓰세요.
통신량
메시지 하나에 붙는 것은 3 바이트입니다. 누가 처리하는지 1, 길이 1, 어떤 형식인지 1. 특정 플레이어에게 보내면 그 플레이어의 id 도 함께 실립니다.
묶음에는 언제 만들어졌는지를 나타내는 4 바이트가 한 번만 붙습니다. 메시지마다가 아니라 묶음마다이므로, 여러 개를 함께 보낼수록 옅어집니다.
실제로 걸리는 한도는 한 비헤이비어의 동기화 상태에 대한 VRChat 의 제한이고, 이것은 메시지 하나가 아니라 묶음 전체에 걸립니다. this.Pending() 이 그것이 다가오는 것을 보는 방법입니다. 거의 언제나 0 이고, 0 보다 큰 상태로 머문다면 한 번의 동기화가 실어갈 양보다 묶음이 크다는 뜻입니다.
이 패키지 없이 쓰는 커스텀 이벤트
VRChat 자신의 이벤트 호출은 위의 것을 쓰든 말든 그대로 있습니다. 컴파일러가 그대로 넘겨주기 때문입니다.
this.SendCustomNetworkEvent(NetworkEventTarget.All, nameof(Ring));
public void Ring()
{
bell.Play();
}
public 메서드를 이름으로 모든 클라이언트에서 부르고, NetworkEventTarget.Owner 면 소유자에게만 부릅니다. 데이터는 싣지 못하며, 형식 있는 메시지가 있는 이유가 그것입니다. SendCustomEventDelayedSeconds 와 SendCustomEventDelayedFrames 는 로컬 호출을 미룰 뿐 아무것도 보내지 않습니다.
OnOwnershipRequest 를 재정의해 false 를 돌려주면 이전을 거부할 수 있고, OnOwnershipTransferred 는 소유자가 바뀔 때 모든 클라이언트에서 돕니다. ClientSim 은 소유권 요청을 일으키지 않으니 이 둘은 업로드한 월드에서 확인해야 합니다.
아무것도 오지 않을 때
짐작하게 두지 않고, 비헤이비어마다 한 번씩 알려줍니다.
- 씬에 버스가 없는데 보내서 메시지가 아무 데도 가지 않았을 때.
- 구독했는데 10 초가 지나도 읽을 버스가 어디에도 없을 때.
둘 다 실제로는 같은 뜻입니다. 룸 메시지를 싣는 오브젝트가 없거나 꺼져 있습니다. 처음 보낼 때 자동으로 추가되므로, 대개는 다른 것과 함께 꺼진 것이 원인입니다.