1. 플레이어 간에 게임 플레이 상호작용이 아닌 바로 없어져 버리는 것들의 전송 - RPC 사용O
2. 시기가 중요하지 않은 상점 내 구매(호출한 플레이어에게만 중요한 구매 - 자금감소, 인벤토리 추가) - RPC 사용 X
3. 이름, 색상, 스킨과 같은 초기 설정 - RPC 사용O
4. 게임 모드, 준비상태 - RPC 사용O
RPC 호출해보기
RPC는 틱 정렬이 되지 않으므로 FixedUpdateNetwork() 등 Fusion 입력 처리를 사용할 필요가 없다.
void Update()
{
// Object.HasInputAuthority는 NetworkObject.Spawn()에서 생성할 때 파라미터로 Runner를 정해주는데 결국 생성이 되었냐에 대한 것이다.
if (Object.HasInputAuthority && Input.GetKeyDown(KeyCode.R))
{
RPC_SendMessage("Hey Mate!");
}
}
RPC 속성 사용
- string을 받아와서 rpc의 info가 로컬이면 You said~ 아니면 Some other said ~ 로 텍스트를 출력한다.(누가 호출했는지)
// RpcSources : 전송할 수 있는 피어
// RpcTargets : 피어가 실행되는 피어
// 1. All : 모두에게 전송 / 세션 내의 모든 피어에 의해서 실행됨(서버 포함)
// 2. Proxies : 나 말고 전송 / 객체에 대하여 입력 권한 또는 상태 권한을 갖고 있지 않는 피어에 의해 실행됨
// 3. InputAuthority : 입력 권한 있는 피어만 전송 / 객체에 대한 입력 권한이 있는 피어에 의해 실행됨
// 4. StateAuthority : 상태 권한 있는 피어만 전송 / 객체에 대한 상태 권한이 있는 피어에 의해 실행됨
// RpcInfo
// - Tick : 어떤 곳에서 틱이 전송되었는지
// - Source : 어떤 플레이어(PlayerRef)가 보냈는지
// - Channel : Unrealiable 또는 Reliable RPC로 보냈는지 여부
// - IsInvokeLocal : 이 RPC를 원래 호출한 로컬 플레이어인지의 여부
// * 공식 문서엔 HostMode를 설정하지 않았지만 이걸 쓰지 않으면 계속 원격 플레이어가 된다. (기본이 서버 모드여서 그런 듯)
[Rpc(RpcSources.All, RpcTargets.InputAuthority, HostMode = RpcHostMode.SourceIsHostPlayer)]
public void RPC_SendMessage(string message, RpcInfo info = default)
{
if (!_messages)
{
_messages = FindObjectOfType<Text>();
}
Debug.Log(info.Channel);
Debug.Log(info.Source.RawEncoded);
Debug.Log(info.Source.PlayerId);
Debug.Log(Runner.Simulation.LocalPlayer.PlayerId);
if (info.Source == Runner.Simulation.LocalPlayer)
{
message = $"You said: {message}\n";
}
else
{
message = $"Some other player said: {message}\n";
}
_messages.text += message;
}
public class Player : NetworkBehaviour
{
// Networked 속성의 파라미터로 OnChanged라는 string 프로퍼티가 있는데
// OnChanged는 'static void OnChanged(Changed<MyClass> changed){}' 형식의 콜백이다.
[Networked(OnChanged = nameof(OnBallSpawned))]
// NetworkBool처럼 true false 값이 두 가지만 있는 경우 변경사항이 감지되지 않을 수가 있으므로
// byte/int 로 대체하고 호출할 때마다 값을 변경하는 방식으로 할수도 있다. (시각적 효과와 대역폭의 소모 중요도 차이)
public NetworkBool spawned { get; set; }
여기선 자식의 Meshrenderer에서 Material을 찾아 따로 Material프로퍼티를 만들어주는데 이 프로젝트의 경우 Player의 Cube 메시를 Player스크립트가 있는 부모와 구분해 자식으로 가게 했으므로 자식에서 찾는 GetComponentInChildren으로 찾아오는 것 같다. -> 매번 가져오지 않고 한 번만 세팅하기 위함
Material _material;
Material material
{
get
{
if (!_material)
{
_material = GetComponentInChildren<MeshRenderer>().material;
}
return _material;
}
}
위의 Networked속성의 파라미터 형식대로 함수를 만들었다.
여기서 spawned 값이 바뀐 Player의 material의 color를 흰색으로 바꿔준다.
이전에 가져온 material의 Color를 Color.Lerp를 통해 현재 컬러에서 목표 컬러까지 Time.deltaTime 비율만큼 부드럽게 변하게 만들었다.
// NetworkBehaviour를 상속하는 SimulationBehaviour에 있는 메서드이다.
// 색 변화 등을 Update가 아니라 Render에서 수행하는 이유는
// Render()가 퓨전의 FixedUpdateNetwork 이후에 호출되는 것이 보장되기 때문이다.
// 또한 Runner.DeltaTime이 아니라 Time.deltaTime을 사용하는 이유는
// 렌더링 자체는 Fusion 시뮬레이션 단위인 Runner.DeltaTime이 아니고
// 유니티의 render loop에서 실행되어서 그렇다.
public override void Render()
{
material.color = Color.Lerp(material.color, Color.blue, Time.deltaTime);
}
이후에 FixedUpdateNetwork의 마지막 부분에 spawned = !spawned를 추가하여 토글로 공을 생성할 때마다 값이 바뀌게 했다.
Fusion - Network Project Config - Server Physics Mode를 Client Prediction으로 하면 더 많이 연산이 들지만 더 정확하게 물리를 계산한다.
물리 객체는 NetworkRigidbody 컴포넌트로 동기화한다.
이전에 transform 이동과 비슷하게 구현했는데 여기선 Rigidbody의 Velocity를 forward로 하여 초기 속도 값을 정해줬다.
public class PhysxBall : NetworkBehaviour
{
[Networked] TickTimer life { get; set; }
public void Init(Vector3 forward)
{
life = TickTimer.CreateFromSeconds(Runner, 5.0f);
GetComponent<Rigidbody>().velocity = forward;
}
public override void FixedUpdateNetwork()
{
if (life.Expired(Runner))
{
Runner.Despawn(Object);
}
}
}
마우스 버튼을 하나 더 매핑해주고
public struct NetworkInputData : INetworkInput
{
// const로 선언한 상수는 내부에서 자동으로 static 가 된다.
public const byte MOUSEBUTTON1 = 0x01;
public const byte MOUSEBUTTON2 = 0x02;
public byte buttons;
public Vector3 direction;
}
조건 또한 비슷하게 만들어주었다.
public class PhotonInstantiate : MonoBehaviour, INetworkRunnerCallbacks
{
// NetworkObject의 요소로 GUID(고유ID)를 가지고 있다.
[SerializeField] NetworkPrefabRef _playerPrefab;
// PlayerRef default 0 : None, 1 : index 0, 2 : index 1
Dictionary<PlayerRef, NetworkObject> _spawnedCharacters = new Dictionary<PlayerRef, NetworkObject>();
NetworkRunner _runner;
bool _mouseButton0;
bool _mouseButton1;
void Update()
{
// 입력은 FixedUpdateNetwork에서 틱 단위로 호출되고
// bool 체크는 Update에서 하니까 frame마다 호출되어 단위가 달라
// OnInput에서 실행하고 false로 만들어주는 식이다.
_mouseButton0 |= Input.GetMouseButton(0);
_mouseButton1 |= Input.GetMouseButton(1);
}
async void StartGame(GameMode mode)
{
// GameMode
// 1. 싱글 2. 쉐어드 - 포톤 클라우드 이용(사용자는 클라이언트) 3. 서버(게임 서버를 직접 지원하고 원격 플레이어만 허용)
// 4. 호스트(로컬 플레이어를 허용하는 게임서버) 5. 클라이언트(호스트나 게임 모드에 클라이언트로 시작)
// 6. 자동(첫 번째 접속시 호스트 모드, 나중 접속시 클라이언트 모드)
// NetworkRunner : 플레이어와 관련된 변수들을 가지고있는 클래스
_runner = gameObject.AddComponent<NetworkRunner>();
// NetworkRunner가 클라이언트로부터 Input을 받을지 여부
_runner.ProvideInput = true;
// NetworkRunner.StartGame(StartGameArgs args) : 매치메이킹 세팅을 해주는 메서드
// 비동기로 NetworkRunner.StartGame(StartGameArgs args) 실행
await _runner.StartGame(new StartGameArgs
{
GameMode = mode,
SessionName = "TestRoom", // 세션 이름(클라이언트와 서버 세션 이름)
Scene = SceneManager.GetActiveScene().buildIndex, // Scene은 구조체인 SceneRef?타입
// NetworkSceneManagerDefault : 비동기 씬 관련 메서드가 포함된 클래스
SceneManager = gameObject.AddComponent<NetworkSceneManagerDefault>(), // SceneManager INetworkSceneManager 타입이다.
});
}
void OnGUI()
{
if (!_runner)
{
if (GUI.Button(new Rect(0, 0, 400, 100), "Host"))
{
StartGame(GameMode.Host);
}
if (GUI.Button(new Rect(0, 150, 400 , 100), "Join"))
{
StartGame(GameMode.Client);
}
}
}
public void OnPlayerJoined(NetworkRunner runner, PlayerRef player)
{
// 연결중이면
if (runner.IsServer)
{
// 특정한 위치 저장
Vector3 spawnPosition =
new Vector3((player.RawEncoded % runner.Config.Simulation.DefaultPlayers) * 3, 1, 0);
// NetworkRunner.Spawn이 네트워크 Instantitate와 비슷한 역할인데 추가로 PlayerRef를 받는다.
NetworkObject networkPlayerObject = runner.Spawn(_playerPrefab, spawnPosition, Quaternion.identity, player);
// 딕셔너리에 추가
_spawnedCharacters.Add(player, networkPlayerObject);
}
}
public void OnPlayerLeft(NetworkRunner runner, PlayerRef player)
{
if (_spawnedCharacters.TryGetValue(player, out NetworkObject networkObject))
{
// 파괴 후 반환
runner.Despawn(networkObject);
_spawnedCharacters.Remove(player);
}
}
public void OnInput(NetworkRunner runner, NetworkInput input)
{
var data = new NetworkInputData();
if (Input.GetKey(KeyCode.W))
data.direction += Vector3.forward;
if (Input.GetKey(KeyCode.S))
data.direction += Vector3.back;
if (Input.GetKey(KeyCode.A))
data.direction += Vector3.left;
if (Input.GetKey(KeyCode.D))
data.direction += Vector3.right;
if (_mouseButton0)
{
data.buttons |= NetworkInputData.MOUSEBUTTON1;
}
if (_mouseButton1)
{
data.buttons |= NetworkInputData.MOUSEBUTTON2;
}
_mouseButton0 = false;
_mouseButton1 = false;
input.Set(data);
}
}
발사하는 부분도 프리펩과 Init()의 인자만 다를 뿐 다 같게 설정해주었다.
public class Player : NetworkBehaviour
{
[Networked] TickTimer delay { get; set; }
[SerializeField] Ball _prefabBall;
[SerializeField] PhysxBall _prefabPhysxBall;
NetworkCharacterControllerPrototype _cc;
Vector3 _forward;
void Awake()
{
_cc = GetComponent<NetworkCharacterControllerPrototype>();
_forward = transform.forward;
}
public override void FixedUpdateNetwork()
{
if (GetInput(out NetworkInputData data))
{
data.direction.Normalize();
_cc.Move(5 * data.direction * Runner.DeltaTime);
// OnInput에서 Set된 NetworkInputData.direction 값이 0보다 클 때
if (data.direction.sqrMagnitude > 0)
{
_forward = data.direction;
}
// NetworkRunner의 TickTimer가 도달했거나 세팅되지 않았을 때 -> 생성 빈도의 제한을 거는 것
if (delay.ExpiredOrNotRunning(Runner))
{
// NetworkInputData.MOUSEBUTTON1이 0x01이니 byte인 data.button이 1이고 &연산(둘 다 1일 때)으로 1이 될 때
if ((data.buttons & NetworkInputData.MOUSEBUTTON1) != 0)
{
delay = TickTimer.CreateFromSeconds(Runner, 0.5f);
// NetworkRunner의 바라보는 forward만큼 앞으로, 회전은 입력의 방향으로(data.direction의 방향)
// Object.InputAuthority는 누구의 입력에서 나왔는지에 대한 PlayerRef이다(PlayerID를 포함한 정보).
// Spawn하기 전에 파괴될 틱을 설정해주어야 하기 때문에(Life.CreateFromSeconds(NetworkRunner, int delayInSeconds))
// 마지막의 람다식 delegate void OnBeforeSpawned(NetworkRunner runner, NetworkObject obj)에서 Ball의 Init을 호출한다.
Runner.Spawn(_prefabBall, transform.position + _forward, Quaternion.LookRotation(_forward),
Object.InputAuthority,
(runner, o) =>
{
o.GetComponent<Ball>().Init();
});
}
else if ((data.buttons & NetworkInputData.MOUSEBUTTON2) != 0)
{
delay = TickTimer.CreateFromSeconds(Runner, 0.5f);
// NetworkRunner의 바라보는 forward만큼 앞으로, 회전은 입력의 방향으로(data.direction의 방향)
// Object.InputAuthority는 누구의 입력에서 나왔는지에 대한 PlayerRef이다(PlayerID를 포함한 정보).
// Spawn하기 전에 파괴될 틱을 설정해주어야 하기 때문에(Life.CreateFromSeconds(NetworkRunner, int delayInSeconds))
// 마지막의 람다식 delegate void OnBeforeSpawned(NetworkRunner runner, NetworkObject obj)에서 Ball의 Init을 호출한다.
Runner.Spawn(_prefabPhysxBall, transform.position + _forward, Quaternion.LookRotation(_forward),
Object.InputAuthority,
(runner, o) =>
{
o.GetComponent<PhysxBall>().Init(10 * _forward);
});
}
}
}
}
}
public void OnInput(NetworkRunner runner, NetworkInput input)
{
var data = new NetworkInputData();
if (Input.GetKey(KeyCode.W))
data.direction += Vector3.forward;
if (Input.GetKey(KeyCode.S))
data.direction += Vector3.back;
if (Input.GetKey(KeyCode.A))
data.direction += Vector3.left;
if (Input.GetKey(KeyCode.D))
data.direction += Vector3.right;
if (_mouseButton0)
{
data.buttons |= NetworkInputData.MOUSEBUTTON1;
}
_mouseButton0 = false;
input.Set(data);
}
포톤 퓨전에는 Network Character Contoller Prototype처럼 이름 그대로 Prototype을 지원하는데 네트워크에서 Character Controller를 다루는 이동 관련된 부분을 미리 만들어 놓은 스크립트이다.
- NetworkTransform을 상속받는데 Transform의 위치를 동기화 시켜주는 메서드가 들어있다.
- InterPolation Target에 움직일 게임 오브젝트를 넣어주기만 하면 된다.
플레이어 세션 연결과 연결 해제시 콜백
- 생성과 파괴를 구현(딕셔너리에 저장하여 추적)
public void OnPlayerJoined(NetworkRunner runner, PlayerRef player)
{
// 연결중이면
if (runner.IsServer)
{
// 특정한 위치 저장
Vector3 spawnPosition =
new Vector3((player.RawEncoded % runner.Config.Simulation.DefaultPlayers) * 3, 1, 0);
// NetworkRunner.Spawn이 네트워크 Instantitate와 비슷한 역할인데 추가로 PlayerRef를 받는다.
NetworkObject networkPlayerObject = runner.Spawn(_playerPrefab, spawnPosition, Quaternion.identity, player);
// 딕셔너리에 추가
_spawnedCharacters.Add(player, networkPlayerObject);
}
}
public void OnPlayerLeft(NetworkRunner runner, PlayerRef player)
{
if (_spawnedCharacters.TryGetValue(player, out NetworkObject networkObject))
{
// 파괴 후 반환
runner.Despawn(networkObject);
_spawnedCharacters.Remove(player);
}
}
클라이언트가 직접 네트워크 객체의 상태를 바꾸는 것이 아닌 호스트가 클라이언트로부터 입력을 받아 다음 상태를 예측하여 갱신하는 식으로 작동한다.
유저의 입력을 받기위해 입력을 담을 공간을 만들어주어야 한다.
INetworkInput을 상속받는 NetworkInputData에 public 변수를 만들고 인스턴싱해 키 입력을 받아놓고(여기선 Vector3) 인자로 받은 NetworkInput에 Set해주는 방식으로 플레이어의 데이터 입력을 저장한다.
public struct NetworkInputData : INetworkInput
{
public Vector3 direction;
}
public void OnInput(NetworkRunner runner, NetworkInput input)
{
var data = new NetworkInputData();
if (Input.GetKey(KeyCode.W))
data.direction += Vector3.forward;
if (Input.GetKey(KeyCode.S))
data.direction += Vector3.back;
if (Input.GetKey(KeyCode.A))
data.direction += Vector3.left;
if (Input.GetKey(KeyCode.D))
data.direction += Vector3.right;
input.Set(data);
}
MonoBehaviour를 상속받는 퓨전의 컴포넌트로 입력을 틱 단위로 받기 위해 FixedUpdateNetwork에 GetInput<T>(out T input)으로 입력이 들어왔는지 확인한다.(입력이 들어오면 true를 안 들어오면 false를 반환)
* 이전에 입력을 담을 변수를 정의한 NetworkInputdata가 out으로 지역변수로 사용되어 data에 접근하여 방향을 Normailize하고 그 값을 NetworkCharacterControllerPrototype의 Move에 담아 이동시킨다.