0. 들어가기 전에
이번에는 메인 씬에 오디오 기능을 적용했다. bgm 과 효과음을 넣었다.
그리고 이전에는 UI 가 위에 있는데도 불구하고 밑에 가려진 점토 게임 오브젝트가 클릭돼서 기능이 수행됐다. 이를 막기 위한 로직을 추가했다.
1. 게임 오브젝트
BGM 게임 오브젝트를 새로 만들었다.
그리고 기존의 있던 게임 오브젝트들에 각각 Audio Source 컴포넌를 부착하고 스크립트 상에서 스스로 효과음을 플레이 하도록 했다.
ClayButton 게임 오브젝트를 예로 들면 인스펙터 창의 모습이 다음과 같다.
우선 Audio Source 컴포넌트가 추가되었다. 그리고 자신의 스크립트에 Audio 기능과 관련된 변수를 선언했다. 그리고 PlaySound() 메서드를 만들었고 이를 호출해서 효과음을 재생하도록 했다. 다른 게임 오브젝트들에도 이와 같은 방식으로 효과음을 내도록 만들었다.
2. 스크립트
효과음 재생 로직은 간단하기 때문에 따로 설명은 하지 않고 스크립트만 부착한다.
중요한건 UI 와 점토의 상호작용을 관리하는 것이다. UI 가 위에 있을 때 아래에 있는 점토 게임 오브젝트는 눌려도 작동이 안 되도록 해야 하는데 이를 위한 코드를 Clay 스크립트에 추가했다.
2.1 효과음 재생 스크립트
효과음을 재생하기 위한 로직은 똑같기 때문에 예시 스크립트를 하나 가져왔다.
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.EventSystems;
public class ClaySellController : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
{
[Header("Clay Info")]
public PoolManager poolManager;
public Clay sellClay; // 판매할 점토
[Header("Effect")]
public int effectIdx = 1;
[Header("Audio")]
public AudioSource sellAudio;
public AudioClip sellClip;
public AudioClip failClip;
private void Awake()
{
sellAudio = GetComponent<AudioSource>();
}
private void Start()
{
// 델리게이트에 메서드 연결
poolManager.OnSellClay -= SellClay; // 중복 방지하기 위해 미리 빼고 시작
poolManager.OnSellClay += SellClay;
}
private void OnDestroy()
{
poolManager.OnSellClay -= SellClay; // 이벤트 구독 해제
}
public void OnPointerEnter(PointerEventData eventData)
{
poolManager.isSellZone = true;
if (poolManager.curClay == null) return; // 현재 선택된 점토가 없으면 그냥 나가도록..
// 만약 점토가 점토 판매 버톤으로 드래그되어서 들어온다면..
if (poolManager.curClay.isDragging)
sellClay = poolManager.curClay; // 판매할 점토를 현재 드래그 중인 클레이로 설정..
}
public void OnPointerExit(PointerEventData eventData)
{
poolManager.isSellZone = false;
sellClay = null;
}
public void SellClay()
{
if (sellClay != null)
{
if (sellClay.clayLevel == 5) // 성체만 판매 가능
{
sellClay.gameObject.GetComponent<SpriteRenderer>().sortingOrder = -1;
poolManager.ReturnToPool(sellClay.gameObject); // 비활성화시키기..
GameManager.instance.GetGold(sellClay.sellPrice); // 돈 얻기
GameManager.instance.poolManager.curClayNum--; // 현재 점토 개수 -1 해주기
PlayEffect(effectIdx);
PlaySound(sellClip); // 효과음
}
else
{
// 안내 문구 시작
GameManager.instance.StartInfoPanel("성체가 된 점토만 판매할 수 있어요!");
PlaySound(failClip); // 효과음
return;
}
}
}
public void PlayEffect(int idx)
{
if (GameManager.instance.effects[idx].gameObject.activeSelf == false)
GameManager.instance.effects[idx].gameObject.SetActive(true); // 활성화하기
GameManager.instance.effects[idx].Play();
}
private void PlaySound(AudioClip clip)
{
sellAudio.clip = clip;
sellAudio.Play();
}
}
2.2 Clay 스크립트
using System.Collections;
using System.Collections.Generic;
using TreeEditor;
using UnityEngine;
using UnityEngine.EventSystems;
public class Clay : MonoBehaviour
{
[Header("Clay Data")]
public float[] loves; // 점토를 클릭했을 때 얻는 애정 수치(1~5 레벨)
public int[] touchCnts; // 해당 요소만큼 클릭되면 레벨업
public int clayLevel = 1; // 1레벨에서 시작(5레벨까지 있음)
public int curTouchCnt = 0;
public int clayIdx; // 점토 소환할 때 필요한 인덱스
public int buyPrice; // 구매 가격
public int sellPrice; // 판매 가격
public string clayName; // 점토 이름
public string clayInfo; // 점토 설명
public int effectIdx = 0;
[Header("Animation")]
Animator anim; // 점토가 터치될 때 애니메이션 실행하기 위함
public RuntimeAnimatorController[] animators;
public float[] claySizes;
[Header("Sprites")]
public Sprite clay; // 점토 모습
public Sprite animal; // 다 자란 동물 모습
public GameObject shadow; // 이거 가구 애니메이션 중일 때는 끄도록..
[Header("Drag Clay")]
public float targetTime = 1; // 드래그 시작할 수 있는 시간
public float curTime; // 현재 시간
public Vector3 prevPos; // 점토를 드래그 하기 전 위치
public bool isDragging; // 드래그 중인지 확인
[Header("Pool Manager")]
public PoolManager poolManager;
[Header("ToyAnimation")]
public bool isUsed; // 이미 사용되고 있는 중이면 클릭 버튼이 안 먹히도록 하는데 이용할 것..
[Header("Light")]
public GameObject clayLight; // 점토 불빛
int cnt = 0;
[Header("Audio")]
public AudioSource clayAudio;
public AudioClip touchClip; // 터치 사운드
public AudioClip levelUpClip; // 레벨업 사운드
private void Awake()
{
anim = GetComponent<Animator>();
clayAudio = GetComponent<AudioSource>();
shadow = transform.GetChild(0).gameObject;
}
private void Start()
{
poolManager = GameObject.Find("PoolManager").GetComponent<PoolManager>();
SetClay(); // 점토 정보 반영 세팅(애니메이션, scale)
}
// 점토가 터치되면 저절로 호출됨
private void OnMouseDown()
{
// 만약 점토 위에 UI 있으면 기능 수행하지 말고 그냥 빠져나가자..
if (IsPointerOverUIObject())
return;
// toy 애니메이션이 수행되고 있는 중이면 빠져나가도록..
if (isUsed) return;
PlayClaySound(touchClip); // 효과음 수행
prevPos = transform.position; // 드래그 하기 전 위치 저장
poolManager.curClay = gameObject.GetComponent<Clay>(); // 현재 선택된 클레이 지정
Debug.Log("터치됨!");
anim.SetTrigger("doTouch");
GameManager.instance.GetLove(loves[clayLevel - 1] * GameManager.instance.clayClickLevel); // 레벨에 맞는 수치를 함수로 넘겨주기
curTouchCnt++;
LevelUpClay();
}
private void LevelUpClay()
{
// 이미 점토의 레벨이 최고 레벨에 도달했으면 밑으로 진입 안 하도록..
if (clayLevel != 5)
{
if (curTouchCnt == touchCnts[clayLevel - 1])
{
clayLevel++; // 레벨 1 증가
curTouchCnt = 0; // 초기화
SetClay(); // 점토 정보 반영 세팅(애니메이션, scale)
PlayEffect(effectIdx);
PlayClaySound(levelUpClip); // 효과음 수행
if (clayLevel == 5)
gameObject.GetComponent<SpriteRenderer>().sprite = animal;
}
}
}
private void PlayEffect(int idx)
{
if (GameManager.instance.effects[idx].gameObject.activeSelf == false)
GameManager.instance.effects[idx].gameObject.SetActive(true); // 활성화하기
GameManager.instance.effects[idx].transform.position = transform.position; // 이펙트 위치를 점토 위치로 바꿔주기
GameManager.instance.effects[idx].Play(); // 실행시키깅
}
// 점토 드래그할 때 호출되는 함수
private void OnMouseDrag()
{
// 만약 점토 위에 UI 있으면 기능 수행하지 말고 그냥 빠져나가자..
if (IsPointerOverUIObject())
return;
// toy 애니메이션이 수행되고 있는 중이면 빠져나가도록..
if (isUsed) return;
curTime += Time.deltaTime;
isDragging = true;
// 만약 현재 시간이 targetTime 보다 크거나 같다면 점토가 마우스를 따라오도록 하기..
if (curTime >= targetTime)
{
// 불 켜는 건 한번만 수행할 수 있도록..
if (cnt == 0)
{
cnt++;
SetLight(); // 점토의 빛 켜주기.. UI 꺼주기, local light 켜주기
}
// 마우스 위치를 가져온 후 Z 축을 카메라와의 거리로 설정한다
Vector3 mousePosition = Input.mousePosition;
mousePosition.z = Camera.main.WorldToScreenPoint(transform.position).z;
// 스크린 좌표를 월드 좌표로 변환
Vector3 worldPosition = Camera.main.ScreenToWorldPoint(mousePosition);
// 오브젝트의 위치를 마우스의 월드 좌표로 이동
transform.position = worldPosition;
// 이 경우에는 UI 보다도 앞에 갈 수 있도록..
gameObject.GetComponent<SpriteRenderer>().sortingOrder = 10;
}
}
// 점토 내려놓을 때 호출되는 함수
private void OnMouseUp()
{
// 만약 점토 위에 UI 있으면 기능 수행하지 말고 그냥 빠져나가자..
if (IsPointerOverUIObject())
return;
// toy 애니메이션이 수행되고 있는 중이면 빠져나가도록..
if (isUsed) return;
cnt = 0; // 다시 0으로 돌려놓기..
curTime = 0;
isDragging = false;
SetLight(); // 점토의 빛 꺼주기.. UI 켜주기, local Light 꺼주기..
if (transform.position.x < -6.5 || transform.position.x > 6.5 || transform.position.y < -3 || transform.position.y > 0.6)
transform.position = prevPos;
// 다시 UI 보다 아래로 가도록..
gameObject.GetComponent<SpriteRenderer>().sortingOrder = -1;
if (poolManager.isSellZone == true)
{
poolManager.isPossibleSell = true; // 판매 신호 주기
poolManager.TrySellClay(); // 이벤트 호출
}
if (GameManager.instance.curToyIdx != -1)
StartToyAnim(GameManager.instance.curToyIdx); // 가구 애니메이션 수행
}
public void ResetInfo(int level, int cnt)
{
if (level == 5)
gameObject.GetComponent<SpriteRenderer>().sprite = animal; // 동물 모습으로 바꿔주기..
// 점토 정보 설정
clayLevel = level;
curTouchCnt = cnt;
SetClay();
}
public void ResetInfo()
{
// 점토 정보 초기화
clayLevel = 1;
curTouchCnt = 0;
gameObject.GetComponent<SpriteRenderer>().sprite = clay; // 점토 모습으로 바꿔주기..
SetClay();
}
private void SetClay()
{
anim.runtimeAnimatorController = animators[clayLevel - 1]; // 레벨에 맞는 애니메이터로 바꿔주기..
gameObject.transform.localScale = new Vector3(claySizes[clayLevel - 1], claySizes[clayLevel - 1], 0); // 레벨에 맞게 사이즈 설정
// 끄고 다시 켜서 애니메이터에 반영
anim.gameObject.SetActive(false);
anim.gameObject.SetActive(true);
}
public void StartToyAnim(int idx)
{
shadow.SetActive(false); // 그림자 활성화 끄기
Debug.Log("가구 애니메이션 시작했어여!!");
isUsed = true; // 메서드 빠져나갈 때 false 로 다시 설정해주기..
GetComponent<ClayMove>().isToyActioning = true;
// 애니메이션 시작
anim.runtimeAnimatorController = GameManager.instance.clayToyAnimators[idx];
// 코루틴 시작
StartCoroutine(PerformAnimation());
}
private IEnumerator PerformAnimation()
{
float duration = 3f;
yield return new WaitForSeconds(duration);
// 애니메이터 원상태로 돌려놓기
SetClay();
isUsed = false;
GetComponent<ClayMove>().isToyActioning = false;
transform.position = prevPos; // 이전 위치로 돌아가도록..
shadow.SetActive(true); // 그림자 활성화 켜기
Debug.Log("애니메이션 완료!");
}
public void SetLight()
{
// 점토를 들 때 점토의 빛이 켜져야 하므로 isDragging 값을 넘겨줄 것..
clayLight.SetActive(isDragging);
// 점토를 들면 화면 상의 UI 와 local light 이 켜져야 하므로 isDragging 값 넘겾귀
// 점토를 내려놓아도 마찬가지..
GameManager.instance.SetLightAndUI(isDragging);
}
private void PlayClaySound(AudioClip clip)
{
// 효과음 수행 메서드
clayAudio.clip = clip;
clayAudio.Play();
}
private bool IsPointerOverUIObject()
{
// 점토 위에 UI 있으면 점토 관련 기능 수행 안 되도록..
PointerEventData eventDataCurrentPosition = new PointerEventData(EventSystem.current)
{
position = new Vector2(Input.mousePosition.x, Input.mousePosition.y)
};
List<RaycastResult> results = new List<RaycastResult>();
EventSystem.current.RaycastAll(eventDataCurrentPosition, results);
// 걸린 UI 가 모두 예외 UI 가 아니라면 true 반환하기(점토 기능 수행 안 되도록..)
foreach (var obj in results)
{
// 예외 UI 가 아니면 바로 true 반환
if (obj.gameObject.layer != 12)
return true;
}
// 여기까지 왔으면 false 반환
return false;
}
}
2.3 Clay 스크립트 변경 사항 설명
효과음 관련 변경 사항 설명은 생략한다.
1. IsPointerOverUIObject()
UI 가 점토와 겹쳐 있다면 점토의 기능을 수행하지 않도록 하기 위해 만든 스크립트이다.
하지만 메인 씬에 존재하는 가구도 UI 이기 때문에 단순히 UI 가 있으면 빠져나가도록 하는 방식은 곤란하다. 이를 해결하기 위해 현재 마우스에 걸린 UI 를 모두 파악한 후 특별 UI 가 아닌 게 하나라도 있다면 true 를 반환하도록 했다.
만약 현재 마우스에 걸린게 모두 특별 UI 라면 false 를 반환해서 점토의 기능이 수행될 수 있도록 했다.
이 메서드는 OnMouseDown(), OnMouseDrag(), OnMouseUP() 메서드 맨 처음에서 호출되도록 했다.
private bool IsPointerOverUIObject()
{
// 점토 위에 UI 있으면 점토 관련 기능 수행 안 되도록..
PointerEventData eventDataCurrentPosition = new PointerEventData(EventSystem.current)
{
position = new Vector2(Input.mousePosition.x, Input.mousePosition.y)
};
List<RaycastResult> results = new List<RaycastResult>();
EventSystem.current.RaycastAll(eventDataCurrentPosition, results);
// 걸린 UI 가 모두 예외 UI 가 아니라면 true 반환하기(점토 기능 수행 안 되도록..)
foreach (var obj in results)
{
// 예외 UI 가 아니면 바로 true 반환
if (obj.gameObject.layer != 12)
return true;
}
// 여기까지 왔으면 false 반환
return false;
}
3. 결과물
4. 참고자료
참고한 것들.
4.1 UI 뒤쪽 게임 오브젝트 클릭 방지
UI뒤쪽의 게임오브젝트 클릭 방지
유니티에서 게임오브젝트를 클릭할 경우 메인 카메라의 ray를 통해서 선택하게 된다. 그런데 이 경우 흔히 ...
blog.naver.com
[Unity] UI 클릭 시, 다른 클릭 이벤트 막기
UI에 있는 오브젝트를 클릭했을 때 다른 클릭 이벤트가 발생하지 않도록 해야할 때가 있습니다. 예를 들어, 스타크래프트에서 UI를 클릭했는데 유닛이 움직이거나 공격한다면 곤란할 것입니다. U
ladun.tistory.com