1.선택 블럭의 정보 보여주기
2. 생산량,피해시 숫자 표현하기 (Damage PopUp)

선택 블럭의 정보 보여주기
선택된 블럭의 정보를 보여주기 위해 Render Texture를 사용해서 미리보기 처럼 화면 아랫쪽에 선택된 오브젝트의 정보가 나오게끔 만들 것이다.

먼저 렌더 텍스쳐를 만들어 미리보기 화면을 만들어 준다.

이후 렌더링에 사용될 카메라와 임시 오브젝트, 배경을 만들어주고, 카메라의 출력 텍스쳐에 만든 렌더텍스쳐를 넣는다.

Canvas에 Raw Image를 만들어서 텍스쳐에 만든 렌더 텍스쳐를 만들게 되면 UI로써 화면하나가 나오게 된다.

추가로 Camera의 Culling Mask로 렌더링할 오브젝트의 레이어로 구분할 수 있다.

PreViewManager
PreView를 관리하는 스크립트를 하나 만들어서 사용할 것이다.
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEditor.SpeedTree.Importer;
using UnityEngine;
public class PreviewManager : MonoBehaviour
{
[SerializeField] Transform InsPosition;
GameObject currentObject; // target 미리보기 옵젝
GameObject target; // 추적 대상
[SerializeField] string layerName = "Preview";
private int layerIndex;
private void Awake()
{
layerIndex = LayerMask.NameToLayer(layerName);
}
private void Update()
{
if (currentObject != null)
{
currentObject.transform.Rotate(new Vector3(0, 0.5f, 0), Space.World);
}
if(target == null)
{
Destroy(currentObject);
}
}
public void PreviewObject(RaycastHit hit) // 미리보기 오브젝트 설정 (주요 함수)
{
target = hit.transform.gameObject;
if(currentObject != null)
{
Destroy(currentObject);
}
currentObject = Instantiate(hit.transform.gameObject, InsPosition.position, InsPosition.rotation);
currentObject.layer = layerIndex;
// 렌더링 관련 없는 컴포넌트 모두 제거
Component[] components = currentObject.GetComponents<Component>();
Debug.Log(components.Length);
foreach(Component comp in components)
{
if(comp is Transform || comp is MeshFilter || comp is MeshRenderer)
{
Debug.Log(comp + " : 통과");
continue;
}
Debug.Log(comp + " : 삭제");
Destroy(comp);
}
// 셰이더 외곽선 삭제
MeshRenderer mesh = currentObject.GetComponent<MeshRenderer>();
Material[] mats = mesh.materials;
List<Material> finalMats = new List<Material>();
// Material[] finalMats = new Material[mats.Length];
// Material[] mats = currentObject.GetComponent<MeshRenderer>().materials;
for(int i = 0; i < mats.Length; i++)
{
if (mats[i].shader.name == "Shader Graphs/MouseOnOutline_Shader") // OnMouse 외곽선 셰이더라면
{
// mats[i] = null;
Debug.Log("노란색 외곽선 제거");
}
else if (mats[i].shader.name == "Shader Graphs/Outline_Shader") // 기준 외곽선 셰이더라면
{
Debug.Log("빨간색 외곽선 제거");
}
else
{
finalMats.Add(mats[i]);
}
}
mesh.materials = finalMats.ToArray();
}
}
먼저 카메라에 보일 레이어를 오브젝트에 넣기 위해 오브젝트를 생성한뒤 layerIndex를 통해 레이어를 넣는다.
이후 미리보기 오브젝트에서 필요없는 컴포넌트를 모두 지워준다. 이때 Transform, Mesh Fillter, Mesh Renderer를 남겨준다.
이후 셰이더 외곽선을 지우기 위해 생성한 오브젝트를 가져오고 저번에 만든 방식과 다르게 List를 사용해서 제거할 셰이더는 List에 넣지않는식으로 List를 만든뒤, MeshRenderer의 Materials에 새로 만든 Materials 리스트를 배열형태로 넣는다.
Update에서는 오브젝트가 가만히 있게되면 심심하니, 월드기준 Yaw축으로 계속 회전하게 하며, 기준이 되는 오브젝트가 사라지면 미리보기의 오브젝트또한 사라지게한다.
InputController
입력은 InputController에 큐브를 선택했을때, 호출한다.
void LeftClick() // 블럭을 좌클릭 하면 생성할 블럭의 종류 UI를 표시한다.
{
if(Input.GetMouseButtonDown(0) && !isSeleting)
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); // 카메라로 부터 클릭한 월드좌표까지 광선
if (Physics.Raycast(ray, out hit, Mathf.Infinity, LayerMask.GetMask("Cube"))) // 광선에 맞은 충돌체를 담는다.
{
if (hit.transform.GetComponent<Block>().myCaster == this)
{
UIManager.Instance.ShowSeleteBlock(Input.mousePosition); // 큐브 선택창 생성
isSeleting = true;
seletePlane.InsPlane(hit);
// cubeCreator.InsCube(hit.normal, hit.transform, this); // 큐브 생성하기
}
camera_M.ChengeTarget();
cubeShader.CubeOutLine(hit.transform.gameObject);
previewManager.PreviewObject(hit);
}
}
}

HP 정보 관리하기 및 UI 관리
현재 target의 hp에 따라서 hp를 표시한다.
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEditor.SpeedTree.Importer;
using UnityEngine;
using UnityEngine.UI;
public class PreviewManager : MonoBehaviour
{
[SerializeField] Transform InsPosition;
GameObject currentObject; // target 미리보기 옵젝
GameObject target; // 추적 대상
[SerializeField] string layerName = "Preview";
private int layerIndex;
[SerializeField] Text hpText;
[SerializeField] GameObject PreviewDisplay; // 프리뷰 화면
private void Awake()
{
layerIndex = LayerMask.NameToLayer(layerName);
}
private void Update()
{
if (currentObject != null)
{
currentObject.transform.Rotate(new Vector3(0, 0.5f, 0), Space.World);
}
// if(target == null)
// {
// TargetOff();
// }
}
public void PreviewObject(RaycastHit hit) // 미리보기 오브젝트 설정 (주요 함수)
{
if(currentObject != null)
{
TargetOff();
}
target = hit.transform.gameObject;
currentObject = Instantiate(hit.transform.gameObject, InsPosition.position, InsPosition.rotation);
currentObject.layer = layerIndex;
// Hp UI초기화
SetInfor();
TargetOn();
~~~~~~~~~~~~~~~
void TargetOn() // 타겟을 확인했을 때,
{
PreviewDisplay.SetActive(true);
target.GetComponent<Cube>().HpDown += SetInfor;
}
void TargetOff() // 타겟이 사라졌을 때,
{
PreviewDisplay.SetActive(false);
target.GetComponent<Cube>().HpDown -= SetInfor;
Destroy(currentObject);
}
void SetInfor() // Target의 정보 관리
{
float hp = target.GetComponent<Cube>().hp;
if(hp <= 0)
{
TargetOff();
}
else
{
hpText.text = "HP : " + hp.ToString();
}
}
}
이번엔 처음 하는 방식으로 HP를 처리해보았는데, OnEnable, OnDisable에서 이벤트구독 및 해지를 하는게 아니라, target을 받을때 구독 target이 사라지거나 바뀔때 해지하는 식으로 처리해 보았다.
이렇게 작성한 이유는 Hp의 UI관리를 Update에서 계속 처리하게 되면 값이 변경되지 않았는데도 쓸데없이 처리해 비효율적이라고 생각했기 때문에다.
TargetOn 함수는 Target이 존재할 때, 처음 한번 실행되고, 미리보기 UI를 활성화하며, SetInfor함수를 구독한다.
TargetOff 함수는 Target이 사라졌을 때, 미리보기 UI를 비활성화며, SetInfor함수를 구독해지하고 미리보기의 오브젝트를 삭제한다.
SetInfor함수는 Hp를 관리하는 함수로, 현재 target의 Hp를 가져와 0이하가 되면 TargetOff를 실행 그렇지 않으면 Hp값을 사용해 Text를 작성한다.
이 외의 변경점은 Update에서
Cube
public class Cube : Block, IDamageable
{
public float hp;
public event Action HpDown; // 공격을 받았을 때
public event Action CubeDissapear;
protected bool isdead = false;
public void OnDamage(float damage) // 데미지를 받는 함수
{
hp -= damage;
HpDown?.Invoke();
if(hp <= 0)
{
hp = 0;
OnDelete();
}
~~~~~~~~~~~~~~~~~~~~
먼저 hp를 public으로 바꿔 접근할수 있게 해주고, hp가 감소될때마다 호출되는 HpDown이라는 델리게이트를 추가한다.
이후 OnDamage가 실행되었을 때, hp감소와 삭제처리 사이에 HpDown델리게이트를 호출시켜준다.
완성본

생산량,피해시 숫자 표현하기 (Damage PopUp)
자원생산자가 자원을 생산했을때, 혹은 블럭이 데미지를 입었을 때의 피해량이 숫자로 표현되게 해볼 것이다.
먼저 PopUpText의 Prefab을 만들어야 한다. 총 2가지로, 자원량 텍스트와 데미지 텍스트 (두개의 차이는 색, 단 한가지뿐이다)

먼저 Canvas를 만들어 주고, 월드 공간상에 Text가 나와야 하기 때문에 Render Mode를 월드공간으로 한다.
이후 월드공간에서 사용할수 있게끔 캔버스의 크기를 줄이고, 자식으로 Text를 넣어준다.
PopUpText
전체적인 구조는 자원을 생산하는 생산자와 데미지를 입는 큐브에서 PopUpText를 가진 캔버스를 생성하고, PopUpText에서 실행을 처리하는 식이다.
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
public class PopUpText : MonoBehaviour
{
private Transform mainCamTrans;
[SerializeField] Text myText;
[SerializeField] float upTime;
private void Start()
{
mainCamTrans = Camera.main.transform;
RandomPos();
}
void RandomPos() // 생성위치 랜덤으로 옮기기
{
transform.position += new Vector3(Random.Range(-0.3f, 0.3f), Random.Range(-0.3f, 0.3f), Random.Range(-0.3f, 0.3f));
}
public void TextSetUp(float value) // 텍스트 값 설정
{
myText.text = value.ToString();
}
void Update()
{
// Billboard 시스템
transform.LookAt(transform.position + mainCamTrans.rotation * Vector3.forward,
mainCamTrans.rotation * Vector3.up);
// transform.rotation = Camera.main.transform.rotation;
// transform.LookAt(Camera.main.transform); 이건 좌우반전되서 나옴
}
}
가장 먼저 봐야하는 것은 Update에 있는 Billboard 시스템이다. 빌보드는 UI가 카메라를 계속 바라보게끔 해주는 기능으로, 카메라가 회전되어도, 그 카메라에서는 UI를 제대로 볼수 있게 만들어야 한다.
코드를 설명하자면 카메라의 z축 화살표와 자신의 위치를 더해 카메라와 UI자신의 회전이 평행되게끔 해주는것이 핵심이다.
아랫줄은 카메라가 옆으로 회전되어도, 땅에 수직으로 UI가 서있게 잡아준다.
주석으로 되어있는 transform.rotation = Camera.main.transform.rotation은 사용하는 방식과 흡사하지만 카메라가 기울어지면 UI또한 땅에 수직으로 서있지 않고 같이 기울어진다.
Start에서 mainCamTrans로 값을 사용하는 이유는 위치를 계속 찾지 않기 위해서읻.
TextSetUp 함수에서는 받은 매개변수로 자식오브젝트 text의 값을 변경해준다.
RandomPos의 경우 생성 위치가 개별마다 일관되지 않게 해주기 위해 x,y,z위치값에 추가랜덤값을 넣었다.
호출받기
ResourceProducer
[SerializeField] GameObject resourcePopUp_T; // 생산될 때 나올 자원량 텍스트
IEnumerator Produce() // 자원을 생산한다.
{
for(int i = 0; i < repeatNum; i++)
{
yield return new WaitForSeconds(delayTime);
float upYPos = 0.75f / repeatNum;
Vector3 currentPos = transform.position + transform.up * upYPos;
transform.position = currentPos;
}
GameObject popUpText = Instantiate(resourcePopUp_T, transform.position,Quaternion.identity);
popUpText.GetComponent<PopUpText>().TextSetUp(resourceValue);
myCaster.totalResource += resourceValue;
// GameManager.Instance.totalResource += resourceValue;
transform.position = initialPos;
StartCoroutine(Produce());
}
생산을 해서 자원량이 늘어날 때, 텍스트를 생성하고, 값을 넣어준다.
CannonBall
[SerializeField] GameObject onDamagePopUp_T;
private void OnTriggerEnter(Collider other)
{
// Debug.Log("충돌");
Cube contactCube = other.gameObject.GetComponent<Cube>();
if (contactCube != null)
{
// Debug.Log(contactCube.gameObject.name);
contactCube.OnDamage(damage); // 닿은 큐브의 체력 감소
GameObject popUpText = Instantiate(onDamagePopUp_T, transform.position, Quaternion.identity);
popUpText.GetComponent<PopUpText>().TextSetUp(damage);
Destroy(gameObject);
}
if(other.gameObject.layer == wallIndex)
{
Destroy(gameObject);
}
}
}
Cube가 아닌 CannonBall에서 큐브와 충돌할 때, 텍스트를 생성하고 값을 넣어준다.
처음에는 Cube에 넣으려고 했는데, 생성위치와 [SerializField]의 번거로운 연결 등으로 CannonBall에 생성하는게 맞는거 같아 이행했다.
PopUpText 추가 수정
숫자가 생성되고 떠오르고, 삭제되는 코드까지 구현해주었다.
[SerializeField] float upTime;
private void Start()
{
mainCamTrans = Camera.main.transform;
RandomPos();
StartCoroutine(FloatingText());
}
IEnumerator FloatingText() // 텍스트가 점점 떠오르고 사라진다.
{
float t = 0;
while(t <= upTime)
{
transform.position += new Vector3(0, Time.deltaTime / upTime, 0);
t += Time.deltaTime;
yield return null;
}
Destroy(gameObject);
}
upTime으로 떠오르는 시간 및 생성시간이 설정되고, Time.deltaTime / upTime으로 1이상 위치가 올라가지 않도록 해주었다.
완성본

'GameDev > Cubidom' 카테고리의 다른 글
| Cubidom - 블렌더 큐브 오브젝트 가져오기 (0) | 2026.04.11 |
|---|---|
| Cubidom 카메라 버그, 맵 배경, 선택 UI 수정 (0) | 2026.04.05 |
| Cubidom 시인성 향상2 (0) | 2026.04.01 |
| Cubidom 시인성 향상1 (0) | 2026.03.31 |
| Cubidom Option창 만들기 - 슬라이더로 카메라 민감도 조절 (0) | 2026.03.30 |