RPC

RPC 주의점

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;
}

추가 RPC 속성

https://doc.photonengine.com/ko-KR/fusion/current/manual/rpc

 

Remote Procedure Calls | Photon Engine

 

doc.photonengine.com

 

 

'공부 모음 > 포톤 퓨전 시작해보기' 카테고리의 다른 글

포톤 퓨전 속성 변경  (0) 2022.12.09
포톤 퓨전 물리  (2) 2022.12.08
포톤 퓨전 Prediction  (0) 2022.12.08
포톤 퓨전 시작하기, 씬 설정하기  (0) 2022.12.07

속성 변경

* 퓨전 프로퍼티에 대한 테스트를 하던 도중 스크립트에 문제가 생겨 새로 만들었습니다.

Player 스크립트

Netwrked는 메서드의 오버로딩처럼 파라미터를 넣는 버전이 있는듯하다.

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를 흰색으로 바꿔준다.

// struct인 Changed<T>의 T는 NetworkBehaviour를 상속받는 컴포넌트이다.
public static void OnBallSpawned(Changed<Player> changed)
{
    // changed.Behaviour가 T이다. (여기선 Player)
    changed.Behaviour.material.color = Color.white;
}

이전에 가져온 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를 추가하여 토글로 공을 생성할 때마다 값이 바뀌게 했다.

public override void FixedUpdateNetwork()
{
    if(GetInput(out NetworkInputData data))
    {
        data.direction.Normalize();
        _cc.Move(5 * data.direction * Runner.DeltaTime);
        
        if (data.direction.sqrMagnitude > 0)
            _forward = data.direction;
        
        if (delay.ExpiredOrNotRunning(Runner))
        {
            if ((data.buttons & NetworkInputData.MOUSEBUTTON1) != 0)
            {
                delay = TickTimer.CreateFromSeconds(Runner, 0.5f);
                Runner.Spawn(_prefabBall,
                    transform.position+_forward, Quaternion.LookRotation(_forward),
                    Object.InputAuthority, (runner, o) =>
                    {
                        // Initialize the Ball before synchronizing it
                        o.GetComponent<Ball>().Init();
                    });
            }
            else if ((data.buttons & NetworkInputData.MOUSEBUTTON2) != 0)
            {
                delay = TickTimer.CreateFromSeconds(Runner, 0.5f);
                Runner.Spawn(_prefabPhysxBall,
                    transform.position+_forward,
                    Quaternion.LookRotation(_forward),
                    Object.InputAuthority,
                    (runner, o) =>
                    {
                        o.GetComponent<PhysxBall>().Init( 10*_forward );
                    });
            }
            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);
                        });
                }
            }
        }
    }
}

Prediction

Network Transform의 Interpolation Data Source를 Predicted로 하고 Interpolation Space를 World로 한다.

포톤 퓨전 Prediction 이해를 위한 영상

- Interpolation Target을 분리해 놓은 이유 : Render는 유니티의 Update에서 불리고 Interpolation으로 계속 그려줘야한다. 그러나 이동 입력은 FixedUpdateNetwork에서 불리기 때문에 둘의 단위가 달라서 구분해 준 것이다.

TickTimer를 설정하기 위해 NetworkedAttribute의 기본 생성자인 [Networked] 를 속성으로 부여했다.

FixedUpdateNetwork에서 TIckTimer.Expired(NetworkRunner Runner)가 다 되면 NetworkRunner.Despawn(Network Object)으로 NetworkObject를 파괴하고 안 되면 앞 쪽으로 이동하게 한다.

Init() 함수에서 TickTimer.CreateFromSeconds(NetworkRunner runner, float delayInSeconds)로 Senconds에 대해 타겟의 Tick을 생성하고 반환한다.

public class Ball : NetworkBehaviour
{
    [Networked] TickTimer Life { get; set; }

    public override void FixedUpdateNetwork()
    {
        if (Life.Expired(Runner))
        {
            Runner.Despawn(Object);
        }
        else
        {
            transform.position += 5 * transform.forward * Runner.DeltaTime;
        }
    }

    public void Init()
    {
        Life = TickTimer.CreateFromSeconds(Runner, 5.0f);
        print(Life.TargetTick);
    }
}

PhotonInstantiate 스크립트

NetworkInputData.MOUSEBUTTON1로부터 mouseButton0을 Set해준다.

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);
}

마우스 좌클릭으로 Ball을 생성하는데 시간차를 두기 위해 Networked속성을 적용한 TickTimer를 생성하여 적용하였다.

public class Player : NetworkBehaviour
{
    [SerializeField] Ball _prefabBall;
    [Networked] TickTimer delay { get; set; }
    
    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) =>
                        {
                            print("Lambda init");
                            o.GetComponent<Ball>().Init();
                        });
                }
            }
        }
    }
}

 

시작하기

SDK와 프로젝트 세팅을 한다.

https://doc.photonengine.com/ko-kr/fusion/current/fusion-100/fusion-101

 

Fusion 101 - 시작하기 | Photon Engine

 

doc.photonengine.com

 

씬 설정하기

https://doc.photonengine.com/ko-kr/fusion/current/fusion-100/fusion-102

 

Fusion 102 - 씬 설정하기 | Photon Engine

 

doc.photonengine.com

 

포톤 퓨전은 INetworkRunnerCallbacks를 사용하여 동기화 작업을 하는 것 같다.

프로젝트를 따라가다가 async라는 개념이 나오는데 코루틴과 비슷한 개념이라고 한다.

async, await, Task를 이용하여 어떤 작업을 순차적으로 실행할 때, 동시에 실행할 때 코루틴 보다 간단하게 만들 수 있다.

https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/

 

Asynchronous programming in C#

An overview of the C# language support for asynchronous programming using async, await, Task, and Task

learn.microsoft.com

async void StartGame(GameMode mode)
{
    // GameMode
    // 1. 싱글 2. 쉐어드 - 포톤 클라우드 이용(사용자는 클라이언트) 3. 서버(게임 서버를 직접 지원하고 원격 플레이어만 허용)
    // 4. 호스트(로컬 플레이어를 허용하는 게임서버) 5. 클라이언트(호스트나 게임 모드에 클라이언트로 시작)
    // 6. 자동(첫 번째 접속시 호스트 모드, 나중 접속시 클라이언트 모드)
    
    // NetworkRunner : 플레이어와 관련된 변수들을 가지고있는 클래스
    _runner = gameObject.AddComponent<NetworkRunner>();
    // PlayerRef와 INetworkInput을 받는 여부
    _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, 200, 40), "Host"))
        {
            StartGame(GameMode.Host);
        }

        if (GUI.Button(new Rect(0, 40, 20, 40), "Join"))
        {
            StartGame(GameMode.Client);
        }
    }
}

Network Object

포톤 펀의 PhotonView처럼 고유한 ID를 가지고 있고 객체의 생성과 파괴 등을 추적한다.

https://doc.photonengine.com/ko-kr/fusion/current/manual/network-object/network-object

 

네트워크 객체 | Photon Engine

 

doc.photonengine.com

포톤 퓨전에는 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에 담아 이동시킨다.

- Runner는 NetworkBehaviour을 상속하는 SimulationBehaviour의 변수이다. (NetworkRunner 클래스)

- Runner.DeltaTime은 FixedDeltaTime처럼 고정된 DeltaTime이다.

public class Player : NetworkBehaviour
{
    NetworkCharacterControllerPrototype _cc;

    void Awake()
    {
        _cc = GetComponent<NetworkCharacterControllerPrototype>();
    }

    public override void FixedUpdateNetwork()
    {
        if (GetInput(out NetworkInputData data))
        {
            data.direction.Normalize();
            _cc.Move(5 * data.direction * Runner.DeltaTime);
        }
        
    }
}

'공부 모음 > 포톤 퓨전 시작해보기' 카테고리의 다른 글

포톤 퓨전 원격 프로시져 호출(RPC)  (0) 2022.12.09
포톤 퓨전 속성 변경  (0) 2022.12.09
포톤 퓨전 물리  (2) 2022.12.08
포톤 퓨전 Prediction  (0) 2022.12.08

+ Recent posts