XR플밍 - 12. UnityEngine3D 네트워크 프로그래밍 - (주말작업) 네트워크 프로젝트 Round5 4.2일차 (7/27)

이형원·2025년 7월 27일
0

XR플밍

목록 보기
147/215

1. 금일 한 업무 정리

  • 로프 기믹까지 모든 오브젝트에 대한 물리 네트워크 동기화 완료
  • 플랫폼용 셰이더 제작
  • ProBuilder를 이용한 다각형 플랫폼 합치기 - Material 정상 적용되게 설정
  • 플랫폼 전체적으로 위치 조정 및 셰이더 적용, 배경 적용 등
  • 캐릭터용 셰이더 추가 제작

  • 카메라워크

  • 물리 네트워크 동기화

2. 문제의 발생과 해결 과정

2.1 오브젝트 물리 동기화

어제 했던 동기화 작업을 바탕으로 나머지 오브젝트도 동기화하는 작업에 돌입했다.
방법 자체는 어제 한 방식과 거의 같기 때문에 코드만 변형해서 넣었다.

  • MultiJointBoxController
using Photon.Pun;
using Unity.VisualScripting;
using UnityEngine;

public class MultiJointBoxController : MonoBehaviourPun, IPunObservable
{
    FixedJoint2D[] fixedJoint;

    private Rigidbody2D rigid;

    // 네트워크 동기화용 변수
    private bool isPhysicsEnabled = false;
    private bool networkPhysicsEnabled = false;
    private Vector3 networkPos;
    private Quaternion networkRot;

    private void Awake()
    {
        rigid = GetComponent<Rigidbody2D>();
        fixedJoint = GetComponents<FixedJoint2D>();
    }

    private void Update()
    {
        if (FixedJointDisabled())
        {
            EnablePhysics();
        }
        if (!photonView.IsMine)
        {
            if (isPhysicsEnabled)
            {
                transform.position = Vector3.Lerp(transform.position, networkPos, Time.deltaTime * 10f);
                transform.rotation = Quaternion.Lerp(transform.rotation, networkRot, Time.deltaTime * 10f);
            }
        }
    }

    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (photonView.IsMine && (collision.gameObject.CompareTag("Bullet") || collision.gameObject.CompareTag("Hammer")))
        {
            EnablePhysics();
        }        
    }

    private bool FixedJointDisabled()
    {
        for (int i = 0; i < fixedJoint.Length; i++)
        {
            if (photonView.IsMine && (fixedJoint[i].IsDestroyed()))
                
            {
                return true;
            }
        }
        return false;
    }

    private void EnablePhysics()
    {
        networkPhysicsEnabled = true;

        if (PhotonNetwork.IsMasterClient)
        {
            rigid.bodyType = RigidbodyType2D.Dynamic;
            rigid.mass = 0.1f;
            rigid.gravityScale = 0.3f;
            isPhysicsEnabled = true;
        }
    }


    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {
        if (stream.IsWriting)
        {
            stream.SendNext(transform.position);
            stream.SendNext(transform.rotation);
            stream.SendNext(isPhysicsEnabled);
            if (isPhysicsEnabled)
            {
                stream.SendNext(rigid.velocity);
                stream.SendNext(rigid.angularVelocity);
            }
        }
        else if (stream.IsReading)
        {
            networkPos = (Vector3)stream.ReceiveNext();
            networkRot = (Quaternion)stream.ReceiveNext();
            networkPhysicsEnabled = (bool)stream.ReceiveNext();
            if (networkPhysicsEnabled != isPhysicsEnabled)
            {
                isPhysicsEnabled = networkPhysicsEnabled;
            }

            if (isPhysicsEnabled)
            {
                Vector2 networkVelocity = (Vector2)stream.ReceiveNext();
                float networkAngularVelocity = (float)stream.ReceiveNext();

                if (!photonView.IsMine)
                {
                    rigid.velocity = networkVelocity;
                    rigid.angularVelocity = networkAngularVelocity;
                }
            }
        }
    }
}

로프 기믹 오브젝트의 경우 로프도 있고 해머 부분도 있지만, 로프는 실행과 동시에 생성되는 방식이다. 하지만 굳이 로프에다가 동기화를 진행해야 할까라는 부분에 대해서는 굳이? 라는 생각이 들어서 해머만 동기화를 진행했다.

  • RopeTiedObject
using Photon.Pun;
using UnityEngine;

public class RopeTiedObject : MonoBehaviourPun, IPunObservable
{
    [SerializeField] float distanceFromChaninedEnd = 0.6f;

    private Rigidbody2D rigid;

    // 네트워크 동기화용 변수
    private bool isPhysicsEnabled = false;
    private bool networkPhysicsEnabled = false;
    private Vector3 networkPos;
    private Quaternion networkRot;

    private void Awake()
    {
        rigid = GetComponent<Rigidbody2D>();
    }

    private void Update()
    {
        if (!photonView.IsMine)
        {
            if (isPhysicsEnabled)
            {
                transform.position = Vector3.Lerp(transform.position, networkPos, Time.deltaTime * 10f);
                transform.rotation = Quaternion.Lerp(transform.rotation, networkRot, Time.deltaTime * 10f);
            }
        }
    }

    public void ConnectRopeEnd(Rigidbody2D endRB)
    {
        HingeJoint2D joint = gameObject.AddComponent<HingeJoint2D>();
        joint.autoConfigureConnectedAnchor = false;
        joint.connectedBody = endRB;
        joint.anchor = Vector2.zero;
        joint.connectedAnchor = new Vector2(0f, -distanceFromChaninedEnd);
    }

    public void EnablePhysics()
    {
        networkPhysicsEnabled = true;

        if (PhotonNetwork.IsMasterClient)
        {
            rigid.bodyType = RigidbodyType2D.Dynamic;
            rigid.mass = 50f;
            rigid.gravityScale = 1f;
            isPhysicsEnabled = true;
        }
    }

    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {
        if (stream.IsWriting)
        {
            stream.SendNext(transform.position);
            stream.SendNext(transform.rotation);
            stream.SendNext(isPhysicsEnabled);
            if (isPhysicsEnabled)
            {
                stream.SendNext(rigid.velocity);
                stream.SendNext(rigid.angularVelocity);
            }
        }
        else if (stream.IsReading)
        {
            networkPos = (Vector3)stream.ReceiveNext();
            networkRot = (Quaternion)stream.ReceiveNext();
            networkPhysicsEnabled = (bool)stream.ReceiveNext();
            if (networkPhysicsEnabled != isPhysicsEnabled)
            {
                isPhysicsEnabled = networkPhysicsEnabled;
            }

            if (isPhysicsEnabled)
            {
                Vector2 networkVelocity = (Vector2)stream.ReceiveNext();
                float networkAngularVelocity = (float)stream.ReceiveNext();

                if (!photonView.IsMine)
                {
                    rigid.velocity = networkVelocity;
                    rigid.angularVelocity = networkAngularVelocity;
                }
            }
        }
    }
}

이제 이걸 호출해주면 된다.

using UnityEngine;

public class DestroyablePlatformController : MonoBehaviour
{
    [SerializeField] RopeController rope;
    [SerializeField] RopeTiedObject tiedObject;

    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.CompareTag("Bullet"))
        {
            gameObject.SetActive(false);
            tiedObject.EnablePhysics();
            rope?.RopeDestroy();
        }
    }
}

2.2 셰이더 다시 만져보기, 그라데이션 셰이더 만들기

셰이더 그래프 자체는 이와 같이 제작했다. 이에 대해선 나름대로의 상세 내용을 정리하자면 다음과 같다.

Position : 위치를 정한다. 여기에서 Split으로 위치를 나누고, R을 연결하면 세로로 나뉘어지고 G를 연결하면 가로로 나눠진다. (B랑 A는 뭔가 이상하게 안 나눠진다)
여기서 BottomOrigin이라는 Float 변수로 Bottom 색깔의 시작 부분을 정하고, Divide로 BottomSpread 변수를 집어넣어 번짐 정도를 정한다.
이후 Clamp를 통해 그라데이션으로 조금 번지도록 조정을 하고 Lerp로 색깔을 넣어 자연스럽게 그라데이션이 이어지도록 한다.

또한 여기서는 세 가지 색깔로 그라데이션을 만들 수 있도록 하였기 때문에 아래쪽 노드에서 똑같이 해당 방식을 진행하고, 이렇게 만든 두 가지 노드를 또 다시 Lerp 시켜서 의도한 대로의 색깔을 뽑는다.

적용된 색상은 다음과 같다.


꽤 그럴싸하게 나와서 맵 플랫폼 셰이더는 이걸 사용하기로 했다.
(다만 이걸 만들고 난 뒤에야 그라데이션을 만드는 더 쉬운 방법을 찾아낸 것이 아쉬울 따름이다...)

2.3 유니티 레지스트리 기능 ProBuilder

셰이더는 만들었지만 문제가 하나 더 있었다.

이와 같이 여러 블럭의 조합으로 만들어져 있던 오브젝트는 셰이더가 정상적으로 적용되지 않는 문제가 발생한다는 것.
이걸 위해서 어떤 방법을 사용해야 할지에 대한 고민이 많았다.

방법을 강구하던 과정을 아래와 같이 서술하고자 한다.

1. 유니티 외부 툴을 사용하는 것

가장 확실한 방법은 3D Max나 Blender 같은 외부 모델링 툴을 이용해 만드는 것이었다.
아무래도 3D 모델러 지인이 있다 보니 이런 걸 어떻게 하면 좋냐며 상담해보기도 했는데, 3D Max로 뚝딱 모델을 만들어오기는 했다. 하지만 이걸 그대로 사용해봤자 내 기술도 아니고... 그렇다고 당장 그 사람만큼의 3D 모델링 툴 다루는 기술을 배울 수도 없었다.
더군다나 3D Max는 유료 툴이라서 무료인 Blender 튜토리얼을 살짝 공부해보긴 했지만, 내가 프로그래밍을 하는 사람이지 모델링까지 해야 하나 하는 자괴감만 느끼고 이 방법은 보류하기로 했다.

2. Primitive Object

다각형 오브젝트를 만드는 방법이란 걸 검색하기 위해 사용하는 키워드이다. 이는 랜더링 파이프라인에서 다각형 오브젝트를 사용하는 방식으로 오브젝트를 직접 만드는 방식으로, 유니티 내의 모든 오브젝트의 메쉬가 삼각형의 집합으로 이루어져 있다는 방식에서 직접 도형을 설계하는 방식이다.

이론은 알겠다. 만드는 방법에 대한 것도 엄청 어려워보이지 않는다.

다만, 엄청난 노가다라는 게 문제다.

고작 2D 사각형을 만드는 데만 이 정도 코드 길이이다.

큐브 정도 되는 오브젝트를 만들려고 하면, 대충 이 정도의 길이의 코드가 필요하다.

결국 노가다를 통하면 어떻게든 오브젝트를 만들 수 있겠지만, 이건 좀 아닌 것 같았다. 아무래도 다른 방법을 찾아봐야 할 것으로 생각되었다.

3. 유니티 내장 기능, ProBuilder에 대하여

처음엔 무료 에셋을 통해 모델링 툴 같은 것을 찾아보려고 했다. 하지만 무료 에셋이든 유료 에셋이든 그럴싸한 게 없었는데, 의외로 이런 엄청 편리한 모델링 기능이 유니티 내장 기능으로 있단 걸 알게 되었다.

ProBuilder라는 유니티 레지스트리의 기능이 있단 걸 알게 되었고, 이걸 한 번 사용해 보기로 했다.

이런 좋은 기능이 있으면 알려주지 그랬냐

아직 제대로 사용해 보지 않았어도 엄청나게 좋은 기능인 것을 알 수 있었다. 아치형 문도 자동으로 만들어주고, 계단도 바로 만들 수 있고 다각형, 기울기, 복잡한 도형도 진짜 편하게 만들 수 있었다. 모델링 지식이 없어도 말이다!
아직 사용 방법이 익숙하지도 않고 제대로 공부해 보는 건 나중으로 미루고, 우선은 내게 필요한 기능을 써 보기로 했다.

4. ProBuilder로 기존에 만들어 둔 도형 합치기

나는 ProBuilder를 알기 이전에 이미 플랫폼을 다 만들어 둔 상태여서, 다시 처음부터 만들어야 하나 싶었다. 하지만, ProBuilder는 이런 기존 도형을 합치는 것까지도 가능하다.

방법은 다음과 같다.

  1. 기존 도형 파츠를 전부 선택한다.
  2. Pro Builderize를 진행한다.

    지금은 이미 합쳐진 상태라서 비활성화되어 있지만, 이와 같이 모든 파츠를 ProBuilder에 사용할 수 있는 부품으로 바꿀 수 있다.
  3. (이건 필요 시에 진행한다.) 해당 다각형에서 중심으로 지정할 부분을 하나 선택한 후, Center Pivot을 누른다. 그러면 해당 오브젝트가 중심이 된다.
  • 해당 과정이 필요한 경우 - 중심 오브젝트를 설정하면, 해당 오브젝트를 중심으로 Material의 적용 각도가 정해진다. 즉, 회전하지 않은 오브젝트를 기준으로 Center Pivot을 설정해야 후에 Material을 적용할 때 정상 적용이 된다.
  1. 마지막으로 모든 오브젝트를 선택한 상태에서 Merge Objects를 선택한다.

이와 같이 오브젝트에 셰이더를 전부 예쁘게 적용할 수 있었고, 이와 같이 적용된 것을 확인할 수 있다.

다만 충돌체의 경우에는 다시 노가다로 만들어야 한다는 힘든 부분이 있기는 했다.

2.4 회전하는 그라데이션 셰이더 만들기

Rounds의 캐릭터는 이와 같이 동그란 원형의 캐릭터이다. 팀장님은 이제 캐릭터를 만들어야 하는데, 캐릭터에 사용할 셰이더를 추가로 만들 수 있는지 요청하셨다.

일단 원하시는 방향은 약간 이런 느낌이다.

그라데이션 셰이더를 만들었는데, 그런 그라데이션 셰이더가 약간 빙글빙글 돌면서 색이 계속 변하는 셰이더를 만들어 줄 수 있을까요?

해당 방법을 위해 리서치를 해 보았고, 이와 같이 만들었다.

방법은 더욱 간단해서 설명은 생략한다.
대충 이런 느낌으로 나온다

3. 개선점 및 과제

3.1 프로토타입 제대로 작동하는 지 확인하기

현재 합치는 과정이 빠르게 진행되고 있으며, 버그 요소 및 물리 동기화 여부 등을 지속적으로 확인할 필요가 있다.

3.2 맵 추가

맵이 현재 4개도 많이 만들긴 했지만, 더 만들어야 할 수도 있다. 맵 디자인을 하고 다시 맵을 만들어보자.

3.3 셰이더 보강

셰이더를 지금 나름 성공적으로 만들기는 했지만, 더 예쁘게 보강할 수 있는 방법이 있는지 생각해보자.

profile
게임 만들러 코딩 공부중

0개의 댓글