12/31 TIL Netcode / NetworkBattle Game (2024년의 마지막)

Cheorii·2024년 12월 31일
post-thumbnail

오늘 했던 내용을 복기 하는 느낌으로 작성

RPC(원격 프로시저 호출)
LayerMask


Unity 백엔드 템플릿
https://github.com/Unity-Technologies/com.unity.services.samples.use-cases?tab=readme-ov-file


RPC(원격프로시저호출)

모든 일이 동시에 생겼을때, 각각의 객체에 생긴 일을 서버(HOST)로 모두 보내고,
클라이언트는 받아들이기만 해라 모든 일은 서버가 한다.

[ServerRpc]를 붙이면 서버에서 실행 된다.
함수 이름을 검사해서 함수 명이 ServerRpc로 끝나야 한다.

[ClientRpc]도 Server와 같은 형식을 가진다.

//PlayerNetwork.cs

using UnityEngine;
using System.Collections.Generic;
using System.Collections;
using Unity.Netcode;
using Unity.VisualScripting;
using Unity.Collections;
using System.Security.Cryptography;

public class PlayerNetwork : NetworkBehaviour
{
    //// 모든 대상자가 읽을 수 있는 권한, 오너만 작성할 수 있는 권한
    //NetworkVariable<int> randomNumber = new NetworkVariable<int>(1, NetworkVariableReadPermission.Everyone,
    //    NetworkVariableWritePermission.Owner);

    struct SomeData : INetworkSerializable
    {
        public bool _bool;
        public int _int;
        public FixedString64Bytes _string;

        public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
        {
            serializer.SerializeValue(ref _bool);
            serializer.SerializeValue(ref _int);
            serializer.SerializeValue(ref _string);
        }
    }

    public override void OnNetworkSpawn()
    {
        randomNumber.OnValueChanged += HandleRandomNumber;
    }

    public override void OnNetworkDespawn()
    {
        randomNumber.OnValueChanged -= HandleRandomNumber;
    }

    void HandleRandomNumber(SomeData oldValue, SomeData newValue)
    {
        Debug.Log("Owner id : " + OwnerClientId + ", random number : " + randomNumber.Value._int + ", " + randomNumber.Value._string);
    }


    NetworkVariable<SomeData> randomNumber = new NetworkVariable<SomeData>(new SomeData
    {
        _bool = true,
        _int = 0
    },
        NetworkVariableReadPermission.Everyone,
        NetworkVariableWritePermission.Owner);

    // Update is called once per frame
    void Update()
    {
        Debug.Log("owner Id : " + OwnerClientId + "random number : " + randomNumber.Value._int);

        // // 해당 오브젝트의 Owner가 아니면 작동하지 않는다.
        if (!IsOwner)
        {
            return;
        }

        if(Input.GetKeyDown(KeyCode.Z))
        {
            TestServerRpc();
        }

        if(Input.GetKeyDown(KeyCode.T))
        {
            randomNumber.Value = new SomeData { _int = Random.Range(0, 100), _string = "32바이트 안넘게" };
            //randomNumber.Value = Random.Range(0, 100);
            //randomNumber.Value = new SomeData;
        }       

        Vector3 moveDir = Vector3.zero;

        moveDir.x = Input.GetAxis("Horizontal");
        moveDir.z = Input.GetAxis("Vertical");

        float moveSpeed = 3;

        transform.Translate(moveDir * moveSpeed * Time.deltaTime);
    }

    [ServerRpc]
    void TestServerRpc()
    {
        Debug.Log("Server RPC called");
    }
}

[ServerRpc 작동 사진]


//PlayerNetwork.cs

using UnityEngine;
using System.Collections.Generic;
using System.Collections;
using Unity.Netcode;
using Unity.VisualScripting;
using Unity.Collections;
using System.Security.Cryptography;

public class PlayerNetwork : NetworkBehaviour
{
    //// 모든 대상자가 읽을 수 있는 권한, 오너만 작성할 수 있는 권한
    //NetworkVariable<int> randomNumber = new NetworkVariable<int>(1, NetworkVariableReadPermission.Everyone,
    //    NetworkVariableWritePermission.Owner);

    struct SomeData : INetworkSerializable
    {
        public bool _bool;
        public int _int;
        public FixedString64Bytes _string;

        public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
        {
            serializer.SerializeValue(ref _bool);
            serializer.SerializeValue(ref _int);
            serializer.SerializeValue(ref _string);
        }
    }

    public override void OnNetworkSpawn()
    {
        randomNumber.OnValueChanged += HandleRandomNumber;
    }

    public override void OnNetworkDespawn()
    {
        randomNumber.OnValueChanged -= HandleRandomNumber;
    }

    void HandleRandomNumber(SomeData oldValue, SomeData newValue)
    {
        Debug.Log("Owner id : " + OwnerClientId + ", random number : " + randomNumber.Value._int + ", " + randomNumber.Value._string);
    }


    NetworkVariable<SomeData> randomNumber = new NetworkVariable<SomeData>(new SomeData
    {
        _bool = true,
        _int = 0
    },
        NetworkVariableReadPermission.Everyone,
        NetworkVariableWritePermission.Owner);

    // Update is called once per frame
    void Update()
    {
        //Debug.Log("owner Id : " + OwnerClientId + "random number : " + randomNumber.Value._int);
        Debug.Log("is server " + IsServer + " is client " + IsClient);

        // // 해당 오브젝트의 Owner가 아니면 작동하지 않는다.
        if (!IsOwner)
        {
            return;
        }

        if(Input.GetKeyDown(KeyCode.Z))
        {
            //TestServerRpc();
            TestClientRpc();
        }

        if(Input.GetKeyDown(KeyCode.T))
        {
            randomNumber.Value = new SomeData { _int = Random.Range(0, 100), _string = "32바이트 안넘게" };
            //randomNumber.Value = Random.Range(0, 100);
            //randomNumber.Value = new SomeData;
        }       

        Vector3 moveDir = Vector3.zero;

        moveDir.x = Input.GetAxis("Horizontal");
        moveDir.z = Input.GetAxis("Vertical");

        float moveSpeed = 3;

        transform.Translate(moveDir * moveSpeed * Time.deltaTime);
    }
	// Server Rpc
    [ServerRpc]
    void TestServerRpc()
    {
        Debug.Log("Server RPC called");
    }
	//	클라이언트 Rpc
    [ClientRpc]
    void TestClientRpc()
    {
        Debug.Log("Client RPC called");
    }
}

[ClientRpc 작동 사진]

게임에서 Z 키를 누르면 HOST, Client에 둘다 Client RPC가 호출된다.


PlayerNetwork.cs 해석

  • 서버가 이 메서드를 호출하면, 네트워크 상의 모든 클라이언트에서 실행되도록 설계된 RPC 메서드입니다.

1. Host의 경우:

  • Host는 서버와 클라이언트 역할을 동시에 하기 때문에, Z 키를 눌렀을 때 TestClientRpc 메서드를 호출합니다.
  • 이 호출은 네트워크 상의 모든 클라이언트(즉, 자신 포함)에서 실행됩니다.
  • 따라서 Host에서 실행되면서 동시에 Host의 클라이언트 역할에서도 호출된 것처럼 보입니다.

2. 클라이언트의 경우:

  • 클라이언트가 Z 키를 눌러도 직접적으로 TestClientRpc를 호출할 수는 없습니다.
  • TestClientRpc는 서버에서 호출되어야 실행됩니다.
  • 즉, 클라이언트에서 Z 키를 눌렀을 때 TestClientRpc가 호출되는 일은 없습니다.

결론

  • Host가 Z 키를 누르면, TestClientRpc가 Host의 클라이언트 역할과 네트워크 상의 다른 모든 클라이언트에서 호출됩니다.
  • 일반 클라이언트가 Z 키를 눌렀을 때는 TestClientRpc가 호출되지 않습니다.
  • ClientRpc메서드는 서버에서만 호출될 수 있기 때문입니다.

만약 다수의 클라이언트가 동시에 무엇인가 실행한다고 하면?

  • ServerRpc를 사용해서 처리 한다.
    - string(문자열) 사용가능
if (Input.GetKeyDown(KeyCode.Z))
{
    TestServerRpc("string 가능");
    //TestClientRpc();
}
>[ServerRpc]
void TestServerRpc(string str)
{
    Debug.Log("Server RPC called" +str);
}
    
  • NetworkVariable를 사용해서 처리 하는 방법도 있다.
    - string 사용불가 (성능과 데이터 처리 관련해서 뭔가 복잡하다고 한다.)
    - FixedString을 사용해서 문자열 처리 가능하다.

NetworkVariable랑 RPC를 언제 구분하는가?

  • 지속적으로 변하는 상태를 동기화 -> NetworkVariable
    - 상태 데이터의 동기화 ( 캐릭터 HP와 같은 것 )
    - 자주 변경되는 값 ( 키입력이 계속 바뀌는 ex.캐릭터의 현재 위치(Vector3) )
    - 지속적 상태 공유 ( 아이템 획득 여부, 타이머 등 )

  • 특정 이벤트나 함수 호출 -> RPC
    - 이벤트 처리 (특정 스킬)
    - 클라이언트-서버 명령
    ( 클라이언트가 서버로 이동 명령을 보내거나, 서버가 클라이언트들에게 "게임 시작"을 알리는 경우.)
    - 비동기 작업 (즉각적인 동기화가 필요 없는 일회성 작업.)


if (Input.GetKeyDown(KeyCode.Z))
{
    TestServerRpc(new ServerRpcParams());
    //TestClientRpc();
}
  
[ServerRpc]
void TestServerRpc(ServerRpcParams rpcParams)
{
    Debug.Log($"Server RPC called {rpcParams.Receive.SenderClientId}");
}

클라이언트 관점의 파라미터

if (Input.GetKeyDown(KeyCode.Z))
{
    //TestServerRpc(new ServerRpcParams());
    TestClientRpc(new ClientRpcParams
    {
        Send = new ClientRpcSendParams
        {
            TargetClientIds = new List<ulong> { 1 } // 1번 클라이언트만 받거라.
        }
    });
}

[ClientRpc]
void TestClientRpc(ClientRpcParams rpcParams)
{
    Debug.Log($"Client RPC called");
}

서버 관점의 파라미터

[SerializeField] GameObject spawnedObjectPrefab;
...
 if (Input.GetKeyDown(KeyCode.X))
 {
     GameObject go = Instantiate(spawnedObjectPrefab);
     go.GetComponent<NetworkObject>().Spawn(true); // Spawn의 권한은 서버에 있다.
 }
 ...

Client에서 실행시 오류가 생성됨.

GameObject spawnedObject;

if (Input.GetKeyDown(KeyCode.X))
{
    spawnedObject = Instantiate(spawnedObjectPrefab);
    spawnedObject.GetComponent<NetworkObject>().Spawn(true);
}

if(Input.GetKeyDown(KeyCode.C))
{
    Destroy(spawnedObject);
}
  • 네트워크에선 Destory 실행되지만, 오브젝트에선 안된다.
  • 같은 것 같지만 -> 네트워크에서 삭제하려면 아래 코드가 효율적이다.
if(Input.GetKeyDown(KeyCode.C))
{
    spawnedObject.GetComponent<NetworkObject>().Despawn();
    //Destroy(spawnedObject);
}

캡슐대신 ThirdPerson Assets을 사용해서 캐릭터를 변경

namespace StarterAssets
{
    ...
    public class ThirdPersonController : NetworkBehaviour
    {
        ...

        public override void OnNetworkSpawn()
        {
            if (!IsOwner)
            {
                GetComponent<PlayerInput>().enabled = false;
            }
        }

        ...

        private void Update()
        {
            //if (!IsOwner) { return; }
            _hasAnimator = TryGetComponent(out _animator);

            JumpAndGravity();
            GroundedCheck();
            Move();
        }
    ...
    }
}

LayerMask 레이어 기반 작업을 단순화하는 기능입니다.

https://mentum.tistory.com/724
네이버 블로그를 보면서 이해를 했다. 뭔가 적고 하기에는 좀 애매해서 링크로 첨부한다.

오늘의 결론

네트워크 오브젝트의 소유권 확인이 중요하다.

profile
코딩테스트는 진짜 못하겠어요 ㅠㅠ

0개의 댓글