XR플밍 - 12. UnityEngine3D 네트워크 프로그래밍 - 네트워크 프로젝트 기술문서 - 1 (8/12)

이형원·2025년 8월 12일
0

XR플밍

목록 보기
163/215

1. Probuilder를 이용한 온라인 매칭의 맵 5종 구현

1.1 구상

맵에는 플랫폼이라는 개념이 필요하며, 플랫폼은 단순한 사각형 형태 이상으로 다양한 형태의 플랫폼이 필요함. 또한 2D 게임이지만 맵의 효과 등의 여러 상황을 고려하여 3D오브젝트를 이용한 맵의 설계가 게임상 잘 어울린다고 판단했기 때문에 단순 박스만으로 구성된 것 이상의 3D 플랫폼 오브젝트를 구성해야 함.

1.2 설계방법

유니티 레지스트리 기능 중 하나인 Probuilder를 사용하여 커스텀 플랫폼 디자인을 진행함.

Probuilder는 Unity의 커스텀 지오메트리를 빌드, 편집, 및 텍스처링할 수 있으며. 테스트 플레이 동안 씬 내 수준 디자인, 프로토타이핑, 충돌 메시 등에 즉시 사용할 수 있다.

여기서 해당 기능을 사용하여 커스텀 구조물을 만드는 방법은 크게 두 가지가 있다.

  1. 커스텀 구조물을 직접 만들기
  2. 기존에 만들어 놓은 오브젝트를 Merge하여 하나의 오브젝트로 구성하기

1. 커스텀 구조물을 직접 만들기

Probuilder는 이와 같이 원하는 형태의 커스텀 오브젝트를 만들어 구성할 수 있으며, 표면 메쉬까지 만들어 사용할 수 있다. 좀 더 확실한 형태를 만들기 위해서는 처음부터 Probuilder를 사용하여 플랫폼을 구성하여 만드는 것이 이상적이었지만, 테스트용 플랫폼을 먼저 박스 묶음 등으로 구성해버린 상황에서는 2번째 방법을 사용하여 오브젝트를 만들기로 결정했다.

2. 기존에 만들어 놓은 오브젝트를 Merge하여 하나의 오브젝트로 구성하기

예시로 이런 오브젝트가 있다고 할 때, 이를 하나의 오브젝트로 Probuilderize하는 방식은 다음과 같다.

Probuilderize를 진행할 오브젝트의 묶음을 선택하고, Pro Builderize를 진행한다.

필요 시에는 이와 같이 Center Pivot으로 정할 오브젝트를 설정한다. Pivot을 정하는 이유는 해당 오브젝트를 중심으로 충돌체 및 메쉬 방향 등이 결정되기 때문이다. 따라서 원햐는 방향이나 중심점이 있으면 해당 Pivot 부분을 체크하도록 한다.
이와 같은 설정을 마친 다음에 Merge Object를 진행한다. 그러면 해당 오브젝트가 하나의 오브젝트로 합쳐지는 것을 확인할 수 있다.

해당 방식으로 플랫폼을 단순 사각형 모양 이상의 다양한 형태로 제작하였다.

1.3 활용 및 기대 효과

Probuilder를 사용해야 하는 가장 단적인 이유로서 메쉬의 적용이 달라진다는 점이 있다.

Probuilder를 사용하지 않은 오브젝트의 예시로서, 이와 같이 단순 오브젝트의 묶음으로 플랫폼을 구성했을 때에는 셰이더가 오브젝트의 각각에 적용되에 부자연스러운 모습으로 나타난다.
하지만 Probuilder를 통해 오브젝트를 하나로 구성했을 때 셰이더가 의도한 대로 적용되는 것을 확인할 수 있다. 따라서 Probuilder를 통해 필요한 형태의 커스텀 오브젝트를 생성하고, 메쉬의 설정을 필요한 대로 커스텀할 수 있다는 기대효과가 있다.

2. 랜덤 맵 생성 & 닷트윈을 이용한 다이나믹 맵 이동 애니메이션

2.1 구상

Rounds는 게임을 시작하면 맵에 캐릭터가 배치된 채로 시작한다. 그리고 한 라운드가 끝나면 맵이 역동적으로 움직이는 연출을 보여준다. 이후 다음 맵으로 진행이 되며, 제공되는 맵 중에 랜덤하게 선택된 맵에서 게임이 반복적으로 진행되는 형태를 보여준다.

이와 같은 인게임 씬 진행 과정을 위해 필요한 과정은 크게 두 가지로, 맵을 랜덤하게 생성하여 배치하는 과정과 맵의 연출을 역동적으로 표현하는 과정이 필요하다.

맵의 랜덤 생성 방식에 관해서는, 팀 내에서 합의된 내용으로서는 한 라운드 내에서 맵이 중복해서 나오지 않도록 하는 것이 중점이 되었다.

맵의 연출에 관해서는 레퍼런스가 된 게임에서의 역동적인 움직임과 비슷하게 연출하기 위해, 어떤 식으로 연출되는지 분석해 보았다. 처음에는 카메라의 움직임으로 연출되는 장면이라고 생각했는데 자세히 분석해보니 카메라의 움직임 보다는 플랫폼 자체가 움직이고 있다는 것을 알 수 있었다.
따라서 카메라는 고정된 상태로, 한번 맵 화면이 흔들렸다가 플랫폼이 역동적으로 움직이는 애니메이션을 구성해야 한다.

2.2 설계방법

맵의 생성과 이동의 방식은 IngameManager에서의 라운드 종료 시점과, 현재 라운드의 수 등의 정보를 받아 아래와 같은 구조로 작동한다.

1. 맵의 랜덤 생성 방식

맵의 랜덤 생성 방식에 관해서는 처음부터 씬 내에서 맵 자체를 프리팹으로 생성하는 방식으로 구성하였으며, 랜덤한 맵을 뽑고 각각의 오프셋의 위치에 Instantiate 하는 방식을 채용했다.

우선 이와 같이 맵 자체를 하나의 프리팹으로 저장한 다음, 해당 맵 목록을 등록하여 맵을 생성해주는 RandomMapCreator를 구성하였다.

해당 컴포넌트는 인게임매니저 상에서 게임이 시작되었을 때 맵 생성을 시도하게 되며, Dictionary의 키로 게임 오브젝트, value로 1번씩만 등장하도록 등록하고 랜덤 뽑기를 수행한다. 만약 해당 맵이 생성되었을 경우 Dictionary에서 Key를 제거하는 방식으로 랜덤 뽑기를 진행한다.
이렇게 각 라운드마다 중복되지 않게 3개의 맵을 각각 생성하는 방식으로 맵이 생성되며, 생성되는 위치는 오프셋 기준의 Transform위치에 생성된다.
여기서 마지막 단계로 MapUpdate가 실행되며, IngameManager를 통해 현재의 라운드가 몇 라운드인지 확인하고서는 해당 라운드의 맵만 활성화하는 형태로 구성했다

2. 닷트윈을 이용한 맵의 애니메이션 연출

해당 연출을 구성하기 위해서 유니티 에셋 스토어에서 제공하는 닷트윈(DOTween)을 활용하였다.

먼저, 닷트윈이라는 에셋의 정의와 기능을 간략하게 정리하자면, 유니티 C# 스크립트로 구성할 수 있는 애니메이션적 연출을 간편하게 구현할 수 있도록 도와주는 툴이다. 단순이 오브젝트의 이동, 회전 및 스케일 조정 외에도 흔들림 연출 및 바운스 등 다양한 기능을 코드로 제공해준다.

이와 같은 에셋을 사용하여 맵의 이동 방식을 다음과 같이 정의하였다.

  • 라운드가 종료되었을 때 1) 맵이 한 번 흔들린 다음, 2) 플랫폼 그룹이 왼쪽으로 이동하는 연출을 보여준다.

맵의 이동 연출은 두 개의 단계로 진행되므로, 이를 연출하기 위한 과정을 다음과 같이 설계하였다.

  • 맵의 흔들림을 연출하기 위해서 닷트윈에서 제공하는 DOShakePosition을 활용했다.

여기서 맵의 적절한 진동 효과를 주기 위해 테스트를 진행하며 수치를 맞추고, 이후 진동 후에 맵의 위치가 영점을 벗어나는 것을 방지하기 위해 해당 애니메이션 종료 후 좌표의 초기화 과정도 거쳤다.

  • 맵의 이동 연출을 위해 이동시킬 플랫폼을 그룹화하고, DOMove를 통해 그룹화한 플랫폼을 이동시키는 방식으로 연출한다.

DOMove는 닷트윈에서 제공하는 오브젝트를 움직이는 함수이다. 오브젝트를 움직이는 방법은 구현 난이도가 높지 않지만 Ease를 이용한 움직임의 방식이나 딜레이를 줄 수 있다는 특성을 사용하기 위해 닷트윈을 활용했다.

이와 같이 각 플랫폼 묶음을 설정한 딜레이 수치만큼 순차적으로 이동하면서 설정한 위치에 자리잡는 식으로 구성하였다.

맵의 경우 프리팹에 이와 같이 움직이는 기능이 부착되어 있는 형태이며, 정해진 순서에 따라 플랫폼을 각각의 자식 오브젝트로 두어 해당 MapMove가 순차적으로 이동하는 방식으로 구성했다.

이에 따라 MapController에서 라운드가 종료되었다는 신호를 받았을 때, 해당 플랫폼이 이동하는 방식으로 구현하였다.

3. 이미 종료된 라운드의 맵에 대한 비활성화 과정

이와 같은 구조에서의 문제점이라고 한다면, 만약 한 라운드에서의 맵 개수가 늘어나면 늘어날수록 맨 처음 생성된 맵부터 순차적으로 이동시키기 때문에 시간적인 딜레이가 발생한다는 부분이다. 따라서, 이미 종료된 라운드의 맵에 관해 비활성화하는 부분을 추가했다.

카메라의 시야에서는 보이지 않는 왼쪽에 Trigger 영역을 달아두고, 해당 영역에 맵이 진입했을 때 해당 맵을 비활성화하는 과정을 추가하였다. 이와 같은 과정을 추가하여 이미 종료된 라운드의 맵까지 이동시키는 비효율성을 줄이고, 동시에 맵 연산 상에서의 이득을 볼 수 있게 설계했다.

2.3 활용 및 기대 효과

해당 시스템으로 맵을 생성하면서 이동하는 방식을 취했을 때, 얻을 수 있는 장점은 다음과 같다.

  1. 맵을 생성하는 로직과 움직이는 로직이 분리되어 있어, 맵의 추가와 삭제에 있어 편리함.

맵을 새로 생성할 때 애니메이션적인 연출을 각각 지정하기 때문에 플랫폼의 이동방식을 유연하게 조절할 수 있으며, 맵에는 MapDynamicMovement 컴포넌트만 추가하고 RandomMapCreator에 등록하는 간단한 방법으로 맵을 추가 및 삭제할 수 있으므로, 확장성을 기대할 수 있다.

  1. 맵의 라운드 수를 유연하게 늘리거나 줄일 수 있으며, 유지보수가 용이함.

해당 시스템상 맵의 랜덤 생성 방식에서 라운드 수를 지정하여 하위 오브젝트로 맵을 생성하는 방식이므로, 라운드 수를 늘리거나 줄이는 데 있어서 유연한 대처가 가능하다. 또한 생성 및 이동이라는 기능이 분리되어 있기 때문에 유지보수가 용이하다는 기대를 할 수 있음.

3. Joint를 이용한 맵 내의 물리 구조물의 설계

3.1 구상

Rounds에는 단순 플랫폼을 오가면서 서로에게 총을 쏘는 게임 방식 외에, 구조물 및 로프 같은 것이 존재하며 해당 오브젝트를 쳐서 날리거나 총을 통해 로프를 끊는 행동 등이 가능하다.

이와 같이 파괴하거나 무너뜨릴 수 있는 오브젝트는 Rounds의 게임성에 있어 중요한 요소이며 이를 구성하기 위해서는 물리 엔진 등의 설계가 중요하다고 판단하였다.

이를 위해선 필요한 요소가 크게 두 가지가 있다.

1) 로프 시스템 및 로프에 달린 오브젝트의 구현
2) 오브젝트에 대한 세심한 물리 엔진 조절 및 총(외부 물리력)과의 상호작용

3.2 설계방법

3.2.1 로프 시스템의 구성

일정상 게임 내에서 로프 시스템이 이용된 맵은 하나밖에 구현하지 못했지만, 해당 맵에서의 로프의 역할은 다음과 같다.

  • 로프는 사선으로 연결된 로프와 수직으로 연결된 로프가 있다. 플레이어는 총을 통해 박스 모양의 물체를 맞춤으로서 수직으로 연결된 로프를 끊을 수 있으며, 해당 로프를 끊었을 때 사선으로 연결된 로프에 따라 해머가 중력으로 포물선을 그리며 떨어지며 주변 구조물을 부술 수 있다.

로프, 특히 로프에 오브젝트까지 달 수 있는 방식을 구성하기 위해서 유니티에서 사용 가능한 Hinge Joint를 사용한 방식으로 로프 물리를 구현하고자 했다.

1. 유니티에서의 로프의 구현 방법

유니티에서 Hinge Joint에 대한 정의를 다음과 같이 하고 있다.

  • 힌지 조인트(Hinge Joint) 는 두 개의 리지드바디를 묶어서 힌지에 연결된 것과 같이 움직이도록 제약을 둡니다. 이는 문을 표현하는 데 완벽하지만 사슬, 진자 운동 등을 모델링하는 데에도 사용할 수 있습니다.

로프와 로프에 달린 오브젝트는 서로의 Rigidbody에 영향을 주면서 물리 연산을 진행해야 하기 때문에, Hinge Joint를 사용한 로프의 구성 방식이 적절하다고 판단했다. Hinge Joint로 로프를 만드는 방식은 다음과 같다.

Hinge Joint는 자신과 연결된 오브젝트의 물리 영향을 받도록 제약을 두기 때문에 Hinge Joint를 둔 오브젝트와 Hinge Joint에 연결될 오브젝트 둘 다 Rigidbody를 갖고 있어야 한다. 따라서 로프와 같은 물체를 구성한다고 했을 때, 로프의 마디 마디에 관절이 있다고 가정하여 일정 길이 단위로 나눈 다음,
Hinge Joint를 각각 생성하여 인접한 오브젝트의 Rigidbody를 연결해주면 된다. 여기서 위에서 아래로 내려오는 로프의 경우, Joint1이 맨 위의 Anchor를 연결하고, Joint2가 Joint1을 연결하여 순차적으로 이어나가면 된다. 이와 같이 구성하고 로프를 흔들어보면, 마치 끈이 흔들리는 것 같은 움직임을 줄 수 있다.

이와 같은 방식으로 로프와 같은 움직임을 연출할 수 있었다. 하지만 테스트로만 제작했던 해당 오브젝트와 같이 로프를 구성하려면 여러모로 불편한 점이 있었다.
아래 두 가지와 같은 불편한 점 때문에 구성 방법에 있어서 개편이 필요했다.

  1. 해당 방식으로 로프를 구현하는 것은 가능하나, 길이 조절 등에서 유연하지가 않고 부자연스럽게 작동함
  2. 부착된 오브젝트를 바꾸는 경우가 많을 건데, 해당 과정이 불편함

이와 같은 이유로 로프를 유연하게 생성할 수 있는 시스템이 필요했다

3.1.2 로프 생성 시스템

실제 게임에서 쓰이는 로프 기믹 오브젝트 프리팹은 이와 같은 방식으로 구성되어 있다.

  1. 프리팹 단계에서는 로프가 보이지 않을 것이다. 이는 로프의 길이 등의 유연함을 위해서 RopeCreator라는 컴포넌트를 통해 Awake 단계에서 로프를 생성하기 때문에 프리팹 상태에서는 보이지 않는다.
  2. 해당 프리팹이 로프를 생성하는 방식은 다음과 같다. RopeCreator가 있는 빈 오브젝트의 위치에 자식 오브젝트로 Hook - 로프가 시작되는 부분이 있으며, 해당 부분을 기준으로 로프의 구성품이 되는 오브젝트 - Link를 Instantiate시킨 후, Hook에 연결한다.

링크는 Hinge Joint가 붙어있는 공 모양의 오브젝트이다. 이를 연달아 연결하면 줄과 같이 연출할 수 있다.

  1. 생성할 링크의 개수를 인스펙터 상에서 조작하는 것으로 줄의 길이를 결정하고, 링크의 맨 끝 부분에 RopeTiedObject 컴포넌트를 가진 오브젝트를 연결한다.
    RopeTiedObject 컴포넌트는 Hinge Joint가 없을 시 Hinge Joint를 추가하는 컴포넌트이다.

이와 같은 방식으로 로프의 생성 로직을 구성한 다음, 초기의 상태부터 해머가 출렁거리고 있으면 부자연스러우므로 이를 고정하는 로직을 하나 더 추가했다.

해머에 수직으로 연결된 로프는 로프 같아 보이는 큐브로 만들었다. 이 Fixed Rope와 해머는 초기 물리 상태를 Kinematic으로 설정한다. 그리고 Fixed Joint를 달아 Fixed Rope와 연결하였다.

여기서 사각형 부분을 총으로 맞추면 해당 Fixed Rope가 비활성화되는 기능을 넣었고, RopeTiedObject에는 해당 로프가 비활성화되었을 때 Rigidbody2D 의 Body Type를 Dynamic로 전환하는 방식을 사용했다.

2.2 오브젝트의 물리 작용 방식

Rounds의 맵 내의 오브젝트를 보면, 박스 같은 오브젝트들이 쌓여 있는 형태가 많다. 해당 오브젝트들은 물리가 작용하기 때문에 모두 Rigidbody를 달아야만 한다.
하지만 단순하게 Rigidbody를 달고 물체를 쌓기만 한다면, 아래와 같이 균형이 맞지 않는 상황에서 외부의 힘에 대한 작용도 없이 무너져버릴 것이다.

이는 단순히 균형을 잘 맞춘다고 해서 해결될 문제가 아니며, 초기에는 이 구조물의 형태가 무너지지 않도록 고정하는 작업이 필요하다. 따라서 초기에는 해당 구조물 오브젝트 자체의 Rigidbody BodyType를 Kinematic으로 설정해야 한다.

여기서 총알에 피격되었을 때 해당 오브젝트의 BodyType를 Dynamic으로 전환하는 방식을 취한다면 또 다른 문제점이 생긴다.
예를 들어 오브젝트의 중간 지점에 총알이 피격되었는데, 아래 부분은 Kinematic 상태가 유지되는 게 맞지만 그 오브젝트의 위쪽 오브젝트는 피격되지 않았으므로 Kinematic 상태가 유지되게 된다. 그러면 중간 부분의 오브젝트만 물리가 작용하여 무너지거나 날아가는 등의 작용을 하면서 그 위의 오브젝트는 물리가 여전히 작용하지 않아 공중에 둥둥 떠 있는 상태가 유지될 것이다.

따라서 이 문제를 해결하기 위해서 여기에서는 Fixed Joint라는 것을 활용했다.

유니티에서 Fixed Joint에 대한 정의를 다음과 같이 하고 있다.

  • 고정 조인트(Fixed Joints) 는 오브젝트의 이동을 제한하여 다른 오브젝트에 의존하도록 합니다. 이는 Parenting 과 비슷하지만 Transform 계층 구조가 아니라 물리를 통해 이루어진다는 점이 다릅니다. 이를 사용하기 가장 적합한 경우는 부서져서 쉽게 떨어질 수 있는 오브젝트를 구현하거나 부모-자식 관계를 설정하지 않고 두 오브젝트의 이동을 연결하고 싶은 경우입니다.

여기서 부서져서 쉽게 떨어질 수 있는 오브젝트를 구성하는 데에 사용할 수 있다는 경우에 집중했다. 이에 따라 Fixed Joint를 통해 무너질 수 있는 오브젝트를 구현한 방식은 다음과 같다.

  1. 로프를 구성했던 방식과 비슷하게 무너질 수 있는 오브젝트에는 모두 Fixed Joint를 달아준다.
    여기서 한 가지 중요한 점으로, Fixed Joint에서의 Break Force를 낮은 수치로 적어주는 것이다. 이렇게 설정하면, 총알 등에 피격되어 물리적인 힘을 받았을 때 해당 Joint가 파괴되도록 할 수 있다.
  2. 무너질 수 있는 오브젝트는 BoxController 혹은 MultiJointBoxController를 컴포넌트로 가진다.
    해당 컴포넌트가 하는 역할은, Joint가 파괴되었을 때(직접 피격시) 혹은 자신의 Connected Rigidbody의 상태가 변화했을 때(직접 피격된 오브젝트의 위에 있는 오브젝트일 시) 물리 BodyType를 Dynamic으로 변화시킨다.

이와 같은 방식으로 구조물의 특정 위치에 총알이 피격되었을 때, 해당 오브젝트 자신과 함께 자신의 위에 있는 오브젝트까지 물리 상태를 Dynamic으로 변화시켜 무너지는 오브젝트를 연출할 수 있다.

3.3 활용 및 기대 효과

맵에 활용되는 오브젝트의 물리 작용 등과 같은 요소는 모든 게임에서 필수적으로 들어갈 기능은 아니지만, 게임의 컨셉에 따라서 필요한 경우도 있을 것이다. 해당 방식의 경우 특정 조건에서의 물리 작용 변화 등을 이용한 기믹을 통해서 다이나믹한 맵 연출을 구성하는 데 유용하게 쓰일 수 있을 것으로 기대한다.

profile
게임 만들러 코딩 공부중

0개의 댓글