ved_Rony
article thumbnail

틀린 데이터 동기화 메커니즘을 선택하면 버그가 발생할 수 있으며, 대역폭을 많이 사용하고 코드를 더 복잡하게 만들 수 있다. Netcode for GameObjects (Netcode)은 플레이어 간의 정보 동기화를 위한 두 가지 주요 방법을 제공한다. RPC (원격 프로시저 호출)와 복제 상태 (NetworkVariable) 두 가지 방법 모두 네트워크를 통해 메시지를 전송하는 데, 어떻게 메시지를 전송할지에 대한 논리와 설계는 둘 중 하나를 선택해야 한다.

 

NetworkVariables vs RPC 

  • RPC는 순간적인 이벤트, 수신될 때에만 유용한 정보에 사용
  • NetworkVariables은 영구적인 상태, 순간 이상 유지되는 정보에 사용
  • 사용할 방법을 빠르게 선택하는 방법은 자신에게 다음과 같다 : 중간 난입을 허용하는 가

 

중간에 난입한 유저에게 현재 상태를 동기화 시켜준다

 

RPC로 상태를 보내게 되는 순단 나중에 들어온 유저는 상태를 받지 못한다.

 

NetworkVariables는 최종적으로 일관성을 갖는다. 이는 RPC와는 다르게 모든 값 변경이 동기화되지 않는다는 것을 의미한다. RPC는 5회의 RPC 호출이 네트워크 상에서 5개의 RPC 전송을 생성하는 반면, NetworkVariables는 모든 값 변경이 네트워크에 동기화되지 않을 수 있음을 의미

 

각tick 마다 변경된 최신 값을 동기화 시켜줍니다.

.대역폭을 많이 아낄수 있지만, 값의 모든 변화가 필요하다면 RPC를 사용하는 게 낫다.

 

RPC와 NetworkVariables 적절히 사용하기

RPC가 networkvariables보다 더 단순하다.

 

예를 들어 폭발 같은 일시적인 이벤트의 경우, 이를 위해 복제된 상태를 사용할 필요가 없다. 이는 의미가 없다. 모든 새로운 플레이어가 연결될 때마다 "폭발되지 않은" 상태를 동기화해야 하는데, 이러한 이벤트를 상태로 나타내는 것은 설계적으로 바람직하지 않기 때문이다.

폭발은 이벤트에 대한 RPC를 사용할 수 있지만, 폭발의 효과는 NetworkVariables을 사용해야 한다 (예: 플레이어의 넉백 및 체력 감소). 새로 연결된 플레이어는 5초 전에 발생한 폭발에 영향이 없을 것이다. 그러나 그 폭발 주변의 플레이어의 현재 체력은 동기화를 받아야 한다.

 

보스 방에서의 액션은 이를 좋은 예시 다. 영역 효과 액션 (AoeAction)은 액션이 활성화될 때 RPC를 트리거 (영향을 받는 영역 주변에 VFX를 표시). 임프의 체력 (NetworkVariables)이 업데이트 된다. 새로운 플레이어가 연결하면 데미지를 입은 임프르 보게 될 것이다. 이 경우 일시적인 RPC와 잘 작동하는 영역 효과 능력의 VFX는 상관이 없다.

 

아래의 클래스는 첫 번째 단계이다. 초기 입력 비주얼 위치를 업데이트하고 사용자 입력을 추적하고 능력이 확인되면 마우스를 클릭하여 서버에 적절한 RPC를 보내어 AoE 서버 측 게임플레이 논리를 트리거하게 된다. 서버 측 게임플레이 액션은 그런 다음 클라이언트 측 결과 FX를 트리거 한다. 이 흐름은 다음과 같다: (클라이언트) AoEActionInput --> (서버) AoEAction --> (클라이언트) AoEActionFX

 public class AoeActionInput : BaseActionInput
    {
        [SerializeField]
        GameObject m_InRangeVisualization;

        [SerializeField]
        GameObject m_OutOfRangeVisualization;

        Camera m_Camera;

        bool m_ReceivedMouseDownEvent;

        NavMeshHit m_NavMeshHit;

        static readonly Plane k_Plane = new Plane(Vector3.up, 0f);

        void Start()
        {
            var radius = GameDataSource.Instance.ActionDataByType[m_ActionType].Radius;
            transform.localScale = new Vector3(radius * 2, radius * 2, radius * 2);
            m_Camera = Camera.main;
        }

        void Update()
        {
            if (PlaneRaycast(k_Plane, m_Camera.ScreenPointToRay(Input.mousePosition), out Vector3 pointOnPlane) &&
                NavMesh.SamplePosition(pointOnPlane, out m_NavMeshHit, 2f, NavMesh.AllAreas))
            {
                transform.position = m_NavMeshHit.position;
            }

            float range = GameDataSource.Instance.ActionDataByType[m_ActionType].Range;
            bool isInRange = (m_Origin - transform.position).sqrMagnitude <= range * range;
            m_InRangeVisualization.SetActive(isInRange);
            m_OutOfRangeVisualization.SetActive(!isInRange);
 
            if (Input.GetMouseButtonDown(0))
            {
                m_ReceivedMouseDownEvent = true;
            }

            if (Input.GetMouseButtonUp(0) && m_ReceivedMouseDownEvent)
            {
                if (isInRange)
                {
                    var data = new ActionRequestData
                    {
                        Position = transform.position,
                        ActionTypeEnum = m_ActionType,
                        ShouldQueue = false,
                        TargetIds = null
                    };
                    m_SendInput(data);
                }
                Destroy(gameObject);
                return;
            }
        }

       
        static bool PlaneRaycast(Plane plane, Ray ray, out Vector3 pointOnPlane)
        {
            // validate that this ray intersects plane
            if (plane.Raycast(ray, out var enter))
            {
                // get the point of intersection
                pointOnPlane = ray.GetPoint(enter);
                return true;
            }
            else
            {
                pointOnPlane = Vector3.zero;
                return false;
            }
        }
    }

 

AOEAction.cs 파일은 서버 측 로직으로, 해당 영역 내의 적을 감지하고 데미지를 적용한다. 그런 다음 모든 클라이언트에게 적절한 위치에서 VFX를 재생하도록 알리는 RPC를 브로드캐스팅 하고. 캐릭터의 상태는 자동으로 해당 NetworkVariables 업데이트와 함께 업데이트된다(예: 체력 및 생존 상태).

 

 public class AoeAction : Action
    {
        const float k_MaxDistanceDivergence = 1;

        bool m_DidAoE;

        public AoeAction(ServerCharacter parent, ref ActionRequestData data)
            : base(parent, ref data) { }

        public override bool Start()
        {
            float distanceAway = Vector3.Distance(m_Parent.physicsWrapper.Transform.position, Data.Position);
            if (distanceAway > Description.Range + k_MaxDistanceDivergence)
            {
                // Due to latency, it's possible for the client side click check to be out of date with the server driven position. Doing a final check server side to make sure.
                return ActionConclusion.Stop;
            }

            Data.TargetIds = new ulong[0];
            m_Parent.serverAnimationHandler.NetworkAnimator.SetTrigger(Description.Anim);
            m_Parent.NetState.RecvDoActionClientRPC(Data);
            return ActionConclusion.Continue;
        }

        public override bool Update()
        {
            if (TimeRunning >= Description.ExecTimeSeconds && !m_DidAoE)
            {
                // actually perform the AoE attack
                m_DidAoE = true;
                PerformAoE();
            }

            return ActionConclusion.Continue;
        }

        private void PerformAoE()
        {
            var colliders = Physics.OverlapSphere(m_Data.Position, Description.Radius, LayerMask.GetMask("NPCs"));
            for (var i = 0; i < colliders.Length; i++)
            {
                var enemy = colliders[i].GetComponent<IDamageable>();
                if (enemy != null)
                {
                    // actually deal the damage
                    enemy.ReceiveHP(m_Parent, -Description.Amount);
                }
            }
        }
    }

AoeActionFX.cs 파일은 서버로부터 오는 RPC에 의해 트리거.

 

 public class AoeActionFX : ActionFX
    {
        public AoeActionFX(ref ActionRequestData data, ClientCharacterVisualization parent)
            : base(ref data, parent) { }

        public override bool Start()
        {
            base.Start();
            GameObject.Instantiate(Description.Spawns[0], m_Data.Position, Quaternion.identity);
            return ActionConclusion.Stop;
        }

        public override bool Update()
        {
            throw new Exception("This should not execute");
        }
    }

 

두개의 값이 동시에 동기화 되길 원한다면 rpc를 사용하자.

Summary

NetworkVariables는 상태를 관리하고 모든 플레이어가 최신 값을 갖도록 하는 데 좋다. 최근에 연결된 플레이어가 최신 세계 상태를 받아야 할 때 사용하자

RPCs는 일시적인 이벤트를 전송할 때 좋다. 수명이 짧은 이벤트를 전송하는 경우에 사용하자

 

https://docs-multiplayer.unity3d.com/netcode/1.0.0/learn/rpcvnetvar/

 

RPC vs NetworkVariable | Unity Multiplayer Networking

Choosing the wrong data syncing mecanism can create bugs, generate too much bandwidth and add too much complexity to your code.

docs-multiplayer.unity3d.com

 

profile

ved_Rony

@Rony_chan

검색 태그