Chapter 9-1. 건축 : 건축창 UI, 건축 프리뷰, 건축 가능 여부 판별
카테고리: Unity Lesson 3
태그: Unity Game Engine
인프런에 있는 케이디님의 [유니티 3D] 실전! 생존게임 만들기 - Advanced 강의를 듣고 정리한 필기입니다. 😀
🌜 강의 들으러 가기 Click
건축 시스템
🚘 UI 만들기
캔버스에 크래프트 메뉴 UI 를 추가한다.
CraftTab
: 크래프트 UI 전체를 대표하는 빈 오브젝트. 📜CraftManual.cs 가 붙을 곳.Craft_Manual_Base
: 크래프트 UI 가장 큰 기본 배경 이미지- 1️⃣
Tab
: 탭들을 대표하는 빈 오브젝트. 왼쪽 부분으로, 건축 종류를 나타낼 것이다.Fire_Tab_Base
: 불🔥 탭의 배경 이미지. 이 탭 버튼을 좌클릭하면 불🔥에 관련된 건축물 슬롯들이 나오도록 하기 위해 버튼 컴포넌트도 추가함Fire_Tab_Image
: 불🔥 탭의 불 이미지
- 2️⃣
Slot
: 슬롯들을 대표하는 빈 오브젝트. 오른쪽 부분으로, 건축 종류에 속한 건축물들을 나타낼 것이다.Campfire_Slot_Base
: 불의 종류 중 하나인 모닥불 슬롯의 배경 이미지. 이 슬롯 버튼을 좌클릭하면 모닥불을 건축할 수 있도록 버튼 컴포넌트도 추가함Campfire_Slot_Image
: 모닥불의 슬롯 이미지Campfire_Slot_Name
: 모닥불의 이름 텍스트Campfire_Slot_Description
: 모닥불의 설명 텍스트
- 1️⃣
Craft_Manual_Base
는 평소에는 안보이도록 비활성화 해둔다. 사용자가 Tab
키를 눌렀을 때만 활성화 하게 할 것이다. CraftTab
는 이런 크래프트 메뉴 활성화 기능을 넣을것이기 때문에 CraftTab
가 비활성화 되면 안된다!!! 그 아래 UI 요소들이 비활성화 되야 함.
🚘 프리뷰 프리팹 만들기
프리팹 만들기
크래프트 메뉴를 통해 짓고자 하는 건축물 슬롯을 좌클릭하면 위와 같은 초록색 Material을 가진 장작 오브젝트(프리뷰 프리팹으로 생성)가 생성되어 플레이어의 시점을 따라다니게 할 것이다. 그리고 건축물을 지을 수 없는 곳을 바라볼 땐 그 순간들만 Material을 빨간색으로 바꿔 이 곳엔 건축할 수 없다는 것을 알려줄 것이다. 초록색인 상태에서 좌클릭 누르면 성공적으로 그 위치에 진짜 건축물이 지어지게 할 것이다.
📜PreviewObject.cs
프리뷰 프리팹에 붙는다. 건축 가능한지 여부를 알 수 있도록 생성할 오브젝트!
- 건축물은
Terrain
즉 지형 위 에만 지을 수 있다. 그 외의 다른 오브젝트들과 충돌이 된다면 그 곳엔 지을 수 없다. 즉, 오로지 Terrain 레이어 혹은 Ignore Raycast 레이어를 가진 오브젝트랑만 충돌할 때 건축이 가능하다.- Box Collider 를 붙이고 Trigger 체크를 하여 OnTriggerEnter, OnTriggerExit 이벤트를 호출할 수 있게 한다.
- 프리뷰 프리팹이 관통한 충돌 대상이 Terrain 레이어(땅)와 Ignore Raycast 레이어를 가진 오브젝트 말고도 더 있다면 땅 이외의 또 다른 오브젝트와도 충돌한 것이므로 그 곳엔 건축물을 지을 수 없다.
- 이 경우엔 프리뷰 프리팹의 Material을 빨간색으로 바꾼다.
- 프리뷰 프리팹이 관통한 충돌 대상이 Terrain 레이어(땅)와 Ignore Raycast 레이어 말고는 없다면 그 곳엔 건축물을 지을 수 있다.
- 이 경우엔 프리뷰 프리팹의 Material을 초록색으로 바꾼다.
- 프리뷰 프리팹이 관통한 충돌 대상이 Terrain 레이어(땅)와 Ignore Raycast 레이어를 가진 오브젝트 말고도 더 있다면 땅 이외의 또 다른 오브젝트와도 충돌한 것이므로 그 곳엔 건축물을 지을 수 없다.
- Box Collider 를 붙이고 Trigger 체크를 하여 OnTriggerEnter, OnTriggerExit 이벤트를 호출할 수 있게 한다.
- 충돌한 오브젝트들을 저장할 수 있는 List 를 선언하고
- OnTriggerEnter 발생시 충돌 대상이 Terrain 레이어(땅)와 Ignore Raycast 레이어가 아닌 다른 레이어라면 List 에 추가한다.
- OnTriggerExit 발생시 더 이상 겹치지 않는 그 대상이 Terrain 레이어(땅)와 Ignore Raycast 레이어가 아닌 다른 레이어라면 List 에서 삭제한다. 더 이상 겹치지 않으므로.
- List 크기가 1 이상이면 땅 이외에 충돌한 오브젝트가 한개 이상이라는 것이므로 그 곳엔 건축물을 지을 수 없다.
- 빨간 색으로 바꿈
- List 크기가 0이면 땅 이외에 충돌한 오브젝트가 없다는 것이므로 그 곳엔 건축물을 지을 수 있다.
- 초록 색으로 바꿈
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PreviewObject : MonoBehaviour
{
private List<Collider> colliderList = new List<Collider>(); // 충돌한 오브젝트들 저장할 리스트
[SerializeField]
private int layerGround; // 지형 레이어 (무시하게 할 것)
private const int IGNORE_RAYCAST_LAYER = 2; // ignore_raycast (무시하게 할 것)
[SerializeField]
private Material green;
[SerializeField]
private Material red;
void Update()
{
ChangeColor();
}
private void ChangeColor()
{
if (colliderList.Count > 0)
SetColor(red);
else
SetColor(green);
}
private void SetColor(Material mat)
{
foreach(Transform tf_Child in this.transform)
{
Material [] newMaterials = new Material[tf_Child.GetComponent<Renderer>().materials.Length];
for (int i = 0; i < newMaterials.Length; i++)
{
newMaterials[i] = mat;
}
tf_Child.GetComponent<Renderer>().materials = newMaterials;
}
}
private void OnTriggerEnter(Collider other)
{
if (other.gameObject.layer != layerGround && other.gameObject.layer != IGNORE_RAYCAST_LAYER)
colliderList.Add(other);
}
private void OnTriggerExit(Collider other)
{
if (other.gameObject.layer != layerGround && other.gameObject.layer != IGNORE_RAYCAST_LAYER)
colliderList.Remove(other);
}
public bool isBuildable()
{
return colliderList.Count == 0;
}
}
colliderList
👉 Terrain 레이어 혹은 Ignore Raycast 레이어 이외의 다른 오브젝트와 충돌했을 경우 그 오브젝트를 저장할 리스트.- 오로지 Terrain 레이어 혹은 Ignore Raycast 레이어를 가진 오브젝트랑만 충돌할 때 건축이 가능하다. 따라서 이 리스트의 크기가 1 이상이라면 건축할 수 없는 상태와 마찬가지다.
- OnTriggerEnter(Collider other)
- 겹쳐지는 충돌 대상이 Terrain 레이어(땅)와 Ignore Raycast 레이어가 아닌 다른 레이어라면 List 에 추가한다.
- OnTriggerExit(Collider other)
- 더 이상 겹치지 않는 그 대상이 Terrain 레이어(땅)와 Ignore Raycast 레이어가 아닌 다른 레이어라면 List 에서 삭제한다. 더 이상 겹치지 않으므로.
- 매 프레임마다 건축 가능 상태 여부에 따라 이 스크립트가 붙은 프리뷰 프리팹의 Material을 바꾼다.
- Update()
- 매 프레임마다 색을 바꾼다. ChangeColor()
colliderList.Count > 0
건축이 불가능하다면 빨간색으로 변경 SetColor(red)colliderList.Count == 0
건축이 가능하다면 초록색으로 변경 SetColor(green)
- 매 프레임마다 색을 바꾼다. ChangeColor()
- SetColor(Material mat)
foreach(Transform tf_Child in this.transform)
⭐ 이 스크립트가 붙은 해당 오브젝트의 자식 오브젝트들을 순회할 수 있다.- 프리뷰 프리팹은 빈 오브젝트이며 자식 오브젝트들은 4개로 하나 하나의 장작 오브젝트들이다. 이들이 하나의 프리뷰 프리팹을 구성하는 것이다. 이 4 개의 장작 자식 오브젝트들의 Material을 다 바꿔 색을 바꿔주어야 하므로 이렇게 for문 순회를 돌아야 한다.
-
아래 사진에서 볼 수 있듯이 하나의 오브젝트는 여러개의 Material을 가질 수 있어서 이렇게 배열로 가지고 있다. 현재는 material이 하나긴 하지만 배열로 가져와야 한다. 배열로 가져와서 material 들을 전부 인수로 받은 material 로 설정해주어야 한다.
-
- 프리뷰 프리팹은 빈 오브젝트이며 자식 오브젝트들은 4개로 하나 하나의 장작 오브젝트들이다. 이들이 하나의 프리뷰 프리팹을 구성하는 것이다. 이 4 개의 장작 자식 오브젝트들의 Material을 다 바꿔 색을 바꿔주어야 하므로 이렇게 for문 순회를 돌아야 한다.
- Update()
- isBuildable()
- 📜CraftManual.cs 에게 해당 프리뷰 프리팹이 현재 건축이 가능한지 알려주어야 하므로 public 으로 선언
colliderList.Count == 0
건축이 가능하다면 True
프리팹 설정
Terrain 에게 “Terrain”이라는 레이어를 추가해준다. Main Camera의 쿨링 마스크에 “Terrain”도 렌더링 되게 추가해준다.
- Terrain 레이어는 13번 레이어라 13 입력, 초록색 빨간색 Material 할당.
- Box Collider 가 필요하다. 프리뷰가 땅 이외에 다른 오브젝트와 충돌한다면 건축 못 하게 하고 빨간색으로 바꿔야 하기 때문이다.
IsTrigger
체크해준다.
- OnTriggerEnter 의 발생 조건
- 두 오브젝트가 모두 Collider 를 가지고 있어야 한다.
- 그리고 둘 중 하나는 Tirgger가 체크되어야 한다.
- 두 오브젝트 중 하나는 Rigidbody 를 가지고 있어야 한다.
- 그래서 프리뷰 프리팹에 붙여주었다.
- 여러 물리 효과는 딱히 받지 않아도 되므로 중력 꺼주고 Kinematic 체크해주었다.
- 두 오브젝트가 모두 Collider 를 가지고 있어야 한다.
🚘 UI 활성화 + 프리뷰 생성 + 건물 짓기
📜CraftManual.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class Craft
{
public string craftName; // 이름
public GameObject go_prefab; // 실제 설치 될 프리팹
public GameObject go_PreviewPrefab; // 미리 보기 프리팹
}
public class CraftManual : MonoBehaviour
{
private bool isActivated = false; // CraftManual UI 활성 상태
private bool isPreviewActivated = false; // 미리 보기 활성화 상태
[SerializeField]
private GameObject go_BaseUI; // 기본 베이스 UI
[SerializeField]
private Craft[] craft_fire; // 🔥불 탭에 있는 슬롯들.
private GameObject go_Preview; // 미리 보기 프리팹을 담을 변수
private GameObject go_Prefab; // 실제 생성될 프리팹을 담을 변수
[SerializeField]
private Transform tf_Player; // 플레이어 위치
private RaycastHit hitInfo;
[SerializeField]
private LayerMask layerMask;
[SerializeField]
private float range;
public void SlotClick(int _slotNumber)
{
go_Preview = Instantiate(craft_fire[_slotNumber].go_PreviewPrefab, tf_Player.position + tf_Player.forward, Quaternion.identity);
go_Prefab = craft_fire[_slotNumber].go_prefab;
isPreviewActivated = true;
go_BaseUI.SetActive(false);
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Tab) && !isPreviewActivated)
Window();
if (isPreviewActivated)
PreviewPositionUpdate();
if (Input.GetButtonDown("Fire1"))
Build();
if (Input.GetKeyDown(KeyCode.Escape))
Cancel();
}
private void PreviewPositionUpdate()
{
if (Physics.Raycast(tf_Player.position, tf_Player.forward, out hitInfo, range, layerMask))
{
if (hitInfo.transform != null)
{
Vector3 _location = hitInfo.point;
go_Preview.transform.position = _location;
Debug.Log(_location);
Debug.Log(go_Preview.transform.position);
}
}
}
private void Build()
{
if(isPreviewActivated && go_Preview.GetComponent<PreviewObject>().isBuildable())
{
Instantiate(go_Prefab, hitInfo.point, Quaternion.identity);
Destroy(go_Preview);
isActivated = false;
isPreviewActivated = false;
go_Preview = null;
go_Prefab = null;
}
}
private void Window()
{
if (!isActivated)
OpenWindow();
else
CloseWindow();
}
private void OpenWindow()
{
isActivated = true;
go_BaseUI.SetActive(true);
}
private void CloseWindow()
{
isActivated = false;
go_BaseUI.SetActive(false);
}
private void Cancel()
{
if (isPreviewActivated)
Destroy(go_Preview);
isActivated = false;
isPreviewActivated = false;
go_Preview = null;
go_Prefab = null;
go_BaseUI.SetActive(false);
}
}
- 하나의 슬롯은 1️⃣이름 2️⃣실제 설치될 프리팹 3️⃣ 미리 보기 프리팹 이렇게 3 가지 정보를 가져야 하므로 이들을 멤버 필드로 가지는
Craft
클래스를 생성해주었다. - Update
- Window() 매 프레임마다
Tab
키 입력이 들어왔고 + 미리보기 활성화 상태가 아니라면 크래프트 UI 를 활성화 혹은 비활성화 한다.- 이미 활성화 되있었던 상태면 CloseWindow() 크래프트 UI 비활성화
- 비활성화였던 상태면 OpenWindow() 크래프트 UI 활성화
- Cancel() 매 프레임마다
ESC
키 입력이 들어왔다면- 현재 미리 보기 활성화 중이라면 프리뷰 프리팹을 없애고
- 여러 상태 변수들 초기화 하고
- 크래프트 UI 비활성화
- PreviewPositionUpdate() 매 프레임마다 미리 보기 활성화 상태라면 프리팹 프리뷰가 플레이어가 보고 있는 시점에서 사정거리 내에 아이템을 놓을 수 있는
Terrain
과 충돌하는 곳(LayerMask)을 따라다니도록 한다.- 크래프트 UI 에서 슬롯 버튼이 클릭되면 SlotClick(int _slotNumber) 가 실행되고 (버튼 이벤트에 넣을 것이다)
isPreviewActivated
가 활성화 되며 프리뷰 프리팹이 생성되고 크래프트 UI 비활성화 한다.- 인수인
_slotNumber
에는 클릭한 해당 슬롯 버튼의 번호를 넘길 것이다.
- 인수인
- Raycast 쏘는 기준은 Player. 즉,
tf_Player
에Main Camera
를 할당할 것이다.- 프리뷰 프리팹은 버튼 이벤트로 호출된 SlotClick(int _slotNumber)에서 생성이 된 후, 플레이어의 시점을 따라다니도록 메인 카메라의 앞에
range
사정 거리내에 충돌되는 곳으로 위치를 매 프레임마다 갱신하게 될 것이다.
- 프리뷰 프리팹은 버튼 이벤트로 호출된 SlotClick(int _slotNumber)에서 생성이 된 후, 플레이어의 시점을 따라다니도록 메인 카메라의 앞에
- 크래프트 UI 에서 슬롯 버튼이 클릭되면 SlotClick(int _slotNumber) 가 실행되고 (버튼 이벤트에 넣을 것이다)
- Window() 매 프레임마다
craft_fire
배열에는 여러 슬롯들의 건축물 이름, 실제 건축물 프리팹, 건축물 미리보기 프리팹 정보를 담고 있는Craft
객체가 들어간다. 현재 슬롯은 하나만 만들어두었으므로 Size 도 1 로 하였다.tf_Player
에 메인카메라 할당layerMask
에 “Terrain” 레이어를 할당하여 “Terrain” 레이어에 충돌하는 곳에만 프리뷰 프리팹이 따라다니게 할 것이다.
해당 모닥불 슬롯의 버튼이 클릭되면 CraftTab(0) 가 호출되도록 등록해준다.
이렇게 사용자의 시선을 따라다니며 지을 수 있는 곳은 초록색으로 표시된다.
지을 수 없는 곳은 빨간색으로 표시된다. 나무와 겹쳐서 이곳엔 모닥불을 건축할 수 없다.
🚔 계속 프리팹이 하늘 위에 생성되었던 이유..
원래 화면 정중앙인 크로스헤어 쪽을 프리뷰가 따라다녀야 하는데 자꾸 플레이어의 한참 머리 위에 있고 생성도 하늘 위에 되어서 정말 난감했었다. 한참을 헤매다 알게 된 사실.. 프리뷰 프리팹의 자식 오브젝트들인 장작 오브젝트들의 위치값 때문이였다. 부모 오브젝트를 기준으로 하는 로컬 위치값이였는데 부모 오브젝트와 한참 떨어진 위치로 설정되어 있는 상태에서 부모 오브젝트이자 빈 오브젝트인 프리뷰 프리팹으로 위치를 잡았었기 때문에 생긴 일이었다. 자식 오브젝트들인 장작들의 로컬 위치를 부모 오브젝트 위치와 일치하도록 거의 원점으로 변경해주었더니 해결 되었다.
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
댓글 남기기