물리

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

+ Recent posts