기록 보관소

[Unity/유니티] 기초-물리 퍼즐게임: 모바일 게임으로 완성하기[BE6] 본문

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

[Unity/유니티] 기초-물리 퍼즐게임: 모바일 게임으로 완성하기[BE6]

JongHoon 2022. 4. 29. 23:18

개요

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

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

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


물리 퍼즐게임: 모바일 게임으로 완성하기[BE6]

1. 변수 정리

//GameManager 스크립트 파일

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

public class GameManager : MonoBehaviour {
	[Header("============[ Core ]============")]
	public bool isOver;
	public int score;
	public int maxLevel;

	[Header("========[ Object Pooling ]========")]
	public GameObject donglePrefab;
	public Transform dongleGroup;
	public List<Dongle> donglePool;
	public GameObject effectPrefab;
	public Transform effectGroup;
	public List<ParticleSystem> effectPool;
	[Range(1, 30)]  //Inspector 창에서 poolSize가 1~30까지 스크롤형태로 표현됨
	public int poolSize;
	public int poolCursor;
	public Dongle lastDongle;

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

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

		donglePool = new List<Dongle>();
		effectPool = new List<ParticleSystem>();

		for (int index = 0; index < poolSize; index++)
			MakeDongle();
	}

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

	Dongle MakeDongle() {
		//이펙트 생성
		GameObject instantEffectObj = Instantiate(effectPrefab, effectGroup);
		instantEffectObj.name = "Effect " + effectPool.Count;
		ParticleSystem instantEffect = instantEffectObj.GetComponent<ParticleSystem>();
		effectPool.Add(instantEffect);

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

		instantDongle.manager = this;
		instantDongle.effect = instantEffect;   //이펙트 전달
		donglePool.Add(instantDongle);

		return instantDongle;
	}

	Dongle GetDongle() {
		for (int index = 0; index < donglePool.Count; index++) {
			poolCursor = (poolCursor + 1) % donglePool.Count;
			if (!donglePool[poolCursor].gameObject.activeSelf)  //비활성화일때
				return donglePool[poolCursor];
		}

		return MakeDongle();
	}

	void NextDongle() {
		if (isOver)
			return;

		lastDongle = GetDongle();
		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;	//값이 범위를 넘지 않도록 나머지 연산 사용
	}
}
  • [Header("string header")] : 인스펙터에 말머리를 추가
  • 변수 순서와 위치만 조금 변경하고 Header를 통해 말머리를 추가하였다

GameManager 변수 창에 말머리가 추가된 모습


2. 점수 시스템 완성

Canvas의 UI Scale Mode 변경
Canvas 아래에 Text를 만든 뒤, 앵커를 왼쪽 상단으로 잡고 폰트와 글자 크기 등을 설정해준다
앞의 Text를 복사해서 오브젝트 이름을 바꾼다. 이후 앵커를 오른쪽 상단으로 잡고, 글 색깔 등을 변경

//GameManager 스크립트 파일

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

public class GameManager : MonoBehaviour {
	[Header("============[ Core ]============")]
	public bool isOver;
	public int score;
	public int maxLevel;

	[Header("========[ Object Pooling ]========")]
	public GameObject donglePrefab;
	public Transform dongleGroup;
	public List<Dongle> donglePool;
	public GameObject effectPrefab;
	public Transform effectGroup;
	public List<ParticleSystem> effectPool;
	[Range(1, 30)]  //Inspector 창에서 poolSize가 1~30까지 스크롤형태로 표현됨
	public int poolSize;
	public int poolCursor;
	public Dongle lastDongle;

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

	[Header("=============[ UI ]=============")]
	public Text scoreText;
	public Text maxScoreText;

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

		donglePool = new List<Dongle>();
		effectPool = new List<ParticleSystem>();

		for (int index = 0; index < poolSize; index++)
			MakeDongle();

		if (!PlayerPrefs.HasKey("MaxScore")) {	//최고 점수가 없다면
			PlayerPrefs.SetInt("MaxScore", 0);
		}

		maxScoreText.text = PlayerPrefs.GetInt("MaxScore").ToString();
	}

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

	Dongle MakeDongle() {
		//이펙트 생성
		GameObject instantEffectObj = Instantiate(effectPrefab, effectGroup);
		instantEffectObj.name = "Effect " + effectPool.Count;
		ParticleSystem instantEffect = instantEffectObj.GetComponent<ParticleSystem>();
		effectPool.Add(instantEffect);

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

		instantDongle.manager = this;
		instantDongle.effect = instantEffect;   //이펙트 전달
		donglePool.Add(instantDongle);

		return instantDongle;
	}

	Dongle GetDongle() {
		for (int index = 0; index < donglePool.Count; index++) {
			poolCursor = (poolCursor + 1) % donglePool.Count;
			if (!donglePool[poolCursor].gameObject.activeSelf)  //비활성화일때
				return donglePool[poolCursor];
		}

		return MakeDongle();
	}

	void NextDongle() {
		if (isOver)
			return;

		lastDongle = GetDongle();
		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);

		int maxScore = Mathf.Max(score, PlayerPrefs.GetInt("MaxScore"));
		PlayerPrefs.SetInt("MaxScore", maxScore);

		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;	//값이 범위를 넘지 않도록 나머지 연산 사용
	}

	void LateUpdate() {
		scoreText.text = score.ToString();
	}
}
  • 앞에 만들었던 점수 Text UI들과 연결하기 위해서 UI namespace를 추가하고, Text 변수 2개를 만들어서 각각 LateUpdate()와 Awake(), GameOverRoutine()에서 점수와 최고 점수를 세팅할 수 있도록 만들었다. 최고 점수의 경우 기존 최고 점수가 없다면 0을, 이후 게임이 오버되면 이전 최고 점수와 현재 점수를 비교해 더 큰 점수를 최고 점수로 저장하고, 다시 실행시 그 값을 보여준다.

GameManager 변수 할당
최초 실행 모습. 점수가 잘 나타나고, 최고 점수가 0점으로 잘 나온다
어느정도 점수가 쌓였으니 바닥을 올려서 강제로 게임오버를 시킨다
게임 오버
재시작하니 최고 점수가 이전 최종 점수인 205점이 되었다


3. 게임 종료 UI

Canvas에 Image 생성 후 설정
앞에서 생성했던 Image End Group 아래 Image를 하나 더 생성하고 변경해준다
버튼 추가 및 설정
버튼 Text 설정
앞에서 수정한 Text를 하나 더 복사해서 SubScore Text로 변경해준다
설정이 끝난 End Group은 비활성화

//GameManager 스크립트 파일

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class GameManager : MonoBehaviour {
	[Header("============[ Core ]============")]
	public bool isOver;
	public int score;
	public int maxLevel;

	[Header("========[ Object Pooling ]========")]
	public GameObject donglePrefab;
	public Transform dongleGroup;
	public List<Dongle> donglePool;
	public GameObject effectPrefab;
	public Transform effectGroup;
	public List<ParticleSystem> effectPool;
	[Range(1, 30)]  //Inspector 창에서 poolSize가 1~30까지 스크롤형태로 표현됨
	public int poolSize;
	public int poolCursor;
	public Dongle lastDongle;

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

	[Header("=============[ UI ]=============")]
	public GameObject endGroup;
	public Text scoreText;
	public Text maxScoreText;
	public Text subScoreText;

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

		donglePool = new List<Dongle>();
		effectPool = new List<ParticleSystem>();

		for (int index = 0; index < poolSize; index++)
			MakeDongle();

		if (!PlayerPrefs.HasKey("MaxScore")) {	//최고 점수가 없다면
			PlayerPrefs.SetInt("MaxScore", 0);
		}

		maxScoreText.text = PlayerPrefs.GetInt("MaxScore").ToString();
	}

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

	Dongle MakeDongle() {
		//이펙트 생성
		GameObject instantEffectObj = Instantiate(effectPrefab, effectGroup);
		instantEffectObj.name = "Effect " + effectPool.Count;
		ParticleSystem instantEffect = instantEffectObj.GetComponent<ParticleSystem>();
		effectPool.Add(instantEffect);

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

		instantDongle.manager = this;
		instantDongle.effect = instantEffect;   //이펙트 전달
		donglePool.Add(instantDongle);

		return instantDongle;
	}

	Dongle GetDongle() {
		for (int index = 0; index < donglePool.Count; index++) {
			poolCursor = (poolCursor + 1) % donglePool.Count;
			if (!donglePool[poolCursor].gameObject.activeSelf)  //비활성화일때
				return donglePool[poolCursor];
		}

		return MakeDongle();
	}

	void NextDongle() {
		if (isOver)
			return;

		lastDongle = GetDongle();
		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);

		//최고 점수 갱신
		int maxScore = Mathf.Max(score, PlayerPrefs.GetInt("MaxScore"));
		PlayerPrefs.SetInt("MaxScore", maxScore);

		//게임 오버 UI
		subScoreText.text = "점수 : " + scoreText.text;
		endGroup.SetActive(true);

		bgmPlayer.Stop();
		SfxPlay(Sfx.Over);
	}

	public void Reset() {
		SfxPlay(Sfx.Button);
		StartCoroutine("ResetCoroutine");
	}

	IEnumerator ResetCoroutine() {
		yield return new WaitForSeconds(1f);
		SceneManager.LoadScene("Main");
	}

	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;	//값이 범위를 넘지 않도록 나머지 연산 사용
	}

	void LateUpdate() {
		scoreText.text = score.ToString();
	}
}
  • 재시작 버튼을 처리하기위해서 SceneManagement namespace를 추가하고, Reset()함수와 ResetCoroutine() 코루틴을 만든다. 또한 앞에서 만든 게임 오버 UI들을 연결하기위해서 UI 헤더에 변수들을 추가하고, GameOverRoutine()에서 이를 처리하도록 만들었다. 게임 오버시 bgm이 멈추는 문장도 추가했다.

GameManager 변수 할당
다시 시작 버튼 클릭 시를 위해서 Scene의 이름을 Main으로 변경
Scene이 변경된 모습
Button의 On Click 함수 지정 및 Navigation 설정 None으로 변경
실행 후 게임을 어느정도 진행한 뒤 바닥을 올려서 강제로 게임 오버
bgm이 멈추고 게임 오버 소리와 함께 게임 오버 UI가 떴다.
버튼을 클릭하니 다시 시작한다


4. 게임 시작

End Group을 복사해서 StartGroup으로 변경
배경을 검은색에서 하얀색으로 변경
그룹 아래 Image를 타이틀로 변경
버튼의 SubScoreText는 삭제하고 Text를 게임 시작으로 변경
배경에 보이는 점수 UI 비활성화
Line과 Bottom도 비활성화 해서 깔끔한 시작 화면을 만들어준다

//GameManager 스크립트 파일

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class GameManager : MonoBehaviour {
	[Header("============[ Core ]============")]
	public bool isOver;
	public int score;
	public int maxLevel;

	[Header("========[ Object Pooling ]========")]
	public GameObject donglePrefab;
	public Transform dongleGroup;
	public List<Dongle> donglePool;
	public GameObject effectPrefab;
	public Transform effectGroup;
	public List<ParticleSystem> effectPool;
	[Range(1, 30)]  //Inspector 창에서 poolSize가 1~30까지 스크롤형태로 표현됨
	public int poolSize;
	public int poolCursor;
	public Dongle lastDongle;

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

	[Header("=============[ UI ]=============")]
	public GameObject startGroup;
	public GameObject endGroup;
	public Text scoreText;
	public Text maxScoreText;
	public Text subScoreText;

	[Header("=============[ ETC ]=============")]
	public GameObject line;
	public GameObject bottom;

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

		donglePool = new List<Dongle>();
		effectPool = new List<ParticleSystem>();

		for (int index = 0; index < poolSize; index++)
			MakeDongle();

		if (!PlayerPrefs.HasKey("MaxScore")) {	//최고 점수가 없다면
			PlayerPrefs.SetInt("MaxScore", 0);
		}

		maxScoreText.text = PlayerPrefs.GetInt("MaxScore").ToString();
	}

	public void GameStart() {
		//오브젝트 활성화
		line.SetActive(true);
		bottom.SetActive(true);
		scoreText.gameObject.SetActive(true);
		maxScoreText.gameObject.SetActive(true);
		startGroup.SetActive(false);

		//사운드 플레이
		bgmPlayer.Play();
		SfxPlay(Sfx.Button);

		//게임 시작(동글 생성)
		Invoke("NextDongle", 1.5f);	//1.5초 딜레이
	}

	Dongle MakeDongle() {
		//이펙트 생성
		GameObject instantEffectObj = Instantiate(effectPrefab, effectGroup);
		instantEffectObj.name = "Effect " + effectPool.Count;
		ParticleSystem instantEffect = instantEffectObj.GetComponent<ParticleSystem>();
		effectPool.Add(instantEffect);

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

		instantDongle.manager = this;
		instantDongle.effect = instantEffect;   //이펙트 전달
		donglePool.Add(instantDongle);

		return instantDongle;
	}

	Dongle GetDongle() {
		for (int index = 0; index < donglePool.Count; index++) {
			poolCursor = (poolCursor + 1) % donglePool.Count;
			if (!donglePool[poolCursor].gameObject.activeSelf)  //비활성화일때
				return donglePool[poolCursor];
		}

		return MakeDongle();
	}

	void NextDongle() {
		if (isOver)
			return;

		lastDongle = GetDongle();
		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);

		//최고 점수 갱신
		int maxScore = Mathf.Max(score, PlayerPrefs.GetInt("MaxScore"));
		PlayerPrefs.SetInt("MaxScore", maxScore);

		//게임 오버 UI
		subScoreText.text = "점수 : " + scoreText.text;
		endGroup.SetActive(true);

		bgmPlayer.Stop();
		SfxPlay(Sfx.Over);
	}

	public void Reset() {
		SfxPlay(Sfx.Button);
		StartCoroutine("ResetCoroutine");
	}

	IEnumerator ResetCoroutine() {
		yield return new WaitForSeconds(1f);
		SceneManager.LoadScene("Main");
	}

	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;	//값이 범위를 넘지 않도록 나머지 연산 사용
	}

	void Update() {
		if (Input.GetButtonDown("Cancel")) {	//모바일 뒤로 가기 버튼(esc와 동일)을 눌렀다면
			Application.Quit();	//끄기
		}
	}

	void LateUpdate() {
		scoreText.text = score.ToString();
	}
}
  • 앞에서 비활성화했던 배경 UI 및 오브젝트들을 게임 시작시 활성화하기 위해서 ETC 헤더를 만들고 거기에 GameObject 변수들을 추가했다.
  • 그리고 버튼 클릭시 게임 시작을 할 수 있도록 GameStart()함수를 만들어서 앞의 변수들을 활성화하는 동시에, 기존의 Start() 함수에 있던 사운드 플레이 및 동글 생성 함수 작동 기능도 가능하도록 했다.
  • 동글 생성의 경우 시작과 동시에 바로 생성하지 않고 Invoke를 통해서 1.5초 뒤에 생성되도록 했다.
  • 또한 Update에서 모바일에서 뒤로가기 키(컴퓨터 ESC 키)를 눌렀을때 종료되도록 추가했다.

GameManager 변수 할당
StartGroup의 버튼의 OnClick() 함수를 GameStart로 변경
게임 실행 화면
게임 시작 버튼을 클릭하니, 약간의 딜레이 후 동글이 생성되었다
게임 오버를 시키고
종료 화면이 뜬 뒤
다시 시작 버튼을 누르니 다시 Start Group이 돌아왔다


5. 모바일 빌드

빌드 전 Player Settings를 눌러 일부 설정을 변경하거나 추가해준다
세로 고정 게임이므로 Landscape는 모두 해제한다
Other Settings의 Configuration에서 Scripting Backend를 Mono에서 IL2CPP로 바꾸고 ARM64를 체크
모든 설정이 끝나면 Build 버튼을 눌러서 빌드한다
빌드된 apk
어플리케이션 설치
게임 실행 모습
게임 플레이 중
게임 오버 직전
최고 점수가 기록되면서 게임오버 UI가 떴다
다시 시작 버튼을 누르니 게임 시작 화면으로 돌아왔다. 이제 뒤로 가기 버튼을 눌러보니
게임이 종료되었다