이번에는 레벨 씬에서 LevelDataList 오브젝트에 있는 Data의 Element를 활용해서 스테이지 버튼의 상태를 보여주는 방법을 포스팅하겠습니다. 어떻게 구현할지를 이미 구현하고 스샷을 남긴 그림을 첨부해보겠습니다.
LevelDataList 오브젝트에 있는 Data Element에는 isOpen 값을 가지고 있고 미션 성공한 스테이지를 포함해서 다음 스테이지까지 값을 1로 바꾸어 저장하도록 되어 있습니다. 이제 이 값을 불러와서 시작함수를 통해서 버튼 상태에 적용하겠습니다. 먼저 위 그림에서 UI를 어떤 것이 필요할지를 분석해 봅시다. 첫번째 할 일은 버튼 UI image 컴포넌트와 button 컴포넌트 접근이며, 두번째 할 일은 스테이지를 표시하는 UI Text 상태 변경을 위한 text 컴포넌트의 color 접근이며 세번째 할 일은 자물쇠 이미지를 나오도록 구현하는 방법을 스크립트로 만들어 보는 것입니다. 결국 아직 클리어 하지 못한 스테이지 버튼은 약간 어둡고 자물쇠 그림이 있으며 버튼이 작동하지 않도록 해야 합니다. 여기서 자물쇠 이미지를 구현하는 방법을 두 가지로 나누어 스크립트를 만들어 보겠습니다. 첫번째 방법은 UI image 컴포넌트를 지니고 있는 오브젝트를 활성화시키거나 비활성화 시키는 방법입니다. 어찌 되었든 간에 컴포넌트를 지니고 있는 것은 게임 오브젝트로써 오브젝트 활성 또는 비활성 시키는 SetActive(true) or SetActive(false)를 사용하는 방법니다.
Hierarchy뷰에서 빈 오브젝트를 생성하고 이름을 LevelDisplay라고 변경해줍니다. 스크립트를 한 개 만들고 이름을 LevelState라고 해주고 LevelDisplay 오브젝트에 추가해 주고 스크립트에서 using UnityEngine.UI; 선언해서 이 스크립트에서 UI를 사용하겠다고 해줍니다.
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class LevelState : MonoBehaviour { public Button[] buttons; public Text[] texts; public GameObject[] lockImages; Color newColor; string colorCode; private LevelDataList levelDataList; // Start is called before the first frame update void Start() { colorCode = "#8A8A8A"; levelDataList = GameObject.Find("LevelDataList").GetComponent<LevelDataList>(); StartCoroutine(Display()); } IEnumerator Display () { yield return new WaitForSeconds(0.001f); for (int i = 0; i < levelDataList.data.Count; i++) { levelDataList.data[0].isOpen = 1; if (levelDataList.data[i].isOpen == 0) { buttons[i].interactable = false; lockImages[i].SetActive(true); if (ColorUtility.TryParseHtmlString(colorCode, out newColor)) { texts[i].color = newColor; } } if (levelDataList.data[i].isOpen == 1) { buttons[i].interactable = true; } } } } |
Canvas에 생성된 버튼 UI는 버튼 오브젝트 밑에 Text UI의 자식 오브젝트 있습니다. Button UI가 버튼의 사각형 배경을 표시하고, Text UI가 스테이지를 숫자로 표시하게 됩니다. 아직 미션을 성공하지 못 한 스테이지의 버튼에는 자물열쇠 이미지를 배치할 것입니다. Button[]에는 각각의 스테이지 버튼들을 드래그 드롭하고, Text[]에는 각각의 텍스트를 드래그 드롭하며 GameObject[] 자물쇠 이미지를 담은 오브젝트를 배치해 줍니다. 그전에 준비물로 자물열쇠 이미지를 준비해서 유니티로 가져옵니다. 버튼 컴포넌트에는 interactable이라는 것이 있습니다. 이 값이 true이면 클릭 가능한 활성화 상태이고, false이면 클릭 불가능한 비활성화 상태가 됩니다. 이를 활용하려면 for루프를 돌 때 levelDataList.data[i].isOpen = 1이면 true로 클릭 가능하도록 하고, levelDataList.data[i].isOpen = 0이면 클릭 불가능하도록 false로 해주면 원하는 결과를 얻을 수 있습니다. 그리고 활성/비활성 상태에 따라서 버튼은 지정된 색상으로 바꿉니다. 그러나 button 컴포넌트가 interactable 상태에 따라서 버튼의 상태가 disabled와 normal로 버튼의 지정된 버튼의 색상이 출력이 되는 반면에 text 컴포넌트는 원하는 색상 출력을 위해서는 colorCode를 파싱해서 텍스트에 적용시켜야만 원하는 글자의 색상이 출력이 됩니다.
Text의 색상 변경을 위해서는 color에 접근해서 Hexadecimal을 적용해 주어야 합니다. Hexadecimal(헥사데시멀)은 6자리 코드 형태를 취하는데 원하는 색상의 코드로써 지문 같은 역할을 합니다. 원하는 Hexadecimal 값을 기록해 두었다가 스크립트 string 타입 변수에 지정해 줍니다. 지정된 값은 ColorUtility.TryParseHtmlString를 통해서 파싱한 값으로 나오게 되고, 이 값은 텍스트 색상에 적용이 가능하게 됩니다.
마지막으로 스크립트로 자물쇠 이미지 오브젝트를 제어하는 방법을 알아보겠습니다. 이미지를 표현시키는 방법을 두 가지의 스크립트를 사용해서 진행해 보겠습니다.
첫번째 위에 스크립트는 자물열쇠 이미지를 담은 lockImages를 GameObject에 담아서 SetActive를 활용해서 오브젝트를 활성 및 비활성 시키도록 하고 있습니다. 이 방법은 오브젝트를 활용하기 때문에 스크립트 명령어 작성이 간단하면서도 쉽지만 대량의 이미지 오브젝트를 일일히 만들어서 각각의 버튼 UI의 자식으로 만들어야 하는 번거로움이 있습니다.
두번째 아래 스크립트는 많은 수의 스테이지가 존재할 때 한꺼번에 이미지를 적용할 수 있도록 부모(parent)와 자식(child) 오브젝트를 사용했습니다.
using TMPro; using Unity.VisualScripting; using UnityEngine; public class LevelStateUI : MonoBehaviour { public GameObject childPrefab; // 자식으로 추가할 프리팹 public Transform[] parentTransform; // 부모가 될 Transform private LevelDataList levelDataList; void Start() { levelDataList = GameObject.Find("Data").GetComponent<LevelDataList>(); for (int i = 0; i < levelDataList.data.Count; i++) { levelDataList.data[i].isOpen = PlayerPrefs.GetInt("Cleared" + i); } AddImageObject(); } void AddImageObject() { if (childPrefab != null && parentTransform != null) { GameObject child = Instantiate(childPrefab); for(int i=0; i<parentTransform.Length; i++) { levelDataList.data[0].isOpen = 1; if (levelDataList.data[i].isOpen == 0) { child = Instantiate(childPrefab); child.transform.SetParent(parentTransform[i], false); // to set parten object, false local position } RectTransform rectTransform = child.GetComponent<RectTransform>(); if (rectTransform != null) { // to modify position rectTransform.anchoredPosition = new Vector2(-26, 26); // to modify scale rectTransform.sizeDelta = new Vector2(22, 26); // to modify anchor rectTransform.anchorMin = new Vector2(0.5f, 0.5f); rectTransform.anchorMax = new Vector2(0.5f, 0.5f); } else { Debug.LogError("Not found RectTransform Component"); } } //RectTransform rectTransform = child.GetComponent<RectTransform>(); } else { Debug.LogError("Not set Prefab or parent Transform"); } } } |
이 스크립트 주석에 보면 '자식으로 추가할 프리팹'이라고 설명해 놓았습니다. 그러면 여기서 프리팹이 뭔지를 알고 가야합니다. 유니티를 다루면서 PreFab은 꼭 사용해야 하는 기능으로 한 번 알아보겠습니다.
프리팹을 간단하게 정의하면 일관성을 유지하는 재사용 가능한 오브젝트 입니다. 프리팹 생성방법은 Hierarchy뷰에 있는 오브젝트를 Project뷰로 드래그 앤 드롭 하면 됩니다. 이렇게 생성된 프리팹은 생성 당시의 컴포넌트 및 값을 그대로 가지고 있는 상태로 저장되기 때문에 언제든지 같은 상태의 오브젝트로 Project뷰에서 끌어와서 사용할 수 있게 됩니다. 프리팹 사용방법은 반대로 Project뷰에 있는 Prefab폴더(직접 만들어서 폴더명 Prefabs라고 정하고 여기에 저장해줌)에서 원하는 프리팹을 드래그해서 Scene뷰 또는 Hierarchy뷰의 원하는 폴더에 드롭하면 됩니다. 단 UI Prefab일 경우에는 반드시 Canvas 폴더 내에 드롭해야만 해당 오브젝트가 정상적으로 나오니 유의하셔야 합니다. 이 프리팹은 갖고 있는 속성을 유지하면서 스크립트 내 instantiate로 생성할 수 있습니다. 뿐만 아니라 일관성의 특징을 가지고 있기 때문에 해당 프리팹은 모든 인스턴스에 똑같이 적용이 됩니다. 예를 들어서 그림의 Lock 이미지를 스테이지 5개의 Canvas에 적용을 해놓았다고 가정합니다. 어느 날 스테이지 3을 작업하다가 갑자기 Lock 이미지가 마음에 안들어서 중간에 프리팹의 이미지를 변경해 버리면 일반 게임 오브젝트는 스테이지 3만 변경된 Lock 이미지로 나오지만 프리팹 오브젝트는 5개의 스테이지 모두 변경된 Lock 이미지로 나오게 됩니다. 이렇게 일관적으로 나오는 프리팹은 Hierarchy뷰에서 삭제를 해도 그 정보를 그대로 갖고 있습니다. 그래서 프리팹은 다시 가져와서 사용할 수 있습니다. 한 가지 예를 더 들어보겠습니다. 체력이 1000이 되는 오크 몬스터 오브젝트가 있습니다. 이 오브젝트들을 10개 복사해서 테스트를 하고 그 중 1개를 프리팹으로 만듭니다. 9개는 프리팹이 아닌 상태입니다. 나중에 게임에 똑같은 성능을 갖은 프리팹 사용을 위해서는 프리팹 외에는 모두 삭제해야 합니다. 또는 스크립트를 통해 오브젝트 생성(instantiate)을 할 수 있기 때문에 사실 프리팹만 있다면 10개 모두 삭제해도 됩니다. 그런데 프리팹 1개를 삭제하고 실수로 프리팹이 아닌 9개 오브젝트 중 8개만 지웁니다. 테스트 중 오크의 체력이 너무 높다고 생각해서 오크 프리팹의 체력을 500으로 조정합니다. 프리팹으로 게임에 존재하거나 스크립트로 생성된 오크는 체력이 500이지만 실수로 남겨진 오크는 겉모습은 같지만 체력이 여전히 1000인 오크 오브젝트입니다. 이처럼 오브젝트는 프리팹일 경우에만 해당 속성들을 적용받습니다.
따라서 프리팹을 만들면 일단 게임에서 UI처럼 씬에 필수로 보여져야 할 요소가 아니라면 씬에서 해당 프리팹은 삭제를 합니다. 프리팹에서는 속성을 변경하는 수정이 중요한데 이를 수정하는 장치가 있습니다. 프리팹 오브젝트를 클릭해서 inspector뷰를 자세히 보면 프리팹을 설정할 수 있도록 Open이라는 버튼이 있습니다.
프리팹의 Select버튼은 해당 프리팹 오브젝트가 어떤 오브젝트인지 어느 위치에 있는지 알려주는 기능이고, Open버튼은 프리팹 수정을 하기 위해 프리팹 수정화면을 여는 기능입니다. 따라서 이런 프리팹은 모든 스테이지에서 동일하게 나와야 하지만 중간에 추가, 삭제, 변경에 의한 수정이 있을 때 일괄적인 적용을 시켜주는 편리한 기능을 제공해 주며 각 스테이지에서 동일하게 표현되어야만 하는 UI에 많이 사용됩니다. 이 외에도 몬스터, 아이템에도 많이 사용되는 기능이니 어느 정도 스크립트, 리지드바디, 콜라이더 등의 컴포넌트 세팅이 끝난 오브젝트는 프리팹으로 만들어 두는 것을 추천합니다.
프리팹에 대한 설명은 여기까지 하며 다시 본론인 스크립트에 대해 알아보겠습니다.
부모 오브젝트의 Transform과 자식 GameObject가 필요하므로 각각 public 변수로 선언해 줍니다. 자식 변수에는 Project뷰에서 프리팹을 드래그 앤 드롭하고, 부모 변수에는 Hierarchy뷰에서 적용시킬 부모 오브젝트에 드래그 앤 드롭합니다. 근데 여기서 부모 오브젝트는 스테이지 버튼들로 부모가 많습니다. 그래서 Transform[] 를 사용해서 각 부모를 지정해 주는데 일일히 드래그 해서 드롭하기에는 너무 번거로우니 팁을 한 개 알려드리겠습니다.
오브젝트 일괄 적용방법
Tip
오른쪽 그림처럼 적용하려면 Stage1 버튼 한 개 클릭해서 Element0에 드래그 드롭하고 다시 State2를 똑같이 합니다. 예시는 5개의 스테이지라서 할만 하지만 만약 스테이지가 100개 이상 1000개라면 매우 번거롭고 귀찮은 작업이 됩니다. 이를 한꺼번에 하는 방법이 있습니다.
1. LveleState 스크립트가 있는 LevelStateObject 버튼을 클릭합니다.
2. LevelStateObject 버튼을 클릭한 상태로 Inspector뷰 좌측 상단의 자물쇠 아이콘을 클릭해서 잠김으로 변경
3. 잠김 상태에서 Hierarchy뷰의 어떤 오브젝트를 클릭해도 Inspector뷰의 화면은 바뀌지 않고 고정
4. 이 상태에서 Stage1~5까지 Shift 누른채로 클릭해서 Inspector뷰 Parent Transform 글자에 드롭!!
Parent Transform 글자에 커서를 올려 놓아야 드롭 가능 상태가 되니 참고하시기 바랍니다.
이렇게 일괄 적용한 후에는 꼭 다시 잠금 버튼을 눌러서 Inspector뷰의 잠금을 해제해야 합니다.
스크립트의 AddImageObject()는 말 그대로 부모 오브젝트에 자식 오브젝트를 추가하는 함수 입니다. 자물쇠 이미지 오브젝트를 추가할 것인데 조건을 줄 예정입니다. levelDataList.data[0].isOpen = 1은 스테이지 1을 뜻합니다. 스테이지1은 반드시 열려 있어야 하므로 이렇게 명령해줍니다. levelDataList.data[i].isOpen == 0은 for 루프로 돌릴 때 0이면 아직 클리어 하지 못 한 스테이지를 뜻하므로 이 경우에 이미지를 만들어서 위치까지 지정해 줍니다. child 오브젝트 변수를 만들어서 여기에 프리팹을 지정하고 child.transform.SetParent(parentTransform[i], false)를 통해서 부모 오브젝트인 각 스테이지 버튼에 이미지 자식 오브젝트를 위치시켜 줍니다. 그러면 오브젝트가 isOpen = 0인 스테이지만 자물쇠 이미지를 만들어서 보여주게 되는데 위치가 마음에 들지 않을 것입니다. 이를 세밀하게 조정하기 위해서 RectTransform rectTransform 을 선언해서 RectTransform 컴포넌트에 접근합니다. 여기서 원하는 위치, 크기, 앵커를 조정하는데 적정한 값은 플레이 해서 위치, 크기, 앵커를 직접 조절해보고 이 값들을 기록해 두었다가 참고해서 적용하면 좋습니다. 수치를 조정하고 다시 플레이를 스테이지 상태에 따라서 원하는 위치에 이미지가 출력됩니다.
다음은 오디오 출력, 부드러운 화면 전환에 대해 알아보겠습니다.