[Unity/유니티] 기초-뱀서라이크: 레벨업 시스템[12]
개요
유니티 독학을 위해 아래 링크의 골드메탈님의 영상들을 보고 직접 따라 해보면서 진행 상황을 쓰고 배웠던 점을 요약한다.
https://youtube.com/playlist?list=PLO-mt5Iu5TeYI4dbYwWP8JqZMC9iuUIW2
📚유니티 기초 강좌
유니티 게임 개발을 배우고 싶은 분들을 위한 기초 강좌
www.youtube.com
뱀서라이크: 레벨업 시스템[12]
1. 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;
}
- 설명에 데이터가 들어가는 자리는 {index} 형태로 작성
//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 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();
}
}
}
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();
}
}
}
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에 넣을까 했지만, 최근에 올라온 댓글 중에 비슷한 질문이 있어 이에 대한 답으로 해당 질문의 답글을 가져왔다.
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;
}
}
}