기록 보관소

[Unity/유니티] 기초-물리 퍼즐게임: 채널링이 포함된 사운드 시스템[B59] 본문

유니티 프로젝트/물리 퍼즐게임

[Unity/유니티] 기초-물리 퍼즐게임: 채널링이 포함된 사운드 시스템[B59]

JongHoon 2022. 4. 24. 19:34

개요

유니티 입문과 독학을 위해서 아래 링크의 골드메탈님의 영상들을 보며 진행 상황 사진 또는 캡처를 올리고 배웠던 점을 요약해서 적는다.

현재는 영상들을 보고 따라하고 배우는 것에 집중할 것이며, 영상을 모두 보고 따라한 후에는 개인 프로젝트를 설계하고 직접 만드는 것이 목표다.

https://youtube.com/playlist?list=PLO-mt5Iu5TeYI4dbYwWP8JqZMC9iuUIW2 

 

유니티 강좌 기초 채널 Basic

유니티 개발을 처음 시작하시는 입문자 분들을 위한 기초 채널. [ 프로젝트 ] B00 ~ B12 (BE1) : 유니티 필수 기초 B13 ~ B19 (BE2) : 2D 플랫포머 B20 ~ B26 (BE3) : 2D 탑다운 대화형 RPG B27 ~ B37 (BE4) : 2D 종스크롤

www.youtube.com


물리 퍼즐게임: 채널링이 포함된 사운드 시스템[B59]

1. 배경 음악

GameManager에 Audio Source 생성
생성한 Audio Source 이름을 바꾸고 에셋에 있는 BGM을 넣어준다
볼륨과 Play On Awake, Loop 설정 변경

//GameManager 스크립트 파일

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour {
	public Dongle lastDongle;
	public GameObject donglePrefab;
	public Transform dongleGroup;
	public GameObject effectPrefab;
	public Transform effectGroup;

	public AudioSource bgmPlayer;

	public int score;
	public int maxLevel;
	public bool isOver;

	void Awake() {
		Application.targetFrameRate = 60;	//60프레임 이하로 유지
	}

	void Start() {
		bgmPlayer.Play();
		NextDongle();
	}

	Dongle GetDongle() {
		//이펙트 생성
		GameObject instantEffectObj = Instantiate(effectPrefab, effectGroup);
		ParticleSystem instantEffect = instantEffectObj.GetComponent<ParticleSystem>();

		//동글 생성
		GameObject instantDongleObj = Instantiate(donglePrefab, dongleGroup);
		Dongle instantDongle = instantDongleObj.GetComponent<Dongle>();

		instantDongle.effect = instantEffect;	//이펙트 전달

		return instantDongle;
	}

	void NextDongle() {
		if (isOver)
			return;

		Dongle newDongle = GetDongle();
		lastDongle = newDongle;
		lastDongle.manager = this;
		lastDongle.level = Random.Range(0, maxLevel);
		lastDongle.gameObject.SetActive(true);

		StartCoroutine(WaitNext());
	}

	IEnumerator WaitNext() {
		while (lastDongle != null) {
			yield return null;
		}

		yield return new WaitForSeconds(2.5f);

		NextDongle();
	}

	public void TouchDown() {
		if (lastDongle == null)
			return;

		lastDongle.Drag();
	}
	
	public void TouchUp() {
		if (lastDongle == null)
			return;

		lastDongle.Drop();
		lastDongle = null;	//드랍 후 조종 불가로 만들기
	}

	public void GameOver() {
		if (isOver)
			return;

		isOver = true;
		StartCoroutine("GameOverRoutine");
	}

	IEnumerator GameOverRoutine() {
		// 1. 장면 안에 활성화 되어있는 모든 동글 가져오기
		Dongle[] dongles = FindObjectsOfType<Dongle>(); //앞에 GameObject.FindObjectsOfType으로도 가능

		// 2. 지우기 전에 모든 동글의 물리효과 비활성화
		for (int index = 0; index < dongles.Length; index++) {
			dongles[index].rigid.simulated = false;
		}

		// 3. 1번의 목록을 하나씩 접근해 지우기
		for (int index = 0; index < dongles.Length; index++) {
			dongles[index].Hide(Vector3.up * 100);
			yield return new WaitForSeconds(0.1f);
		}
	}
}
  • 간단하게 AudioSource 변수를 만들어 Start에서 Play하도록 만들어주었다.

GameManager 변수 할당


2. 효과음 관리

새로운 Audio Source를 만들고 설정해준다
복사해서 2개 더 만들어준다

  • AudioClip을 교체하는 것이 아닌 여러개의 플레이어를 만든느 이유는 하나의 플레이어만 가지고있다면 중간에 다른 사운드로 바뀌기 때문이다.
//GameManager 스크립트 파일

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour {
	public Dongle lastDongle;
	public GameObject donglePrefab;
	public Transform dongleGroup;
	public GameObject effectPrefab;
	public Transform effectGroup;

	public AudioSource bgmPlayer;
	public AudioSource[] sfxPlayer;
	public AudioClip[] sfxClip;	//효과음
	public enum Sfx { LevelUp, Next, Attach, Button, Over };
	int sfxCursor;

	public int score;
	public int maxLevel;
	public bool isOver;

	void Awake() {
		Application.targetFrameRate = 60;	//60프레임 이하로 유지
	}

	void Start() {
		bgmPlayer.Play();
		NextDongle();
	}

	Dongle GetDongle() {
		//이펙트 생성
		GameObject instantEffectObj = Instantiate(effectPrefab, effectGroup);
		ParticleSystem instantEffect = instantEffectObj.GetComponent<ParticleSystem>();

		//동글 생성
		GameObject instantDongleObj = Instantiate(donglePrefab, dongleGroup);
		Dongle instantDongle = instantDongleObj.GetComponent<Dongle>();

		instantDongle.effect = instantEffect;	//이펙트 전달

		return instantDongle;
	}

	void NextDongle() {
		if (isOver)
			return;

		Dongle newDongle = GetDongle();
		lastDongle = newDongle;
		lastDongle.manager = this;
		lastDongle.level = Random.Range(0, maxLevel);
		lastDongle.gameObject.SetActive(true);

		StartCoroutine(WaitNext());
	}

	IEnumerator WaitNext() {
		while (lastDongle != null) {
			yield return null;
		}

		yield return new WaitForSeconds(2.5f);

		NextDongle();
	}

	public void TouchDown() {
		if (lastDongle == null)
			return;

		lastDongle.Drag();
	}
	
	public void TouchUp() {
		if (lastDongle == null)
			return;

		lastDongle.Drop();
		lastDongle = null;	//드랍 후 조종 불가로 만들기
	}

	public void GameOver() {
		if (isOver)
			return;

		isOver = true;
		StartCoroutine("GameOverRoutine");
	}

	IEnumerator GameOverRoutine() {
		// 1. 장면 안에 활성화 되어있는 모든 동글 가져오기
		Dongle[] dongles = FindObjectsOfType<Dongle>(); //앞에 GameObject.FindObjectsOfType으로도 가능

		// 2. 지우기 전에 모든 동글의 물리효과 비활성화
		for (int index = 0; index < dongles.Length; index++) {
			dongles[index].rigid.simulated = false;
		}

		// 3. 1번의 목록을 하나씩 접근해 지우기
		for (int index = 0; index < dongles.Length; index++) {
			dongles[index].Hide(Vector3.up * 100);
			yield return new WaitForSeconds(0.1f);
		}
	}

	public void SfxPlay(Sfx type) {
		switch(type) {
			case Sfx.LevelUp:
				sfxPlayer[sfxCursor].clip = sfxClip[Random.Range(0, 3)];
				break;
			case Sfx.Next:
				sfxPlayer[sfxCursor].clip = sfxClip[3];
				break;
			case Sfx.Attach:
				sfxPlayer[sfxCursor].clip = sfxClip[4];
				break;
			case Sfx.Button:
				sfxPlayer[sfxCursor].clip = sfxClip[5];
				break;
			case Sfx.Over:
				sfxPlayer[sfxCursor].clip = sfxClip[6];
				break;
		}

		sfxPlayer[sfxCursor].Play();
		sfxCursor = (sfxCursor + 1) % sfxPlayer.Length;	//값이 범위를 넘지 않도록 나머지 연산 사용
	}
}
  • 앞에서 만들었던 3개의 플레이어를 담을 AudioSource 배열 sfxPlayer와 재생할 효과음을 담을 AudioClip 배열 sfxClip, 효과음별로 enum을 통해 타입들을 만들어주고, SfxPlayer 함수를 만들어 인자(sfx 타입)에 따라서 재생할 수 있도록 만들었다.

GameManager 변수 할당


3. 효과음 배치

//Dongle 스크립트 파일

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Dongle : MonoBehaviour {
	public GameManager manager;
	public ParticleSystem effect;
	public int level;
	public bool isDrag;
	public bool isMerge;
	public bool isAttach;

	public Rigidbody2D rigid;
	CircleCollider2D circle;
	Animator anim;
	SpriteRenderer spriteRenderer;

	float deadTime;

	void Awake() {
		rigid = GetComponent<Rigidbody2D>();
		circle = GetComponent<CircleCollider2D>();
		anim = GetComponent<Animator>();
		spriteRenderer = GetComponent<SpriteRenderer>();
	}

	void OnEnable() {
		anim.SetInteger("Level", level);
	}

	void Update() {
		if (isDrag) {
			Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
			float leftBorder = -4.2f + transform.localScale.x / 2f; //좌측 벽 경계 설정
			float rightBorder = 4.2f - transform.localScale.x / 2f; //우측 벽 경계 설정

			if (mousePos.x < leftBorder) {  //벽에 x축 접근 제한
				mousePos.x = leftBorder;
			}
			else if (mousePos.x > rightBorder) {
				mousePos.x = rightBorder;
			}

			mousePos.y = 8; //Y축 고정해서 경계선 밑으로 내려가지 않도록 설정
			mousePos.z = 0; //Z축 고정해서 맵 밖으로 나가지 않도록 설정
			transform.position = Vector3.Lerp(transform.position, mousePos, 0.2f);
		}

	}

	public void Drag() {
		isDrag = true;
	}

	public void Drop() {
		isDrag = false;
		rigid.simulated = true;
	}

	void OnCollisionEnter2D(Collision2D collision) {
		StartCoroutine("AttachRoutine");
	}

	IEnumerator AttachRoutine() {
		if (isAttach) {
			yield break;
		}

		isAttach = true;
		manager.SfxPlay(GameManager.Sfx.Attach);

		yield return new WaitForSeconds(0.2f);

		isAttach = false;
	}

	void OnCollisionStay2D(Collision2D collision) {
		if (collision.gameObject.tag == "Dongle") {
			Dongle other = collision.gameObject.GetComponent<Dongle>();

			if (level == other.level && !isMerge && !other.isMerge && level < 7) {
				//상대편 위치 가져오기
				float meX = transform.position.x;
				float meY = transform.position.y;
				float otherX = other.transform.position.x;
				float otherY = other.transform.position.y;

				//1. 내가 아래에 있을 때
				//2. 동일한 높이 or 내가 오른쪽에 있을 때
				if (meY < otherY || (meY == otherY && meX > otherX)) {
					//상대방 숨기기
					other.Hide(transform.position);
					//나는 레벨 업
					LevelUp();
				}
			}
		}
	}

	public void Hide(Vector3 targetPos) {
		isMerge = true;

		rigid.simulated = false;
		circle.enabled = false;

		if (targetPos == Vector3.up * 100)
			EffectPlay();

		StartCoroutine(HideRoutine(targetPos));
	}

	IEnumerator HideRoutine(Vector3 targetPos) {
		int frameCount = 0;

		while(frameCount < 20) {
			frameCount++;
			if (targetPos != Vector3.up * 100) {
				transform.position = Vector3.Lerp(transform.position, targetPos, 0.5f);
			}
			else if (targetPos == Vector3.up * 100) {
				transform.localScale = Vector3.Lerp(transform.localScale, Vector3.zero, 0.2f);
			}

			yield return null;
		}

		manager.score += (int)Mathf.Pow(2, level);

		isMerge = false;
		gameObject.SetActive(false);
	}

	void LevelUp() {
		isMerge = true;

		rigid.velocity = Vector2.zero;
		rigid.angularVelocity = 0;  //회전 속도

		StartCoroutine(LevelUpRoutine());
	}

	IEnumerator LevelUpRoutine() {
		yield return new WaitForSeconds(0.2f);

		anim.SetInteger("Level", level + 1);    //애니메이션 실행
		EffectPlay();   //이펙트 실행
		manager.SfxPlay(GameManager.Sfx.LevelUp);

		yield return new WaitForSeconds(0.3f);
		level++;    //변수 값 증가

		manager.maxLevel = Mathf.Max(level, manager.maxLevel);

		isMerge = false;
	}

	void OnTriggerStay2D(Collider2D collision) {
		if (collision.tag == "Finish") {
			deadTime += Time.deltaTime;

			if (deadTime > 2) {
				spriteRenderer.color = new Color(0.9f, 0.2f, 0.2f);	//Color.red; 도 가능
			}
			if (deadTime > 5) {
				manager.GameOver();
			}
		}
	}

	void OnTriggerExit2D(Collider2D collision) {
		if (collision.tag == "Finish") {
			deadTime = 0;
			spriteRenderer.color = Color.white;
		}
	}

	void EffectPlay() {
		effect.transform.position = transform.position;
		effect.transform.localScale = transform.localScale;
		effect.Play();
	}
}
//GameManager 스크립트 파일

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour {
	public Dongle lastDongle;
	public GameObject donglePrefab;
	public Transform dongleGroup;
	public GameObject effectPrefab;
	public Transform effectGroup;

	public AudioSource bgmPlayer;
	public AudioSource[] sfxPlayer;
	public AudioClip[] sfxClip;	//효과음
	public enum Sfx { LevelUp, Next, Attach, Button, Over };
	int sfxCursor;

	public int score;
	public int maxLevel;
	public bool isOver;

	void Awake() {
		Application.targetFrameRate = 60;	//60프레임 이하로 유지
	}

	void Start() {
		bgmPlayer.Play();
		NextDongle();
	}

	Dongle GetDongle() {
		//이펙트 생성
		GameObject instantEffectObj = Instantiate(effectPrefab, effectGroup);
		ParticleSystem instantEffect = instantEffectObj.GetComponent<ParticleSystem>();

		//동글 생성
		GameObject instantDongleObj = Instantiate(donglePrefab, dongleGroup);
		Dongle instantDongle = instantDongleObj.GetComponent<Dongle>();

		instantDongle.effect = instantEffect;	//이펙트 전달

		return instantDongle;
	}

	void NextDongle() {
		if (isOver)
			return;

		Dongle newDongle = GetDongle();
		lastDongle = newDongle;
		lastDongle.manager = this;
		lastDongle.level = Random.Range(0, maxLevel);
		lastDongle.gameObject.SetActive(true);

		SfxPlay(Sfx.Next);
		StartCoroutine(WaitNext());
	}

	IEnumerator WaitNext() {
		while (lastDongle != null) {
			yield return null;
		}

		yield return new WaitForSeconds(2.5f);

		NextDongle();
	}

	public void TouchDown() {
		if (lastDongle == null)
			return;

		lastDongle.Drag();
	}
	
	public void TouchUp() {
		if (lastDongle == null)
			return;

		lastDongle.Drop();
		lastDongle = null;	//드랍 후 조종 불가로 만들기
	}

	public void GameOver() {
		if (isOver)
			return;

		isOver = true;
		StartCoroutine("GameOverRoutine");
	}

	IEnumerator GameOverRoutine() {
		// 1. 장면 안에 활성화 되어있는 모든 동글 가져오기
		Dongle[] dongles = FindObjectsOfType<Dongle>(); //앞에 GameObject.FindObjectsOfType으로도 가능

		// 2. 지우기 전에 모든 동글의 물리효과 비활성화
		for (int index = 0; index < dongles.Length; index++) {
			dongles[index].rigid.simulated = false;
		}

		// 3. 1번의 목록을 하나씩 접근해 지우기
		for (int index = 0; index < dongles.Length; index++) {
			dongles[index].Hide(Vector3.up * 100);
			yield return new WaitForSeconds(0.1f);
		}

		yield return new WaitForSeconds(1f);

		SfxPlay(Sfx.Over);
	}

	public void SfxPlay(Sfx type) {
		switch(type) {
			case Sfx.LevelUp:
				sfxPlayer[sfxCursor].clip = sfxClip[Random.Range(0, 3)];
				break;
			case Sfx.Next:
				sfxPlayer[sfxCursor].clip = sfxClip[3];
				break;
			case Sfx.Attach:
				sfxPlayer[sfxCursor].clip = sfxClip[4];
				break;
			case Sfx.Button:
				sfxPlayer[sfxCursor].clip = sfxClip[5];
				break;
			case Sfx.Over:
				sfxPlayer[sfxCursor].clip = sfxClip[6];
				break;
		}

		sfxPlayer[sfxCursor].Play();
		sfxCursor = (sfxCursor + 1) % sfxPlayer.Length;	//값이 범위를 넘지 않도록 나머지 연산 사용
	}
}
  • 두 스크립트 모두 앞 파트에서 만들었던 SfxPlay 함수를 이용해 각 상황에 맞게 사운드를 플레이할 수 있도록 만들어주었다.
  • Button과 Attach를 제외한 LevelUp, Next, Over는 구현이 되어있다. Button은 아마 이름으로 미뤄보건데 마지막 게임 마무리 UI 작업에서 할 것으로 보인다. Attach는 동글이 바닥이나 다른 동글에 닿을때, 즉 Collider가 닿을때여서 Dongle에서 OnCollisionEnter2D를 통해 코루틴으로 효과음을 Play하도록 추가해주었다.
  • 이번 영상은 사운드 작업만 했기에 따로 캡처가 없다. 아무튼 큰 문제 없이 잘 실행되었다.