GameMakeStory

DownFallSequence - UI 제작 본문

DownFallSequence(다폴시)

DownFallSequence - UI 제작

HunterRabbit_Kim 2023. 7. 13. 03:30

1. 설계

이전 글에서는 인벤토리와 Tooltip에 대해서 정리하였다.

https://gamemakestory.tistory.com/9

 

DownFallSequence- 인벤토리 시스템과 아이템 (4)

1. 설계 이전 글에서는 Tooltip에 대해서 설명하였다. https://gamemakestory.tistory.com/8 DwonFallSequence - 인벤토리 시스템과 아이템(3) 1. 설계 이전 글에서는 인벤토리 UI와 인벤토리에 아이템이 들어가도록

gamemakestory.tistory.com

이번 글에서는 인벤토리를 띄우고 시간과 돈 등등 다양한 UI를 제작하고 관리하는 시스템을 제작할 것이다.

 

현재까지 제작했던 UI로는 탐색 아이템, 대화창, 인벤토리 등등의 UI를 제작하였다.

이번에는 맵, 돈, 시간, 행동력 등등의 UI를 제작하고 관리하는 시스템들을 만든다.

 

이번 글에서 개발에 필요한 것들이 무엇이 있는지 살펴보자

  • 각종 UI 제작
  • UI 스크립트 및 UI 관련된 스크립트 작성

2. UI 제작

2-1. 맵 UI

맵을 이동 하면서 NPC를 만날 수 있도록 UI를 제작한다.

빨간색 원은 내가 있는 지역인 곳이고 나머지는 이동할 수 있는 곳이다.

이처럼 전체적인 맵(월드 맵)과 미니 맵(세타르, 두르칸, 고향, 솔라오로스)으로 이루져 있으며, 모두 위 그림의 형태의 원과 점선으로 표시되어 있다.

또한 여기 어디인지 알 수 있도록 맵 이름을 표시하였다.


2-2. 날짜, 시간, 행동력, 돈 UI

본 게임에서는 날, 시간, 행동력, 돈을 한곳으로 모아 UI를 제작하였다.

  • 날은 1일 부터 시작하며, 며칠이 지났는지에 대해서 알려준다.
  • 시간은 새벽, 아침, 점심, 저녁, 심야로 총 5개로 이루어져 있으며, 행동력이 5를 다 쓰거나 월드 맵에서 이동할 때 바뀐다.
  • 행동력은 5로 지역 맵에서 돌아다닐때나 NPC와 이야기할 때 사용된다.
  • 행동력 5을 다 쓰면 시간은 변경된다.
  • 돈은 말 그대로 본 게임의 돈을 나타낸다.
  • 상점이나, 정보를 구입할 때 사용된다.

2-3. 기타 UI

본 게임에 사용하는 호위 시스템, 각종 아이템들에 대해서 UI를 제작한다.

호위 시스템은 하늘과 바람과 별과 게임의 블로그에서 자세히 확인할 수 있다.


https://swsg.tistory.com/11

 

DownFallSequence - 기획 및 시스템 구현(호위&세이브)

1. 설계 및 기획 이전 글 : https://swsg.tistory.com/10 DownFallSequence - 기획 및 시스템 구현(이벤트) 1. 설계 및 기획 다운폴 시퀀스에는 여러 유형의 NPC(캐릭터)를 만나도록 설계한 게임이다. 하지만 대화

swsg.tistory.com


  • UI 이미지

왼쪽부터 하나씩 설명하자면

 

호감도 측정기

  •  외알 안경 아이템, 러브 스코프라는 아이템에서 생성
  •  클릭 시 NPC의 호감도를 볼 수 있다.

호위

  • 클릭 시 호위와 관련된 UI들을 보여준다.

  • 잠을 잘 수 있도록 해주는 클릭형 UI

  • 클릭시 월드 맵으로 이동한다.

가방

  • 인벤토리를 열고 닫는 기능을 가진 UI

거짓말 탐지기

  • NPC가 거짓말할 시 흔들린다.

3. 스크립트 제작

3-1. UI 스크립트

  • 변수 선언

    [SerializeField]
    private Camera cam;

    public GameObject map;
    public GameObject inventoryMenu;
    public GameObject Escort;
    
    public Text GoldText;
    public int gold = 100;

    public Text Lovetext;
    public int count;

    public MoveManager moveManager;
    public TimeManager timeManager;

    GameManager gameManager;

    public bool LittleMap;
    public bool EscortCheck;
    public bool lovescout;
    public bool OptionOnOff = false;

    public GameObject SleepButton;
    public GameObject MapButton;
    public GameObject EscortButton;
    
    private void Start()
    {
        Escort.gameObject.SetActive(false);
        inventoryMenu.gameObject.SetActive(false);
        map.gameObject.SetActive(false);
        gameManager = GameObject.Find("GameManager").GetComponent<GameManager>();
        LittleMap = false;
        EscortCheck = false;
        count = 0;
    }

UI 관리를 위한 변수들을 선언하고 초기화와 변수에 대한 내용들을 찾아준다.

 

    • Update()

    private void Update()
    {
        GoldText.text = "돈 : "+ gold.ToString() + " G";
        if (Input.GetKeyDown(KeyCode.Escape))
        {
            if(OptionOnOff == false)
            {
                OptionOnOff = true;
                StartChatColliderOff();
                GameObject.Find("UICanvas").transform.Find("EscPanel").gameObject.SetActive(true);
                
            }
            else
            {
                OptionOnOff = false;
                StartChatColliderOn();
                GameObject.Find("UICanvas").transform.Find("EscPanel").gameObject.SetActive(false);
                
            }
            
        }
    }

돈은 사용하면 변경을 하기 때문이고 옵션도 on/off 있도록 Update()에 넣어준다.

나머지 날, 시간, 행동력은 TimeManager 스크립트에 저장되어 있다.

 

  • Mapcontrol()

맵 컨트롤은 맵을 이동하기 위한 함수이다.

이 함수는 2가지의 방법으로 나누어진다.

  • 지역 맵이 있을 경우
  • 월드 맵만 있을 경우

    public void MapContorl()
    {
        //미니맵이 있을 경우
        if (gameManager.place == "HomeTown"|| gameManager.place == "Home1" || gameManager.place == "Home2" || gameManager.place == "Setar" || gameManager.place == "SetarSmithy" ||
            gameManager.place == "SetarChurch" || gameManager.place == "SetarArena" || gameManager.place == "SetarStable" || gameManager.place == "SetarGuild" || gameManager.place == "SetarInn" ||
            gameManager.place == "Solar" || gameManager.place == "SolarSmithy" || gameManager.place == "SolarChurch" || gameManager.place == "SolarGuild" || gameManager.place == "SolarGuildIn" ||
            gameManager.place == "SolarChurchIn" || gameManager.place == "SolarStable" || gameManager.place == "SolarInn" || gameManager.place == "SolarLibrary" || gameManager.place == "SolarCastle" ||
            gameManager.place == "SolarKnight" || gameManager.place == "Durkan" || gameManager.place == "DurkanFish" || gameManager.place == "DurkanPort" || gameManager.place == "DurkanChurch" ||
            gameManager.place == "DurkanStable" || gameManager.place == "DurkanGuild" || gameManager.place == "DurkanInn")
        {
            switch (gameManager.place)
            {
                case "HomeTown":
                    if (LittleMap == false)
                    {
                        GameObject.Find("UICanvas").transform.Find("WorldMapBtn").gameObject.SetActive(true);
                        cam.transform.position = new Vector3(-80, 60, -10);
                        LittleMap = true;
                    }
                    else
                    {
                        map.gameObject.SetActive(false);
                        GameObject.Find("UICanvas").transform.Find("WorldMapBtn").gameObject.SetActive(false);
                        cam.transform.position = new Vector3(0, -0, -10);
                        LittleMap = false;
                    }
                    break;

                case "Home1":
                    if (LittleMap == false)
                    {
                        GameObject.Find("UICanvas").transform.Find("WorldMapBtn").gameObject.SetActive(true);
                        cam.transform.position = new Vector3(-80, 60, -10);
                        LittleMap = true;
                    }
                    else
                    {
                        map.gameObject.SetActive(false);
                        GameObject.Find("UICanvas").transform.Find("WorldMapBtn").gameObject.SetActive(false);
                        cam.transform.position = new Vector3(0, -30, -10);
                        LittleMap = false;
                    }
                    break;

                case "Home2":
                    if (LittleMap == false)
                    {
                        GameObject.Find("UICanvas").transform.Find("WorldMapBtn").gameObject.SetActive(true);
                        cam.transform.position = new Vector3(-80, 60, -10);
                        LittleMap = true;
                    }
                    else
                    {
                        map.gameObject.SetActive(false);
                        GameObject.Find("UICanvas").transform.Find("WorldMapBtn").gameObject.SetActive(false);
                        cam.transform.position = new Vector3(30, -30, -10);
                        LittleMap = false;
                    }
                    break;
                    
                    // ... 이하 코드 생략 ...
            }
        }
        else //월드맵만 있을 경우
        {
            map.gameObject.SetActive(!map.activeSelf);
            if (map.activeSelf)
            {
                cam.transform.position = new Vector3(-80, 90, -10);
            }
            else
            {
                switch (gameManager.place)
                {
                    case "Home":
                        cam.transform.position = new Vector3(0, 0, -10);
                        break;
                    case "Rito_Forest":
                        cam.transform.position = new Vector3(30, 0, -10);
                        break;
                    case "Ruin":
                        cam.transform.position = new Vector3(60, 0, -10);
                        break;
                    case "Endes_Derst":
                        cam.transform.position = new Vector3(90, 0, -10);
                        break;
                    case "Setar":
                        cam.transform.position = new Vector3(120, 0, -10);
                        break;
                    case "Ureta_Mount":
                        cam.transform.position = new Vector3(240, 0, -10);
                        break;
                    case "Zailpiton_Forest":
                        cam.transform.position = new Vector3(270, 0, -10);
                        break;
                        
                    // ... 이하 코드 생략 ...
                }
            }
        }      
    }

위 코드에서 먼저 미니 맵이 있는지를 판단한다.

if문에 미니 맵이 있는 장소를 넣어주는 방식으로 했다.

그리고 switch-case문을 사용하여 미니맵으로 이동하게 만들었으며, 이때 LittleMap이라는 bool 변수를 이용하여 켜져 있는지 꺼져 있는지를 파악한다.

 

미니 맵이 없는 장소들은 바로 이동하게 만들어주었다.

 

  • 기타 UI 코드

1. EscortSys() 함수


    public void EscortSys()
    {
        if (EscortCheck == false)
        {
            GameObject.Find("EscortCanvas").transform.Find("EscortPanel").gameObject.SetActive(true);
            EscortCheck = true;
        }
        else
        {
            GameObject.Find("EscortCanvas").transform.Find("EscortPanel").gameObject.SetActive(false);
            EscortCheck = false;
        }

    }

EscortSys 함수는 호위를 보여줄 수 있는 Panel를 on/off 있도록 제작된 함수이다.

이 또한 EscortCheck라는 bool 변수를 이용하여 켜져 있는지 꺼져 있는지를 판단하여 작동한다.

 

2. 인벤토리 on/off


    public void InventoryControl()
    {
        moveManager.Sell = "InventoryBtn";
        //게임이 일시 중지된 경우 Esc 키를 누르고 게임을 다시 시작합니다.
        if (GameManager.instance.isPaused)
        {
            Resume();
        }
        else
        {
            //게임이 reusme이면 탈출을 누르고 게임을 일시 중지하십시오.
            Pause();
        }
        tooltip.HideTooltip();
    }
    public void Resume()
    {
        inventoryMenu.gameObject.SetActive(false);
        StartChatColliderOn();
        GameManager.instance.isPaused = false;
    }
    public void Pause()
    {
        inventoryMenu.gameObject.SetActive(true);
        StartChatColliderOff();
        GameManager.instance.isPaused = true;
    }

위 코드에서 isPaused라는 bool 변수를 사용하여 on/off를 한다.

이때 게임을 정지하고 인벤토리만 사용할 수 있도록 하였고, 대화창 박스콜라이더도 꺼두어 인벤토리가 켜졌을 경우 클릭이 안되도록 하게 만들었다.

 

3. 호감도 측정기

아이템 사용 시 UI가 나오도록 하는 것은 ItemUsedButton 스크립트에 작성하였다.

UI Manager 스크립트에서는 호감도 측정기 사용에 대해서 작성하였다.

또한 호감도 측정기는 두 가지 아이템에서 사용되기 때문에 두 가지로 만들어줘야 한다.

 

  • Glasses() 함수

    public void Glasses()
    {

        //호감도 3번 사용
        if (count > 3)
        {
            GameObject.Find("UICanvas").transform.Find("GlassesFavoriteBtn").gameObject.SetActive(false);
        }
        else
        {

            count += 1;
        }
    }

외알 안경은 3번 사용하면 끝내야 하기 때문에 if문을 사용하여 카운트를 하였다.

호감도 측정은 밑의 코드를 이용한다.

 

  • Lovescout() 함수

    public void Lovescout() //FPBtn에 넣기
    {
        if(lovescout == true)
        {
            // 호감도 사용
            switch (gameManager.charater)
            {
                case "LeeSidol":
                    Lovetext.text = gameManager.Npc_FP_Man[0].ToString();
                    GameObject.Find("UICanvas").transform.Find("FPImage").gameObject.SetActive(true);
                    lovescout = false;
                    break;
                case "Hammer":
                    Lovetext.text = gameManager.Npc_FP_Man[1].ToString();
                    GameObject.Find("UICanvas").transform.Find("FPImage").gameObject.SetActive(true);
                    lovescout = false;
                    break;
                case "Pupu":
                    Lovetext.text = gameManager.Npc_FP_Man[2].ToString();
                    GameObject.Find("UICanvas").transform.Find("FPImage").gameObject.SetActive(true);
                    lovescout = false;
                    break;
                case "Zoa":
                    Lovetext.text = gameManager.Npc_FP_Man[3].ToString();
                    GameObject.Find("UICanvas").transform.Find("FPImage").gameObject.SetActive(true);
                    lovescout = false;
                    break;
                    
                    // ... 이하 코드 생략 ...
            }
            GameObject.Find("UICanvas").transform.Find("FavoriteBtn").gameObject.SetActive(false);
        }
    }

switch-case문을 사용하여 캐릭터의 호감도를 볼 수 있도록 제작하였다.

캐릭터는 배열을 이용하여 쉽게 찾을 수 있게 하였고 호감도를 볼 수 있도록 text를 생성하였다.

또한 Lovescout는 하루에 한 번만 사용할 수 있기 때문에 lovescout라는 bool 변수를 이용하여 체크한다.

 

4. Wave 스크립트

NPC가 거짓말할 시 LieDetector가 흔들리기 위하여 설계된 스크립트


    public float shakeTime = 0.1f;
    public float shakeSpeed = 2.0f;
    public float shakeAmount = 1.0f;

    public Transform Object;

    public void Lie()
    {
        StartCoroutine(Shake());
    }

    IEnumerator Shake()
    {
        Vector3 originPosition = Object.localPosition;
        float elapsedTime = 0.0f;

        while (elapsedTime < shakeTime)
        {
            Vector3 randomPoint = originPosition + Random.insideUnitSphere * shakeAmount;
            Object.localPosition = Vector3.Lerp(Object.localPosition, randomPoint, Time.deltaTime * shakeSpeed);

            yield return null;

            elapsedTime += Time.deltaTime;
        }

        Object.localPosition = originPosition;
    }

위 코드에서 코루틴을 사용하여 흔들림을 제작하였다.

자기 자신의 포지션을 받아서 몇 초 동안 흔들리기 위하여 elapsedTime를 증가시켜 줘서 shakeTime 이상으로 넘어가면 멈추도록 하였다.

랜덤을 이용하여 흔들림을 진동하는 것처럼 제작하였다.


4. 어려웠던 점

맵 컨트롤 할 때 장소가 많아서 하나씩 구별하기 힘들었다. 하지만 정리를 하고 나서 다시 작성하니 빠르게 작성할 수 있었다. 여기서 기획 부분에서 많은 생각을 하게 되었다.

두 번째 NPC와 대화할 때 인벤토리를 열고 아이템을 클릭하는 순간 대화가 함께 진행되는 점과 인벤토리의 아이템을 클릭할 때 뒤의 NPC도 같이 클릭되는 현상이 일어났다.

처음에는 왜 같이 클릭되는지를 이해를 못 했지만 나중에 raycast가 겹치는다는 것을 깨닫게 되었고 박스콜라이더를 꺼주었더니 문제를 해결하였다.


5. 마치며

이번 글에서는 UI에 대해서 설명하였고 다음 글로는 상점에 대해서 설명하도록 할 것이다.