Unity/Dongle Family

인보크(Invoke)와 코루틴(Corutine) / 게임 매니저(Game Manager)

hwan91 2025. 3. 27. 20:06

Invoke

특정 메서드를 일정 시간 후에 한 번 실행할 때 사용한다.

(아주 간단하게 시간 지연 호출이 필요할 때)

 

사용 방법

Invoke("MethodName", 3f); // 3초 후 해당 메서드 실행
InvokeRepeating("MethodName", delayTime, repeatRate); // 일정 시간 뒤에 실행하고, 반복 실행
CancelInvoke("MethodName"); // 반복 중단

Corutine

https://hwan91.tistory.com/47

 

코루틴(Corutine) / 적 생성(Enemy, Spawn)

CorutineIEnumerator는 **코루틴 전용 반환형(인터페이스)**이다.코루틴은 일반 함수와 다르게, 실행 도중 yield return을 만나면 일시 중지되었다가 나중에 다시 실행된다.이 방식 덕분에 시간이 걸리

hwan91.tistory.com


비교

  Invoke Coroutine
반복 실행 ❌ (InvokeRepeating, 한 번만) ⭕ 가능
조건부 실행 흐름 ❌ 불가 ⭕ 가능
매개변수 전달 ❌ 불가 ⭕ 가능
종료/중지 🔸 취소만 가능 (CancelInvoke) ⭕ StopCoroutine()으로 중지 가능
코드 가독성 ⭕ 단순 🔸 복잡하지만 유연

 

각각 언제 써야할까?

Invoke

- 그냥 몇 초 뒤에 한 번 실행만 하면 될 때

- 일정 시간 간격으로 반복해야 할 때 (InvokeRepeating)

 

Corutine

- 일정 시간 간격으로 반복해야할 때 (공통)

- 실행 중간에 조건 체크, 반복 루프 등 복잡한 제어가 필요할 때

- 여러 개의 지연 동작을 순서대로 하고 싶을 때
- 매개변수를 넘기며 유연한 실행을 원할 때


GameManager.cs

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

public class GameManager : MonoBehaviour
{
    [Header("=== Core ===")]
    public bool isOver; // 게임오버 상태
    public int score; // 점수
    public int maxLevel; // 동글 생성 최대 레벨

    [Header("=== Object Pooling ===")]
    public GameObject donglePrefab;
    public Transform dongleGroup;
    public List<Dongle> donglePool;

    public GameObject effectPrefab;
    public Transform effectGroup;
    public List<ParticleSystem> effectPool;

    [Range(1, 30)]
    public int poolSize;
    public int poolCursor;
    public Dongle lastDongle;

    [Header("=== Audio ===")]
    public AudioSource bgmPlayer;
    public AudioSource[] sfxPlayer;
    public AudioClip[] sfxClip;

    [Header("=== UI ===")]
    public GameObject startGroup; // 시작 화면 UI
    public GameObject endGroup; // 종료 화면 UI
    public Text scoreText; // 현재 점수
    public Text maxScoreText; // 최고 점수
    public Text subScoreText; // 결과창 점수

    [Header("=== ETC ===")]
    public GameObject line; // 위쪽 경계선
    public GameObject bottom; // 바닥 라인

    public enum Sfx { LevelUp, Next, Attach, Button, Over }; // 효과음 종류
    int sfxCursor; // 다음에 재생할 오디오 소스

    void Awake()
    {
        Application.targetFrameRate = 60; // 60 프레임 고정

        donglePool = new List<Dongle>();
        effectPool = new List<ParticleSystem>();

        // 풀 사이즈만큼 동글을 미리 생성해서 풀에 넣음
        for (int index = 0; index < poolSize; index++) {
            MakeDongle();
        }

        // 저장된 최고 점수가 없으면 0으로 초기화
        if (!PlayerPrefs.HasKey("MaxScore")) PlayerPrefs.SetInt("MaxScore", 0); // 예외 처리
        
        // 최고 점수 UI 텍스트에 저장된 최고 점수를 표시
        maxScoreText.text = PlayerPrefs.GetInt("MaxScore").ToString();
    }

    public void GameStart()
    {
        // 오브젝트 활성화
        line.SetActive(true);
        bottom.SetActive(true);
        scoreText.gameObject.SetActive(true);
        maxScoreText.gameObject.SetActive(true);
        startGroup.SetActive(false);

        bgmPlayer.Play();
        SfxPlay(Sfx.Button);

        // Invoke("실행시킬 함수 이름", 딜레이 시간(초)) = 함수 호출에 딜레이를 주고 싶을 때 사용하는 함수
        Invoke("NextDongle", 1.5f); // 1.5ch 후 해당 함수 실행
    }

    Dongle MakeDongle()
    {
        // 이펙트 오브젝트 생성
        GameObject instantEffectObj = Instantiate(effectPrefab, effectGroup);
        instantEffectObj.name = "Effect " + effectPool.Count;
        ParticleSystem instantEffect = instantEffectObj.GetComponent<ParticleSystem>();
        effectPool.Add(instantEffect);

        // 동글 오브젝트 생성
        GameObject instantDongleObj = Instantiate(donglePrefab, dongleGroup);
        instantDongleObj.name = "Dongle " + donglePool.Count;
        Dongle instantDongle = instantDongleObj.GetComponent<Dongle>();
        instantDongle.manager = this;
        instantDongle.effect = instantEffect;
        donglePool.Add(instantDongle);

        return instantDongle;
    }

    // 동글 꺼내기
    Dongle GetDongle()
    {
        for (int index = 0; index < donglePool.Count; index++) {
            poolCursor = (poolCursor + 1) % donglePool.Count;

            // activeSelf = bool 형태의 오브젝트 활성화 속성
            if (!donglePool[poolCursor].gameObject.activeSelf) { // 비활성화 상태인 오브젝트 가져오기
                return donglePool[poolCursor];
            }
        }

        return MakeDongle(); // 전부 사용 중이면 새로 하나 만들기
    }

    // 다음 동글 가져오기
    void NextDongle()
    {
        if (isOver) return;
        
        lastDongle = GetDongle();
        lastDongle.level = Random.Range(0, maxLevel); // 최대값 포함 X
        lastDongle.gameObject.SetActive(true);

        SfxPlay(Sfx.Next);
        StartCoroutine("WaitNext");
    }

    // 다음 동글 코루틴
    IEnumerator WaitNext()
    {
        while (lastDongle != null) yield return null; // 동글이 빌 떄까지 계속 대기

        yield return new WaitForSeconds(2f); // 2초 대기했다가 아래 로직 실행

        NextDongle(); // 다음 동글 생성
    }

    // 잡기
    public void TouchDown()
    {
        if (lastDongle == null) return;            

        lastDongle.Drag();
    }

    // 놓기
    public void TouchUp()
    {
        if (lastDongle == null) return;

        lastDongle.Drop();
        lastDongle = null;
    }

    // 게임 오버
    public void GameOver()
    {
        if (isOver) return;

        isOver = true;

        StartCoroutine("GameOverRoutine");
    }

    // 게임 오버 코루틴
    IEnumerator GameOverRoutine()
    {
        Dongle[] dongles = FindObjectsOfType<Dongle>(); // Dongle 스크립트를 컴포넌트로 가지고 있는 모든 오브젝트에 접근해서 배열에 저장

        for (int index = 0; index < dongles.Length; index++) {
            dongles[index].rigid.simulated = false; // 모든 동글의 물리 효과 비활성화
        }

        for (int index = 0; index < dongles.Length; index++) {
            dongles[index].Hide(Vector3.up * 100); // 게임 플레이 중에는 나올 수 없는 큰 값을 전달하여 동글 숨기기(꼼수)
            yield return new WaitForSeconds(0.1f);
        }

        yield return new WaitForSeconds(1f);


        // 최고 점수 갱신
        int maxScore = Mathf.Max(score, PlayerPrefs.GetInt("MaxScore"));
        PlayerPrefs.SetInt("MaxScore", maxScore);

        // 게임 오버 UI 표시
        subScoreText.text = "점수 : " + scoreText.text;
        endGroup.SetActive(true);

        bgmPlayer.Stop();
        SfxPlay(Sfx.Over);
    }

    // 다시 시작
    public void Reset()
    {
        SfxPlay(Sfx.Button);

        StartCoroutine("ResetCorutine");
    }

    // 다시 시작 코루틴
    IEnumerator ResetCorutine()
    {
        yield return new WaitForSeconds(0.5f);

        SceneManager.LoadScene("Main");
    }

    // 효과음 재생
    public void SfxPlay(Sfx type)
    {
        switch (type) {
            case Sfx.LevelUp:
                sfxPlayer[sfxCursor].clip = sfxClip[Random.Range(0, 3)];
                break;
            case Sfx.Next:
                sfxPlayer[sfxCursor].clip = sfxClip[3];
                break;
            case Sfx.Attach:
                sfxPlayer[sfxCursor].clip = sfxClip[4];
                break;
            case Sfx.Button:
                sfxPlayer[sfxCursor].clip = sfxClip[5];
                break;
            case Sfx.Over:
                sfxPlayer[sfxCursor].clip = sfxClip[6];
                break;
        }

        sfxPlayer[sfxCursor].Play();
        sfxCursor = (sfxCursor + 1) % sfxPlayer.Length;
    }

    void Update()
    {
        // 뒤로가기 버튼 누르면 앱 종료(안드로이드 등)
        if (Input.GetButtonDown("Cancel")) {
            Application.Quit();
        }
    }

    // 현재 점수 갱신
    void LateUpdate()
    {
        scoreText.text = score.ToString();
    }
}