Coding Feature.

[Unity 3D] 3D 퐁 만들기 #10 아이템 매니저 구현 본문

Toy Project/MICRO-PONG [Unity3D]

[Unity 3D] 3D 퐁 만들기 #10 아이템 매니저 구현

codingfeature 2024. 1. 23. 23:35

아이템을 구현해보도록 하겠습니다.

 

제가 구현할 아이템은 현재 총 3 가지 입니다.

 

1. 공 크기 증가

2. 플레이어 크기 증가

3. 점수 획득량 두 배 증가

 

위 아이템들은 공이 닿아야 획득할 수 있도록 할 것이며, 처음에는 모든 아이템을 스폰시키고 아이템을 획득하면 발동시킨 뒤, 발동 효과가 끝이 나고 몇 초 뒤에 다시 스폰이 되도록 하겠습니다.

 

우선 아이템을 관리할 아이템 매니저 스크립트를 작성하였습니다.

 

 

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

public class ItemManager : MonoBehaviour
{
    private static ItemManager instance;

    public GameObject item_BallSizeGrow; // 공 크기 증가 아이템
    public GameObject item_PlayerSizeGrow; // 플레이어 크기 증가 아이템
    public GameObject item_ScoreDouble; // 점수 획득 두 배 증가 아이템

    public GameObject ItemSpawnStartPoint; // 아이템 스폰 시작 위치 표시를 위한 게임 오브젝트.
    public GameObject ItemSpawnEndPoint; // 아이템 스폰 끝 위치 표시를 위한 게임 오브젝트.

    public float itemDuration; // 아이템 효과 지속 시간
    public float itemSpawnTime; // 아이템 효과 끝난 뒤 다시 스폰하기까지의 시간.
    public float ItemMaxScale; // 아이템 최대 크기

    public float ballMaxScale; // 공 최대 증가 크기

    public float playerMaxScale; // 플레이어 최대 증가 크기
    public float itemRotationSpeed; // 아이템 회전 속도

    public Coroutine[] coroutineEffect; // 아이템 효과 관리 위한 코루틴 array.

    int itemNum; // 아이템 전체 갯수.

    public static ItemManager Instance
    {
        get
        {
            return instance;
        }
    }

    private void Awake()
    {
        if (instance)
        {
            Destroy(instance);
            return;
        }

        instance = this;
        DontDestroyOnLoad(this.gameObject);
    }

    private void Start()
    {
        itemNum = 3;
        coroutineEffect = new Coroutine[itemNum];

        // 아이템 애니메이션 시작.
        StartCoroutine("PlayBallSizeGrowAnimation");
        StartCoroutine("PlayPlayerItemAnimation");
        StartCoroutine("PlayScoreItemAnimation");
        StartCoroutine("PlayDoubleScoreTextAnimation");
    }

    public void SpawnItem(GameObject item)
    {
        Vector3 spawnPosition;

        item.SetActive(true);

        spawnPosition.x = Random.Range(ItemSpawnStartPoint.transform.position.x, ItemSpawnEndPoint.transform.position.x);
        spawnPosition.y = Random.Range(ItemSpawnStartPoint.transform.position.y, ItemSpawnEndPoint.transform.position.y);
        spawnPosition.z = Random.Range(ItemSpawnStartPoint.transform.position.z, ItemSpawnEndPoint.transform.position.z);

        item.transform.position = spawnPosition;
    }

    public void ActivateItemEffect(string item)
    {
        AudioManager.Instance.Play("ItemGet");
        switch (item)
        {
            case "Item-BallSizeGrow":
                coroutineEffect[0] = StartCoroutine(IncreaseBallSize());
                break;
            case "Item-PlayerSizeGrow":
                coroutineEffect[1] = StartCoroutine(IncreasePlayerSize());
                break;
            case "Item-ScoreDouble":
                coroutineEffect[2] = StartCoroutine(DoubleScore());
                break;
            default:
                break;
        }
    }

    public void DeactivateItemEffect()
    {
        // 모든 아이템 효과 코루틴 멈춤.
        for(int i = 0; i < itemNum; i++)
        {
            if (coroutineEffect[i] != null)
                StopCoroutine(coroutineEffect[i]);
        }

    }
}

 

 

아이템 효과는 코루틴으로 발동하게 됩니다.

이때 아이템 효과가 발동된 상태에서 게임오버가 되는 경우 코루틴을 종료할 필요가 있습니다.

 

코루틴을 도중에 종료하기 위해 각 아이템의 효과 코루틴을 배열 형태로 저장하였습니다.

만약 StartCoroutine으로 그냥 코루틴의 함수를 그대로 던져줄 경우에는 나중에 Stop할 수 없다고 합니다..!

 

더 자세한 내용은 아래를 참고하였습니다!

C# 코루틴 변수 저장 | Charo (charotiti9.github.io)

 

C# 코루틴 변수 저장

C#, Unity에서 코루틴을 사용할 때 변수에 저장해서 사용하는 2가지 방법과 이에 따른 주의사항을 알아본다.

charotiti9.github.io

 

ActivateItemEffect 함수를 통해서 아이템의 오브젝트 이름을 받고, 그에 맞는 코루틴 배열을 수행하게 됩니다. 만약 게임오버가 된다면 DeactivateItemEffect 함수를 통해서 모든 코루틴 배열을 탐색하여 코루틴이 실행되고 있는 경우 종료하도록 했습니다.

 

 

그 다음 각 아이템 효과를 구현하였습니다.

    IEnumerator IncreasePlayerSize()
    {
        yield return null;
        GameObject player = PlayerScript.Instance.gameObject;
        Vector3 originalScale = PlayerScript.Instance.originalPlayerScale;

        while (player.transform.localScale.x < playerMaxScale)
        {
            yield return new WaitForSeconds(0.05f);
            player.transform.localScale += new Vector3(0.05f, 0f, 0.05f);
        }

        player.transform.localScale = new Vector3(playerMaxScale, originalScale.y, playerMaxScale);
        yield return new WaitForSeconds(itemDuration);

        while (player.transform.localScale.x > originalScale.x)
        {
            yield return new WaitForSeconds(0.05f);
            player.transform.localScale -= new Vector3(0.05f, 0f, 0.05f);
        }

        player.transform.localScale = originalScale;

        yield return new WaitForSecondsRealtime(itemSpawnTime);

        SpawnItem(item_PlayerSizeGrow);
    }

    IEnumerator IncreaseBallSize()
    {
        yield return null;
        GameObject ball = BallScript.Instance.GetBallObject();
        Vector3 originalBallScale = BallScript.Instance.originalBallScale;

        while(ball.transform.localScale.x < ballMaxScale)
        {
            yield return new WaitForSeconds(0.05f);
            ball.transform.localScale += new Vector3(0.05f, 0.05f, 0.05f);
        }

        ball.transform.localScale = new Vector3(ballMaxScale, ballMaxScale, ballMaxScale);
        yield return new WaitForSeconds(itemDuration);

        while (ball.transform.localScale.x > originalBallScale.x)
        {
            yield return new WaitForSeconds(0.05f);
            ball.transform.localScale -= new Vector3(0.05f, 0.05f, 0.05f);
        }

        ball.transform.localScale = originalBallScale;

        yield return new WaitForSecondsRealtime(itemSpawnTime);

        SpawnItem(item_BallSizeGrow);
    }

    IEnumerator DoubleScore()
    {
        yield return null;
        UIManager.Instance.doubleScoreText.SetActive(true);
        PlayerScript.Instance.addingScore = 2;

        yield return new WaitForSeconds(itemDuration);

        UIManager.Instance.doubleScoreText.SetActive(false);
        PlayerScript.Instance.addingScore = 1;

        yield return new WaitForSecondsRealtime(itemSpawnTime);

        SpawnItem(item_ScoreDouble);
    }

 

 

WaitForSeconds와 WaitForSecondsRealtime을 적절히 활용해서 아이템의 스폰 시간, 아이템 효과 지속 시간을 설정했습니다. 그리고 각 아이템 효과가 발동된 다음에는 다시 SpawnItem 함수를 호출해서 아이템을 다시 스폰하도록 했습니다.

 

 

아래는 각 아이템이 게임 내에서 움직이는 애니메이션을 다루는 코루틴입니다.

    public IEnumerator PlayBallSizeGrowAnimation()
    {
        yield return null;
        Vector3 originalItemScale = item_BallSizeGrow.transform.localScale;
        while (true)
        {
            while (item_BallSizeGrow.transform.localScale.x < ItemMaxScale)
            {
                yield return new WaitForSeconds(0.05f);
                item_BallSizeGrow.transform.localScale += new Vector3(0.05f, 0.05f, 0.05f);
            }

            item_BallSizeGrow.transform.localScale = new Vector3(ItemMaxScale, ItemMaxScale, ItemMaxScale);

            while (item_BallSizeGrow.transform.localScale.x > originalItemScale.x)
            {
                yield return new WaitForSeconds(0.05f);
                item_BallSizeGrow.transform.localScale -= new Vector3(0.05f, 0.05f, 0.05f);
            }

            item_BallSizeGrow.transform.localScale = originalItemScale;
        }
    }

    public IEnumerator PlayPlayerItemAnimation()
    {
        yield return null;
        while (true)
        {
            yield return new WaitForSeconds(0.05f);
            item_PlayerSizeGrow.transform.Rotate(new Vector3(itemRotationSpeed, itemRotationSpeed, itemRotationSpeed));
        }
    }

    public IEnumerator PlayScoreItemAnimation()
    {
        yield return null;
        while (true)
        {
            yield return new WaitForSeconds(0.05f);
            item_ScoreDouble.transform.Rotate(new Vector3(itemRotationSpeed, itemRotationSpeed, itemRotationSpeed));
        }
    }

    public IEnumerator PlayDoubleScoreTextAnimation()
    {
        yield return 0;
        float text_alpha = 1.0f;

        while (true)
        {
            while (text_alpha > 0.0f)
            {
                UIManager.Instance.SetDoubleScoreTextAlpha(text_alpha);
                text_alpha -= 0.03f;
                yield return new WaitForSeconds(0.01f);
            }
            while (text_alpha < 1.0f)
            {
                UIManager.Instance.SetDoubleScoreTextAlpha(text_alpha);
                text_alpha += 0.03f;
                yield return new WaitForSeconds(0.01f);
            }
        }
    }

 

 

 

또한 아이템 효과가 발동된 상태에서 게임 오버가 되고 다시 게임을 시작하면 아이템 발동 효과가 전부 사라져야 하기 때문에 공의 크기, 플레이어 크기, 점수 두 배 증가 등을 모두 초기화하는 코드를 게임 매니저의 초기화 함수에 추가하였습니다.

 

    void InitializeSettings() // 게임 초깃값 설정.
    {
        score = 0;

..
        UIManager.Instance.doubleScoreText.SetActive(false);

        PlayerScript.Instance.gameObject.transform.localScale = PlayerScript.Instance.originalPlayerScale;
        PlayerScript.Instance.addingScore = 1;
        PlayerScript.Instance.tempScore = 0;

..
        BallScript.Instance.SetBallSize(BallScript.Instance.originalBallScale);

        ItemManager.Instance.SpawnItem(ItemManager.Instance.item_BallSizeGrow);
        ItemManager.Instance.SpawnItem(ItemManager.Instance.item_PlayerSizeGrow);
        ItemManager.Instance.SpawnItem(ItemManager.Instance.item_ScoreDouble);

        ItemManager.Instance.DeactivateItemEffect();
    }

 

 

 

 

 

 

게임 매커니즘과 관련해서 거의 다 구현이 되었습니다.

이제는 리더보드, UI, 밸런스, 버그 해결, 효과 추가 등 UX 관련 퀄리티를 높이도록 하겠습니다!