[ VR Project ] 세종대학교 창의설계경진대회 수상작

devKyoun·2024년 10월 25일
0

Unity

목록 보기
13/27

컴퓨터 이사 이슈로 자료 다소 없음

소개

창의설계경진대회 나가서 수상했던 기록을 포스팅 해보려고 한다

무엇을 만들었는가하면

청각 장애인을 위한 수어 교육 VR 을 만들었다

주제에 게임 개발 주제가 없었지만 Unity라서 신나게 개발 했던 기억이..

보면 알다시피 구현을 위해 다양한 Asset 들을 사용했다.

그러나 모바일, VR기기에 더 최적화된 URP를 사용하다보니 프로젝트를 옮기다가 갑자기 Material 깨짐 현상이 있었는데 대충 색깔을 넣으면 됐었지만 좀 퀄리티 있게 할려고 뒤지다가 못했었다.. 뭣보다 대회에서 보여주려고 하는 부분에서는 그게 중요한게 아니기 때문에 넘어 갔었다.

  1. 처음 캐릭터를 선택
  2. 가상의 시나리오 선택
  3. 해당 시나리오에서 수어 손 동작 일치시 초록색 글자 색으로 정답 표시
  4. 불일치시 빨간색으로 오답 표시

필요성

청각 장애인을 위한 수어 교육이 VR로 왜??

  • 수어는 언어가 굉장히 복잡하고 다양
  • 2D 화면으로 보기에는 수어의 큰 동작에 혼란
  • 수어 교육 장소의 적은 수
  • 수어 교육은 다른 사람과 소통해야 더 실력이 월등히 상승 ( 언어와 마찬가지 )
  • 언어는 현실에서 꼭 마주치거나 필요한 환경에서 더욱 실력이 상승한다

내 역할

VR이다보니 멀티 플랫폼에 특화된 유니티로 진행 했고 내가 맡은 역할이다

  • 유니티 VR 환경 구축
  • 멀티플레이 지원 ( Photon )
  • 교육 데이터 DB 저장 ( Firebase )
  • 교육 실행 UI 구축

아쉽게도 Photon과 Firebase 는 프로젝트에 담았으나 대회 제출에는 빼두었다

Photon 같은 경우에는 멀티 시연 영상이 필요했는데 그것을 촬영 할 시간이 없었다
( 무엇보다 기기가 없었고 있던 기기 마저도 같은 팀원이 연구실에 있었기에 가능한 일.. )

Firebase는 프로젝트를 옮길때 마다 호환성 문제가 너무 일어났다.
유명하게도 Firebase같은 경우는 VR Project와 굉장히 호환성이 안좋기로 소문남
어찌저찌 해결은 했으나 마지막에 다른 팀원의 PC로 프로젝트 옮겼을때 또 호환성 문제가 발생해서 다시 호환성 해결하려면 시간이 소요 되기 때문에 과감하게 뺌


유니티 VR 환경 구축

당시 환경 구축은 기기로 사용되다 보니 그래픽 API를 Open GLE3이었나 적합한 환경으로 바꾼 뒤 Occulus Interaction 에셋을 설치 해줬어야했다

사용한 기기가 Occulus pro 인데 즉시 우리 손동작과 상호작용이 가능한 기기이다
그래서 그 기기와 Unity 프로젝트에 대한 플러그인을 설치해둘 필요가 있었다

외에는 가이드라인이 잘 돼 있어서 그것이 도움이 많이 됐다
대부분 설정 문제였고 이 부분에선 크게 문제 될 건 없었다

Photon

멀티 플레이 지원을 위해서 포톤을 사용했다
해당 부분은 Unity 내에서 포톤 에셋을 설치 할 수 있었고
포톤 사이트에서 서버를 개설한 뒤에 해당 서버의 ID를 유니티 내에서 포톤 에셋에 ID를 치면 연동이 가능했다

PUN이나 FUSION은 오래됐고 FUSION2가 새로나와서 새로운 서버 API를 채택했다
FUSION2 위에 보면 App ID가 있는데 이것을
App ID에 입력해주면 된다

이제 해당 네트워크를 관리할 NetworkManager
이는 싱글톤 패턴으로 구현해야한다 그렇지 않으면 Scene 이동을 할때 여러 NetworkManager가 생겨서 문제가 생겼었음

using Fusion;
using Fusion.Sockets;
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.SceneManagement;
public class NetworkManager : MonoBehaviour, INetworkRunnerCallbacks
{
    public static NetworkManager instance { get; private set; }

    [SerializeField] private GameObject networkRunnerPrefab;
    [SerializeField] private NetworkObject playerPrefab;
    //[SerializeField] private NetworkObject networkCharacterPrefab;

    private Dictionary<PlayerRef, NetworkPlayer> NetworkPlayers = new();
    public NetworkRunner runner;

    public UnityEvent OnConnectionStart;
    public UnityEvent OnConnectionSuccessfull;

    public delegate void OnPlayerSpawn(NetworkRunner runner, PlayerRef playerRef);
    public event OnPlayerSpawn onPlayerSpawn;

    public delegate void OnSceneLoadStartDelegate(NetworkRunner runner);
    public event OnSceneLoadStartDelegate onSceneLoadStart;

    public delegate void OnSceneLoadDoneDelegate(NetworkRunner runner);
    public event OnSceneLoadDoneDelegate onSceneLoadDone;
    private void Awake()
    {

        if (!instance)
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
        CreateNetworkRunner();
    }
    private void Start()
    {
        ConnectGame();
    }
    private void CreateNetworkRunner()
    {
        if (!runner) runner = Instantiate(networkRunnerPrefab, transform).GetComponent<NetworkRunner>();
        runner.AddCallbacks(this);
    }

    public async void ConnectGame()
    {
      

        OnConnectionStart.Invoke();

        var args = new StartGameArgs()
        {
            GameMode = GameMode.Shared,
            SessionName = "Test",
            Scene = SceneRef.FromIndex(SceneManager.GetActiveScene().buildIndex),
            SceneManager = gameObject.AddComponent<NetworkSceneManagerDefault>()
        };

        var connectionResult = await runner.StartGame(args);

        if (connectionResult.Ok)
        {
            OnConnectionSuccessfull.Invoke();
            Debug.Log("StartGame successfull");
        }
        else
        {
            Debug.LogError(connectionResult.ErrorMessage);

        }
    }
    public void AddPlayer(PlayerRef player, NetworkPlayer networkPlayer)
    {
        NetworkPlayers[player] = networkPlayer;
        networkPlayer.transform.SetParent(runner.transform);
    }
    public NetworkPlayer GetPlayer(PlayerRef player = default)
    {
        if (!runner) return null;
        if (player == default) player = runner.LocalPlayer;

        NetworkPlayers.TryGetValue(player, out NetworkPlayer networkPlayer);
        return networkPlayer;
    }

    public void RemovePlayer(PlayerRef player)
    {
        if (NetworkPlayers.ContainsKey(player))
        {
            NetworkPlayers.Remove(player);
        }
        else
        {
            Debug.LogWarning("This player: " + player + " not found");
        }

    }

    private void SpawnPlayer(NetworkRunner runner, PlayerRef player)
    {
        if (player == runner.LocalPlayer)
        {
            runner.Spawn(playerPrefab, transform.position, transform.rotation, player);
            //   runner.Spawn(networkCharacterPrefab, Vector3.zero, quaternion.identity, player);
            onPlayerSpawn?.Invoke(runner, player);
        }

    }

    #region NetworkRunnerCallbacks

    public void OnPlayerJoined(NetworkRunner runner, PlayerRef player)
    {
        Debug.Log("NewPlayer Joined" + player);
        SpawnPlayer(runner, player);
    }
    public void OnPlayerLeft(NetworkRunner runner, PlayerRef player)
    {

    }

    public void OnInput(NetworkRunner runner, NetworkInput input)
    {

    }

    public void OnInputMissing(NetworkRunner runner, PlayerRef player, NetworkInput input)
    {

    }

    public void OnShutdown(NetworkRunner runner, ShutdownReason shutdownReason)
    {

    }

    public void OnConnectedToServer(NetworkRunner runner)
    {

    }

    public void OnDisconnectedFromServer(NetworkRunner runner)
    {

    }

    public void OnConnectRequest(NetworkRunner runner, NetworkRunnerCallbackArgs.ConnectRequest request, byte[] token)
    {

    }

    public void OnConnectFailed(NetworkRunner runner, NetAddress remoteAddress, NetConnectFailedReason reason)
    {

    }

    public void OnUserSimulationMessage(NetworkRunner runner, SimulationMessagePtr message)
    {

    }

    public void OnSessionListUpdated(NetworkRunner runner, List<SessionInfo> sessionList)
    {

    }

    public void OnCustomAuthenticationResponse(NetworkRunner runner, Dictionary<string, object> data)
    {

    }

    public void OnHostMigration(NetworkRunner runner, HostMigrationToken hostMigrationToken)
    {

    }

    public void OnReliableDataReceived(NetworkRunner runner, PlayerRef player, ArraySegment<byte> data)
    {

    }

    public void OnSceneLoadDone(NetworkRunner runner)
    {
        onSceneLoadDone?.Invoke(runner);
    }
    public void OnSceneLoadStart(NetworkRunner runner)
    {
        onSceneLoadStart?.Invoke(runner);
    }

    public void OnObjectExitAOI(NetworkRunner runner, NetworkObject obj, PlayerRef player)
    {

    }

    public void OnObjectEnterAOI(NetworkRunner runner, NetworkObject obj, PlayerRef player)
    {

    }

    public void OnDisconnectedFromServer(NetworkRunner runner, NetDisconnectReason reason)
    {

    }

    public void OnReliableDataReceived(NetworkRunner runner, PlayerRef player, ReliableKey key, ArraySegment<byte> data)
    {

    }

    public void OnReliableDataProgress(NetworkRunner runner, PlayerRef player, ReliableKey key, float progress)
    {

    }
    #endregion
}

너무 오래전에 만든거라 기억이 가물한데 유니티 내에서는 Network Runner라는 것이 있어야 플레이어간 상호작용이 가능했다.

이게 무슨말이냐면 플레이어가 두명일때 한 플레이어가 움직이는 것을 업데이트 하기 위해서는 동기화가 필요하다

포톤에서는 그 동기화를 실행해주는 녀석이 실질적으로 Network Runner이다
이 NetworkRunner가 포톤의 Callback System을 이용해서 플레이어들을 참가 시키고 동기화를 해주는 것이다

개별적으로 해당 부분을 확인하기 위해서 프로젝트 하나를 만들었는데 해당 자료가 영상으로 없다..

다만 이것이 작동하는지 확인하기 위해서는 VR 기기가 필요했는데 연구실에 있는 팀원의 VR기기는 세종대학교에서 지원해주는 거라서 이용 못했다

하지만 유용한 XR Intraction Tool kit이 있었다!
이거는 VR기기가 없더라도 테스팅 하도록 도와주는 Tool 이다

해당 부분으로 프로젝트를 테스팅 했다

그전에 3D object Cube를 두개 생성하고 투컴으로 이동을 통해서 네트워크 동기화를 확인하는 과정이 있었음

Firebase

나를 가장 애먹인 친구..
이 Firebase는 VR에 Import 하는 Package 마다 전부 호환성 문제를 일으킨다
특히 이 VR로 빌드를 하면은 Android APK 를 설치해야 하는데 이게 Firebase와 호환이 가장 맞지않는다

Gradle 버전 문제부터해서 Andoroid APK 가 경로가 이상하면 문제가 생기고
또 유니티 허브에서 원하는 Andorid APK 버전을 다운하기 쉽지가 않다

그래서 예전에 북마킹해가지고 참고한 사이트가 있다
https://wincnt-shim.tistory.com/402
도움 많이 됐습니다 감사합니다..

어쨋든 Firebase 홈페이지로 가서 Unity 환경으로 생성해주면 Firebase.package를 다운 받을 수 있다. ( Author Package 등등 설정에 따라 달랐었음 )
이 패키지를 추가하는 순간 대참사가 일어났었다.

VR과 관련된 Package들은 절대로 업데이트 하지말자.. 바로 또 오류가 난다 아마 참조에서 문제가 되는듯

그래서 Firebase를 빼기로 마지막에 결정이 났다..

어쨋든 포톤 멀티플레이 환경을 위해선 계정관리가 필요한데 이 계정에 대한 데이터를 파이어베이스를 통해 관리 해주려고 했다

근데 Firebase가 빠져버리니 자연스레 Photon도 넣지 않는것으로 됐었다

UI 구축

시작부터 로비 설정들은 Scene Loader 사용하면 된다

그 외에는

영상을 보면 손으로 버튼을 눌렀을때 클릭이 된다
해당 버튼은 유니티 UI 이벤트를 활용한 것이고, 이 이벤트가 진행이 되면
팀원이 만들어 놓은 애니메이션을 진행 시키도록 설정하였다

정답,오답 같은 경우는 버튼 폰트로 설정해준건데 앞에 애니메이션으로 인해서 아바타가 움직이는 동작을 사용자가 90% 일치하도록 따라하면 정답(초록색), 아니면 오답(빨간색)을 나오게 했다

그렇다면 저 버튼은 어떻게 클릭 할 수 있도록 했냐면
VR 상에서 Occulus Interaction Toolkit에 그리드 설정이 있다
그 그리드 설정을 버튼의 Transform과 일치 시켜서 사용자의 얼굴 각도에 따라 조정해주면 해당 그리드 위치에 손을 댔을때 상호작용이 일어난다

참조한 유튜브 영상인데 해당 유튜브 영상의 주제는 아니지만 Demo Scene을 통해서 참조 해가지고 구현했다
https://www.youtube.com/watch?v=Jingen9O9Wc&t=310s


결론

해당 자료들을 노트북으로 했었어서 남은 자료가 많이 없었지만 환경 구축면에서 굉장히 도움 됐던 프로젝트
VR은 특히 외국자료가 월등히 많아서 문서 참고하는 방법도 알게 됐고 외국 강의를 통해 영어 실력도 좀 올라갔던 시간들이었다.

이거 덕분에 좀 어려운 코드나 어려운 상황에 놓였을때 해결하는 속도가 엄청 향상된듯하다
좋은 캡스톤 이었다


내가 맡은 역할

profile
Game Developer

0개의 댓글