[Unity 게임 만들기] Unity Netcode 적용하기 2

lighteko·2024년 6월 24일

Unity 게임 만들기

목록 보기
5/11
post-thumbnail

참고영상

따라하라는 대로 했는데 잘 동작하지 않아서 새로운 영상을 참고해 코드를 다시 고쳤다.

저번 글에서 Refactoring 했던 클래스 (PlayerMovement)를 두 클래스로 나누었다.

PlayerController 클래스와 PlayerNetwork 클래스다.

PlayerController 클래스에서는 이전과 같이 이동과 회전을 다룬다. 다만, 해당 네트워크 오브젝트를 조종하는 것은 Owner의 권한이므로 NetworkBehavior를 상속하는 것은 변경하지 않았다.

using UnityEngine;
using Unity.Netcode;

public class PlayerController : NetworkBehaviour
{
    // Start is called once before the first execution of Update after the MonoBehaviour is created
    public float speed = 0;
    private float angle = 0;
    // Update is called once per frame
    void FixedUpdate()
    {
        if (!IsOwner) return;
        Vector2 worldMousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        Vector3 direction = ((Vector3)worldMousePosition - transform.position).normalized;
        angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
        Vector3 currentPosition = transform.position;
        MoveTowardMouse(currentPosition, direction, speed * 0.1f, angle); 
    }

    public void MoveTowardMouse(Vector3 pos, Vector3 dir, float speed, float angle)
    {
        transform.position = new Vector3(pos.x + dir.x * speed, pos.y + dir.y * speed, 0);
        transform.Find("Jet").transform.rotation = Quaternion.Euler(new Vector3(0, 0, angle - 90));
    }
}

이렇게 코드가 변경되었다. 기존의 ServerRpc 메서드는 일반 메서드로 변경하였고, FixedUpdate의 IsOwner 조건만 남겨두었다.

PlayerNetwork 클래스는 서버와 통신하며 데이터를 동기화 하는 것을 맡았다.

using UnityEngine;
using Unity.Netcode;

public class PlayerNetwork : NetworkBehaviour
{
    private readonly NetworkVariable<PlayerNetworkData> _netState = new(writePerm: NetworkVariableWritePermission.Owner);
    private Vector3 _vel;
    private float _rotVel;
    [SerializeField] private float _cheapInterpolationTime = 0.1f;
    void FixedUpdate()
    {
        if (IsOwner)
        {
            _netState.Value = new PlayerNetworkData() {
                Position = transform.position,
                Rotation = transform.GetChild(0).rotation.eulerAngles
            };
        }
        else
        {
            transform.position = Vector3.SmoothDamp(transform.position, _netState.Value.Position, ref _vel, _cheapInterpolationTime);
            transform.GetChild(0).rotation = Quaternion.Euler(0,0,Mathf.SmoothDampAngle(transform.GetChild(0).rotation.eulerAngles.z, _netState.Value.Rotation.z, ref _rotVel, _cheapInterpolationTime));
        }
    }
}

struct PlayerNetworkData : INetworkSerializable
{
    private float _x, _y;
    private short _zRot;

    internal Vector3 Position {
        get => new(_x, _y, 0);
        set {
            _x = value.x;
            _y = value.y;
        }
    }

    internal Vector3 Rotation {
        get => new(0,0,_zRot);
        set => _zRot = (short) value.z;
    }
    
    public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
    {
        serializer.SerializeValue(ref _x);
        serializer.SerializeValue(ref _y);
        serializer.SerializeValue(ref _zRot);
    }
}

코드가 길어져서 무슨 일이 생긴건지 헷갈릴 수 있으니 설명하자면 다음과 같다.

NetworkVariable을 사용한 데이터 동기화

우선 PlayerNetwork 클래스는 NetworkTransform 스크립트를 대체하는 역할을 한다. 따라서 Player 오브젝트에서 NetworkTranform 컴포넌트를 위 코드와 교체해주면 된다.

private readonly NetworkVariable<PlayerNetworkData> _netState = new(writePerm: NetworkVariableWritePermission.Owner);

먼저 netState라는 NetworkVariable을 선언한다. 이때 저장되는 타입은 PlayerNetworkData라는 Struct다.

struct PlayerNetworkData : INetworkSerializable
{
    private float _x, _y;
    private short _zRot;

    internal Vector3 Position {
        get => new(_x, _y, 0);
        set {
            _x = value.x;
            _y = value.y;
        }
    }

    internal Vector3 Rotation {
        get => new(0,0,_zRot);
        set => _zRot = (short) value.z;
    }
    
    public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
    {
        serializer.SerializeValue(ref _x);
        serializer.SerializeValue(ref _y);
        serializer.SerializeValue(ref _zRot);
    }
}

이 구조체는 INetworkSerializable이라는 인터페이스를 상속하는데, 그렇기 때문에 NetworkSerialize 메서드를 implement 해야 한다.
PlayerNetworkData는 Position과 Rotation을 저장한다. 그 중에서도 변화하는 값은 x, y 좌표와 z축 회전각이기 때문에 필드변수를 세개만 선언했다.
공간 효율성을 위해서 회전각은 short로 저장하고, 그에 따라 getter 메서드와 setter 메서드를 구현한다.

동기화 중 위치 및 회전각 보간

private Vector3 _vel;
private float _rotVel;
[SerializeField] private float _cheapInterpolationTime = 0.1f;

위 세 필드 변수는 위치와 회전각 보간을 위해서 필요하다. 특히 cheapInterpolationTime은 참고 영상에서는 Production 레벨에서는 사용하지 말라고 했지만, 어떻게 처리해야하는지 아직 모르기 때문에 일단 보류했다.

void FixedUpdate()
{
    if (IsOwner)
    {
        _netState.Value = new PlayerNetworkData() {
            Position = transform.position,
            Rotation = transform.GetChild(0).rotation.eulerAngles
        };
    }
    else
    {
        transform.position = Vector3.SmoothDamp(transform.position, _netState.Value.Position, ref _vel, _cheapInterpolationTime);
            transform.GetChild(0).rotation = Quaternion.Euler(0,0,Mathf.SmoothDampAngle(transform.GetChild(0).rotation.eulerAngles.z, _netState.Value.Rotation.z, ref _rotVel, _cheapInterpolationTime));
    }
}

Fixed Update 함수에서는 상황에 따라 두 가지 코드를 실행 한다.
먼저, Player 오브젝트를 조종할 권한이 있는 Owner일 때는, netState에 현재 위치와 회전각을 저장한다.
실제 이동은 PlayerController 클래스에서 담당하기 때문에 관련 코드를 작성할 필요가 없다.

두번째로, 만약 Owner가 아닐 때는, 보간법을 활용해서 상대 플레이어의 위치와 회전각을 보정한다.

0개의 댓글