유니티 프로젝트/뱀서라이크

[Unity/유니티] 기초-뱀서라이크: 레벨업 시스템[12]

JongHoon 2023. 7. 14. 19:32

개요

유니티 독학을 위해 아래 링크의 골드메탈님의 영상들을 보고 직접 따라 해보면서 진행 상황을 쓰고 배웠던 점을 요약한다.

https://youtube.com/playlist?list=PLO-mt5Iu5TeYI4dbYwWP8JqZMC9iuUIW2 

 

📚유니티 기초 강좌

유니티 게임 개발을 배우고 싶은 분들을 위한 기초 강좌

www.youtube.com


뱀서라이크: 레벨업 시스템[12]

1. UI 완성하기

Canvas에 UI - Image를 생성. 전체 화면을 덮도록 앵커를 변경한 후, 검은색으로 바꾸고 투명도를 150으로 지정. 이름도 LevelUp으로 변경.
기존에 LevelUp으로 했던 오브젝트의 이름을 ItemGroup으로 바꿔주자
LevelUp 아래에 Image를 생성. 이름과 크기(높이), 그리고 색상을 변경한다
Panel 아래에 Text(Legacy) 생성 후 앵커, 크기, 폰트, 폰트 크기, 정렬, 색상 변경.
Shadow 컴포넌트를 추가하고 Effect Distance를 변경한다. 그리고 텍스트와 폰트 크기, Y축 값을 수정.
텍스트 오브젝트의 이름을 변경
ItemGroup 오브젝트를 Panel의 자식 오브젝트로 옮기고, 앵커(전체), 여백, Control Child Size를 변경.
아이템은 3개만 보이면 되므로 아래 두 아이템은 잠시 비활성화 한다.
Item 0의 Icon과 Text Level을 왼쪽으로 앵커를 변경하고 위치를 조정
Text Level을 복사해서 Text Name을 만든다. 폰트 크기, 위치 등 변경.
이번에는 Text Name을 복사해서 Text Desc를 만든다.
Text Level과 Text Desc 글자 색상 수정
이전에 만들었던 Item들을 0을 제외하고 모두 삭제하고, Item 0을 복사해서 이름을 변경
각 아이템에 맞게 Data도 변경한다
이후 다시 Item 3, 4는 비활성화 해보고 테스트 실행
테스트 실행시 정상적으로 UI가 반영된 것을 확인할 수 있다.


2. 아이템 텍스트

//ItemData Script

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

[CreateAssetMenu(fileName = "Item", menuName = "Scriptble Object/ItemData")]

public class ItemData : ScriptableObject {
    public enum ItemType { Melee, Range, Glove, Shoe, Heal }

    [Header("# Main Info")]
    public ItemType itemType;
    public int itemId;
    public string itemName;
    [TextArea]  //Inspector창에서 텍스트를 여러 줄 넣을 수 있음
    public string itemDesc;
    public Sprite itemIcon;

    [Header("# Level Info")]
    public float baseDamage;
    public int baseCount;
    public float[] damages;
    public int[] counts;

    [Header("# Weapon")]
    public GameObject projectile;
    public Sprite hand;
}

Text Area로 Item Desc가 커진 것을 확인할 수 있다.

  • 설명에 데이터가 들어가는 자리는 {index} 형태로 작성

Item 0부터 Item 4까지 각 아이템에 맞게 설명을 추가

//Item Script

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

public class Item : MonoBehaviour {
    public ItemData data;
    public int level;
    public Weapon weapon;
    public Gear gear;

    Image icon;
    Text textLevel;
    Text textName;
    Text textDesc;

    void Awake() {
        icon = GetComponentsInChildren<Image>()[1];
        icon.sprite = data.itemIcon;

        Text[] texts = GetComponentsInChildren<Text>(); //GetComponents의 순서는 계층 구조의 순서를 따라감
        textLevel = texts[0];
        textName = texts[1];
        textDesc = texts[2];
        textName.text = data.itemName;
    }

    void OnEnable() {
        textLevel.text = "Lv." + (level + 1);

        switch (data.itemType) {
            case ItemData.ItemType.Melee:
            case ItemData.ItemType.Range:
                textDesc.text = string.Format(data.itemDesc, data.damages[level] * 100, data.counts[level]);
                break;
            case ItemData.ItemType.Glove:
            case ItemData.ItemType.Shoe:
                textDesc.text = string.Format(data.itemDesc, data.damages[level] * 100);
                break;
            default:
                textDesc.text = string.Format(data.itemDesc);
                break;
        }
    }

    public void OnClick() {
        switch(data.itemType) {
            case ItemData.ItemType.Melee:
            case ItemData.ItemType.Range:
                if (level == 0) {
                    GameObject newWeapon = new GameObject();
                    weapon = newWeapon.AddComponent<Weapon>();
                    weapon.Init(data);
                }
                else {
                    float nextDamage = data.baseDamage;
                    int nextCount = 0;

                    nextDamage += data.baseDamage * data.damages[level];
                    nextCount += data.counts[level];

                    weapon.LevelUp(nextDamage, nextCount);
                }

                level++;
                break;
            case ItemData.ItemType.Glove:
            case ItemData.ItemType.Shoe:
                if (level == 0) {
                    GameObject newGear = new GameObject();
                    gear = newGear.AddComponent<Gear>();
                    gear.Init(data);
                }
                else {
                    float nextRate = data.damages[level];
                    gear.LevelUp(nextRate);
                }

                level++;
                break;
            case ItemData.ItemType.Heal:
                GameManager.instance.health = GameManager.instance.maxHealth;
                break;
        }

        if (level == data.damages.Length) {
            //작성한 레벨 데이터 개수를 넘지 않도록 만들기
            GetComponent<Button>().interactable = false;
        }
    }
}

테스트 실행 결과. 아이템 정보가 잘 반영된 것을 확인할 수 있다.


3. 창 컨트롤

LevelUp 스크립트 생성

//LevelUp Script

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

public class LevelUp : MonoBehaviour {
    RectTransform rect;

    void Awake() {
        rect = GetComponent<RectTransform>();
    }

    public void Show() {
        rect.localScale = Vector3.one;
    }

    public void Hide() {
        rect.localScale = Vector3.zero;
    }
}
//GameManager Script

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

public class GameManager : MonoBehaviour {
	public static GameManager instance;
	[Header("# Game Control")]
	public float gameTime;	//게임 시간 변수
	public float maxGameTime = 2 * 10f; //최대 게임 시간 변수(20초).
	[Header("# Player Info")]
	public int health;
	public int maxHealth = 100;
    public int level;
	public int kill;
	public int exp;
	public int[] nextExp = { 3, 5, 10, 100, 150, 210, 280, 360, 450, 600 };
    [Header("# Game Object")]
    public PoolManager pool;
    public Player player;
	public LevelUp uiLevelUp;

    void Awake() {
		instance = this;
	}

	void Start() {
		health = maxHealth;
	}

	void Update() {
		gameTime += Time.deltaTime;

		if (gameTime > maxGameTime) {
			gameTime = maxGameTime;
		}
	}

	public void GetExp() {
		exp++;

		if (exp == nextExp[level]) {
			level++;
			exp = 0;
			uiLevelUp.Show();
		}
	}
}

LevelUp 스크립트 파일을 LevelUp 오브젝트에 넣기
이후 GameManager의 UI Level Up에 LevelUp 오브젝트 넣기
Item들을 다중 선택한 후, Button 컴포넌트의 On Click에 LevelUp을 추가해 Hide()를 실행시키도록 만들기
LevelUp 오브젝트가 보이지 않도록, 미리 Scale을 0으로 변경


4. 기본 무기 지급

//LevelUp Script

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

public class LevelUp : MonoBehaviour {
    RectTransform rect;
    Item[] items;

    void Awake() {
        rect = GetComponent<RectTransform>();
        items = GetComponentsInChildren<Item>(true);
    }

    public void Show() {
        rect.localScale = Vector3.one;
    }

    public void Hide() {
        rect.localScale = Vector3.zero;
    }

    public void Select(int index) {
        items[index].OnClick();
    }
}
//GameManager Script

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

public class GameManager : MonoBehaviour {
	public static GameManager instance;
	[Header("# Game Control")]
	public float gameTime;	//게임 시간 변수
	public float maxGameTime = 2 * 10f; //최대 게임 시간 변수(20초).
	[Header("# Player Info")]
	public int health;
	public int maxHealth = 100;
    public int level;
	public int kill;
	public int exp;
	public int[] nextExp = { 3, 5, 10, 100, 150, 210, 280, 360, 450, 600 };
    [Header("# Game Object")]
    public PoolManager pool;
    public Player player;
	public LevelUp uiLevelUp;

    void Awake() {
		instance = this;
	}

	void Start() {
		health = maxHealth;

		//임시 스크립트 (첫번째 캐릭터 선택)
		uiLevelUp.Select(0);
	}

	void Update() {
		gameTime += Time.deltaTime;

		if (gameTime > maxGameTime) {
			gameTime = maxGameTime;
		}
	}

	public void GetExp() {
		exp++;

		if (exp == nextExp[level]) {
			level++;
			exp = 0;
			uiLevelUp.Show();
		}
	}
}

테스트 실행을 해보니 시작부터 삽이 있는채로 잘 시작되었다
레벨 업을 해서 아이템 UI가 뜨는 것과 아이템 클릭시 UI가 사라지는 것도 확인할 수 있었다.


5. 시간 컨트롤

//GameManager Script

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

public class GameManager : MonoBehaviour {
	public static GameManager instance;
	[Header("# Game Control")]
	public bool isLive;	//시간 정지 여부 확인 변수
	public float gameTime;	//게임 시간 변수
	public float maxGameTime = 2 * 10f; //최대 게임 시간 변수(20초).
	[Header("# Player Info")]
	public int health;
	public int maxHealth = 100;
    public int level;
	public int kill;
	public int exp;
	public int[] nextExp = { 3, 5, 10, 100, 150, 210, 280, 360, 450, 600 };
    [Header("# Game Object")]
    public PoolManager pool;
    public Player player;
	public LevelUp uiLevelUp;

    void Awake() {
		instance = this;
	}

	void Start() {
		health = maxHealth;

		//임시 스크립트 (첫번째 캐릭터 선택)
		uiLevelUp.Select(0);
	}

	void Update() {
		if (!isLive)
			return;

		gameTime += Time.deltaTime;

		if (gameTime > maxGameTime) {
			gameTime = maxGameTime;
		}
	}

	public void GetExp() {
		exp++;

		if (exp == nextExp[level]) {
			level++;
			exp = 0;
			uiLevelUp.Show();
		}
	}

	public void Stop() {
		isLive = false;
		Time.timeScale = 0;
	}

    public void Resume() {
        isLive = true;
        Time.timeScale = 1;	//값이 1보다 크면 그만큼 시간이 빠르게 흐름. 모바일 게임에서 시간 가속하는 것이 이것..
    }
}
//LevelUp Script

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

public class LevelUp : MonoBehaviour {
    RectTransform rect;
    Item[] items;

    void Awake() {
        rect = GetComponent<RectTransform>();
        items = GetComponentsInChildren<Item>(true);
    }

    public void Show() {
        rect.localScale = Vector3.one;
        GameManager.instance.Stop();
    }

    public void Hide() {
        rect.localScale = Vector3.zero;
        GameManager.instance.Resume();
    }

    public void Select(int index) {
        items[index].OnClick();
    }
}
//Player Script

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

public class Player : MonoBehaviour {
    public Vector2 inputVec;   //키보드 입력 값 변수
	public float speed; //속도 관리 변수
    public Scanner scanner;
    public Hand[] hands;

    Rigidbody2D rigid;
    SpriteRenderer spriter;
    Animator anim;

    void Awake() {
        rigid = GetComponent<Rigidbody2D>();
		spriter = GetComponent<SpriteRenderer>();
        anim = GetComponent<Animator>();
        scanner = GetComponent<Scanner>();
        hands = GetComponentsInChildren<Hand>(true);    //인자의 true를 통해서 비활성화된 오브젝트도 가능
	}

    void FixedUpdate() {
        if (!GameManager.instance.isLive)
            return;

		//위치 이동
		Vector2 nextVec = inputVec * speed * Time.fixedDeltaTime;
		rigid.MovePosition(rigid.position + nextVec);
    }

    void OnMove(InputValue value) {
        inputVec = value.Get<Vector2>();
    }

    void LateUpdate() {
        if (!GameManager.instance.isLive)
            return;

        anim.SetFloat("Speed", inputVec.magnitude); //Magnitude : 벡터의 순수한 크기 값
        
        if (inputVec.x != 0) {
            spriter.flipX = inputVec.x < 0;
        }
    }
}
//Enemy Script

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

public class Enemy : MonoBehaviour {
    public RuntimeAnimatorController[] animCon;
    public float health;
    public float maxHealth;
    public float speed;
    public Rigidbody2D target;  //쫓아갈 타겟(플레이어)

    bool isLive;

    Rigidbody2D rigid;
    Collider2D coll;
    Animator anim;
    SpriteRenderer spriter;
    WaitForFixedUpdate wait;

    void Awake() {
        rigid = GetComponent<Rigidbody2D>();
        coll = GetComponent<Collider2D>();
        anim = GetComponent<Animator>();
        spriter = GetComponent<SpriteRenderer>();
        wait = new WaitForFixedUpdate();
    }

    void FixedUpdate() {
        if (!GameManager.instance.isLive)
            return;

        if (!isLive || anim.GetCurrentAnimatorStateInfo(0).IsName("Hit"))   //GetCurrentAnimationStateInfo : 현재 상태 정보를 가져오는 함수
            return;

        Vector2 dirVec = target.position - rigid.position;  // 방향 = 위치 차이의 정규화(Normalized). 위치 차이 = 타겟 위치 - 나의 위치.
        Vector2 nextVec = dirVec.normalized * speed * Time.fixedDeltaTime;
        rigid.MovePosition(rigid.position + nextVec);   //플레이어의 키 입력값을 더한 이동 = 몬스터의 방향 값을 더한 이동
        rigid.velocity = Vector2.zero; //물리 속도가 이동에 영향을 주지 않도록 속도 제거
    }

    void LateUpdate() {
        if (!GameManager.instance.isLive)
            return;

        if (!isLive)
            return;
        spriter.flipX = target.position.x < rigid.position.x;
    }

    void OnEnable() {
        target = GameManager.instance.player.GetComponent<Rigidbody2D>();   //플레이어 할당
        isLive = true;
        coll.enabled = true;
        rigid.simulated = true;
        spriter.sortingOrder = 2;
        anim.SetBool("Dead", false);
        health = maxHealth;
    }

    //데이터를 가져오기 위한 초기화 함수
    public void Init(SpawnData data) {
        anim.runtimeAnimatorController = animCon[data.spriteType];
        speed = data.speed;
        maxHealth = data.health;
        health = data.health;
    }

    void OnTriggerEnter2D(Collider2D collision) {
        if (!collision.CompareTag("Bullet") || !isLive)
            return;

        health -= collision.GetComponent<Bullet>().damage;  //총알 데미지만큼 Enemy 체력 감소
        StartCoroutine(KnockBack());

        if(health > 0) {
            anim.SetTrigger("Hit");

        }
        else {
            isLive = false;
            coll.enabled = false;
            rigid.simulated = false;    //rigidbody의 물리적 비활성화는 simulated를 false로 설정.
            spriter.sortingOrder = 1;   //SpriteRenderer의 Sorting Order를 감소시켜 다른 몬스터를 가리지 않게 함.
            anim.SetBool("Dead", true);
            GameManager.instance.kill++;
            GameManager.instance.GetExp();
        }
    }

    IEnumerator KnockBack() {
        yield return wait;  //다음 하나의 물리 프레임 딜레이
        Vector3 playerPos = GameManager.instance.player.transform.position;
        Vector3 dirVec = transform.position - playerPos;
        rigid.AddForce(dirVec.normalized * 3, ForceMode2D.Impulse);
    }

    void Dead() {
        gameObject.SetActive(false);
    }
}
// Spawner Script

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

public class Spawner : MonoBehaviour {
    public Transform[] spawnPoint;
    public SpawnData[] spawnData;

    int level;  //소환 레벨
    float timer;

    void Awake() {
        spawnPoint = GetComponentsInChildren<Transform>();
    }

    void Update() {
        if (!GameManager.instance.isLive)
            return;

        timer += Time.deltaTime;
        //FloorToInt : 소수점 아래는 버리고 Int형으로 바꾸는 함수. CeilToInt : 소수점 아래를 올리고 Int형으로 바꾸는 함수.
        level = Mathf.Min(Mathf.FloorToInt(GameManager.instance.gameTime / 10f), spawnData.Length - 1); //오류 수정

        if (timer > spawnData[level].spawnTime) {
            timer = 0;
            Spawn();
        }
    }

    void Spawn() {
        GameObject enemy = GameManager.instance.pool.Get(0);    //프리펩이 하나가 되었으므로 0으로 변경
        enemy.transform.position = spawnPoint[Random.Range(1, spawnPoint.Length)].position; // Random Range가 1부터 시작하는 이유는 spawnPoint 초기화 함수 GetComponentsInChildren에 자기 자신(Spawner)도 포함되기 때문에.
        enemy.GetComponent<Enemy>().Init(spawnData[level]);
    }
}

//직렬화(Serialization) : 개체를 저장/전송하기 위해 변환
[System.Serializable]
public class SpawnData {
    public float spawnTime; //몬스터 소환 시간
    public int spriteType;  //몬스터 스프라이트 타입
    public int health;      //몬스터 체력
    public float speed;     //몬스터 스피드
}
//Weapon Script

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

public class Weapon : MonoBehaviour {
    public int id;
    public int prefabId;
    public float damage;
    public int count;
    public float speed;

    float timer;
    Player player;

    void Awake() {
        player = GameManager.instance.player;
    }

    void Update() {
        if (!GameManager.instance.isLive)
            return;

        switch (id) {
            case 0:
                transform.Rotate(Vector3.back * speed * Time.deltaTime);
                break;
            default:
                timer += Time.deltaTime;

                if (timer > speed) {
                    timer = 0f;
                    Fire();
                }
                break;
        }

        // .. Test Code ..
        if (Input.GetButtonDown("Jump")) {
            LevelUp(10, 1);
        }
    }
    
    public void LevelUp(float damage, int count) {
        this.damage = damage;
        this.count += count;

        if (id == 0)
            Batch();

        player.BroadcastMessage("ApplyGear", SendMessageOptions.DontRequireReceiver);
    }

    public void Init(ItemData data) {
        // Basic Set
        name = "Weapon " + data.itemId;
        transform.parent = player.transform;
        transform.localPosition = Vector3.zero;

        // Property Set
        id = data.itemId;
        damage = data.baseDamage;
        count = data.baseCount;

        for (int index = 0; index < GameManager.instance.pool.prefabs.Length; index++) {
            if (data.projectile == GameManager.instance.pool.prefabs[index]) {  //프리펩이 같은건지 확인
                prefabId = index;
                break;
            }
        }

        switch (id) {
            case 0:
                speed = 150;
                Batch();
                break;
            default:
                speed = 0.4f;
                break;
        }

        // Hand Set
        Hand hand = player.hands[(int)data.itemType];
        hand.spriter.sprite = data.hand;
        hand.gameObject.SetActive(true);

        //플레이어가 가지고 있는 모든 Gear에 대해 ApplyGear를 실행하게 함. 나중에 추가된 무기에도 영향을 주기 위함.
        player.BroadcastMessage("ApplyGear", SendMessageOptions.DontRequireReceiver);
    }

    void Batch() {  //생성된 무기를 배치하는 함수
        for (int index = 0; index < count; index++) {
            Transform bullet;

            if (index < transform.childCount) {
                bullet = transform.GetChild(index); //기존 오브젝트가 있으면 먼저 활용
            }
            else {
                bullet = GameManager.instance.pool.Get(prefabId).transform; //모자라면 풀링에서 가져옴
                bullet.parent = transform;
            }

            bullet.localPosition = Vector3.zero;        //무기 위치 초기화
            bullet.localRotation = Quaternion.identity; //무기 회전값 초기화

            Vector3 rotVec = Vector3.forward * 360 * index / count; //개수에 따라 360도 나누기
            bullet.Rotate(rotVec);
            bullet.Translate(bullet.up * 1.5f, Space.World); //무기 위쪽으로 이동
            bullet.GetComponent<Bullet>().Init(damage, -1, Vector3.zero); //-1 is Infinity Per. 무한 관통.
        }
    }

    void Fire() {
        if (!player.scanner.nearestTarget)
            return;

        Vector3 targetPos = player.scanner.nearestTarget.position;
        Vector3 dir = targetPos - transform.position;
        dir = dir.normalized;

        Transform bullet = GameManager.instance.pool.Get(prefabId).transform;
        bullet.position = transform.position;
        bullet.rotation = Quaternion.FromToRotation(Vector3.up, dir);   //FromToRotation(지정된 축을 중심으로 목표를 향해 회전하는 함수
        bullet.GetComponent<Bullet>().Init(damage, count, dir);
    }
}
  • 나는 Player에서 Update가 아닌, OnMove라는 함수를 통해 이동을 하도록 구현했으므로, 위에 Update마다 추가했던 if문을 추가하지 않았다.
  • 이때문에 OnMove에 넣을까 했지만, 최근에 올라온 댓글 중에 비슷한 질문이 있어 이에 대한 답으로 해당 질문의 답글을 가져왔다.

골드메탈님 유튜브(레벨업 시스템 유니티 뱀서라이크 12) 질문의 답글
GameManager에서 isLive를 체크
테스트 실행. 업그레이드 화면이 뜨자 isLive가 체크 해제되면서 시간이 멈췄다.


6. 랜덤 아이템

//LevelUp Script

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

public class LevelUp : MonoBehaviour {
    RectTransform rect;
    Item[] items;

    void Awake() {
        rect = GetComponent<RectTransform>();
        items = GetComponentsInChildren<Item>(true);
    }

    public void Show() {
        Next();
        rect.localScale = Vector3.one;
        GameManager.instance.Stop();
    }

    public void Hide() {
        rect.localScale = Vector3.zero;
        GameManager.instance.Resume();
    }

    public void Select(int index) {
        items[index].OnClick();
    }

    void Next() {
        // 1. 모든 아이템 비활성화
        foreach(Item item in items) {
            item.gameObject.SetActive(false);
        }

        // 2. 그 중에서 랜덤 3개 아이템 활성화
        int[] ran = new int[3];
        while (true) {
            ran[0] = Random.Range(0, items.Length);
            ran[1] = Random.Range(0, items.Length);
            ran[2] = Random.Range(0, items.Length);

            if (ran[0] != ran[1] && ran[1] != ran[2] && ran[0] != ran[2])
                break;
        }

        for (int index = 0; index < ran.Length; index++) {
            Item ranItem = items[ran[index]];

            // 3. 만렙 아이템의 경우, 소비 아이템으로 대체
            if (ranItem.level == ranItem.data.damages.Length) {
                items[4].gameObject.SetActive(true);
            }
            else {
                ranItem.gameObject.SetActive(true);
            }
        }

    }
}
//GameManager Script

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

public class GameManager : MonoBehaviour {
	public static GameManager instance;
	[Header("# Game Control")]
	public bool isLive;	//시간 정지 여부 확인 변수
	public float gameTime;	//게임 시간 변수
	public float maxGameTime = 2 * 10f; //최대 게임 시간 변수(20초).
	[Header("# Player Info")]
	public int health;
	public int maxHealth = 100;
    public int level;
	public int kill;
	public int exp;
	public int[] nextExp = { 3, 5, 10, 100, 150, 210, 280, 360, 450, 600 };
    [Header("# Game Object")]
    public PoolManager pool;
    public Player player;
	public LevelUp uiLevelUp;

    void Awake() {
		instance = this;
	}

	void Start() {
		health = maxHealth;

		//임시 스크립트 (첫번째 캐릭터 선택)
		uiLevelUp.Select(0);
	}

	void Update() {
		if (!isLive)
			return;

		gameTime += Time.deltaTime;

		if (gameTime > maxGameTime) {
			gameTime = maxGameTime;
		}
	}

	public void GetExp() {
		exp++;

		if (exp == nextExp[Mathf.Min(level, nextExp.Length - 1)]) {
			level++;
			exp = 0;
			uiLevelUp.Show();
		}
	}

	public void Stop() {
		isLive = false;
		Time.timeScale = 0;
	}

    public void Resume() {
        isLive = true;
        Time.timeScale = 1;	//값이 1보다 크면 그만큼 시간이 빠르게 흐름. 모바일 게임에서 시간 가속하는 것이 이것..
    }
}
//HUD Script

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;   //Text

public class HUD : MonoBehaviour {
    public enum InfoType {  Exp, Level, Kill, Time, Health }    //열거형
    public InfoType type;

    Text myText;
    Slider mySlider;

    void Awake() {
        myText = GetComponent<Text>();
        mySlider = GetComponent<Slider>();
    }

    void LateUpdate() {
        switch (type) {
            case InfoType.Exp:  //슬라이더로 표현할 경험치 = 현재 경험치 / 최대 경험치
                float curExp = GameManager.instance.exp;
                float maxExp = GameManager.instance.nextExp[Mathf.Min(GameManager.instance.level, GameManager.instance.nextExp.Length - 1)];
                mySlider.value = curExp / maxExp;
                break;
            case InfoType.Level:
                myText.text = string.Format("Lv.{0:F0}", GameManager.instance.level); //F0, F1, F2 ... : 소수점 자리를 지정
                break;
            case InfoType.Kill:
                myText.text = string.Format("{0:F0}", GameManager.instance.kill);
                break;
            case InfoType.Time:
                float remainTime = GameManager.instance.maxGameTime - GameManager.instance.gameTime;
                int min = Mathf.FloorToInt(remainTime / 60);
                int sec = Mathf.FloorToInt(remainTime % 60);
                myText.text = string.Format("{0:D2}:{1:D2}", min, sec); //D0, D1, D2 ... : 자리수를 지정
                break;
            case InfoType.Health:
                float curHealth = GameManager.instance.health;
                float maxHealth = GameManager.instance.maxHealth;
                mySlider.value = curHealth / maxHealth;
                break;
        }
    }
}

최종 테스트 실행 전 원할한 레벨업을 위해 경험치량 수정
최종 테스트 실행. 레벨업시 업그레이드 선택과 업그레이드 상황에 따른 선택지 생략도 잘 적용된다.