이번에 제작한 유저 가이드화살표에 대한 얘기이다.
우선 제작 자체는 생각보다는 복잡했고 생각보다는 적용이 빨랐지만 자잘한 버그 덕분에 시간이 제법 걸렸다.
만들게 된 이유는 미니맵 만으로는 아직 활성화 되지 않은 룸의 위치를 알수가 없는것이 문제고 (이건 현재 룸들의 모습이 테스트용 맵인 것도 한몫한다.) 유저가 길을 잃는 것 보단 유저가 룸간을 이동하며 플로우가 끊기지 않게 하는 것을 보조할 UI 가 필요하다고 판단하여 제작하게 되었다.
유저 가이드 화살표 UI의 경우 프로젝트에서의 적용 사항은 아래와 같다.
- 유저가 있는 룸의 종류에 따라 가이드 화살표의 활성화 여부가 결정된다.
- 비전투 구역일시 바로 활성화, 전투 구역일시 클리어 조건 달성시 활성화 된다.
- 룸을 이동시 가이드 화살표는 기본적으로 oFF 되며 위의 기준에 따라 다시 활성화 될지 여부가 결정된다.
- 일반 전투 룸의 경우 미니맵을 참고 할수 있으므로 처음 클리어 시에만 활성화 되고 이후 재 방문시에는 비활성화 되게 해두었다 (수정 될 가능성이 있다.)
- 가이드 화살표는 다음 룸이 어떤 룸이냐에 따라 색상 혹은 모양이 바뀌며 (현재는 색상으로 구분한다) 포탈의 위치에 따라 이동한다.
- 포탈이 카메라 시야 안에 들어왔을 경우 보이지 않아야 한다.
- 유저에게 보이지만 유저의 플레이를 방해해서는 안된다.
위의 조건을 지키며 활성화 비활성화를 반복해야 하는데 이게 현재 구조에 맞춰 제작하다보니 생각보다 복잡해지는 부분이 있어 수정을 조금 많이 하게 되었다.
영상을 보면 쉽게 역할을 이해할수 있다.

코드는 유저가 룸을 이동시 해당 룸이 어떤 룸인지 확인하여 (MAIN 스크립트에서 확인) 활성화 되며 활성화 되면 넘어가는 룸의 트랜스폼을 받아 자식들중 포탈이 있는 포인트를 확인, 이후 해당 포인트에 각각 화살표를 할당하여 코루틴을 돌리게 된다. 만약 유저가 해당 룸을 떠나게 된다면 (포탈을 타게 된다면 ) 활성화를 비활성화하고 초기화 상태로 돌리며 코루틴을 모두 중지시킨다. (몬스터 제너레이터의 룸클리어 조건에서의 문제로 초기화 상태에 많은 조건이 들어갔는데 실제로는 코루틴을 멈추는 것으로 충분하다 * 버그 잡는다고 초기화를 명시적으로 빡세게 했다 ;; ㅎㅎ)
코드는 아래와 같다.
using DG.Tweening;
using System.Collections;
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UI;
public class UIPortalArrowController : MonoBehaviour
{
[SerializeField]
private List<GameObject> arrowArr; // 포탈 방향에 따른 Ui 화살표 이미지 배열
private GameObject[] portalArr; // 동서남북 방향에 있는 포탈 오브젝트 배열
private Camera mainCamera; // 메인 카메라
private bool isActive;
public void Init()
{
this.mainCamera = Camera.main;
this.isActive = false;
EventDispatcher.Instance.AddListener<Transform>(EventDispatcher.EventName.UIPortalArrowControllerInitializingArrows,
this.InitializingArrows);
EventDispatcher.Instance.AddListener(EventDispatcher.EventName.UIPortalArrowControllerStopAllArrowCorutines,
this.StopAllArrowCorutines);
this.gameObject.SetActive(false);
}
private void InitializingArrows(Transform roomTrans)
{
this.gameObject.SetActive(true);
this.arrowArr.ForEach((x) => x.SetActive(false));
GameObject[] temp = { null, null, null, null };
this.isActive = true;
for (int i = 0; i < temp.Length; i++)
{
if (roomTrans.GetChild(i).childCount == 1)
{
temp[i] = roomTrans.GetChild(i).gameObject;
}
else
{
temp[i] = null;
}
}
this.portalArr = temp;
for (int i = 0; i < this.portalArr.Length; i++)
{
if (this.portalArr[i] != null) {
this.arrowArr[i].SetActive(true);
this.ChageArrowColor(this.portalArr[i], this.arrowArr[i]);
this.StartCoroutine(this.CoFollowPortals(this.portalArr[i], this.arrowArr[i]));
}
else{
this.arrowArr[i].SetActive(false);
}
}
}
private void ChageArrowColor(GameObject portal, GameObject arrow)
{
if(portal.transform.GetChild(0).gameObject.name.Contains("Safe")){
arrow.GetComponent<Image>().color = Color.green;
}
else if (portal.transform.GetChild(0).gameObject.name.Contains("Boss")){
arrow.GetComponent<Image>().color = Color.red;
}
else if(portal.transform.GetChild(0).gameObject.name.Contains("Hidden")){
arrow.GetComponent<Image>().color = Color.yellow;
}
else{
arrow.GetComponent<Image>().color = Color.white;
}
}
private IEnumerator CoFollowPortals(GameObject portal, GameObject arrow)
{
while (this.isActive)
{
var portalPos = this.mainCamera.WorldToScreenPoint(portal.transform.position);
if (!this.IsInCameraView(portal.transform.position))
{
arrow.gameObject.SetActive(true);
if (portal.name.Contains("0") || portal.name.Contains("2"))
{
this.ChangeArrowPosition(portalPos, arrow, false);
this.ChangeArrowRotation(portalPos, arrow);
}
else
{
this.ChangeArrowPosition(portalPos, arrow, true);
this.ChangeArrowRotation(portalPos, arrow);
}
}
else
{
arrow.gameObject.SetActive(false);
}
yield return null;
}
}
private bool IsInCameraView(Vector3 position)
{
Vector3 screenPoint = this.mainCamera.WorldToViewportPoint(position);
return screenPoint.x >= 0f && screenPoint.x <= 1f && screenPoint.y >= 0f && screenPoint.y <= 1f;
}
private void ChangeArrowPosition(Vector3 portalPos, GameObject arrow, bool vertical)
{
if (!vertical)
{
if (portalPos.x < 80)
{
portalPos.x = 80;
}
else if (portalPos.x > Screen.width - 80)
{
portalPos.x = Screen.width - 80;
}
arrow.transform.position = new Vector2(portalPos.x, arrow.transform.position.y);
}
else
{
if (portalPos.y < 80)
{
portalPos.y = 80;
}
else if (portalPos.y > Screen.height - 80)
{
portalPos.y = Screen.height - 80;
}
arrow.transform.position = new Vector2(arrow.transform.position.x, portalPos.y);
}
}
private void ChangeArrowRotation(Vector3 portalPos, GameObject arrow)
{
Vector3 direction = portalPos - arrow.transform.position;
float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
arrow.transform.rotation = Quaternion.AngleAxis(angle, Vector3.forward);
// 포탈 방향으로 뾰족한 방향 설정
Vector3 localScale = arrow.transform.localScale;
if (direction.x >= 0) // 수정된 부분
{
localScale.x = Mathf.Abs(localScale.x);
arrow.transform.localScale = localScale;
}
else
{
localScale.x = -Mathf.Abs(localScale.x);
arrow.transform.localScale = localScale;
arrow.transform.rotation = Quaternion.AngleAxis(angle - 180f, Vector3.forward);
}
}
private void StopAllArrowCorutines()
{
this.StopAllCoroutines();
this.portalArr = null;
this.isActive = false;
this.arrowArr.ForEach((x) => x.SetActive(false));
this.gameObject.SetActive(false);
}
}
앞으로 고민해 보아야 하는 사항은 아래와 같다.
- 한번 클리어 한 전투룸에는 상시 UI를 뜨게 할 것인가?
- UI의 경우 이미지 처럼 다른 UI들의 아래로 들어가거나 위로 들어가게 해야하는데 (현재는 위) 위치는 어떻게 할것인가? 유저의 플레이를 방해하지는 않는가?
이러한 사항들을 빌드하여 직접 플레이 해보며 판단해야 할듯 하다.
'프로젝트 > 건즈앤 레이첼스' 카테고리의 다른 글
| [유니티 프로젝트] 상자 아이템 제너레이터 구조 (0) | 2023.05.01 |
|---|---|
| [유니티 프로젝트] DOTween을 이용한 팝업 UI 애니메이션 (0) | 2023.05.01 |
| [유니티 프로젝트] 타일맵 오브젝트 --> PNG 통짜 스프라이트 이미지 변경 코드 (0) | 2023.04.27 |
| [유니티 프로젝트] 타이틀 씬 to 던전 씬 까지 (2) | 2023.04.26 |
| [유니티 프로젝트] 허브 (성소) -> 던전 -> 허브 루프 (윤회) (1) | 2023.04.23 |