[VR - 시뮬레이션 롤플레잉 우주 협력 게임 Meta Space] 2. 개발

백지윤·2024년 8월 26일

프로젝트

목록 보기
8/8

프로젝트 기간

2024-05-22 ~ 2024-08-26

VR - 시뮬레이션 롤플레잉 우주 협력 게임

최종 GITHUB

VR 버전

GITHUB 프로젝트

PC 버전

GITHUB 프로젝트

주요 게임 플레이

1.의사
	- 조종사 발견 후, 탈 것을 통해 옮기기
    - 조종사 정밀 검사 진행
    - 조종사 치료
    - 조종사에게 약 투여 후, 2층으로 이동
2.생화학자
	- 모니터 UI 상호작용
    - 화이트 보드, 메모 힌트 활용
    - 액화 장치, 혼합기, 설명서
    - 로켓 연료
3.기계공학자
	- 모니터와 메모, 방 내부 힌트
4.전기공학자
	- 제어 패널 전선 연결
    - 2층 계단 내리기
    - 계단에 놓인 장애물 파괴하기
5.협동 퀘스트
	- 물 처리 시스템 수리하기
    - 파이프 연결
    - 비상식량 가져오기
    - 탈출정 타고 탈출하기

의사

1. 의사
	- 조종사 발견 후, 탈 것을 통해 옮기기
    - 조종사 정밀 검사 진행
    - 조종사 치료
    - 조종사에게 약 투여 후, 2층으로 이동
  • 일단 Meta Space는 크게 5가지의 직업이 있다. 직업에 해당해서 각자 해결해야하는 퀘스트들이 주어지게 된다. 필자는 의사라는 직업을 구현하게 되었고, 퀘스트를 해결해야하는 방은 대부분 의무실 안이었다.

  • 일단, 해당 게임의 대략적인 스토리가, 우주선에 소행성이 충돌해 살아남은 사람이 탈출정을 찾아서 대피하는 스토리인데, 여기서 의사는 탈출정의 정보를 주는 사람을 치료해주는 역할로 상정하고 시작하였다.

  • 해당 기능을 구현하면서 제일 힘들었던 점은....수술 같은 장면을 구현할 때, 사전지식이 너무 부족하여 사실적인 구현을 하기가 너무 힘들었다는 점이다. 사실상 이런 스토리가 있는 주제를 개발할때 해당 스토리에 대한 탄탄한 이해를 위해 자문이나, 충분한 사전 조사를 행하고 진행했어야했지만 개발 과정에서 마감기한이 임박해 충분한 자문이나 사전조사를 하기 힘들었고, 조금 아쉽게 구현이 되었다..

    • 다음에 스토리 있는 게임을 구현할 때는, 충분한 사전 조사가 기반이 되어야한다는 것을 느꼈다.

조종사 NPC 구현

조종사 에셋

  • 일단 우리 캐릭터와 톤앤 무드가 맞는 다친 조종사 모델을 찾기가 너무 힘들었다.
  • 적당히 톤앤 무드가 맞는 다친 에셋을 찾았지만, 조종사의 느낌은 타협하기로 하였다.. 다음에는 TA와 함께 개발하고 싶다는 생각이 간절하게 느껴지는 순간이었다.

조종사 애니메이션 구현

  • 애니메이션 같은 경우 자동 rig + 애니메이션 제공을 해주는 사이트인 Mixamo 에서 fbx를 업로드해 고통스러워하는 애니메이션을 넣어주었다.
  • 해당 애니메이션을 기본 IDLE 상태로 설정해두고, 수술대나 정밀 기계 침대 위에 올릴때 눕는 애니메이션 이렇게 두개의 애니메이션을 두고 laying이라는 trigger를 설정해서 침대에 눕는 상태로 변이하도록 해주었다.

  • 그리고, 조종사에게 휠체어를 태우는 기능을 구현하기 위해서 앉는 애니메이션과 bool값을 상정하여 상태 변이를 할까 고민하였는데, 캐릭터 리깅이 팔다리가 조금 부자연스러웠으며, 위치 값을 어차피 조정해줘야하는 불편함이 있었다. 근데 캐릭터의 rotation값만 조금 조정해서 휠체어 위에 두니, 앉은것과 같이 너무 자연스러워 그냥 wheelchair와 trigger하게 되면 wheelchair의 자식값으로 설정해줘서 wheelchair의 움직임에 따라 캐릭터가 앉아서 움직이는 것 같이 구현하였다.

void Update()
    {

        if (IsWheelchairNearby() && playerNearby && !isSittingInWheelchair)
        {
            sitWheelChairBtn.gameObject.SetActive(true);
        }
        else
        {
            sitWheelChairBtn.gameObject.SetActive(false);
        }
private void OnTriggerEnter(Collider other)
    {
        if (other.gameObject == player)
        {
            playerNearby = true;
            Debug.Log("Player nearby");
        }
    }
 }

private void sitWheelChairBtnClick()
    {
        if (wheelchair != null && IsWheelchairNearby())
        {
            SitInWheelchair();
        }
    }
 
private bool IsWheelchairNearby()
    {
        float distance = Vector3.Distance(transform.position, wheelchair.transform.position);
        return distance <= interactionDistance;
    }

    public void SitInWheelchair()
    {
        transform.position = sitArea.position;
        transform.rotation = sitArea.rotation;
        transform.SetParent(wheelchair.transform);
        isSittingInWheelchair = true;
        npcAnimator.SetBool("Laying", false);
        sitWheelChairBtn.gameObject.SetActive(false);
        Debug.Log("NPC is now in the wheelchair.");
    }
  • 일단 playerNearby라는 bool 값을 통해 player가 NPC 근처에 오면 onTriggerEnter를 통해 bool값을 true로 변환하고, WheelChair를 가지고 오면 IsWheelchairNearby() 함수를 통해 휠체어가 근처에 있는지 판별 한다.
    이렇게 판별을 하여 wheelchairBtn을 활성화 시켜 클릭시 SitInWheelChair()함수를 호출해 미리 지정해둔 휠체어의 자식 중 SitArea라는 transform값으로 npc위치를 조정해준다.

TriggerArea 구현

  • 방탈출 게임이라면, 응당 비밀번호를 알아내며 추리하는 기능이 필요하다고 생각하였고, 비밀번호 입력을 위해서 현재 카메라를 고정시켜서 비밀번호 클릭을 용이하게 해야한다고 생각했다.

  • OnTriggerEnter로 Player를 감지해서 플레이어가 영역 안으로 들어왔을 때, '자세히 보기(E)' 라는 텍스트를 띄워서 PC 버전에서는 E버튼 클릭시, VR 모드에선 컨트롤러의 A버튼을 누를시에, 현재 플레이어의 카메라의 움직임을 고정시키고, 카메라의 위치를 이미 지정한 적당한 transform값으로 이동시키고, Canvas (world)를 비치도록 구현하였다.

public void EnterInteraction()
    {
        // 상호작용 위치로 이동
        mainCamera.transform.position = screenViewTransform.position;
        mainCamera.transform.rotation = screenViewTransform.rotation;

        // 카메라가 모니터를 바라보게 함
        mainCamera.transform.LookAt(screenViewTransform);

        LockCameraControl();
        screenUIManager.ShowScreenUI(); // UI 활성화
        otherCanvas.SetActive(false); // 다른 캔버스 숨기기
        interactionPrompt.SetActive(false); // 텍스트 비활성화
        isInteracting = true;
        Debug.Log("상호작용 시작");
    }

휠체어 구현

  • 위의 NPC 애니메이션에서 이미 구현했지만, 휠체어를 플레이어가 끌면서 가는 것을 구현하기 위해서 여러 가지 고민을 하다가, 휠체어를 isInteracting 상태를 통해 상태를 구분해서 isInteracting 부모를 플레이어로 하여서, 적당한 힘을 줘서 자연스럽게 끄는것 같이 구현하였다.

  • 휠체어 위치와 회전 정렬
    상호작용이 시작되면 휠체어의 방향이 플레이어의 방향과 일치하도록 설정된다.

wheelchair.transform.rotation = player.transform.rotation

  • 이를 통해 휠체어가 플레이어와 같은 방향을 바라보게 되어 시각적으로 자연스러운 상호작용이 된다.

  • 또한 휠체어의 위치는 플레이어의 앞쪽에 배치되며
    Vector3 newPosition = player.transform.position + player.transform.forward * offsetZ

  • 휠체어의 y축 위치는 유지하여 휠체어가 공중에 뜨지 않도록 한다.

  • 휠체어의 물리 설정
    wheelchairRigidbody는 기본적으로 isKinematic = true로 설정되어 물리 계산에 영향을 받지 않도록 한다.

  • 이는 휠체어가 상호작용 중에 물리적인 상호작용을 하지 않고 오직 플레이어의 움직임에 따라 이동하게 만들게 구현한것이다.

  • 플레이어 이동과 휠체어 동기화
    상호작용이 활성화되면, 플레이어의 입력에 따라 휠체어가 플레이어와 함께 움직이도록 설정하였다. Update() 메서드에서 플레이어의 입력(가로, 세로 축에 대한)을 받아 휠체어와 플레이어가 함께 움직인다.
    movement.y = 0;을 통해 휠체어가 수직 방향으로 움직이는 것을 방지하여 수평 이동만 가능하도록 구현하였다.

수술 장면 구현

  • 수술 장면 같은 경우, operating 기계에 환자를 눕힌 후, 해당 모니터에 환자의 머리를 바라보는 카메라 뷰로 구현을 하고자 하였다.

  • 이를 위해 로봇 치료실에서 수술 장면을 촬영할 카메라를 하나 두고, Render Texture를 생성하여 카메라의 Target Texture을 해당 텍스처로 설정한 뒤 material의 Shader를 Unlit/Texture 로 설정하여 해당 texture를 메인 텍스처로 두어 수술 장면을 라이브로 볼 수 있는 모니터를 구현하였다.

  • 수술은 절개, 흡입, 붕대로 감싸기의 절차로 진행되며, clipping-plane을 이용해 절개를 구현하였다. 흡입과 같은 경우 흡입 버튼을 누를 시, 피 오브젝트를 하나씩 제거할 수 있도록 기능을 구현하였다.


캐릭터 애니메이션 구현

트러블 슈팅 및 이슈

[ VR게임 ]

  • Meta Space 게임은 멀티, vr 게임으로 기획을 하였고, 멀티 담당을 했던 팀원이 1명이었고, 이외에는 모두 게임 클라이언트로 참여하였다. 따라서, 우리는 VR 기기 등록 전과 멀티 적용 전에 미리 게임 기능을 어느정도 완성해놓은 상태였다.
  • 일단 VR로 상호작용을 하기 위해서 모든 오브젝트의 대응 키를 컨트롤러의 키로 바꿔야했으며, VR ray interactable을 도입하여 버튼 클릭 및 UI 클릭을 구현해야했다.
    • 시간이 너무 소요되어서, 미리 VR 테스트를 도입해서 기능을 구현할것이라는 후회가 막심했다. 다음 기획 때는 무조건적으로 이런 요소에 대응해서 시간계획을 잡아야할 것 같다.
  • 그리고 VR로 빌드 하였을때, 버퍼링이 너무 심해 게임 플레이에 지장을 주었다.
    • VR로 게임을 제작할때, 성능 최적화를 굉장히 신경쓰지 않으면 버벅임이 심하다고 한다는 것을 이때 알았다.. 우리는 게임 씬이 하나의 씬에 모두 있었으며, 에셋들도 크기가 상당했기 때문에 VR정도의 사양이 감당하기 힘든 수준의 게임이었던 것 같다.
  • 이후에 VR로 개발을 할때는 키 설정이나, 캔버스, 위와 같은 성능 최적화를 염두에 두고 개발해야겠다고 느꼈다.

[ PC - 멀티 버전 ]

  • 동기화의 문제.
    • 일단, 멀티와 같은 경우도 모든 기능을 구현한 후에 적용을 하였었다. 때문에 이후에 플레이어와 상호작용하는 모든 물체에 networked 세팅을 해주어야하였다.
    • 또 , 우리는 포톤 퓨전2를 사용하여서 구현을 하였는데, 포톤 퓨전2 에서는 Host mode와 Share mode가 존재한다.

Host Mode

  • 호스트가 서버 역할을 수행:
  • 게임에서 한 클라이언트가 호스트로 지정되며, 이 클라이언트가 서버 역할을 한다.
    모든 클라이언트는 변경 사항을 호스트로 전달해야 하며, 호스트가 이 정보를 처리하고 나머지 클라이언트에게 동기화한다.
    호스트가 게임의 상태를 완전히 관리하고, 다른 클라이언트가 게임 내 데이터를 변경하면, 그 정보는 호스트로 전송되어 동기화된다.

Share Mode

  • 모든 클라이언트가 동등한 권한을 가짐:
  • 서버와 클라이언트의 구분이 없이, 모든 클라이언트가 동일한 권한을 가진다.
  • 각 클라이언트는 게임의 상태를 직접 업데이트하며, 동기화는 클라이언트 간에 공유된다.

우리는 이 두가지 중에 HostMode로 구현하였고, 동기화를 위해서 Networked Object로 객체를 관리하며, 상태 동기화를 위해 State Authority를 사용하였다. 또, 물리 상호작용의 동기화는 Networked RigidBody를 통해 구현하였으며, 호스트가 물리 계산을 담당했다.


  • Networked Rigidbody 3D와 동기화 문제

  • Networked Rigidbody 3D를 사용하면 Fusion Runner가 물리 시뮬레이션과 관련된 처리(ex. 위치, 속도)를 자동으로 생성하게 된다. 그러나 중요한 점은 기본 설정으로 클라이언트 동기화가 비활성화되어 있다는 점이다.. Networked Rigidbody 3D를 씬에서 사용하니 Runner에서 Default로 클라이언트 시뮬레이션과 관련된 설정이 생성되어있었으며,

  • 이렇게, Client Physics Simulation 값이 Disabled로 설정되어있다.. 이거 때문에 되던 client 동기화가 아무것도 안돼서 며칠간 고통 받았지만 다른 분들은 참고해서 수정하시길 바라는 마음에 업로드합니다..

  • 이렇게, Simulate Always로 설정해주었다.


onTriggerEnter 이슈 (2번 실행되는 문제)

  • onTriggerEnter 이벤트를 많이 사용하였는데, 클라이언트에서 이상하게 2번 발생하는 문제가 발생됐다.
  • 이는 아마도 동기화의 프레임 단위 차이에서 비롯된것 같은데, onTriggerEnter는 1프레임 기준으로 호출되며, 트리거 충돌이 감지되는 즉시 이벤트를 발생된다.
  • 반면, 네트워크 동기화는 물리적인 이벤트와 함께 onFixedUpdate를 기준으로 동작하므로, 두개가 실행되는 시간적 차이 때문에 두번 실행되지 않을까 하는 합리적 의심이 들었다.
  • 이 문제와 같은 경우 도저히 해결책을 찾지못해, 코루틴을 호출해서 한번 함수 호출하고 몇초간의 딜레이를 강제로 주어서 또 다른 키 입력이 들어오지 못하게 막아버렸다...

그렇게, 장장 3개월동안

1. 맵 모델링

2. VR 적용

3. 포톤 멀티 구현

4. 캐릭터 움직임 및 NPC 상호작용

등등 여러가지 도전을 해보았던 프로젝트였다. 아쉬운 점도 많았지만, 이 프로젝트를 통해 많은 공부가 되었다. 다음 프로젝트는 꼭 좋은 결과를 내보고 싶다..!

끝...!

profile
새싹 BJY

0개의 댓글