VR 콘텐츠 제작

디스크 골프 [#14] - 씬 구조 만들기2

송현호 2023. 12. 14. 17:38

저번 시간에 이어서 타이틀 씬도 마찬가지로 vr환경에서 작동하도록 만들어보자

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class TitleMain : MonoBehaviour
{
    public Button btn;
    public System.Action onLoadLobbyScene;

    public Transform player;
    public Transform targetCamera;

    public OVRInput.Controller controller;


    private void Update()
    {
        this.player.position = this.targetCamera.position;

        if(OVRInput.Get(OVRInput.Button.One,controller) || OVRInput.Get(OVRInput.Button.Two, controller))
        {
            onLoadLobbyScene();
        }
    }
}

 

시네머신 브레인의 카메라를 삭제하고 OVR의 Camera를 추가해서 시네머신 브레인으로 바꿔주었는데 OVR의 카메라는 기본적으로 OVRCameraRig에 붙어 있기 때문에 돌리카트를 따라가지 않았다. 그래서 코드를 작성해서 돌리머신의 위치와 카메라리그의 위치를 동기화 시켜 주었다.

 

그래서 실행시켜 봤는데 실제 VR기기로 봤을때 타이틀 씬의 타이틀이 보이지 않는 다는 문제점이 생겼다.

그래서 타이틀씬을 월드 캔버스로 바꿔서 OVRCamera의 정면을 바라보도록 바꿔주자.

 

 

그런데도 타이틀이 보이지 않았는데 알고 보니까 OVRCameraRig의 카메라는 UI를 컬링하도록 설정되어 있어서 UI가 보이지 않았다.

오큘러스 카메라의 UI를 켜주니 정상적으로 타이틀이 보였다.

 

타이틀이 정상적으로 나오는 걸 확인했으니 타이틀에 있는 터레인을 로비씬으로 가져가서 그대로 배경으로 사용하자

로비UI를 보여주되 타이틀씬처럼 카메라를 움직이게 하지는 않을 것이다.

 

DontDestroyOnLoad를 메서드를 사용해서 앱씬에서 타이틀씬으로 넘어갈때 터레인을 생성한다음  

로비씬에서 게임씬으로 넘어갈때 배경용 터레인을 비활성화하도록 하자,

 

먼저 터레인이 들어 있는 오브젝트인 nature를 프리펩화 해준다음 Resources 폴더에 넣어주었다.

 

코드를 작성하자

private void Start()
{
    AsyncOperation oper = SceneManager.LoadSceneAsync("TitleScene");
    GameObject go = Instantiate(Resources.Load<GameObject>("nature"));
    DontDestroyOnLoad(go);
    oper.completed += OnLoadCompleteTitleScene;
}

 

 

 

로비씬까지 터레인이 파괴되지 않았지만 위치가 맞지 않았다 로비씬의 위치를 터레인에 맞춰주었다.

 

 

이제 로비씬의 UI를 좀 꾸며주자.

로비UI의 오른쪽 부분에서 맵을 선택하면 왼쪽상단 맵 부분에 선택한 맵의 사진이 보이도록 할 것이다.

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class LobbyMain : MonoBehaviour
{
    enum eMap
    {
        Mountain,
        Island
    }

    public System.Action<int> onSelectGameScene;
    public System.Action onLoadGameScene;
    public Toggle[] toggles;
    public Button playBtn;
    public Image mapImage;

    private void Start()
    {
        for (int i = 0; i < toggles.Length; i++)
        {
            int j = i;
            toggles[j].onValueChanged.AddListener((toggle) =>
            {
                onSelectGameScene(j);
                mapImage.sprite = Resources.Load<Sprite>("Images/" + (eMap)j);
            });
        }

        playBtn.onClick.AddListener(() =>
        {
            onLoadGameScene();
        });
    }
}

 

 

 

그리고 에셋을 사용해서 로비씬의 UI를 꾸며주..려다가 어떤 에셋을 사용할지 선택하기 어려워서 그냥 씬전환 사이에 넣어줄 페이드인 페이드아웃을 먼저 만들기로 했다.

 

페이드인 페이드아웃은 보통 캔버스에 검은색 이미지를 띄워서 알파값을 더하거나 빼는 방식으로 만드는데 일단 평소 만들던 방식으로 만들어 보고 vr기기에서도 제대로 작동하는지 시험해봐야 할 것 같다.

 

먼저 앱씬에 페이드 이미지를 만들고 페이드 이미지에도 DontDestroyOnLoad를 적용해서 Title씬에서 검은색 이미지가 그대로 유지되는지 확인해보았다.

 

 

VR에선 카메라가 UI이미지를 인식하지 못하는 걸 볼 수 있다.

이러면 카메라 앞에 월드캔버스나 게임오브젝트 이미지를 만들어서 화면이 암전된 것처럼 보이게 하는 방법을 사용해야 할 것 같다.

이러면 App씬에 페이드 메서드를 만드는게 아니라 페이드 스크립트를 따로 만들어서 App씬에서 페이드 스크립트의 메서드를 실행하도록 만들자.

 

Fade Transition은 다음 동영상을 참고하였다.

https://youtu.be/JCyJ26cIM0Y?si=5PShobSRkRJIN7QS

 

먼저 테스트용 씬을 2개를 파서 OVRCamera를 설치한 뒤 2번째 씬의 카메라 앞에 페이드용 이미지를 넣어주었다.

그리고 페이드용 메테리얼을 만든 다음 이미지에 넣어주고, 영상에서 추천한 페이드 쉐이더를 다운받아서 메테리얼에 넣어 주었다.

 

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Fade : MonoBehaviour
{
    public float fadeDuration = 2;
    public Color fadeColor;
    private Renderer renderer;

    private void Start()
    {
        renderer = GetComponent<Renderer>();
    }

    public void FadeIn()
    {
        FadeScreen(1, 0);
    }

    public void FadeOut()
    {
        FadeScreen(0, 1);
    }

    public void FadeScreen(float alphaIn, float alphaOut)
    {
        StartCoroutine(FadeRoutine(alphaIn, alphaOut));
    }

    public IEnumerator FadeRoutine(float alphaIn, float alphaOut)
    {
        float timer = 0;
        while(timer <= fadeDuration)
        {
            Color newColor = fadeColor;
            newColor.a = Mathf.Lerp(alphaIn, alphaOut, timer / fadeDuration);

            renderer.material.SetColor("_Color", newColor);

            timer += Time.deltaTime;
            yield return null;
        }

        Color newColor2 = fadeColor;
        newColor2.a = alphaOut;
        renderer.material.SetColor("_Color", newColor2);
    }
}

 

newColor의 알파값을 Lerp를 사용해서 늘리거나 줄이는 메서드로 timer값이 2를 넘으면 Lerp값도 최대값이 되면서 while문이 종료된다.

material.SetColor 메서드를 사용해서 알파값을 넣어주는데 괄호 안의 _Color는 메테리얼의 기본 색상을 의미한다.

 

실제로 스크립트가 작동하는지 Start문을 이용해서 한번 실험해보자.

 

private void Start()
{
    renderer = GetComponent<Renderer>();
    FadeIn();
}

 

 

잘 작동한다 씬마다 페이드 이미지를 만들고 App씬에서 FadeIn과 FadeOut을 수행하게 만들자 페이드 이미지는 프리펩화 해주었다.

App씬에서 씬을 이동할때마다 페이드 스크립트를 찾아서 페이드인아웃을 할 수있게 만들었다.

private void OnLoadCompleteTitleScene(AsyncOperation obj)
{
    Fade fade = GameObject.FindObjectOfType<Fade>();
    fade.FadeIn();
    TitleMain titleMain = GameObject.FindObjectOfType<TitleMain>();
    titleMain.onLoadLobbyScene = () =>
    {
        LoadLobbyScene();
    };
}

 

 

그런데 페이드인이 적용되지않고 그래도 검은색인 상태로 있었다. 에러코드가 있어서 살펴보니

 

 

renderer.material.SetColor("_Color", newColor);

 

널레퍼런스가 떴는데 아무래도 렌더러를 찾지 못하는 것 같았다. 직감적으로 호출순서의 문제라고 생각해서 페이드 스크립트의 렌더러 컴포넌트를 찾는 코드를 Awake로 바꿔주었다.

 

 

Awake로 바꾸자 정상적으로 페이드인이 작동했다. 이제 App스크립트를 수정해서 페이드 이미지를 찾은 다음,

씬을 전환할때마다 페이드인 페이드아웃을 수행하도록 만들어주겠다.

 

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class App : MonoBehaviour
{
    private int selectedGameSceneNum;
    private Fade sceneFade;
    private void Awake()
    {
        DontDestroyOnLoad(this.gameObject);
    }

    private void Start()
    {
        AsyncOperation oper = SceneManager.LoadSceneAsync("TitleScene");
        GameObject go = Instantiate(Resources.Load<GameObject>("nature"));
        DontDestroyOnLoad(go);
        oper.completed += OnLoadCompleteTitleScene;
    }
    private void OnLoadCompleteTitleScene(AsyncOperation obj)
    {
        sceneFade = GameObject.FindObjectOfType<Fade>();
        sceneFade.FadeIn();
        TitleMain titleMain = GameObject.FindObjectOfType<TitleMain>();
        titleMain.onLoadLobbyScene = () =>
        {
            StartCoroutine(LoadLobbyScene());
        };
    }

    private IEnumerator LoadLobbyScene()
    {
        sceneFade.FadeOut();
        AsyncOperation oper = SceneManager.LoadSceneAsync("LobbyScene");
        oper.completed += OnLoadCompleteLobbyScene;
        oper.allowSceneActivation = false;

        float timer = 0;
        while(timer <= sceneFade.fadeDuration && !oper.isDone)
        {
            timer += Time.deltaTime;
            yield return null;
        }

        oper.allowSceneActivation = true;
    }

    private void OnLoadCompleteLobbyScene(AsyncOperation obj)
    {
        sceneFade = GameObject.FindObjectOfType<Fade>();
        sceneFade.FadeIn();
        LobbyMain lobbyMain = GameObject.FindObjectOfType<LobbyMain>();
        lobbyMain.onLoadGameScene = () =>
        {
            StartCoroutine(LoadGameScene());
        };

        lobbyMain.onSelectGameScene = (int num) =>
        {
            SelectGameScene(num);
        };
    }

    private void SelectGameScene(int num)
    {
        this.selectedGameSceneNum = num;
    }

    private IEnumerator LoadGameScene()
    {
        float timer = 0;
        switch (this.selectedGameSceneNum)
        {
            case 0:
                sceneFade.FadeOut();
                AsyncOperation oper0 = SceneManager.LoadSceneAsync("GameScene");
                oper0.completed += OnLoadCompleteGameScene;
                oper0.allowSceneActivation = false;

                while (timer <= sceneFade.fadeDuration && !oper0.isDone)
                {
                    timer += Time.deltaTime;
                    yield return null;
                }

                oper0.allowSceneActivation = true;
                break;
            case 1:
                sceneFade.FadeOut();
                AsyncOperation oper1 = SceneManager.LoadSceneAsync("IslandScene");
                oper1.completed += OnLoadCompleteGameScene;
                oper1.allowSceneActivation = false;

                while (timer <= sceneFade.fadeDuration && !oper1.isDone)
                {
                    timer += Time.deltaTime;
                    yield return null;
                }

                oper1.allowSceneActivation = true;
                break;
            default:
                Debug.Log("아직 안 만듬");
                break;
        }
    }

    private void OnLoadCompleteGameScene(AsyncOperation obj)
    {
        //페이드이미지 찾아서 페이드인
        //대리자에 씬로드 할당
    }
}