[Project] 두더지 잡기(4)



Intro Scene

(1) 여태 만들었던 씬의 이름을 Game으로 변경한다.

(2) 새로운 씬을 만들고 이름을 Intro라고 바꾼다음 저장해준다.

(3) Intro씬의 MainCamer오브젝트의 위치(0 / 0 / -10) Camera컴포넌트의 ClearFlags : SolidColor 배경색 : 검정

(4) 게임 타이틀 출력을 위해 TextMeshPro(TextGameTitle)을 만든다.

(5) Canvas오브젝트의 UI Scale Mode : Scale With Screen Size로 바꾸고 Reference Resolution을 1920 / 1080으로 바꿔주고 Match를 0.5로 설정한다.

(6) Press Any Key 텍스트 출력을 위한 TextMeshPro(TextPressAnyKey)를 생성하고 알맞게 설정한다.

(7) Press Any Key 텍스트는 지속적으로 깜빡거린다. 깜빡거리는 효과를 구현하기 위해 FadeEffect_TMP스크립트를 만든다. (원리는 텍스트의 알파값을 최소에서 최대로, 최대에서 최소로 계속해서 바꿔주는 것이다.)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
public class FadeEffect_TMP : MonoBehaviour
{
    [SerializeField]
    private float fadeTime = 0.5f;
    private TextMeshProUGUI fadeText;

    private void Awake()
    {
        fadeText = GetComponent<TextMeshProUGUI>();
    }

    private void OnEnable()
    {
        StartCoroutine("FadeLoop");
    }

    private IEnumerator FadeLoop()
    {
        while (true)
        {
            yield return StartCoroutine(Fade(1,0));

            yield return StartCoroutine(Fade(0, 1));
        }
    }
    private IEnumerator Fade(float start,float end)
    {
        float currentTime = 0;
        float percent = 0;

        while (percent < 1)
        {
            currentTime += Time.deltaTime;
            percent = currentTime / fadeTime;

            Color color=fadeText.color;
            color.a = Mathf.Lerp(start, end, percent);
            fadeText.color = color;

            yield return null;
        }
    }
}

(8) TextPressAnyKey오브젝트에 FadeEffect_TMP스크립트를 컴포넌트로 추가해준다.

(9) 간단한 인트로 연출을 위해 두더지들이 숨는 벽 Cube(Hide Wall)을 만들고 크기(25 / 2.5 / 1) 충돌처리는 하지 않기 때문에 콜라이더는 삭제 해준다.

(10) 기본 두더지 오브젝트 Capsule(MoleNormal)을 생성하고 위치(-4.35 / 0 / 1) 크기(1.2 / 1.2 / 0.3) 그리고 머테리얼에 Mole머테리얼을 넣어주고 충돌처리는 하지 않으니 콜라이더는 삭제해주고 Movement3D 스크립트를 추가해준다.

(11) 빨간 두더지에게 사용 할 머테리얼(MoleRed)을 만든다.

(12) MoleNormal오브젝트를 복제하고 이름을 MoleRed로 바꾸고 위치(0.75 / 0 / 1) 머테리얼을 바꿔준다.

(13) 파란두더지에게 사용 할 머테리얼(MoleBlue)을 만든다.

(14) MoleNormal오브젝트를 복제하고 이름을 MoleBlue로 바꾸고 위치(4.65/ 0 / 1) 머테리얼을 바꿔준다.

(15) 기본 두더지의 정보 출력을 위해 TextMeshPro(TextMoleNormalInfo)를 만들고 알맞게 설정한다.

(16) 빨간 두더지의 정보 출력을 위해 TextMeshPro(TextMoleRedInfo)를 만들고 알맞게 설정한다.

(17) 파란 두더지의 정보 출력을 위해 TextMeshPro(TextMoleBlueInfo)를 만들고 알맞게 설정한다.

(18) 게임을 시작하면 게임 타이틀만 우선 등장하기 때문에 나머지 텍스트는 비활성화 해준다.

(19) 인트로 씬을 시작하면 두더지가 순서대로 등장하고 모든 두더지가 등장하면 PressAnyKey가 나오게 만든다. 인트로 씬의 연출을 위해 IntroScenario스크립트를 만든다.

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

public class IntroScenario : MonoBehaviour
{
    [SerializeField]
    private Movement3D[] moveMentMoles;     // 두더지들을 위로 이동시키기 위한 Movement3D
    [SerializeField]
    private GameObject[] textMoles;     // 두더지 색상에 따라 획득 가능한 효과 출력 Text
    [SerializeField]
    private GameObject textPressAnyKey;     // "Press Any Key" 출력 Text
    [SerializeField]
    private float maxY=1.5f;     // 두더지가 올라올 수 있는 최대 높이
    private int currentIndex = 0;       // 두더지가 순서대로 등장하도록 순번을 관리

    private void Awake()
    {
        StartCoroutine("Scenario");
    }
    private IEnumerator Scenario()
    {
        // 두더지가 Normal -> Red -> Blue 순서대로 등장
        while(currentIndex< moveMentMoles.Length)
        {
            yield return StartCoroutine("MoveMole");
        }

        // "Press Any Key" 출력
        textPressAnyKey.SetActive(true);

        // 마우스 왼쪽 버튼을 누르면 "Game" 씬으로 이동
        while (true)
        {
            if (Input.GetMouseButtonDown(0))
            {
                SceneManager.LoadScene("Game");
            }
            yield return null;
        }
    }
    private IEnumerator MoveMole()
    {
        moveMentMoles[currentIndex].MoveTo(Vector3.up);
        while (true)
        {
            // 두더지가 목표지점에 도착하면 while() 반복문 종료
            if (moveMentMoles[currentIndex].transform.position.y >= maxY)
            {
                moveMentMoles[currentIndex].MoveTo(Vector3.zero);
                break;
            }
            yield return null;
        }
        // 두더지를 획득했을 때 효과 텍스트 출력
        textMoles[currentIndex].SetActive(true);
        // 다음 두더지를 설정하도록 인덱스 증가
        currentIndex++;
    }
}

(20) 빈 오브젝트(IntroScenario)를 생성하고 IntroScenario스크립트를 넣어준다.

(21) 씬 전환을 위해 제작한 씬들을 등록해준다.

이제 게임을 시작하면 두더지 정보들이 다 출력되고 마우스 좌클릭을 하면 Game씬으로 넘어가는 모습을 볼 수 있다.

스테이지의 여러 정보 저장

(1) 획득 점수, 최대 콤보, 기본, 빨강, 파랑 두더지 타격 정보를 저장 및 출력하기 위해 GameController스크립트를 수정한다.

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

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;
            }
            // 최대 콤보 저장
            /*●*/if (combo > MaxCombo)
            {
                MaxCombo = combo;
            }
        }
        get => combo;
    }
    /*●*/public int MaxCombo { private set; get; }
    
    /*●*/public int NormalMoleHitCount { set; get; }
    /*●*/public int RedMoleHitCount { set; get; }
    /*●*/public int BlueMoleHitCount { set; get; }

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

        // CurrentTime이 0이 되면 GameOver() 메소드를 호출
        /*●*/GameOver();
    }
    /*●*/private void GameOver()
    {
        // 현재 스테이지에서 획득한 여러 정보 저장
        PlayerPrefs.SetInt("CurrentScore",Score);
        PlayerPrefs.SetInt("CurrentMaxCombo",MaxCombo);
        PlayerPrefs.SetInt("CurrentNormalMoleHitCount", NormalMoleHitCount);
        PlayerPrefs.SetInt("CurrentRedMoleHitCount", RedMoleHitCount);
        PlayerPrefs.SetInt("CurrentBlueMoleHitCount", BlueMoleHitCount);

        // "GameOver" 씬으로 이동
        SceneManager.LoadScene("GameOver");
    }
}

(2) Hammer스크립트를 두더지를 타격했을 때 두더지의 속성에 따라 Gamecontroller에 있는 NormalHitCount, RedHitCount, BlueHitCount를 증가시킨다.

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.NormalMoleHitCount++;
            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.RedMoleHitCount++;
            gameController.Combo = 0;
            gameController.Score -= 300;
            // 빨간색 텍스트로 점수 감소 표현
            molehitTextViewer[mole.MoleIndex].OnHit("Score -300", Color.red);
        }
        else if (mole.MoleType == MoleType.Blue)
        {
            // 파랑 두더지 타격 횟수 증가
            /*●*/gameController.BlueMoleHitCount++;
            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();
    }
}

Game Over Scene

(1) 씬 하나를 새로 만들어서 이름을 GameOver라고 바꾸고 MainCamer오브젝트의 위치(0 / 0 / -10) Camera컴포넌트의 ClearFlags : SolidColor 배경색 : 검정으로 설정한다.

(2) 랭크 정보 배치 및 정렬을 위해 Panel(PanelRankInfo) UI를만든다.

(3) Canvas오브젝트의 UI Scale Mode : Scale With Screen Size로 바꾸고 Reference Resolution을 1920 / 1080으로 바꿔주고 Match를 0.5로 설정한다.

(4) PanelRankInfo오브젝트에 Grid Layout Group, Content Size Fitter컴포넌트를 추가한다.

image

Grid Layout Group : 레이아웃 그룹이 되는 오브젝트(주로 Panel)에 컴포넌트로 적용하며, 레이아웃 그룹 오브젝트의 자식으로 등록되는 UI 오브젝트들을 격자 맵 형태로 배치 가능하다.(각 UI의 크기를 동일하게 제어할 수 있다.)

Content Size Fitter : 레이아웃 그룹이 되는 오브젝트(주로 Panel)에 컴포넌트로 적용하며, 레이아웃 그룹 오브젝트의 크기를 제어하는 용도

(5) 위 컴포넌트들의 설정을 다음과 같이 바꾼다.

image

Preferred Size : 배치되는 레이아웃 요소들의 전체 크기를 바탕으로 현재 오브젝트 크기 설정

(5) 랭크 텍스트 출력을 위해 PanelRankInfo의 자식으로 TextMeshPro(TextrankText)를 생성하고 알맞게 설정한다.

(6) 점수 최대콤보 노말, 빨강, 파랑 타격횟수를 출력해야하므로 TextrankText를 5개 복제해서 이름과 설정을 바꿔준다.

image

(7) 랭크 데이터 출력을 위해 TextMeshpro(TextRankData)를 만들고 알맞게 설정한 다음 프리펩으로 만들어준다.

(8) 랭크 데이터를 관리하는 RankSystem스크립트를 만들어준다.

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

public class RankSystem : MonoBehaviour
{
    [SerializeField]
    private int maxRankCount = 10;      // 최대 랭크 표시 개수
    [SerializeField]
    private GameObject textPrefab;      // 랭크 정보를 출력하는 Text UI 프리펩
    [SerializeField]
    private Transform panelRankInfo;    // Text가 배치되는 부모 Panel Transform

    private RankData[] rankDataArray;   // 랭크 정보를 저장하는 RankData 타입의 배열
    private int currentIndex = 0;

    private void Awake()
    {
        rankDataArray=new RankData[maxRankCount];

        // 1. 기존의 랭크 정보 불러오기
        LoadRankData();
        // 2. 1등부터 차례로 현재 스테이지에서 획득한 점수와 비교
        CompareRank();
        // 3. 랭크 정보 출력
        PrintRankData();
        // 4. 새로운 랭크 정보 저장
        SaveRankData();
    }
    private void LoadRankData()
    {
        for(int i = 0; i < maxRankCount; i++)
        {
            rankDataArray[i].score                          = PlayerPrefs.GetInt("RankScore" + i);
            rankDataArray[i].maxCombo                  = PlayerPrefs.GetInt("RankMaxCombo" + i);
            rankDataArray[i].normalMoleHitCount     = PlayerPrefs.GetInt("RankNormalMoleHitCount" + i);
            rankDataArray[i].redMoleHitCount          = PlayerPrefs.GetInt("RankRedMoleHitCount" + i);
            rankDataArray[i].blueMoleHitCount         = PlayerPrefs.GetInt("RankBlueMoleHitCount" + i);
        }
    }
    private void SpawnText(string print, Color color)
    {
        // Instantiate()로 textPrefab 복사체를 생성하고, clone 변수에 저장
        GameObject clone=Instantiate(textPrefab);
        // clone의 TextMeshProUGUI 컴포넌트 정보를 얻어와 text 변수에 저장
        TextMeshProUGUI text=clone.GetComponent<TextMeshProUGUI>();

        // 생성한 Text UI 오브젝트의 부모를 PanelRankInfo 오브젝트로 설정
        clone.transform.SetParent(panelRankInfo);
        // 자식으로 등록되면서 크기가 변환될 수 있기 때문에 크기를 1로 설정
        clone.transform.localScale = Vector3.one;
        // Text UI에 출력할 내용과 폰트 색상 설정
        text.text = print;
        text.color = color;
    }
    private void CompareRank()
    {
        // 현재 스테이지에서 달성한 정보
        RankData currentData=new RankData();
         currentData.score                          = PlayerPrefs.GetInt("CurrentScore");
         currentData.maxCombo                  = PlayerPrefs.GetInt("CurrentMaxCombo");
         currentData.normalMoleHitCount     = PlayerPrefs.GetInt("CurrentNormalMoleHitCount");
         currentData.redMoleHitCount          = PlayerPrefs.GetInt("CurrentRedMoleHitCount");
         currentData.blueMoleHitCount           = PlayerPrefs.GetInt("CurrentBlueMoleHitCount");

        // 1 ~ 10등의 점수와 현재 스테이지에서 달성한 점수 비교
        for(int i = 0; i < maxRankCount; i++)
        {
            if (currentData.score > rankDataArray[i].score)
            {
                // 랭크에 들어갈 수 있는 점수를 달성했으면 반복문 중지
                currentIndex = i;
                break;
            }
        }

        // currentData의 등수 아래로 점수를 한칸씩 밀어서 저장
        for(int i=maxRankCount-1; i>0; i--)
        {
            rankDataArray[i]=rankDataArray[i-1];

            if (currentIndex == i - 1)
            {
                break;
            }
        }

        // 새로운 점수를 랭크에 넣기
        rankDataArray[currentIndex] = currentData;
    }
    private void PrintRankData()
    {
        Color color = Color.white;

        for(int i = 0; i < maxRankCount; i++)
        {
            // 방금 플레이의 점수가 랭크에 등록되면 색상을 노란색으로 표시
            color = currentIndex != i ? Color.white : Color.yellow;

            // Text - TextMeshPro 생성 및 원하는 데이터 출력
            SpawnText((i + 1).ToString(), color);
            SpawnText(rankDataArray[i].score.ToString(), color);
            SpawnText(rankDataArray[i].maxCombo.ToString(), color);
            SpawnText(rankDataArray[i].normalMoleHitCount.ToString(), color);
            SpawnText(rankDataArray[i].redMoleHitCount.ToString(), color);
            SpawnText(rankDataArray[i].blueMoleHitCount.ToString(), color);
        }
    }
    private void SaveRankData()
    {
        for(int i = 0; i < maxRankCount; i++)
        {
            PlayerPrefs.SetInt("RankScore"+i,rankDataArray[i].score);
            PlayerPrefs.SetInt("RankMaxCombo" + i, rankDataArray[i].maxCombo);
            PlayerPrefs.SetInt("RankNormalMoleHitCount" + i, rankDataArray[i].normalMoleHitCount);
            PlayerPrefs.SetInt("RankRedMoleHitCount" + i, rankDataArray[i].redMoleHitCount);
            PlayerPrefs.SetInt("RankBlueMoleHitCount" + i, rankDataArray[i].blueMoleHitCount);
        }
    }
}
[System.Serializable]
public struct RankData
{
    public int score;
    public int maxCombo;
    public int normalMoleHitCount;
    public int redMoleHitCount;
    public int blueMoleHitCount;
}

(9) 빈 오브젝트(RankSystem)를 생성하고 RankSystem스크립트를 추가하고 변수를 할당해준다.

(10) 게임 재시작을 위해 재시작 버튼을 만들자.

(11) Button-TextmeshPro(ButtonRestart) UI를 만들고 알맞게 설정한다.

(12) 게임 종료를 위해 Button-TextmeshPro(ButtonExit) UI를 만들고 알맞게 설정한다.

(13) 버튼을 클릭했을 때 이벤트 처리를 위해 ButtonClickEvent스크립트를 만든다.

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

public class ButtonClickEvent : MonoBehaviour
{
    public void SceneLoader(string sceneName)
    {
        SceneManager.LoadScene(sceneName);
    }

    public void GameExit()
    {
            #if UNITY_EDITOR
                    UnityEditor.EditorApplication.isPlaying = false;
            #elif UNITY_ANDROID
                     Application.Quit();
            #endif
    }
}

(14) RankSystem 오브젝트에 ButtonClickEvent스크립트를 추가해준다. 그리고 각각 버튼에 알맞는 On Click()이벤트를 등록해준다.




© 2022. by KSC

Powered by sora