[Project] 두더지 잡기(3)



콤보 시스템

우선 밸런스를 잘 생각해서 콤보에 따라 획득 가능한 점수, 최대 생성 두더지를 정한다.

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

이제 게임을 실행하면 콤보에 따라 한번에 등장하는 두더지의 수가 달라진다.




© 2022. by KSC

Powered by sora