Chapter 9-1. 건축 : 건축창 UI, 건축 프리뷰, 건축 가능 여부 판별

Date:     Updated:

카테고리:

태그:

인프런에 있는 케이디님의 [유니티 3D] 실전! 생존게임 만들기 - Advanced 강의를 듣고 정리한 필기입니다. 😀
🌜 강의 들으러 가기 Click

건축 시스템

🚘 UI 만들기

image

캔버스에 크래프트 메뉴 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 : 모닥불의 설명 텍스트

image

Craft_Manual_Base는 평소에는 안보이도록 비활성화 해둔다. 사용자가 Tab 키를 눌렀을 때만 활성화 하게 할 것이다. CraftTab는 이런 크래프트 메뉴 활성화 기능을 넣을것이기 때문에 CraftTab가 비활성화 되면 안된다!!! 그 아래 UI 요소들이 비활성화 되야 함.


🚘 프리뷰 프리팹 만들기

프리팹 만들기

image

크래프트 메뉴를 통해 짓고자 하는 건축물 슬롯을 좌클릭하면 위와 같은 초록색 Material을 가진 장작 오브젝트(프리뷰 프리팹으로 생성)가 생성되어 플레이어의 시점을 따라다니게 할 것이다. 그리고 건축물을 지을 수 없는 곳을 바라볼 땐 그 순간들만 Material을 빨간색으로 바꿔 이 곳엔 건축할 수 없다는 것을 알려줄 것이다. 초록색인 상태에서 좌클릭 누르면 성공적으로 그 위치에 진짜 건축물이 지어지게 할 것이다.


📜PreviewObject.cs

프리뷰 프리팹에 붙는다. 건축 가능한지 여부를 알 수 있도록 생성할 오브젝트!

  • 건축물은 Terrain 즉 지형 위 에만 지을 수 있다. 그 외의 다른 오브젝트들과 충돌이 된다면 그 곳엔 지을 수 없다. 즉, 오로지 Terrain 레이어 혹은 Ignore Raycast 레이어를 가진 오브젝트랑만 충돌할 때 건축이 가능하다.
    • Box Collider 를 붙이고 Trigger 체크를 하여 OnTriggerEnter, OnTriggerExit 이벤트를 호출할 수 있게 한다.
      • 프리뷰 프리팹이 관통한 충돌 대상이 Terrain 레이어(땅)와 Ignore Raycast 레이어를 가진 오브젝트 말고도 더 있다면 땅 이외의 또 다른 오브젝트와도 충돌한 것이므로 그 곳엔 건축물을 지을 수 없다.
        • 이 경우엔 프리뷰 프리팹의 Material을 빨간색으로 바꾼다.
      • 프리뷰 프리팹이 관통한 충돌 대상이 Terrain 레이어(땅)와 Ignore Raycast 레이어 말고는 없다면 그 곳엔 건축물을 지을 수 있다.
        • 이 경우엔 프리뷰 프리팹의 Material을 초록색으로 바꾼다.
  • 충돌한 오브젝트들을 저장할 수 있는 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)
    • SetColor(Material mat)
      • foreach(Transform tf_Child in this.transform)⭐ 이 스크립트가 붙은 해당 오브젝트의 자식 오브젝트들을 순회할 수 있다.
        • 프리뷰 프리팹은 빈 오브젝트이며 자식 오브젝트들은 4개로 하나 하나의 장작 오브젝트들이다. 이들이 하나의 프리뷰 프리팹을 구성하는 것이다. 이 4 개의 장작 자식 오브젝트들의 Material을 다 바꿔 색을 바꿔주어야 하므로 이렇게 for문 순회를 돌아야 한다.
          • 아래 사진에서 볼 수 있듯이 하나의 오브젝트는 여러개의 Material을 가질 수 있어서 이렇게 배열로 가지고 있다. 현재는 material이 하나긴 하지만 배열로 가져와야 한다. 배열로 가져와서 material 들을 전부 인수로 받은 material 로 설정해주어야 한다.

            image

  • isBuildable()
    • 📜CraftManual.cs 에게 해당 프리뷰 프리팹이 현재 건축이 가능한지 알려주어야 하므로 public 으로 선언
    • colliderList.Count == 0 건축이 가능하다면 True


프리팹 설정

image

Terrain 에게 “Terrain”이라는 레이어를 추가해준다. Main Camera의 쿨링 마스크에 “Terrain”도 렌더링 되게 추가해준다.

image

  • Terrain 레이어는 13번 레이어라 13 입력, 초록색 빨간색 Material 할당.
  • Box Collider 가 필요하다. 프리뷰가 땅 이외에 다른 오브젝트와 충돌한다면 건축 못 하게 하고 빨간색으로 바꿔야 하기 때문이다.
    • IsTrigger 체크해준다.
  • OnTriggerEnter 의 발생 조건
    • 두 오브젝트가 모두 Collider 를 가지고 있어야 한다.
      • 그리고 둘 중 하나는 Tirgger가 체크되어야 한다.
    • 두 오브젝트 중 하나는 Rigidbody 를 가지고 있어야 한다.
      • 그래서 프리뷰 프리팹에 붙여주었다.
      • 여러 물리 효과는 딱히 받지 않아도 되므로 중력 꺼주고 Kinematic 체크해주었다.


🚘 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_PlayerMain Camera를 할당할 것이다.
        • 프리뷰 프리팹은 버튼 이벤트로 호출된 SlotClick(int _slotNumber)에서 생성이 된 후, 플레이어의 시점을 따라다니도록 메인 카메라의 앞에 range 사정 거리내에 충돌되는 곳으로 위치를 매 프레임마다 갱신하게 될 것이다.

image

  • craft_fire 배열에는 여러 슬롯들의 건축물 이름, 실제 건축물 프리팹, 건축물 미리보기 프리팹 정보를 담고 있는 Craft 객체가 들어간다. 현재 슬롯은 하나만 만들어두었으므로 Size 도 1 로 하였다.
  • tf_Player에 메인카메라 할당
  • layerMask에 “Terrain” 레이어를 할당하여 “Terrain” 레이어에 충돌하는 곳에만 프리뷰 프리팹이 따라다니게 할 것이다.

image

해당 모닥불 슬롯의 버튼이 클릭되면 CraftTab(0) 가 호출되도록 등록해준다.

image

이렇게 사용자의 시선을 따라다니며 지을 수 있는 곳은 초록색으로 표시된다.

image

지을 수 없는 곳은 빨간색으로 표시된다. 나무와 겹쳐서 이곳엔 모닥불을 건축할 수 없다.


🚔 계속 프리팹이 하늘 위에 생성되었던 이유..

원래 화면 정중앙인 크로스헤어 쪽을 프리뷰가 따라다녀야 하는데 자꾸 플레이어의 한참 머리 위에 있고 생성도 하늘 위에 되어서 정말 난감했었다. 한참을 헤매다 알게 된 사실.. 프리뷰 프리팹의 자식 오브젝트들인 장작 오브젝트들의 위치값 때문이였다. 부모 오브젝트를 기준으로 하는 로컬 위치값이였는데 부모 오브젝트와 한참 떨어진 위치로 설정되어 있는 상태에서 부모 오브젝트이자 빈 오브젝트인 프리뷰 프리팹으로 위치를 잡았었기 때문에 생긴 일이었다. 자식 오브젝트들인 장작들의 로컬 위치를 부모 오브젝트 위치와 일치하도록 거의 원점으로 변경해주었더니 해결 되었다.



🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기

Unity Lesson 3 카테고리 내 다른 글 보러가기

댓글 남기기