기록 보관소

[Unity/유니티] 기초-물리 퍼즐게임: 물리 이벤트로 동글 합치기[B56] 본문

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

[Unity/유니티] 기초-물리 퍼즐게임: 물리 이벤트로 동글 합치기[B56]

JongHoon 2022. 4. 17. 17:13

개요

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

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

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


물리 퍼즐게임: 물리 이벤트로 동글 합치기[B56]

1. 동글 충돌

//Dongle 스크립트 파일

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

public class Dongle : MonoBehaviour {
	public int level;
	public bool isDrag;
	public bool isMerge;

	Rigidbody2D rigid;
	Animator anim;

	void Awake() {
		rigid = GetComponent<Rigidbody2D>();
		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)) {
					//상대방 숨기기
					//나는 레벨 업
				}
			}
		}
	}
}
  • 합치기를 위해서 OnCollisionStay2D 함수를 통해 tag가 Dongle이고 같은 레벨이면, bool형 변수 isMerge를 통해서 동글이 1:1로만 합쳐지도록 밑바탕을 만들었다.

2. 동글 흡수 (숨김)

//Dongle 스크립트 파일

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

public class Dongle : MonoBehaviour {
	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);
					//나는 레벨 업
				}
			}
		}
	}

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

		isMerge = false;
		gameObject.SetActive(false);
	}
}
  • 같은 레벨의 동글이 닿으면 닿은 동글의 물리효과와 콜라이더가 꺼지면서 닿은 다른 동글쪽으로 천천히 이동하고 이후 비활성화되어 합쳐진 것처럼 보이게 만들었다.

레벨 1의 동글이 있는 왼쪽으로 떨어트리면
닿자마자 합쳐지면서
하나가 된다
다른 레벨과는 닿아도 합쳐지지 않는다
레벨 0도 잘 합쳐진다


3. 동글 성장 (레벨 업)

//Dongle 스크립트 파일

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

public class Dongle : MonoBehaviour {
	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;
		}

		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);	//애니메이션 실행

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

		isMerge = false;
	}
}
  • 간단하게 합쳐진 동글의 속도와 회전 속도를 모두 0으로 만들고, 코루틴을 통해서 다음 레벨의 애니메이션을 실행하고 레벨을 증가시킨다. 코루틴에서 anim.SetInteger("Level", ++level)을 하지 않은 이유는 애니메이션이 실행시간 때문에 순식간에 레벨이 올라가는 것을 방지하기 위함이다.

1레벨과 1레벨 동글이 닿기 직전
닿으니 2 레벨 동글이 생성되면서 애니메이션이 실행된다
0 레벨 동글이 합쳐지기 직전
마찬가지로 1레벨이 되었다. 그 사이에 1레벨을 던지면
거의 동시에 닿았지만 isMerge 변수로 1:1만 합쳐져서 왼쪽이 2레벨이 되었다
2레벨과 2레벨이 합쳐져 3레벨이된 모습


4. 물리 보정

Project Settings의 Physics2D에서 Auto Sync Transforms 체크.

  • Auto Sync Transform : 트랜스폼과 물리의 차이를 보정해준다. 이번 프로젝트는 물리 퍼즐 게임이므로 체크하는 것이 좋다.

5. 최대 레벨

//GameManager 스크립트 파일

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

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

	public int maxLevel;

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

	void Start() {
		NextDongle();
	}

	Dongle GetDongle() {
		GameObject instant = Instantiate(donglePrefab, dongleGroup);
		Dongle instantDongle = instant.GetComponent<Dongle>();

		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;	//드랍 후 조종 불가로 만들기
	}
}
  • GameManager에서는 프리펩인 Dongle에게 GameManager 자신을 넘겨주고, 최대 레벨 변수 값인 maxLevel을 사용할 수 있도록 해준다. 또한 maxLevel에 따라서 동글 레벨이 0에서 maxLevel - 1까지 랜덤하게 생성되게 바꾸었다.
//Dongle 스크립트 파일

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

public class Dongle : MonoBehaviour {
	public GameManager manager;

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

		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);	//애니메이션 실행

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

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

		isMerge = false;
	}
}
  • Dongle에서는 넘겨받은 GameManager의 maxLevel을 지정해준다. 
  • Mathf.Max(int a, int b) : 인자 값(a, b) 중에서 최대값을 반환하는 함수

GameManager maxLevel 변수 지정
2레벨 동글 2개가 합쳐지면 3레벨 동글이 나올 것이므로, Max Level이 3으로 증가하게 될 것이다.
Max Level이 3으로 증가했다
Max Level이 3으로 증가했으니 이제 2레벨 동글까지 생성된다