기록 보관소

[Unity/유니티] 기초-물리 퍼즐게임: 게임 오버 구현하기[B58] 본문

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

[Unity/유니티] 기초-물리 퍼즐게임: 게임 오버 구현하기[B58]

JongHoon 2022. 4. 23. 22:38

개요

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

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

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


물리 퍼즐게임: 게임 오버 구현하기[B58]

1. 점수 추가

//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;

	Rigidbody2D rigid;
	CircleCollider2D circle;
	Animator anim;

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

	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 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;

		StartCoroutine(HideRoutine(targetPos));
	}

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

		while(frameCount < 20) {
			frameCount++;
			transform.position = Vector3.Lerp(transform.position, targetPos, 0.5f);
			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();	//이펙트 실행

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

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

		isMerge = false;
	}

	void EffectPlay() {
		effect.transform.position = transform.position;
		effect.transform.localScale = transform.localScale;
		effect.Play();
	}
}
  • public int score로 점수를 만들고, HideRoutine을 통해서 점수가 2의 동글 레벨 제곱만큼 올라가도록 만들었다.
  • Mathf.Pow(float f, float p) : 지정 숫자의 거듭제곱. f^p가 된다.

실행 후 합쳐지기 전에는 score이 0점이다
합쳐지니 score가 2점 증가했다
0레벨 동글과 1레벨 동글을 합쳐서 5점이되었다
2레벨 동글끼리 합쳐지니 2^2만큼 증가했다


2. 경계선 이벤트

Line에 Finish 태그 설정
Dongle Prefab 설정 변경

  • Sleeping Mode : 물리 연산을 멈추고 쉬는 상태 모드 설정.
//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 int score;
	public int maxLevel;
	public bool isOver;

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

	void Start() {
		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() {
		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;
	}
}
  • GameOver 함수와 게임이 끝났는지 확인하는 bool형 변수 isOver를 만들어서 임시로 게임 종료를 알리도록 했다.
//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;

	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 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;

		StartCoroutine(HideRoutine(targetPos));
	}

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

		while(frameCount < 20) {
			frameCount++;
			transform.position = Vector3.Lerp(transform.position, targetPos, 0.5f);
			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();	//이펙트 실행

		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();
	}
}
  • 화면에 있는 Line의 Collider에서 isTrigger가 체크되어 있으므로, OnTriggerStay2D와 OnTriggerExit2D를 통해서 라인에 닿은 시간을 재서 2초가 넘으면 스프라이트를 빨간색으로 바꾸고, 5초가 넘으면 게임 매니저의 GameOver 함수를 발동시키도록 했다. 만약 그 전에 라인에서 떨어지면 시간과 스프라이트 색을 초기화하도록 한다.

임시로 바닥을 위로 올려 동글이 닿게 만든 모습
2초 뒤 빨갛게 변한 모습
시간이 지나니 isOver가 true로 바뀌었다


3. 게임 오버

//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 int score;
	public int maxLevel;
	public bool isOver;

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

	void Start() {
		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);
		}
	}
}
  • GameManager에서는 코루틴을 통해서 GameOver가 되었을때 모든 동글 정보를 가져와 물리 효과를 비활성화하고(아래 동글들이 사라질때 이동하면서 문제 발생 가능), 0.1초마다 하나씩 동글이 사라지도록 만들었다.
  • 또한 게임오버시 동글을 더 이상 생성하지 못하도록 NextDongle 함수에 isOver 변수가 true면 return하도록 만들었다.
  • FindObjectsOfType<T> : 장면에 올라온 T 컴포넌트들을 탐색
//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 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 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();	//이펙트 실행

		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();
	}
}
  • Dongle에서는 GameManager에서 Hide를 통해 동글을 제거할때 이펙트가 발생하도록 if문을 통해 특정 벡터 값일때 EffectPlay 하도록 하고, HideRoutine에서도 벡터값에 따라 특정 벡터값에서는 그 자리 그대로 사라지도록 만들었다.
  • 또한 GameManager에서 Rigidbody를 쓸 수있도록 public을 추가해주었다.

라인에 걸려서 게임 오버가 되기 직전인 상황
시간이 지나 게임 오버가 되어 이펙트가 하나씩 터진다
모든 동글이 사라지고 게임 오버가 되어 새로운 동글도 생성되지 않는다