public class UIController : MonoBehaviour
{
    [SerializeField]
    private GameController  gameController;
    
    [Header("Main UI")]
    [SerializeField]
    private GameObject      mainPanel;
    [SerializeField]
    private TextMeshProUGUI textMainGrade;

    [Header("Game UI")]
    [SerializeField]
    private GameObject      gamePanel;
    [SerializeField]
    private TextMeshProUGUI textScore;

    [Header("Result UI")]
    [SerializeField]
    private GameObject      resultPanel;
    // 결과 화면에서 현재 점수, 등급, 등급에 따른 대사, 최고점수 설정을 위한 변수 선언
    [SerializeField]
    private TextMeshProUGUI textResultScore;
    [SerializeField]
    private TextMeshProUGUI textResultGrade;
    [SerializeField]
    private TextMeshProUGUI textResultTalk;
    [SerializeField]
    private TextMeshProUGUI textResultHighScore;

    [Header("Result UI Animation")]
    [SerializeField]
    private ScaleEffect     effectGameOver;
    [SerializeField]
    private CountingEffect  effectResultScore;
    [SerializeField]
    private FadeEffect  effectResultGrade;

    public void Awake()
    {
        // 처음 씬이 시작되어 Main UI가 true일 때 최고 등급 불러오기
        textMainGrade.text = PlayerPrefs.GetString("HIGHGRADE");
    }

    // "Start" 버튼을 눌러 게임을 시작할 때 GameStart() 메소드에서 Main UI는 비활성화하고, Game UI는 활성화한다.
    public void GameStart()
    {
        mainPanel.SetActive(false);     // Main 화면 비활성화
        gamePanel.SetActive(true);      // Game 화면 활성화
    }

    // 플레이어의 체력이 0이 되었을 때 호출하는 메소드
    public void GameOver()
    {
        int currentScore = (int)gameController.CurrentScore;

        // 현재 등급 출력, 현재 등급에 해당하는 대사 출력
        CalculateGradeAndTalk(currentScore);
        // 최고 점수 출력
        CalculateHighScore(currentScore);

        gamePanel.SetActive(false);     // Game 화면 비활성화
        resultPanel.SetActive(true);    // Result 화면 활성화

        // "Game Over" 텍스트 크기 축소 애니메이션
        effectGameOver.Play(500, 200);
        // 현재 점수를 0부터 카운팅하는 애니메이션
        // 카운팅 애니메이션 종료 후 등급 Fade In 애니메이션 재생
        effectResultScore.Play(0, currentScore, effectResultGrade.FadeIn);
    }

    // "Main" 버튼을 클릭했을 때 호출하는 메소드
    public void GoToMainMenu()
    {
        // 플레이어의 위치, 점수, 체력 등 초기화할 게 많기 때문에 그냥 현재씬을 다시 로드
        SceneManager.LoadScene(SceneManager.GetActiveScene().name);
    }

    // "Notion" 버튼을 클릭했을 때 호출하는 메소드
    public void GoToNotion()
    {
        Application.OpenURL("<https://www.notion.so/DodgeGame-0184af897628482497050e31184d8486/@unitynote>");
    }

    void Update()
    {
        // 현재 점수를 소수점 자리수가 없는 실수(F0)로 출력
        textScore.text = gameController.CurrentScore.ToString("F0");
    }

    private void CalculateGradeAndTalk(int score)
    {
        if ( score < 500 )
        {
            textResultGrade.text = "C+";
            textResultTalk.text = "I got C+";
        }
        else if ( score < 1000 )
        {
            textResultGrade.text = "B0";
            textResultTalk.text = "I got B0";
        }
        else if ( score < 1500 )
        {
            textResultGrade.text = "B+";
            textResultTalk.text = "I got B+";
        }
        else if ( score < 2000 )
        {
            textResultGrade.text = "A0";
            textResultTalk.text = "I got A0";
        }
        else
        {
            textResultGrade.text = "A+";
            textResultTalk.text = "I got A+";
        }
    }

    private void CalculateHighScore(int score)
    {
        int highScore = PlayerPrefs.GetInt("HIGHSCORE");

        // 최고 점수보다 높은 점수를 획득
        if ( score > highScore )
        {
            // 최고 등급 갱신(string)
            PlayerPrefs.SetString("HIGHGRADE", textResultGrade.text);
            // 최고 점수 갱신(int)
            PlayerPrefs.SetInt("HIGHSCORE", score);

            textResultHighScore.text = score.ToString();
        }
        else
        {
            textResultHighScore.text = highScore.ToString();
        }
    }
}
public class GameController : MonoBehaviour
{
    [SerializeField]
    private UIController    uiController;
    [SerializeField]
    private GameObject      pattern01;

    private readonly float scoreScale = 20;  // 점수 증가 계수 (읽기전용)

    // 플레이어 점수 (죽지않고 버틴 시간)
    public  float    CurrentScore   { private set; get; } = 0;

    // bool, 게임 진행 여부
    // false일 때, 점수 증가, 플레이어 제어에 제약을 받음
    public  bool     IsGamePlay     { private set; get; } = false;

    // "Start"버튼을 눌렀을 때 호출하는 메소드
    public void GameStart()
    {
        // uiController에 있는 GameStart() 메소드 호출
        uiController.GameStart();

        // 적 출현 패턴 활성화
        pattern01.SetActive(true);

        // true: 플레이어 제어, 점수 증가 허용
        IsGamePlay = true;
    }

    // "Exit"버튼을 눌렀을 때 호출하는 메소드
    public void GameExit()
    {
        // 에디터 모드일 때, 플레이 중지
        #if UNITY_EDITOR
        UnityEditor.EditorApplication.ExitPlaymode();

        // 실행 파일일 때는 앱을 종료
        #else
        Application.Quit();
        #endif
    }

    // 게임 오버 시 호출되는 메소드
    public void GameOver()
    {
        uiController.GameOver();

        pattern01.SetActive(false);

        IsGamePlay = false;
    }

    void Update()
    {
        // isGamePlay(false): 메인 화면, 결과 화면일 때
        // 점수가 증가하지 않도록 false 상태 return
        if (IsGamePlay == false) return;

        // 버틴 시간을 점수로 환산하기 때문에 Update() 메소드에서
        // CurrentScore 프로퍼티에 Time.deltaTime * scoreScale을 더함
        CurrentScore += Time.deltaTime * scoreScale;
    }
}
public class PlayerController : MonoBehaviour
{
    [SerializeField]
    private KeyCode jumpKey = KeyCode.Space;
    [SerializeField]
    private GameController gameController;

    private MovementRidgidbody2D    movement2D;
    private PlayerHP                playerHP;

    private void Awake()
    {
        movement2D  = GetComponent<MovementRidgidbody2D>();
        playerHP    = GetComponent<PlayerHP>();
    }

    // Update is called once per frame
    void Update()
    {
        // IsGamePlay가 false일 때는 플레이어의 이동, 점프가 불가능
        if (gameController.IsGamePlay == false) return;

        // 메소드 실행
        UpdateMove();
        UpdateJump();
    }

    private void UpdateMove()
    {
        // left, a = -1 / none = 0 / right, d = +1
        float x = Input.GetAxisRaw("Horizontal");

        // 좌우 이동
        movement2D.MoveTo(x);
    }

    private void UpdateJump()
    {
        if (Input.GetKeyDown(jumpKey))
        {
            movement2D.JumpTo();
        }
        else if (Input.GetKey(jumpKey))
        {
            movement2D.IsLongJump = true;
        }
        else if (Input.GetKeyUp(jumpKey))
        {
            movement2D.IsLongJump = false;
        }
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if ( collision.CompareTag("Obstacle"))
        {
            bool isDie = playerHP.TakeDamage();
            if ( isDie == true )
            {
                GetComponent<Collider2D>().enabled = false;     // 충돌처리 비활성화
                gameController.GameOver();                      // GameOver() 메소드 호출 >> 게임오버
            }
        }
    }
}
public class PlayerHP : MonoBehaviour
{
    [SerializeField]
    private GameObject[]    imageHP;
    private int             currentHP;

    [SerializeField]
    private float           invincibilityDuration;  // 무적 지속시간
    private bool            isInvincibility = false;        // 무적 여부

    private SoundController soundController;
    private SpriteRenderer  spriteRenderer;         // 플레이어의 색상 변겅

    private Color           originColor;            // 최초 플레이어의 색상 변수

    private void Awake()
    {
        soundController = GetComponentInChildren<SoundController>();
        spriteRenderer = GetComponent<SpriteRenderer>();

        currentHP       = imageHP.Length;
        originColor     = spriteRenderer.color;
    }

    public bool TakeDamage()
    {
        // 무적 상태일 때는 체력이 감소하지 않는다
        if ( isInvincibility == true ) return false;

        if ( currentHP > 1 )
        {
            soundController.Play(0);
            StartCoroutine(nameof(OnInvincibility));

            // 무슨 의미?
            currentHP--;
            imageHP[currentHP].SetActive(false);
        }
        else
        {
            // 플레이어의 체력이 없어 사망할 땐 true 반환
            return true;
        }

        return false;
    }

    private IEnumerator OnInvincibility()
    {
        isInvincibility = true;

        float current = 0;
        float percent = 0;
        float colorSpeed = 10;

        while ( percent < 1 )
        {
            current += Time.deltaTime;
            percent = current / invincibilityDuration; // invincibilityDuration: 무적 시간

            // 무적 시간동안 플레이어의 색상이 기본 색상(255, 180, 0, 255)에서 (255, 0, 200, 255) 사이로 왔다갔다 하도록 한다.
            spriteRenderer.color = Color.Lerp(originColor, Color.red, Mathf.PingPong(Time.time * colorSpeed, 1));

            yield return null;
        }

        // 무적을 종료할 때 플레이어의 색상을 원래 색상(originColor)로 설정하고,
        // isInvincibility를 false로 설정한다.
        spriteRenderer.color = originColor;
        isInvincibility      = false;

    }
}
public class SoundController : MonoBehaviour
{
    [SerializeField]
    private AudioClip[] sounds;
    private AudioSource audioSource;

    private void Awake()
    {
        audioSource = GetComponent<AudioSource>();
    }

    public void Play(int index=0)
    {
        audioSource.Stop();
        audioSource.clip = sounds[index];
        audioSource.Play();

    }
}
public class ScaleEffect : MonoBehaviour
{
    [SerializeField]
    [Range(0.01f, 10f)]
    private float effectTime;               // 크기 확대/축소 되는 시간

    private TextMeshProUGUI effectText;     // 크기 확대/축소 효과에 사용되는 텍스트

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

    public void Play(float start, float end)
    {
        StartCoroutine(Process(start, end));
    }

    private IEnumerator Process(float start, float end)
    {
        float current = 0;
        float percent = 0;

        while ( percent < 1 )
        {
            current += Time.deltaTime;
            percent = current / effectTime;

            effectText.fontSize = Mathf.Lerp(start, end, percent);

            yield return null;
        }
    }
}
using System.Collections;
using UnityEngine;
using TMPro;
using UnityEngine.Events;

public class CountingEffect : MonoBehaviour
{
    [SerializeField]
    [Range(0.01f, 10f)]
    private float effectTime;

    private TextMeshProUGUI effectText;

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

    public void Play(int start, int end, UnityAction action=null)
    {
        StartCoroutine(Process(start, end, action));
    }

    private IEnumerator Process(int start, int end, UnityAction action)
    {
        float current = 0;
        float percent = 0;

        while ( percent < 1)
        {
            current += Time.deltaTime;
            percent = current / effectTime;

            effectText.text = Mathf.Lerp(start, end, percent).ToString("F0");

            yield return null;
        }

        // action이 null이 아니면 action에 저장되어 있는 메소드 실행
        action?.Invoke();
    }
}
using System.Collections;
using UnityEngine;
using TMPro;

public class FadeEffect : MonoBehaviour
{
    [SerializeField]
    [Range(0.01f, 10f)]
    private float           effectTime;     // fade 되는 시간

    private TextMeshProUGUI effectText;     // fade 효과에 사용되는 텍스트

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

    public void FadeIn()
    {
        StartCoroutine(Fade(0, 1));
    }
    public void FadeOut()
    {
        StartCoroutine(Fade(1, 0));
    }
    private IEnumerator Fade(float start, float end)
    {
        float current = 0;
        float percent = 0;

        while ( percent < 1 )
        {
            current += Time.deltaTime;
            percent = current / effectTime;

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

            yield return null;
        }
    }
}
using UnityEngine;

public class PatternController : MonoBehaviour
{
    [SerializeField]
    private GameController gameController;
    [SerializeField]
    private GameObject[]    patterns;           // 보유하고 있는 모든 패턴
    private GameObject      currentPattern;     // 현재 패턴
    private int[]           patternIndexs;      // 겹치지 않는 patterns.Length 개수의 숫자
    private int             current = 0;        // patternIndexs 배열의 순번
}