[Project] 두더지 잡기(3)
in Unity
콤보 시스템
우선 밸런스를 잘 생각해서 콤보에 따라 획득 가능한 점수, 최대 생성 두더지를 정한다.
(1) GameController스크립트를 수정한다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameController : MonoBehaviour
{
[SerializeField]
private CountDown countDown;
[SerializeField]
private MoleSpawner moleSpawner;
private int score;
/*●*/private int combo;
private float currentTime;
public int Score
{
get => score;
set=>score=Mathf.Max(0,value);
}
/*●*/public int Combo
{
set=>combo=Mathf.Max(0,value);
get => combo;
}
// [field:SerializeField] : 자동 구현 프로퍼티를 인스펙터창에서 보이게 할 때 사용
[field:SerializeField]
public float MaxTime { private set; get; }
public float CurrentTime
{
set=> currentTime = Mathf.Clamp(value,0,MaxTime);
get => currentTime;
}
private void Start()
{
countDown.StartCountDown(GameStart);
}
private void GameStart()
{
moleSpawner.SetUp();
StartCoroutine("OnTimeCount");
}
private IEnumerator OnTimeCount()
{
CurrentTime = MaxTime;
while (CurrentTime > 0)
{
CurrentTime-=Time.deltaTime;
yield return null;
}
}
}
(2) Hammer스크립트를 기본 두저지, 파란 두더지를 잡으면 콤보가 올라가고 빨간 두더지를 잡으면 콤보가 0으로 초기화 되게 수정한다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Hammer : MonoBehaviour
{
[SerializeField]
private float maxY; // 망치의 최대 y 위치
[SerializeField]
private float minY; // 망치의 최소 y 위치
[SerializeField]
private GameObject moleHitEffectPrefab;
[SerializeField]
private AudioClip[] audioClips; // 두더지를 타격했을 때 재생되는 사운드
[SerializeField]
private MolehitTextViewer[] molehitTextViewer; // 타격한 두더지 위치에 타격 정보 텍스트 출력
[SerializeField]
private GameController gameController; // 점수 증가를 위한 GameController
[SerializeField]
private ObjectDetector objectDetector; // 마우스 클릭으로 오브젝트 선택을 위한 ObjectDetector
private Movement3D movement3D; // 망치 오브젝트 이동을 위한 Movement
private AudioSource audioSource;
private void Awake()
{
movement3D = GetComponent<Movement3D>();
audioSource= GetComponent<AudioSource>();
// OnHit 메소드를 ObjectDetector Class의 raycastEvent에 이벤트로 등록
// ObjectDetector의 raycastEvent.Invoke(hit,transform); 메소드가
// 호출될 때마다 OnHit(Transform target) 메소드가 호출된다
objectDetector.raycastEvent.AddListener(OnHit);
}
private void OnHit(Transform target)
{
if (target.gameObject.tag == "Mole")
{
MoleFSM mole =target.GetComponent<MoleFSM>();
// 두더지가 홀 안에 있을때는 공격 불가
if (mole.MoleState == MoleState.UnderGround) return;
// 망치의 위치 설정
transform.position = new Vector3(target.position.x, minY, target.position.z);
// 망치에 맞았기 때문에 두더지의 상태를 바로 "UnderGround"로 설정
mole.ChangeState(MoleState.UnderGround);
// 카메라 흔들기
ShakeCamera.Instance.OnShakeCamera(0.1f, 0.1f);
// 두더지 타격 효과 생성 (Particle의 색상을 두더지 색상과 동일하게 설정)
GameObject clone =Instantiate(moleHitEffectPrefab,transform.position,Quaternion.identity);
ParticleSystem.MainModule main=clone.GetComponent<ParticleSystem>().main;
main.startColor=mole.GetComponent<MeshRenderer>().material.color;
// 두더지의 색상에 따라 처리 (점수, 시간, 사운드 재생)
MoleHitProcess(mole);
// 망치를 위로 이동시키는 코루틴 재생
StartCoroutine("MoveUp");
}
}
private IEnumerator MoveUp()
{
// 이동 방향 (0 / 1 / 0) [위]
movement3D.MoveTo(Vector3.up);
while (true)
{
if (transform.position.y >= maxY)
{
movement3D.MoveTo(Vector3.zero);
break;
}
yield return null;
}
}
private void MoleHitProcess(MoleFSM mole)
{
if(mole.MoleType == MoleType.Normal)
{
/*●*/gameController.Combo++;
gameController.Score += 50;
// MoleIndex로 순번을 설정해 두었기 때문에 같은 자리에 있는 TextGetScore 텍스트 출력
// 하얀색 텍스트로 점수 증가 표현
molehitTextViewer[mole.MoleIndex].OnHit("Score +50", Color.white);
}
else if (mole.MoleType == MoleType.Red)
{
/*●*/gameController.Combo = 0;
gameController.Score -= 300;
// 빨간색 텍스트로 점수 감소 표현
molehitTextViewer[mole.MoleIndex].OnHit("Score -300", Color.red);
}
else if (mole.MoleType == MoleType.Blue)
{
/*●*/gameController.Combo++;
gameController.CurrentTime += 3;
// 파란색 텍스트로 시간 증가 표현
molehitTextViewer[mole.MoleIndex].OnHit("Time +3", Color.blue);
}
// 사운드 재생 (Normal=0, Red=1, Blue=2)
PlaySound((int)mole.MoleType);
}
private void PlaySound(int index)
{
audioSource.Stop();
audioSource.clip=audioClips[index];
audioSource.Play();
}
}
(3) 기본 두더지 잡기에 실패했을 때 콤보를 초기화하기 위해 MoleFSM스크립트를 수정한다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 지하에 대기, 지상에 대기, 지하->지상 이동, 지상->지하 이동
public enum MoleState { UnderGround=0, OnGround,MoveUp, MoveDown}
// 두더지 종류 (기본, 점수-, 시간 +)
public enum MoleType { Normal=0,Red,Blue}
public class MoleFSM : MonoBehaviour
{
/*●*/[SerializeField]
private GameController gameController; // 콤보 초기화를 위한 GameController
[SerializeField]
private float waitTimeOnGround; // 지면에 올라와서 내려가기까지 기다리는 시간
[SerializeField]
private float limitMinY; // 내려갈 수 있는 최소 y 위치
[SerializeField]
private float limitMaxY; //올라올 수 있는 최대 y 위치
private Movement3D movement3D; // 위 아래 이동을 위한 Movement3D
private MeshRenderer meshRenderer; // 두더지 색설정을 위한 MeshRenderer
private MoleType moleType; // 두더지의 종류
private Color defaultColor; // 기본 두더지의 색상(173, 135, 24)
// 두더지의 현재 상태 (set은 MoleFSM 클래스 내부에서만)
public MoleState MoleState { get; private set; }
// 두더지의 종류(MoleType에 따라 두더지 색상 변경)
public MoleType MoleType
{
set
{
moleType = value;
switch(moleType)
{
case MoleType.Normal:
meshRenderer.material.color = defaultColor;
break;
case MoleType.Red:
meshRenderer.material.color = Color.red;
break;
case MoleType.Blue:
meshRenderer.material.color= Color.blue;
break;
}
}
get => moleType;
}
// 두더지가 배치되어 있는 순번 (왼쪽 상단부터 0)
[field:SerializeField]
public int MoleIndex { private set; get; }
private void Awake()
{
movement3D = GetComponent<Movement3D>();
meshRenderer = GetComponent<MeshRenderer>();
defaultColor = meshRenderer.material.color; // 두더지의 최초 색상 저장
ChangeState(MoleState.UnderGround);
}
public void ChangeState(MoleState newState)
{
// 열거형 변수를 ToString() 메소드를 이요해 문자열로 변환하면
// "UnderGround"와 같이 열거형 요소 이름 변환
// 이전에 재생중이던 상태 종료
StopCoroutine(MoleState.ToString());
// 상태 변경
MoleState = newState;
// 새로운 상태 재생
StartCoroutine(MoleState.ToString());
}
// 두더지가 바닥에서 대기하는 상태로 바닥 위치로 두더지 위치 설정
private IEnumerator UnderGround()
{
// 이동 방향을 : (0, 0, 0) [정지]
movement3D.MoveTo(Vector3.zero);
// 두더지의 y 위치를 홀에 숨어있는 limitMinY 위치로 설정
transform.position=new Vector3(transform.position.x,limitMinY,transform.position.z);
yield return null;
}
private IEnumerator OnGround()
{
// 이동 방향을 : (0, 0, 0) [정지]
movement3D.MoveTo(Vector3.zero);
// 두더지의 y 위치를 홀에 숨어있는 limitMinY 위치로 설정
transform.position = new Vector3(transform.position.x, limitMaxY, transform.position.z);
yield return new WaitForSeconds (waitTimeOnGround);
ChangeState (MoleState.MoveDown);
}
private IEnumerator MoveUp()
{
// 이동 방향 : (0, 1, 0 )[위]
movement3D.MoveTo(Vector3.up);
while (true)
{
// 두더지 y 위치가 limitMaxY에 도달하면 상태 변경
if (transform.position.y >= limitMaxY)
{
// OnGround 상태로 변경
ChangeState (MoleState.OnGround);
}
yield return null;
}
}
private IEnumerator MoveDown()
{
// 이동 방향 : (0, -1, 0 )[아래]
movement3D.MoveTo(Vector3.down);
while (true)
{
// 두더지 y 위치가 limitMinY에 도달하면 상태 변경
if (transform.position.y <= limitMinY)
{
// while() 아래쪽 실행을 위해 이동 완료시 break;
/*●*/break;
}
yield return null;
}
// 망치에 타격 당하지 않고 자연스럽게ㅔ 구멍으로 들어갈 경우 호출
// 망치로 때리지 못하고 땅속으로 들어간 두더지의 속성이 Normal이면 콤보 초기화
/*●*/if (moleType == MoleType.Normal)
{
gameController.Combo = 0;
}
// UnderGround로 상태 변경
/*●*/ChangeState (MoleState.UnderGround);
}
}
(4) 콤보 출력을 위해 TextMeshPro(TextCombo)를 생성하고 알맞게 설정한다.
(5) 콤보 텍스트 출력을 위해 InGameTextViewer스크립트를 수정한다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class InGameTextViewer : MonoBehaviour
{
[SerializeField]
private GameController gameController;
[SerializeField]
private TextMeshProUGUI textScore;
[SerializeField]
private TextMeshProUGUI textPlayTime;
[SerializeField]
private Slider sliderPlayTime;
/*●*/[SerializeField]
private TextMeshProUGUI textCombo;
private void Update()
{
textScore.text = "Score " + gameController.Score;
// ToString("F1") : 소수점 자릿수 정하기 F1은 소수점 한자릿수까지 표시
textPlayTime.text = gameController.CurrentTime.ToString("F1");
sliderPlayTime.value = gameController.CurrentTime / gameController.MaxTime;
/*●*/textCombo.text = "Combo " + gameController.Combo;
}
}
이제 게임을 실행하면 콤보에 따라 텍스트가 갱신되는 모습을 볼 수 있다.
콤보에 따라 획득 가능한 점수
(1) 콤보에 따라 획득 가능한 점수를 설정하기 위해 Hammer스크립트를 수정한다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Hammer : MonoBehaviour
{
[SerializeField]
private float maxY; // 망치의 최대 y 위치
[SerializeField]
private float minY; // 망치의 최소 y 위치
[SerializeField]
private GameObject moleHitEffectPrefab;
[SerializeField]
private AudioClip[] audioClips; // 두더지를 타격했을 때 재생되는 사운드
[SerializeField]
private MolehitTextViewer[] molehitTextViewer; // 타격한 두더지 위치에 타격 정보 텍스트 출력
[SerializeField]
private GameController gameController; // 점수 증가를 위한 GameController
[SerializeField]
private ObjectDetector objectDetector; // 마우스 클릭으로 오브젝트 선택을 위한 ObjectDetector
private Movement3D movement3D; // 망치 오브젝트 이동을 위한 Movement
private AudioSource audioSource;
private void Awake()
{
movement3D = GetComponent<Movement3D>();
audioSource= GetComponent<AudioSource>();
// OnHit 메소드를 ObjectDetector Class의 raycastEvent에 이벤트로 등록
// ObjectDetector의 raycastEvent.Invoke(hit,transform); 메소드가
// 호출될 때마다 OnHit(Transform target) 메소드가 호출된다
objectDetector.raycastEvent.AddListener(OnHit);
}
private void OnHit(Transform target)
{
if (target.gameObject.tag == "Mole")
{
MoleFSM mole =target.GetComponent<MoleFSM>();
// 두더지가 홀 안에 있을때는 공격 불가
if (mole.MoleState == MoleState.UnderGround) return;
// 망치의 위치 설정
transform.position = new Vector3(target.position.x, minY, target.position.z);
// 망치에 맞았기 때문에 두더지의 상태를 바로 "UnderGround"로 설정
mole.ChangeState(MoleState.UnderGround);
// 카메라 흔들기
ShakeCamera.Instance.OnShakeCamera(0.1f, 0.1f);
// 두더지 타격 효과 생성 (Particle의 색상을 두더지 색상과 동일하게 설정)
GameObject clone =Instantiate(moleHitEffectPrefab,transform.position,Quaternion.identity);
ParticleSystem.MainModule main=clone.GetComponent<ParticleSystem>().main;
main.startColor=mole.GetComponent<MeshRenderer>().material.color;
// 두더지의 색상에 따라 처리 (점수, 시간, 사운드 재생)
MoleHitProcess(mole);
// 망치를 위로 이동시키는 코루틴 재생
StartCoroutine("MoveUp");
}
}
private IEnumerator MoveUp()
{
// 이동 방향 (0 / 1 / 0) [위]
movement3D.MoveTo(Vector3.up);
while (true)
{
if (transform.position.y >= maxY)
{
movement3D.MoveTo(Vector3.zero);
break;
}
yield return null;
}
}
private void MoleHitProcess(MoleFSM mole)
{
if(mole.MoleType == MoleType.Normal)
{
gameController.Combo++;
// 기본 x1에 10콤보당 0.5씩 더한다
/*●*/float scoreMultiple = 1 + gameController.Combo / 10 * 0.5f;
/*●*/int getScore = (int)(scoreMultiple * 50);
// 계산된 점수 getScore를 Score에 더한다.
gameController.Score += getScore;
// MoleIndex로 순번을 설정해 두었기 때문에 같은 자리에 있는 TextGetScore 텍스트 출력
// 하얀색 텍스트로 점수 증가 표현
/*●*/molehitTextViewer[mole.MoleIndex].OnHit("Score +"+getScore, Color.white);
}
else if (mole.MoleType == MoleType.Red)
{
gameController.Combo = 0;
gameController.Score -= 300;
// 빨간색 텍스트로 점수 감소 표현
molehitTextViewer[mole.MoleIndex].OnHit("Score -300", Color.red);
}
else if (mole.MoleType == MoleType.Blue)
{
gameController.Combo++;
gameController.CurrentTime += 3;
// 파란색 텍스트로 시간 증가 표현
molehitTextViewer[mole.MoleIndex].OnHit("Time +3", Color.blue);
}
// 사운드 재생 (Normal=0, Red=1, Blue=2)
PlaySound((int)mole.MoleType);
}
private void PlaySound(int index)
{
audioSource.Stop();
audioSource.clip=audioClips[index];
audioSource.Play();
}
}
이제 게임을 실행하면 콤보가 10의 배수가 될 때 마다 획득하는 점수가 증가하는 모습을 볼 수 있다.
콤보에 따라 생성 가능한 두더지 수
(1) 한번에 등장할 수 있는 최대 두더지 수를 설정하기 위해 MoleSpawner스크립트를 수정한다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MoleSpawner : MonoBehaviour
{
[SerializeField]
private MoleFSM[] moles; // 맵에 존재하는 두더지들
[SerializeField]
private float spawnTime; // 두더지 등장 주기
// 두더지 등장 확률 (Normal : 85%, Red : 10%, Blue : 5%)
private int[] spawnPercents = new int[3] { 85, 10, 5 };
// 한번에 등장하는 최대 두더지 수
/*●*/public int MaxSpawnMole { set; get; } = 1;
// 두더지를 바로 생성하지 말고 카운트 다운이 완료된 후에 생성되도록 GameController
// 에서 SetUp() 메소드를 호출
public void SetUp()
{
StartCoroutine("SpawnMole");
}
private IEnumerator SpawnMole()
{
while (true)
{
/* // 0 ~ Moles.Lenth-1 중 임의의 숫자 선택
int index = Random.Range(0, moles.Length);
// 선택된 두더지의 속성 설정
moles[index].MoleType = SpawnMoleType();
// index번째 두더지의 상태를 "MoveUp"으로 변경
moles[index].ChangeState(MoleState.MoveUp);*/
// MaxSpawnMole 숫자만큼 두더지 등장
/*●*/StartCoroutine("SpawnMultiMoles");
// 스폰시간 동안 대기
yield return new WaitForSeconds(spawnTime);
}
}
private MoleType SpawnMoleType()
{
int percent = Random.Range(0, 100);
float cumulative = 0;
for(int i = 0; i < spawnPercents.Length; i++)
{
cumulative+=spawnPercents[i];
if (percent < cumulative)
{
return (MoleType)i;
}
}
return MoleType.Normal;
}
/*●*/private IEnumerator SpawnMultiMoles()
{
// 0 ~ moles.Length-1 사이의 겹치치 않는 난수를 모두 생성
int[] indexs = RandomNumerics(moles.Length, moles.Length);
int currentSpawnMole = 0; // 현재 등장한 두더지 숫자
int currentIndex = 0; // indexs 배열 인덱스
// 현재 등장해야할 두더지 숫자만큼 두더지 등장
while (currentIndex < indexs.Length)
{
// 두더지가 바닥에 있을 때만 등장 가능 (현재 등장한 두더지를 사용하지 않도록)
if (moles[indexs[currentIndex]].MoleState == MoleState.UnderGround)
{
// 선택된 두더지의 속성 설정
moles[indexs[currentIndex]].MoleType=SpawnMoleType();
// 선택된 두더지의 상태를 "MoveUp"으로 바꿈
moles[indexs[currentIndex]].ChangeState(MoleState.MoveUp);
// 등장한 두더지 숫자 1증가
currentSpawnMole++;
yield return new WaitForSeconds(0.1f);
}
// 최대 등장 숫자만큼 등장했으면 SpawnMultiMoles() 코루틴 함수 종료
if (currentSpawnMole == MaxSpawnMole)
{
break;
}
currentIndex++;
yield return null;
}
}
/*●*/private int[] RandomNumerics(int maxCount, int n)
{
// 0 ~ maxCount까지의 숫자 중 겹치지 않는 n개의 난수가 필요할 때 사용
int[] defaults=new int[maxCount]; // 0 ~ maxCount까지 순서대로 저장하는 배열
int[] results=new int[n]; // 결과 값들을 저장하는 배열
// 배열 전체에 0부터 maxCount의 값을 순서대로 저장
for(int i = 0; i < maxCount; ++i)
{
defaults[i] = i;
}
for(int i = 0; i < n; ++i)
{
int index = Random.Range(0, maxCount);
results[i] = defaults[index];
defaults[index] = defaults[maxCount - 1];
maxCount--;
}
return results;
}
}
(2) 콤보에따라 등장하는 최대 두더지 숫자를 설정하기 위해 GameController스크립트를 수정한다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameController : MonoBehaviour
{
[SerializeField]
private CountDown countDown;
[SerializeField]
private MoleSpawner moleSpawner;
private int score;
private int combo;
private float currentTime;
public int Score
{
get => score;
set=>score=Mathf.Max(0,value);
}
public int Combo
{
/*●*/set
{
combo = Mathf.Max(0, value);
// 70 이상 일 땐 MaxSpawnMole이 5로 고정되기 때문에 70까지만 체크
if (combo <= 70)
{
// 콤보에 따라 생성되는 최대 두더지 숫자
moleSpawner.MaxSpawnMole = 1 + (combo + 10) / 20;
}
}
get => combo;
}
// [field:SerializeField] : 자동 구현 프로퍼티를 인스펙터창에서 보이게 할 때 사용
[field:SerializeField]
public float MaxTime { private set; get; }
public float CurrentTime
{
set=> currentTime = Mathf.Clamp(value,0,MaxTime);
get => currentTime;
}
private void Start()
{
countDown.StartCountDown(GameStart);
}
private void GameStart()
{
moleSpawner.SetUp();
StartCoroutine("OnTimeCount");
}
private IEnumerator OnTimeCount()
{
CurrentTime = MaxTime;
while (CurrentTime > 0)
{
CurrentTime-=Time.deltaTime;
yield return null;
}
}
}
이제 게임을 실행하면 콤보에 따라 한번에 등장하는 두더지의 수가 달라진다.