목차
멋쟁이사자처럼 로켓단 인턴쉽
25.08.17
도장 생성 함수
: 도장 Sprite를 가진 GameObject를 프리팹으로 받아서 승인 버튼을 눌렀을 때는 승인 도장 Sprite를, 반려 버튼을 눌렀을 때는 반려 도장 Sprite를 서류 위에 띄우도록 구현
--> 이때, 도장 Sprite는 서류 Object의 자식으로 붙어서 서류가 처리되어 사라질 때 서류와 같이 붙어서 이동하고 사라지도록
>> 도장 프리팹 생성
: 빈 게임 오브젝트에 SpriteRenderer만 추가하여 Sprite를 바인딩한 후, Prefab화
>> 코드 작성
: 처음엔 InteractionUIController.cs에서 도장 프리팹을 받아서 누른 버튼에 따라 해당하는 도장이 찍히도록 했음.
- DocumentController.cs
<hide/>
// 도장 생성 함수
public void ShowStamp(GameObject stampPrefab)
{
if (stampPrefab == null || _docObj == null) return;
// 서류 Object를 도장 Object의 부모로 설정하여 생성
GameObject stampObj = Instantiate(stampPrefab, _docObj.transform);
// 도장 위치 설정
Vector2 stampPos = new Vector2(1f, -2f); // 도장 위치 고정, 추후 수정 가능
stampObj.transform.localPosition = stampPos;
// TODO: 도장 찍히는 연출
}
- InteractionUIController.cs
<hide/>
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class InteractionUIController : MonoBehaviour
{
[SerializeField] private RectTransform _rectTransform;
[SerializeField] private CanvasGroup _canvasGroup;
[SerializeField] private Button _acceptButton;
[SerializeField] private Button _negativeButton;
[SerializeField] private Button _pauseButton;
[Header("도장 관련")]
[SerializeField] private GameObject approvalStampPrefab;
[SerializeField] private GameObject deniedStampPrefab;
private void Awake()
{
//버튼 클릭이벤트 등록
_acceptButton.onClick.AddListener(OnClickAcceptButton);
_negativeButton.onClick.AddListener(OnClickNegativeButton);
_pauseButton.onClick.AddListener(OnClickPauseButton);
}
public void OnClickAcceptButton()
{
Classification.Instance.confirm = true; //승인버튼 클릭시 서류 승인
Classification.Instance.DocumentClassification(); // 서류 분류 메소드 호출
AudioManager.Instance.SFX.PlayStamp();
GameManager.Instance.GetDocumentController().ShowStamp(approvalStampPrefab);
}
public void OnClickNegativeButton()
{
Classification.Instance.confirm = false; //반려버튼 클릭시 서류 반려
Classification.Instance.DocumentClassification(); // 서류 분류 메소드 호출
AudioManager.Instance.SFX.PlayStamp();
GameManager.Instance.GetDocumentController().ShowStamp(deniedStampPrefab);
}
public void OnClickPauseButton()
{
GameManager.Instance.PauseGame();
UIManager.Instance.popupUIController.ShowPauseUI();
AudioManager.Instance.SFX.PlayButtonClick();
}
}
>> 코드 수정
: 하지만 역할을 깔끔하게 정리하려면 DocumentController에서 도장 프리팹을 받고, 도장을 선택하는 것까지 관리하는 것이 나을 것 같다는 생각에 DocumentController에서 처리하도록 옮김
- DocumentController.cs
<hide/>
[Header("도장 관련")]
[SerializeField] private GameObject approvalStampPrefab;
[SerializeField] private GameObject deniedStampPrefab;
public void ShowStamp(bool isApproved)
{
if (_docObj == null) return;
GameObject prefab = isApproved ? approvalStampPrefab : deniedStampPrefab;
GameObject currentStamp = Instantiate(prefab, _docObj.transform);
Vector2 stampPos = new Vector2(1f, -2f);
currentStamp.transform.localPosition = stampPos;
// TODO: 도장 찍히는 연출
}
- InteractionUIController.cs
<hide/>
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class InteractionUIController : MonoBehaviour
{
[SerializeField] private RectTransform _rectTransform;
[SerializeField] private CanvasGroup _canvasGroup;
[SerializeField] private Button _acceptButton;
[SerializeField] private Button _negativeButton;
[SerializeField] private Button _pauseButton;
private void Awake()
{
//버튼 클릭이벤트 등록
_acceptButton.onClick.AddListener(OnClickAcceptButton);
_negativeButton.onClick.AddListener(OnClickNegativeButton);
_pauseButton.onClick.AddListener(OnClickPauseButton);
}
public void OnClickAcceptButton()
{
Classification.Instance.confirm = true; //승인버튼 클릭시 서류 승인
Classification.Instance.DocumentClassification(); // 서류 분류 메소드 호출
AudioManager.Instance.SFX.PlayStamp();
GameManager.Instance.GetDocumentController().ShowStamp(true);
}
public void OnClickNegativeButton()
{
Classification.Instance.confirm = false; //반려버튼 클릭시 서류 반려
Classification.Instance.DocumentClassification(); // 서류 분류 메소드 호출
AudioManager.Instance.SFX.PlayStamp();
GameManager.Instance.GetDocumentController().ShowStamp(false);
}
public void OnClickPauseButton()
{
GameManager.Instance.PauseGame();
UIManager.Instance.popupUIController.ShowPauseUI();
AudioManager.Instance.SFX.PlayButtonClick();
}
}
>> 도장 프리팹 바인딩
문제 발생 및 해결 과정
: 도장 Sprite가 서류보다 아래에 생성되는 문제
--> 여러 번 테스트 했을 때, 어떤 경우는 맨 위, 어떤 경우는 맨 뒤에 나오는 버그가 발생
>> 해결 과정
1. SetAsLastSibling()을 사용
: Unity에서 Hierarchy에 나열된 순서대로 UI 요소가 렌더링되기 때문에, Hierarchy상 가장 아래에 있는 요소가 마지막에 렌더링되어 UI상 가장 위에 위치하게 된다.
--> 따라서 SetAsLastSibling()를 사용하여 도장 Prefab을 Hierarchy의 가장 아래로 내려서 가장 위에 위치하도록 한다.
※ Unity Documentation - Transform.SetAsLastSibling()
https://docs.unity3d.com/6000.2/Documentation/ScriptReference/Transform.SetAsLastSibling.html
- 작성한 코드
<hide/>
public void ShowStamp(bool isApproved)
{
if (_docObj == null) return;
GameObject prefab = isApproved ? approvalStampPrefab : deniedStampPrefab;
GameObject currentStamp = Instantiate(prefab, _docObj.transform);
Vector2 stampPos = new Vector2(1f, -2f);
currentStamp.transform.localPosition = stampPos;
currentStamp.transform.SetAsLastSibling();
// TODO: 도장 찍히는 연출
}
▶ 그래도 여전히 같은 문제가 발생했다.
2. 월드 좌표를 유지하지 않도록 설정
: SetParent()에서 월드 좌표를 유지(true)하면 도장 프리팹을 Instantiate()하는 과정에서 월드 좌표/로컬 좌표 처리 방식이 Unity 내부 상황에 따라 달라져서 Stamp가 최상단이 되지 않는 경우가 발생할 수 있다.
--> 도장 프리팹을 생성할 때 worldPositionStays를 false로 설정
※ Unity Documentation - Transform.SetParent()
https://docs.unity3d.com/ScriptReference/Transform.SetParent.html
- 작성한 코드
<hide/>
public void ShowStamp(bool isApproved)
{
if (_docObj == null) return;
GameObject prefab = isApproved ? approvalStampPrefab : deniedStampPrefab;
GameObject currentStamp = Instantiate(prefab, _docObj.transform, false);
Vector2 stampPos = new Vector2(1f, -2f);
currentStamp.transform.localPosition = stampPos;
currentStamp.transform.SetAsLastSibling();
// TODO: 도장 찍히는 연출
}
▶ 수정해도 동일한 문제 발생, 심지어 어떤 경우에는 승인 도장은 서류 위에, 반려 도장은 서류 아래에 나오기도 한다.
3. Order in Layer 설정
: Document와 Stamp 모두 Image가 아니라 SpriteRenderer 기반이기 때문에 Image처럼 Hierarchy 순서에 따라 그려지는 것이 아니라 Sorting Layer와 Order in Layer 값에 의해 결정된다.
--> 결국 SetAsLastSibling()은 SpriteRenderer에 전혀 영향을 주지 않으며, Stamp 프리팹의 Order in Layer를 10으로 설정 (반려 요소의 Order in Layer는 5, 장애물의 Order in Layer는 6이기 때문에 그보다 우선순위를 높임)
- 작성한 코드
<hide/>
// 도장 생성 함수
public void ShowStamp(bool isApproved)
{
if (_docObj == null) return;
GameObject prefab = isApproved ? approvalStampPrefab : deniedStampPrefab;
GameObject currentStamp = Instantiate(prefab, _docObj.transform, false);
currentStamp.transform.localPosition = new Vector2(1f, -2f);
// TODO: 도장 찍히는 연출
}
▶ 드디어 문제가 해결됐다!
최종 코드
1. DocumentController.cs
<hide/>
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
using DG.Tweening;
using UnityEngine.UI;
public class DocumentController : MonoBehaviour
{
//서류 프리팹
[SerializeField] private GameObject _documentPrefab;
//반려 요소 프리팹 (0:낙서, 1:잉크번짐, 2:커피 쏟음, 3:물쏟음, 4:인쇄불량)
[SerializeField] private List<GameObject> _rejectObjPrefabs;
//장애물 프리팹 (0:날벌레, 1:포스트 잇, 2:요구자 손, 3:파일철, 4:서류봉투)
[SerializeField] private List<ObstacleData> _obstacleObjDatas;
//서류 및 장애물 데이터
private DocumentData _currentDocument;
private List<ObstacleInstance> _currentObstacles = new List<ObstacleInstance>();
//생성된 서류 및 장애물 오브젝트
private GameObject _docObj;
private GameObject _rejectObj;
private List<GameObject> _obstacleObjs = new List<GameObject>();
//서류 사이즈(반려요소 스폰지점 산출에 사용)
private Vector3 _documentSize;
//날짜 수
private int _day;
//서류 스폰 포인트
[SerializeField] private float _docSpawnX;
[SerializeField] private float _docSpawnY;
//서류 도착 포인트
[SerializeField] private float _docStopPosX;
[SerializeField] private float _docStopPosY;
//서류 디스폰 포인트
[SerializeField] private float _docDespawnX;
[SerializeField] private float _docDespawnY;
//이동 시간
[SerializeField] private float _duration;
[Header("도장 관련")]
[SerializeField] private GameObject approvalStampPrefab;
[SerializeField] private GameObject deniedStampPrefab;
public void InitDocuments()
{
Classification.Instance.docController = this;
_currentObstacles.Clear();
_obstacleObjs.Clear();
var renderer = _documentPrefab.GetComponent<SpriteRenderer>();
_documentSize = renderer != null ? renderer.bounds.size : Vector3.one;
CreateDocument();
}
//서류 타입 결정 함수
void CreateDocument()
{
_currentDocument = new DocumentData();
_currentDocument.documentType = (Random.Range(0, 2) == 0);
Classification.Instance.clean = _currentDocument.documentType;
_currentDocument.rejectObjIdx = Random.Range(0, _rejectObjPrefabs.Count);
// todo: if 피버타임이라면
// {
// _currentDocument.documentType = true
// }
// 반려 요소 크기 캐싱
var rejectRenderer = _rejectObjPrefabs[_currentDocument.rejectObjIdx].GetComponent<SpriteRenderer>();
Vector3 rejectSize = rejectRenderer != null ? rejectRenderer.bounds.size : Vector3.zero;
float minX = -_documentSize.x / 2f + rejectSize.x / 2f;
float maxX = _documentSize.x / 2f - rejectSize.x / 2f;
float minY = -_documentSize.y / 2f + rejectSize.y / 2f;
float maxY = _documentSize.y / 2f - rejectSize.y / 2f;
_currentDocument.spawnPosX = Random.Range(minX, maxX);
_currentDocument.spawnPosY = Random.Range(minY, maxY);
//장애물 타입 결정 함수로
CreateObstacle();
}
//장애물 타입 결정 함수
void CreateObstacle()
{
_day = GameManager.Instance.GetTimeController()._day;
int diffiycult = (_day / 5) + 1;
int obstacleType = Random.Range(0, _obstacleObjDatas.Count);
if (obstacleType == 0 || obstacleType == 1) // 날벌레, 포스트잇
{
for (int i = 0; i < diffiycult; i++)
{
var obstacle = new ObstacleInstance();
obstacle.obstacleObjIdx = obstacleType;
obstacle.prefab = _obstacleObjDatas[obstacleType].obstaclePrefab;
obstacle.spawnPos = new Vector2(
Random.Range(-_documentSize.x / 2f, _documentSize.x / 2f),
Random.Range(-_documentSize.y / 2f, _documentSize.y / 2f)
);
obstacle.processCount = 1;
_currentObstacles.Add(obstacle);
}
}
else if (obstacleType == 2) // 손
{
var obstacle = new ObstacleInstance();
obstacle.obstacleObjIdx = obstacleType;
obstacle.prefab = _obstacleObjDatas[obstacleType].obstaclePrefab;
obstacle.spawnPos = new Vector2(1f, -2f); //로컬 위치 고정 (todo:추후 도장찍는 위치 결정나면 바꿔야함)
obstacle.processCount = diffiycult;
_currentObstacles.Add(obstacle);
}
else // 서류철, 폴더
{
var obstacle = new ObstacleInstance();
obstacle.obstacleObjIdx = obstacleType;
obstacle.prefab = _obstacleObjDatas[obstacleType].obstaclePrefab;
obstacle.spawnPos = Vector2.zero;
obstacle.processCount = diffiycult;
_currentObstacles.Add(obstacle);
}
//서류 생성 함수로
SpawnDocument();
}
// 서류 생성 함수
void SpawnDocument()
{
_obstacleObjs.Clear();
// 서류 생성
_docObj = DocumentPool.Instance.GetObject(_documentPrefab, new Vector3(7, 0.5f, 0), Quaternion.identity);
_docObj.transform.SetParent(this.transform, true);
// 서류 타입에 따라 반려 요소 생성
if (!_currentDocument.documentType)
{
_rejectObj = DocumentPool.Instance.GetObject(
_rejectObjPrefabs[_currentDocument.rejectObjIdx], Vector3.zero, Quaternion.identity
);
// (부모: 서류 오브젝트)
_rejectObj.transform.SetParent(_docObj.transform, false);
_rejectObj.transform.localPosition = new Vector3(
_currentDocument.spawnPosX, _currentDocument.spawnPosY, 0f
);
}
// 확률에 따라 장애물 생성
float chance = Mathf.Clamp(_day * 5f, 0f, 100f);
if (Random.Range(0f, 100f) < chance)
{
Classification.Instance.obstacle = true;
foreach (ObstacleInstance obstacle in _currentObstacles)
{
var obj = DocumentPool.Instance.GetObject(obstacle.prefab, Vector3.zero, Quaternion.identity);
//(부모: 서류 오브젝트)
obj.transform.SetParent(_docObj.transform, false);
obj.transform.localPosition = new Vector3(
obstacle.spawnPos.x,
obstacle.spawnPos.y,
0f
);
_obstacleObjs.Add(obj);
var obstacleController = obj.GetComponent<ObstacleController>();
if (obstacleController != null)
{
obstacleController.Initialize(this, obstacle.processCount);
}
}
}
//서류 등장 연출
_docObj.transform.DOMove(new Vector3(_docStopPosX, _docObj.transform.position.y, _docObj.transform.position.z), _duration)
.SetEase(Ease.Linear)
.OnComplete(() =>
{
_docObj.transform.DOMove(new Vector3(_docObj.transform.position.x, _docStopPosY, _docObj.transform.position.z), _duration)
.SetEase(Ease.Linear);
});
//todo: 버튼 클릭 활성화
}
// 도장 생성 함수
public void ShowStamp(bool isApproved)
{
if (_docObj == null) return;
GameObject prefab = isApproved ? approvalStampPrefab : deniedStampPrefab;
GameObject currentStamp = Instantiate(prefab, _docObj.transform, false);
currentStamp.transform.localPosition = new Vector2(1f, -2f);
// TODO: 도장 찍히는 연출
}
//장애물이 치워지면 호출될 함수
public void ObstacleCleared(GameObject obstacleObj)
{
if (_obstacleObjs.Contains(obstacleObj))
{
_obstacleObjs.Remove(obstacleObj);
}
if (_obstacleObjs.Count == 0)
{
Classification.Instance.obstacle = false;
}
}
// 서류 치우기 함수
public void RemoveDocument()
{
//todo: 버튼 연타 방지
//서류 퇴장 연출
_docObj.transform.DOMove(new Vector3(_docObj.transform.position.x, _docDespawnY, _docObj.transform.position.z), _duration)
.SetEase(Ease.Linear)
.OnComplete(() =>
{
_docObj.transform.DOMove(new Vector3(_docDespawnX, _docObj.transform.position.y, _docObj.transform.position.z), _duration)
.SetEase(Ease.Linear)
.OnComplete(() => ReloadDocument());
});
}
public void ReloadDocument()
{
if (_docObj != null)
{
// 자식 오브젝트들을 먼저 풀에 반환
for (int i = _docObj.transform.childCount - 1; i >= 0; i--)
{
var child = _docObj.transform.GetChild(i).gameObject;
DocumentPool.Instance.ReturnObject(child);
}
// 마지막에 서류 자체 반환
DocumentPool.Instance.ReturnObject(_docObj);
}
_currentObstacles.Clear();
_obstacleObjs.Clear();
_rejectObj = null;
_docObj = null;
Classification.Instance.obstacle = false;
CreateDocument();
}
void Update()
{
if (IsInputDown(out Vector2 inputPos))
{
RaycastHit2D hit = Physics2D.Raycast(inputPos, Vector2.zero);
if (hit.collider != null)
{
ObstacleController obstacle = hit.collider.GetComponent<ObstacleController>();
if (obstacle != null)
{
obstacle.ProcessHit();
}
}
}
}
private bool IsInputDown(out Vector2 inputPos)
{
inputPos = Vector2.zero;
#if UNITY_EDITOR || UNITY_STANDALONE
if (Input.GetMouseButtonDown(0))
{
inputPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
return true;
}
#elif UNITY_IOS || UNITY_ANDROID || UNITY_WEBGL
if (Input.touchCount > 0 && Input.touches[0].phase == TouchPhase.Began)
{
inputPos = Camera.main.ScreenToWorldPoint(Input.touches[0].position);
return true;
}
#endif
return false;
}
}
2. InteractionUIController.cs
<hide/>
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class InteractionUIController : MonoBehaviour
{
[SerializeField] private RectTransform _rectTransform;
[SerializeField] private CanvasGroup _canvasGroup;
[SerializeField] private Button _acceptButton;
[SerializeField] private Button _negativeButton;
[SerializeField] private Button _pauseButton;
private void Awake()
{
//버튼 클릭이벤트 등록
_acceptButton.onClick.AddListener(OnClickAcceptButton);
_negativeButton.onClick.AddListener(OnClickNegativeButton);
_pauseButton.onClick.AddListener(OnClickPauseButton);
}
public void OnClickAcceptButton()
{
Classification.Instance.confirm = true; //승인버튼 클릭시 서류 승인
Classification.Instance.DocumentClassification(); // 서류 분류 메소드 호출
AudioManager.Instance.SFX.PlayStamp();
GameManager.Instance.GetDocumentController().ShowStamp(true);
}
public void OnClickNegativeButton()
{
Classification.Instance.confirm = false; //반려버튼 클릭시 서류 반려
Classification.Instance.DocumentClassification(); // 서류 분류 메소드 호출
AudioManager.Instance.SFX.PlayStamp();
GameManager.Instance.GetDocumentController().ShowStamp(false);
}
public void OnClickPauseButton()
{
GameManager.Instance.PauseGame();
UIManager.Instance.popupUIController.ShowPauseUI();
AudioManager.Instance.SFX.PlayButtonClick();
}
}
'Development > Internship' 카테고리의 다른 글
[멋사 로켓단 인턴쉽] 9일차 - Result Effect 및 BGM Bug Fix (0) | 2025.08.19 |
---|---|
[멋사 로켓단 인턴쉽] 8일차 - Stamp Effect 및 Result Effect (6) | 2025.08.18 |
[멋사 로켓단 인턴쉽] 7일차 - Audio Manager 완성 (0) | 2025.08.14 |
[멋사 로켓단 인턴쉽] 6일차 - Audio Manager 제작 (4) | 2025.08.14 |
[멋사 로켓단 인턴쉽] 5일차 - UI Manager (2) | 2025.08.12 |