IGDC 입문 프로젝트입니다
Site를 눌렀을 때, 아래 사진과 같이 버튼이 나오도록 구현을 해봅시다.
Button들을 생성해봅시다.
하이어리키창에서 우클릭 > UI > Legacy > Button으로 생성할 수 있습니다.
다음과 같이 Button과 자식으로 Text가 생성되었습니다.
Text의 Text 컴포넌트에서 글씨를 넣을 수도 있고, 그 외의 몇 가지 스타일을 제공합니다.
UI는 Transform이 아닌 RectTransform이 붙어있는데요,
좌측에 사각형이 있습니다. Anchor Presets라는 것인데요, 아래 접은 글에서 간단히만 설명하겠습니다.
이 아이의 위치를 옮기려고 보면, Transform이 아닌 Rect Transform이 붙어있는 것을 볼 수 있습니다.
게다가 Anchor Presets라는 것도 생겼습니다. (위 사진의 빨간 부분을 클릭)
클릭을 통해 앵커의 위치를 바꿀 수 있습니다. 그냥 클릭하면 앵커의 위치만 바뀌지, 오브젝트의 위치는 바뀌지 않습니다.
Stretch(푸른색)을 누르면 부모의 크기에 쫙 맞추는 것을 볼 수 있습니다. 부모의 크기가 변경되면, 자신의 크기도 맞춥니다.
Shift를 누른 채로 클릭하면 pivot까지 변경할 수 있고, Alt를 누른 채로 클릭하면 position까지 바꿀 수 있습니다.
물론, 둘 다 누른 채로 클릭할 수도 있습니다.
Anchor Presets를 통해 기종마다 다른 해상도에 유동적으로 대응할 수 있습니다. 자세한 것은 추후 다른 글을 통해 다뤄보도록 하겠습니다. 흠, 중요한 건데 너무 대충이라 죄송합니다.
UI 구성의 기초이므로, 한 번 연구해보시길 바랍니다.
저는 Button을 3개 만들고 Empty Object를 하나 생성하여 버튼들을 묶었습니다.
SiteClick.cs를 생성합니다.
SiteClick에 들어갈 코드는 아래와 같습니다.
using UnityEngine;
public class SiteClick : MonoBehaviour
{
// 띄울 모달 UI 오브젝트입니다
public GameObject modal;
// 모달 UI가 띄워졌는지 감지하는 변수입니다
public bool visible;
// 모달 UI의 RectTransform 컴포넌트입니다
private RectTransform modalRectTransform;
// 어떤 site가 띄웠는지 저장하는 오브젝트의 컴포넌트입니다
private PanelButtonClick panelButtonClick;
void Start() {
// 시작하면서 모달 UI를 닫습니다
visible = false;
SetModalVisible();
// 패널과 PanelButtonClick을 가져옵니다
modalRectTransform = modal.GetComponent<RectTransform>();
panelButtonClick = GameObject.Find("ButtonClickManager").GetComponent<PanelButtonClick>();
}
void Update() {
// 다른 부분을 클릭하면 모달 UI를 닫습니다
if (!visible && Input.GetMouseButtonDown(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
// Site의 태그를 AllyTower로 변경해줍니다
// 그렇지 않을 시, OnMouseDown과 함께 작동하는 경우가 발생합니다
if (!Physics.Raycast(ray, out var hit) || !hit.collider.CompareTag("AllyTower")) {
SetModalVisible();
}
}
}
// Site 오브젝트를 클릭하면 모달 UI를 띄웁니다
private void OnMouseDown() {
if (visible) {
SetModalVisible();
// 모달 UI를 띄우면서 위치를 재조정, 어떤 site가 모달을 띄웠는지 panelButtonClick에 저장합니다
modalRectTransform.position = Camera.main.WorldToScreenPoint(transform.position) + Vector3.down * 40;
panelButtonClick.site = gameObject;
}
}
// 모달을 활성화/비활성화 합니다. 여러 번 입력되는 것을 방지하기 위해 visible 변수를 뒤바꿔줍니다
private void SetModalVisible() {
modal.SetActive(visible);
visible = !visible;
}
}
처음 보는 것들이 너무 많습니다.
Physics.Raycast는 무엇이고, Ray며, hit 변수며 Camera.main은 무엇인가요?
우선 OnMouseDown은 유니티에서 제공하는 이벤트 함수로, 마우스가 오브젝트를 클릭하면 실행되는 함수입니다. OnMouseDown이 실행되기 위해서는 반드시 오브젝트에 Collider가 붙어있어야 합니다.
SetModalVisible 함수에서는 modal 변수의 활성을 조정합니다.
SetActive는 오브젝트를 파괴하지 않고 껐다 켰다 합니다. 자식들이 있다면, 자식들까지도 비활성화시킵니다.
Ray는 간단히 얘기하면 Origin으로부터 방향을 가지고 있는 벡터입니다.
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
위 코드는 현재 화면을 보여주는 main Camera의 화면으로부터 마우스의 위치로 ray를 발사하는 것입니다.
!Physics.Raycast(ray, out var hit) || !hit.collider.CompareTag("AllyTower")
if문 안의 조건문은 아래의 Physics.Raycast 함수를 이용한 것입니다. ray를 넣고, 이를 hit 변수로 out 한 것입니다.
Physics.Raycast(Ray ray, out RaycastHit hitInfo, float maxDistance)
Raycast함수는 만약 콜라이더와 부딪힌다면 true, 그렇지 않다면 false를 반환합니다.
hit 변수는 var로 선언되어 있긴 하지만, RaycastHit 형으로 origin과의 거리, 부딪힌 콜라이더의 정보를 담습니다.
그래서 이걸 왜 쓰느냐!
OnMouseDown 함수는 gameObject를 클릭함으로써 Panel을 열었지만, 닫으려면 다시 gameObject를 클릭해야만 합니다. 하지만, 아무 의미 없는 다른 곳을 눌러도 버튼들이 사라지지 않는다면 이상하죠. 그렇기 때문에 아무 곳이나 눌렀을 때 Ray를 통해 버튼들을 비활성화하기 위함입니다.
다음 공식문서에서 더 다양한 방법들을 알아볼 수 있습니다.
https://docs.unity3d.com/2021.3/Documentation/ScriptReference/Physics.Raycast.html
Unity - Scripting API: Physics.Raycast
Description Casts a ray, from point origin, in direction direction, of length maxDistance, against all colliders in the Scene. You may optionally provide a LayerMask, to filter out any Colliders you aren't interested in generating collisions with. Specifyi
docs.unity3d.com
버튼을 누르면 어떤 일이 일어나야겠죠?
PanelButtonClick.cs를 생성합니다.
using UnityEngine;
public class PanelButtonClick : MonoBehaviour
{
public GameObject site;
public void BuildButtonClick() {
}
public void UpgradeButtonClick() {
}
public void RemoveButtonClick() {
}
}
다음과 같이 각각 버튼에 대응하는 함수를 작성해줍니다.
이때는 외부에서 함수에 접근할 수 있도록 public으로 접근제한자를 두어야 합니다.
후에는 ButtonClickManager라는 빈 오브젝트를 생성해 PanelButtonClick 클래스를 붙여줍니다.
그리고 각 버튼에 맞게 생성한 함수를 추가합니다. 추가 방법은 아래 GIF를 참고해주세요.
그 후, 공격 타워 건설을 위해 조금 수정할 것이 있습니다.
Site, 타워, 업그레이드 된 타워까지 하나로 묶은 빈 오브젝트를 프리팹으로 만들어 관리를 수월하게 해봅시다.
Site에 붙은 BoxCollider와 SiteClick을 빈 오브젝트(저는 AttackTower라 이름을 지었습니다)로 옮겨줍시다.
새로 생성하기에는 이미 설정해놓은 수치가 있으니, 컴포넌트를 복붙해봅시다.
![]() |
![]() |
' ⋮ ' 기호를 누르면 다음과 같이 메뉴창이 뜨는데요, 여기서 'Copy Component'를 클릭해줍니다. Ctrl + C, V가 안 된다는 점이 귀찮지만.. 이렇게 해줘야 합니다.
그 후, 옮길 오브젝트의 Transform의 우측에 ' ⋮ ' 기호를 눌러 Paste > Component As New 를 클릭해 붙여넣기를 완료합니다. 복사된 컴포넌트(Site에 있는 Collider)는 지워지지 않고 남아있습니다. 얘는 Remove Component로 지워줍시다.
동일한 방식으로 SiteClick.cs 도 옮겨줍시다.
또한, Site 프리팹 안에는 아직 Box Collider와 SiteClick이 남아있습니다. 중복되지 않도록 지워줍시다.
AttackTower 오브젝트에는 다음과 같이 Site, tower_sampleB, tower_sampleC (sampleC 타워의 경우, 프리팹화 시키고 sampleB의 컴포넌트를 붙여줬습니다. 다른 거라고는.. B의 업그레이드 버전이니까 coolTime을 줄인 게 있네요)가 나열되어 있습니다. 그리고, 3개의 자식 오브젝트의 position은 xyz 모두 0으로 맞춥시다(Reset도 가능). 참고로 Site를 제외하고 비활성화 되어 있습니다!
AttackTower 오브젝트도 프리팹화 시켜 수정을 마무리합니다.
공격 타워 건설하기
자, 공격 타워 부지는 어느정도 해결되었으니, 공격 타워 건설을 해봅시다.
아까 전에 PanelButtonClick.cs 에 '건설' 버튼을 눌렀을 때 작동하게 한 함수가 있었습니다.
그 안에 구현을 해봅시다.
public void BuildButtonClick() {
Debug.Log("건설합니다!");
site.transform.GetChild(0).gameObject.SetActive(false);
site.transform.GetChild(1).gameObject.SetActive(true);
}
site는 SiteClick.cs에서 버튼들을 활성화시킬 때 할당한 '클릭한 오브젝트'입니다.
GetChild는 transform의 멤버함수입니다. 눈치채셨다시피, 자식 오브젝트를 인덱스로 가져오는 역할을 합니다.
그러므로 위 코드는 0번째인 Site를 비활성화하고, 1번째인 sampleB 건물을 활성화시킵니다.
실행시켜보면 잘 작동하지 않습니다. 위 GIF는 제가 조금 수정했어요.
버튼을 누르는 것을 인식하기도 전에 버튼들이 비활성화되어서 그런 것 같습니다. 살짝 꼼수를 부려줍시다.
// SiteAttack.cs의 Update() 안의 내용입니다.
if (!Physics.Raycast(ray, out var hit) || hit.collider.CompareTag("AllyTower")) {
// 원래 코드
// SetModalVisible();
// 변경된 코드입니다
Invoke("SetModalVisible", 0.25f);
// 여기까지가 변경된 코드입니다
}
Invoke라는 새로운 함수가 등장했습니다. 겁먹을 것 없어요.
InvokeRepeating과 비슷하게 함수를 지연된 시간에 실행시키는 함수입니다. 딱 한 번 실행시킨다는 게 다르지만요.
다음 코드로 실행시키면 버튼들이 클릭하고 0.25초 뒤에 사라집니다. 그 사이 버튼이 눌리는 것을 인식시켜 건물을 세우는 코드가 작동합니다.
하지만, 아무렇게나 건물을 지을 수는 있어서는 안 되겠죠?
Cost 기능을 넣어 cost가 있을 때에만 건물을 지을 수 있도록 합시다.
cost는 GameManager.cs에게 관리하도록 합시다.
// GameManager.cs에 다음 변수와 함수를 추가합니다
private int cost;
private float currentTime;
// cost를 표기할 UI 오브젝트입니다
public Text costText;
// cost를 음수까지 사용하지 못하도록 체크하는 함수입니다
public bool CanUseCost(int useCost) {
if (cost - useCost >= 0) {
cost -= useCost;
return true;
}
return false;
}
void Start() {
currentTime = 0;
}
void Update() {
// Time.deltaTime을 더합니다. 1초마다 currentTime은 1이 늘어납니다
currentTime += Time.deltaTime;
// currentTime이 1초를 넘는다면 cost를 1충전합니다
if (currentTime >= 1f) {
currentTime = 0;
cost++;
// UI를 업데이트합니다
costText.text = cost.ToString();
}
}
그 다음 Text를 생성해봅시다. 하이어리키창을 우클릭 > UI > Legacy > Text 로 생성합니다.
생성된 Text는 GameManager 오브젝트에 넣어줍니다.
Text를 적절한 위치로 옮기고 실행시키면 다음과 같이 초마다 1씩 코스트가 증가하는 것을 볼 수 있습니다.
그럼 코스트를 체크하도록 코드를 수정합니다.
// PanelButtonClick.cs
// 다음 변수와 함수를 추가해줍니다
private GameManager gameManager;
void Start() {
gameManager = GameObject.Find("GameManager").GetComponent<GameManager>();
}
// 여기까지가 추가할 코드입니다
public void BuildButtonClick() {
// 여기서부터 코드를 추가합니다
if (!gameManager.CanUseCost(10)) {
Debug.Log("코스트가 부족하여 건설할 수 없습니다");
return;
}
// 여기까지가 추가된 코드입니다
Debug.Log("건설합니다!");
site.transform.GetChild(0).gameObject.SetActive(false);
site.transform.GetChild(1).gameObject.SetActive(true);
}
실행을 55초에 시켰습니다. 당연히 코스트가 부족하니 55초에는 건설할 수 없다는 로그를 띄우죠.
하지만, 10초가 지난(코스트 10이 충전된) 5초에는 건설을 하는 로그를 띄우네요.
좋습니다. 잘 실행되네요!
그렇다면 얼마나 좋을까요...
눈치채신 분도 계실거라 생각하는데요, 건설해서 sampleB를 세우고는 다시 클릭해봐도 버튼들이 뜨지 않습니다.
게다가 아직은 건설도 안 했는데 업그레이드가 눌리네요. (뭐.. 작성된 코드는 없어 아무 일도 안 일어나지만)
다음 글에서 이 문제들을 수정해봅시다.
이번 글은 조금 길고 어려운 개념도 많이 등장했는데, 따라오시느라 수고 많으셨습니다.
'Unity > IGDC 입문 프로젝트' 카테고리의 다른 글
10.5 - 버튼 클릭 수정 (0) | 2023.11.07 |
---|---|
10. 에러 수정 (0) | 2023.09.19 |
8. 공격 타워 구현 (0) | 2023.09.03 |
7. GameOver 알림 (0) | 2023.09.02 |
6. 몬스터 스폰 및 이동 (0) | 2023.09.01 |