유니티 수박게임 만들기
Intro
- 게임개요
- 게임 이름 : 수박게임
- 게임 장르 : 퍼즐게임(모바일 싱글)
- 게임 플랫폼 : 모바일(Android)
- 게임 설명 : 주어진 공간 안에서 같은 종류의 과일을 합쳐 더 큰 과일을 만들어내는 퍼즐게임입니다.
- 게임 목표 : 과일을 합쳐 최종적으로 수박 2개를 만들어내는것이 목표입니다.
- 개발 기간 : 2일
- 게임 시스템
- 게임 규칙 : 화면을 터치(드래그)하여 과일을 떨어뜨릴 위치를 정하고 터치를 떼어 과일을 떨어뜨릴 수 있습니다. 한정된 공간을 최대한 활용하여 제한선을 넘지않으면서 수박을 만들어내야합니다.
- 레벨 디자인 : 체리부터 수박까지 총 11개의 레벨로 구성됩니다. 과일의 레벨이 높아질수록 크기가 커져 합쳐지기 어렵도록 합니다.
- 게임 다운로드
게임화면 구성
게임 화면 구성은 아래와같습니다.
- 플레이 화면
- 갈색의 네모 틀안에 과일을 떨어뜨려 과일을 합치게됩니다.
- 화면을 터치 & 드래그 하여 떨어뜨릴 위치를 선정할 수 있습니다.
- 터치하면 떨어뜨릴 위치를 보여주는 Drop Line이 표시됩니다.
- 같은 과일을 합치면 더 높은 단계의 과일이 생성됩니다.
- 일정 높이 이상 과일이 쌓이면 경계선 이펙트가 출력되며, 경계선에 과일이 닿은지 일정시간이 지나면 게임오버가됩니다.
- UI
- 현재 점수가 표시됩니다.
- 다음에 나올 과일이 표시됩니다.
- 일시정지 버튼을 눌러 게임을 일시정지할 수 있습니다.
- 게임오버, 게임클리어시 UI가 활성화됩니다.
- 게임을 재시작할 수 있습니다.
구현
수박게임의 주요 로직은 다음과 같습니다.
- 과일 생성
- 떨어뜨릴 과일을 생성합니다.
- 다음 순서에 생성될 과일도 미리 생성합니다.
- 과일 움직이기
- 화면 터치(클릭) & 드래그를 통해 과일을 움직일 수 있고 터치를 떼면 과일을 떨어뜨립니다.
- 과일 합치기
- 같은 과일이 접촉하면 다음단계의 과일이 생성됩니다.
- 게임 종료
- 경계선을 넘어 게임오버가된경우
- 수박 두개를 만들어 게임이 끝난경우
과일
수박게임의 과일 에셋을 구하여 프리팹을 만들어주었습니다.
각 프리팹에는
물리를 적용하기위한 Rigidbody, 충돌을 위한 Collider 컴포넌트가 추가되어있습니다.
또한 컨트롤과 충돌이벤트등을 구현하기위한 Fruit 스크립트가 추가되어있습니다.
과일 생성
과일을 생성하고 관리하는것은 FruitManager 클래스가 담당하였습니다.
/*
<< FruitManager >>
- 현재/다음 과일의 생성 및 관리(풀링)
- 다음 과일은 UI에 이미지로 표시
- 터치 & 드래그 이벤트 관리
- 게임 리셋
- 씬에 활성화되어있는 과일들을 모두 풀로 반환
*/
public class FruitManager : MonoBehaviour
{
[SerializeField] private Transform spawnPoint; // 과일 생성 위치
[SerializeField] private Transform poolParent; // 풀링 위치
[SerializeField] private GameObject[] fruitPrefabs; // 과일 프리팹
[SerializeField] private Sprite[] fruitSprites; // 과일 이미지
[SerializeField] private Image nextFruitImage; // 다음 과일 이미지
#region ** Fields **
public Fruit currentFruit; // 현재 과일
public Fruit nextFruit; // 다음 과일
public bool isWaitingNextFruit = false; // 중복방지용 플래그
public int watermelonCount; // 수박 갯수(2개가 되면 목표달성)
private int nextFruitLevel; // 다음 과일 레벨
private int poolSize = 5; // 과일 풀 사이즈(과일별)
private Dictionary<int, Queue<Fruit>> fruitPool = new(); // 과일 풀
private List<Fruit> activeFruits = new(); // 활성화 되어있는 과일(게임 리셋용)
#endregion
private static FruitManager instance;
public static FruitManager Instance => instance;
private void Awake()
{
if (instance != null && instance != this)
{
Destroy(gameObject);
return;
}
instance = this;
InitPool();
}
private void Start()
{
MakeFruit();
}
// 풀 초기화
private void InitPool()
{
// 과일 레벨별
for(int i = 0;i<fruitPrefabs.Length;i++)
{
fruitPool[i] = new Queue<Fruit>();
// 과일당 10개
for(int j = 0;j<poolSize;j++)
{
GameObject obj = Instantiate(fruitPrefabs[i], poolParent);
obj.SetActive(false);
Fruit fruit = obj.GetComponent<Fruit>();
fruitPool[i].Enqueue(fruit);
}
}
}
// 풀에서 과일 꺼내쓰기
private Fruit GetFruitFromPool(int level)
{
// 풀에 여분이 있을 때
if(fruitPool[level].Count > 0)
{
Fruit fruit = fruitPool[level].Dequeue();
activeFruits.Add(fruit);
fruit.gameObject.SetActive(true);
return fruit;
}
// 풀에 여분이 없을 때
else
{
GameObject obj = Instantiate(fruitPrefabs[level]);
return obj.GetComponent<Fruit>();
}
}
// 과일 생성
private Fruit SpawnFruitObj(int level)
{
Fruit fruit = GetFruitFromPool(level);
// 위치 설정
fruit.transform.position = spawnPoint.position;
fruit.transform.rotation = Quaternion.identity;
// 드랍되기 전까지 비활성화
fruit.GetComponent<Rigidbody2D>().simulated = false;
return fruit;
}
// 다음 과일 만들기
public void MakeFruit()
{
// 게임 시작시
if (currentFruit == null)
{
// 제일 처음 과일은 체리
currentFruit = SpawnFruitObj(0);
}
else
{
currentFruit = SpawnFruitObj(nextFruit.level);
}
// 랜덤(체리~오렌지)
nextFruitLevel = Random.Range(0, 5);
// 다음에 올 과일
nextFruit = fruitPrefabs[nextFruitLevel].GetComponent<Fruit>();
// 다음에 올 과일 아이콘 설정
nextFruitImage.sprite = fruitSprites[nextFruitLevel];
nextFruitImage.SetNativeSize();
}
// 다음 레벨 과일 생성
public void SpawnNextLevel(int level, Vector3 position)
{
Fruit fruit = GetFruitFromPool(level + 1);
fruit.transform.position = position;
fruit.transform.rotation = Quaternion.identity;
fruit.GetComponent<Rigidbody2D>().simulated = true;
}
// 사용한 과일을 풀에 반환하기
public void ReturnFruitToPool(Fruit fruit)
{
fruit.gameObject.SetActive(false);
fruitPool[fruit.level].Enqueue(fruit);
}
// 게임 재시작시
public void Reset()
{
foreach(var fruit in activeFruits)
{
ReturnFruitToPool(fruit);
}
activeFruits.Clear();
currentFruit = null;
nextFruit = null;
isWaitingNextFruit = false;
MakeFruit();
}
// 드래그
public void TouchDown()
{
if (currentFruit == null || isWaitingNextFruit || GameManager.Instance.isPaused ||
GameManager.Instance.isGameOver || GameManager.Instance.isGameClear) return;
currentFruit.Drag();
}
// 드랍
public void TouchUp()
{
if (currentFruit == null || isWaitingNextFruit || GameManager.Instance.isPaused ||
GameManager.Instance.isGameOver || GameManager.Instance.isGameClear) return;
currentFruit.Drop();
StartCoroutine(WaitForNextFruit());
}
// 1초 딜레이 후 과일 세팅
IEnumerator WaitForNextFruit()
{
isWaitingNextFruit = true;
yield return new WaitForSeconds(1f);
MakeFruit();
isWaitingNextFruit = false;
}
}
-
오브젝트 풀링을 사용하여 과일을 미리 생성하였습니다.
-
첫번째 과일은 체리로 고정하였고, 그 이후의 과일은 체리~오렌지까지 랜덤하게 생성하도록 하였습니다.
과일 컨트롤
과일의 컨트롤과 충돌이벤트의 구현을 Fruit 클래스가 담당하였습니다.
과일은 터치&드래그로 움직일 수 있으며, 터치를 뗄 때 과일을 떨어뜨릴 수 있도록 하였습니다.
/*
<< Fruit >>
- 과일 움직임
- 터치 & 드래그
- 과일 합쳐짐
- Collsion 이벤트로 구현
- 경계선 이펙트 활성화 요청 및 게임오버
- Trigger 이벤트로 구현
*/
public class Fruit : MonoBehaviour
{
[SerializeField] private GameObject dropLine; // 드랍위치 표시줄
private bool isDrag; // 드래그 여부
private bool isMerge; // 합쳐짐 여부
private float boundaryEffectTimer = 0f; // 경계선 활성화 타이머
private float deadTimer = 0f; // 게임오버 타이머
private Rigidbody2D rigid;
private Animator anim;
public int level; // 과일 레벨
readonly private int hashLevel = Animator.StringToHash("Level");
private void Awake()
{
Init();
}
private void OnEnable()
{
anim.SetInteger(hashLevel, level);
}
private void Update()
{
if (GameManager.Instance.isGameOver || GameManager.Instance.isGameClear || GameManager.Instance.isPaused)
return;
MoveFruit();
}
// 컴포넌트 초기화
private void Init()
{
rigid = GetComponent<Rigidbody2D>();
anim = GetComponent<Animator>();
}
// 과일 합치기
private void OnCollisionEnter2D(Collision2D collision)
{
if (!collision.gameObject.CompareTag("Fruit")) return;
Fruit other = collision.gameObject.GetComponent<Fruit>();
// 같은 과일만 합치기
if (level == other.level && !isMerge && !other.isMerge && level < 10)
{
// 두 과일의 중간위치 계산
Vector3 midPos = (transform.position + other.transform.position) / 2f;
isMerge = true;
other.isMerge = true;
FruitManager.Instance.ReturnFruitToPool(this);
FruitManager.Instance.ReturnFruitToPool(other);
// 다음 레벨 과일 생성
FruitManager.Instance.SpawnNextLevel(level, midPos);
AudioManager.Instance.PlaySFX(AudioManager.Sfx.Pop);
// 점수 획득
AddScore();
// 수박(레벨10)을 완성시켰을 때
if(level == 9)
{
FruitManager.Instance.watermelonCount++;
// 수박을 두개 만들었을경우 게임 종료
if(FruitManager.Instance.watermelonCount == 2)
{
GameManager.Instance.GameClear();
}
}
}
}
// 경계선 이펙트 활성화 및 게임오버 처리
private void OnTriggerStay2D(Collider2D collision)
{
// 경계선 활성화
if (collision.CompareTag("BoundaryEffect"))
{
boundaryEffectTimer += Time.deltaTime;
if (boundaryEffectTimer > 1f)
{
collision.GetComponent<BoundaryEffect>().boundaryRenderer.enabled = true;
}
}
// 게임 오버
else if(collision.CompareTag("Boundary"))
{
deadTimer += Time.deltaTime;
if (deadTimer > 4f)
{
GameManager.Instance.GameOver();
}
}
}
// 경계선 비활성화
private void OnTriggerExit2D(Collider2D collision)
{
if (collision.CompareTag("BoundaryEffect"))
{
boundaryEffectTimer = 0f;
collision.GetComponent<BoundaryEffect>().boundaryRenderer.enabled = false;
}
if(collision.CompareTag("Boundary"))
{
deadTimer = 0f;
}
}
// 과일 움직이기
private void MoveFruit()
{
if (!isDrag || FruitManager.Instance.isWaitingNextFruit) return;
// 마우스 포인터 따라가기
Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
// 가로로 최대 이동가능한 영역
float minX = -1.96f + transform.localScale.x / 2f;
float maxX = 1.96f - transform.localScale.x / 2f;
float clampedX = Mathf.Clamp(mousePos.x, minX, maxX);
// 가로로만 이동
mousePos.z = 0;
mousePos.y = transform.position.y;
transform.position = new Vector3(clampedX, transform.position.y, transform.position.z);
}
// 점수 획득
private void AddScore()
{
GameManager.Instance.score += (level+1) * 2 + 1;
}
// 드래그
public void Drag()
{
isDrag = true;
dropLine.SetActive(true);
}
// 과일 드랍
public void Drop()
{
isDrag = false;
dropLine.SetActive(false);
rigid.simulated = true;
}
}
- CollisionEnter 이벤트에서 과일끼리의 충돌을 처리하였습니다.
- 같은 과일의 경우 합쳐지도록하였고, 수박이 만들어졌을때 count를 늘려 게임의 클리어 여부를 판단하였습니다.
- Trigger 이벤트에서 게임 오버를 판단하였습니다.
- 과일이 특정 높이 이상 쌓이게되면 경계선이 나타나며, 경계선에 일정시간 닿아있을경우 게임오버가됩니다.
게임오버 및 게임클리어
게임 클리어 및 실패시 UI를 아래와같이 구현하였습니다.
- 게임 클리어
- UI의 시각적 이펙트를 넣어보았습니다.
- 게임 오버
그 밖의것은 깃허브에 방문하면 확인할 수 있습니다.
댓글남기기