2022. 1. 26. 21:54ㆍUnity3D/수업 과제
소스코드 GitHub 주소 : https://github.com/hanamc99/UnityGukbi/tree/main/Simple%20RPG
[도전 과제]
1) 웨폰 데이타 스프레드시트에서 가져오기
2) 저장한 json파일을 유니티 상에서 읽어들이기 위해 weapon_data를 받을 매핑클래스를 만든다.
3) 매핑클래스로 만든 딕셔너리에 json데이터를 담는다.
4) 게임의 정보를 담으며, 나중에 저장할 대상인 GameInfo클래스를 만든다.
5) 게임의 모든 데이터를 관리하는 DataManage클래스에 GameInfo객체도 관리하게 한다.
6) 플레이어의 움직임을 따라오는 카메라를 만든다.
7) 닿으면 다음 층으로 넘어가는 포탈을 만든다.
8) 7번에서 만든 포탈로 캐릭터가 가게 하는 코드를 작성한다.
9) 씬이 시작될 때마다 현재 몇 층인지에 따라 몬스터를 생성함.
10) 일반 몹 다음으로는 보스몹을 생성해보자
11) 보스몹을 처치하면 아이템이 나오도록 해보자
모작의 기본 토대는 simpleRPG이다.
[사전 작업]
무기의 데이터를 스프레드시트에서 가져오고,
저장 기능을 만들 것이기 때문에
DataManage 클래스를 만들 것이다.
1) 웨폰 데이타 스프레드시트에서 가져오기
위의 스프레드시트 데이터를 json 형태로 변환해서 Resources폴더에 json 텍스트파일로 저장한다.
2) 저장한 json파일을 유니티 상에서 읽어들이기 위해 weapon_data를 받을 매핑클래스를 만든다.
3) 매핑클래스로 만든 딕셔너리에 json데이터를 담는다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Newtonsoft.Json;
using System.IO;
using System.Linq;
public class DataManage : MonoBehaviour
{
private Dictionary<int, WeaponDataClass> dictWeaponData = new Dictionary<int, WeaponDataClass>();
void Awake()
{
instance = this;
LoadDatas();
}
private void LoadDatas()
{
TextAsset data = Resources.Load<TextAsset>("Weapon_data");
string json = data.text;
dictWeaponData = JsonConvert.DeserializeObject<WeaponDataClass[]>(json).ToDictionary(x => x.id);
}
public WeaponDataClass GetWeaponData(int i)
{
return dictWeaponData[i];
}
}
4) 게임의 정보를 담으며, 나중에 저장할 대상인 GameInfo클래스를 만든다.
GameInfo 클래스는 현재 층 정보를 알리는 floor 변수와 Weapon객체를 가진다.
Weapon객체에 Init을 통해 첫 시작 무기를 쥐여주고 현재 층을 1층으로 설정하자.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameInfo
{
public int floor;
public Weapon weapon;
public void Init()
{
floor = 1;
WeaponDataClass data = DataManage.instance.GetWeaponData(0);
this.weapon = new Weapon(data.id, data.name, data.damage);
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MyClasses
{
}
public class WeaponDataClass
{
public int id;
public string name;
public int damage;
}
public class Weapon
{
public int id;
public string name;
public int damage;
public Weapon(int id, string name, int damage)
{
this.id = id;
this.name = name;
this.damage = damage;
}
}
5) 게임의 모든 데이터를 관리하는 DataManage클래스에 GameInfo객체도 관리하게 한다.
그리고 DiscernUserType()을 통해 사용자의 컴퓨터에 GameInfo.json파일이 없을 경우
신규 유저로 판별하고 DataManage가 갖고 있는 GameInfo 클래스 객체를 초기화한다.
그리고 MakeInstance()메서드를 통해 DataManage객체를 씬이 전환되도 사라지지 않는
static객체로 만든다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Newtonsoft.Json;
using System.IO;
using System.Linq;
public class DataManage : MonoBehaviour
{
public static DataManage instance;
public GameInfo gi;
private const string GAME_INFO_PATH = "Assets/Resources/GameInfo.json";
private Dictionary<int, WeaponDataClass> dictWeaponData = new Dictionary<int, WeaponDataClass>();
void Awake()
{
MakeInstance();
LoadDatas();
DiscernUserType();
}
void MakeInstance()
{
if (instance != null)
{
Destroy(gameObject);
}
else
{
instance = this;
DontDestroyOnLoad(gameObject);
}
}
private void DiscernUserType()
{
if (File.Exists(GAME_INFO_PATH))
{
Debug.Log("기존 유저입니다.");
string getJson = File.ReadAllText(GAME_INFO_PATH);
this.gi = JsonConvert.DeserializeObject<GameInfo>(getJson);
}
else
{
Debug.Log("신규 유저입니다.");
this.gi = new GameInfo();
this.gi.Init();
SaveData();
}
}
private void LoadDatas()
{
TextAsset data = Resources.Load<TextAsset>("Weapon_data");
string json = data.text;
dictWeaponData = JsonConvert.DeserializeObject<WeaponDataClass[]>(json).ToDictionary(x => x.id);
}
public void SaveData()
{
Debug.Log("데이터를 저장했습니다.");
string saveJson = JsonConvert.SerializeObject(this.gi);
File.WriteAllText(GAME_INFO_PATH, saveJson);
}
public WeaponDataClass GetWeaponData(int i)
{
return dictWeaponData[i];
}
}
+
유니티 에디터 상에서 씬에 DataManager라는 이름의 빈 오브젝트를 만들고,
DataManage 스크립트를 부착한다.
[유니티 작업]
6) 일단 플레이어의 움직임을 따라오는 카메라를 만든다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CameraControl : MonoBehaviour
{
[SerializeField] private GameObject player;
Vector3 diff;
void Start()
{
diff = player.transform.position - transform.position;
}
void LateUpdate()
{
transform.position = player.transform.position - diff;
}
}
7) 닿으면 다음 층으로 넘어가는 포탈을 만든다.
포탈에 트리거 콜라이더를 주었고, 태그를 Finish로 바꿔서
캐릭터가 트리거를 감지했을 때 태그가 Finish이면 다음 층으로 넘어가게 할 것이다.
캐릭터 스크립트의 OnTriggerEnter()에서 포탈의 태그를 감지하고 현재 씬을 리로드하는
코드를 넣는다. 여기서는 delPortal이라는 대리자를 사용했다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerControlNK : MonoBehaviour
{
private bool isMoving = false;
private bool isDelay = false;
private float speed = 2f;
[HideInInspector] public int damage = 1;
[HideInInspector] public Animator anim;
private Coroutine routine;
private float attackRange = 1f;
public System.Action delAttack;
public System.Action delPortal;
void Start()
{
anim = GetComponent<Animator>();
}
IEnumerator MoveRoutine(Vector3 pos)
{
isMoving = true;
anim.SetBool("IsMoving", isMoving);
transform.LookAt(pos);
while (true)
{
transform.Translate(Vector3.forward * speed * Time.deltaTime);
if (Vector3.Distance(pos, transform.position) <= attackRange)
{
isMoving = false;
anim.SetBool("IsMoving", isMoving);
break;
}
yield return null;
}
}
public void MoveKnight(Vector3 pos)
{
if (this.routine != null)
{
StopCoroutine(this.routine);
}
this.routine = StartCoroutine(MoveRoutine(pos));
}
private void OnCollisionStay(Collision collision) //Enter
{
if (collision.gameObject.CompareTag("Enemy"))
{
isMoving = false;
anim.SetBool("IsMoving", isMoving);
if (!isDelay)
{
isDelay = true;
delAttack();
StartCoroutine(AttackDelay());
}
}
}
private void OnTriggerEnter(Collider other)
{
switch (other.tag)
{
case "Finish":
delPortal();
break;
}
}
IEnumerator AttackDelay()
{
yield return new WaitForSeconds(2f);
isDelay = false;
}
}
delPortal은 게임이 시작될 때 GameManager의 Start()에서 GameInfo의 floor 변수를 1 증가시키고
몇 초 후 다음 층으로 넘어가게 하는 함수, NextFloor()를 받는다.
NextFloor()는 포탈에 닿을 때마다 실행되게 된다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class GameManager : MonoBehaviour
{
private PlayerControlNK player;
[SerializeField] private Button moveBtn;
private GameObject[] enemies;
private int killCount = 0;
void Start()
{
enemies = GameObject.FindGameObjectsWithTag("Enemy");
player = FindObjectOfType<PlayerControlNK>();
moveBtn.onClick.AddListener(NextEnemy);
player.delAttack = KnightAttack;
player.delPortal = NextFloor;
EnemiesInit();
}
void LetDataSaved()
{
DataManage.instance.SaveData();
}
void EnemiesInit()
{
foreach (GameObject em in enemies)
{
EnemyControlNK emc = em.GetComponent<EnemyControlNK>();
emc.GetKnightDamage(player.damage);
emc.OnDie = NextEnemy;
emc.OnDie += () => this.killCount++;
emc.DisplayStat();
}
}
IEnumerator MoveToEnemy()
{
if (this.killCount != 0)
{
yield return new WaitForSeconds(2.2f);
}
GameObject hero = player.gameObject;
float minDis = 100f;
GameObject target = null;
foreach (GameObject em in enemies)
{
if (em != null)
{
float dis = Vector3.Distance(em.transform.position, hero.transform.position);
if (dis < minDis)
{
target = em;
minDis = dis;
}
}
}
if (target != null)
{
Vector3 pos = new Vector3(target.transform.position.x, 0f, target.transform.position.z);
player.MoveKnight(pos);
}
}
void NextEnemy()
{
StartCoroutine(MoveToEnemy());
}
void KnightAttack()
{
StartCoroutine(AttackRoutine());
}
IEnumerator AttackRoutine()
{
player.anim.Play("Attack01", -1, 0);
yield return new WaitForSeconds(0.42f);
player.anim.Play("Attack02", -1, 0);
yield return new WaitForSeconds(0.835f);
player.anim.Play("Idle_Battle", -1, 0);
}
void NextFloor()
{
StartCoroutine(MoveSceneDelay());
}
IEnumerator MoveSceneDelay()
{
DataManage.instance.gi.floor++;
DataManage.instance.SaveData();
yield return new WaitForSeconds(2f);
SceneManager.LoadScene(0);
}
}
8) 7번에서 만든 포탈로 캐릭터가 가게 하는 코드를 작성한다.
게임매니져는 매 씬이 시작될 때마다 씬에 존재하는 모든 몬스터들을
배열에 담고 EnemiesInit()에서 배열을 순회하며 몬스터들의 수를 세어서
allMonsterNum 변수에 할당한다.
몬스터는 죽을 때마다 게임매니져의 killCount 변수를 1씩 올리고,
killCount가 allMonsterNum보다 크거나 같을 때, 포탈로 이동하는 코드를
MoveToEnemy()메서드 마지막 줄에 넣었다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class GameManager : MonoBehaviour
{
[SerializeField] private GameObject portal;
private PlayerControlNK player;
[SerializeField] private Button moveBtn;
private GameObject[] enemies;
private int killCount = 0;
private int allMonsterNum = 0;
void Start()
{
enemies = GameObject.FindGameObjectsWithTag("Enemy");
player = FindObjectOfType<PlayerControlNK>();
player.delPortal = NextFloor;
moveBtn.onClick.AddListener(NextEnemy);
EnemiesInit();
}
void EnemiesInit()
{
foreach (GameObject em in enemies)
{
allMonsterNum++;
EnemyControlNK emc = em.GetComponent<EnemyControlNK>();
emc.GetKnightDamage(player.damage);
emc.OnDie = NextEnemy;
emc.OnDie += () => this.killCount++;
emc.DisplayStat();
}
}
IEnumerator MoveToEnemy()
{
if (this.killCount != 0)
{
yield return new WaitForSeconds(2.2f);
}
GameObject hero = player.gameObject;
float minDis = 100f;
GameObject target = null;
foreach (GameObject em in enemies)
{
if (em != null)
{
float dis = Vector3.Distance(em.transform.position, hero.transform.position);
if (dis < minDis)
{
target = em;
minDis = dis;
}
}
}
if (target != null)
{
Vector3 pos = new Vector3(target.transform.position.x, 0f, target.transform.position.z);
player.MoveKnight(pos);
}
if(this.killCount >= allMonsterNum)
{
Vector3 pos = new Vector3(portal.transform.position.x, 0f, portal.transform.position.z);
player.MoveKnight(pos);
}
}
void NextEnemy()
{
StartCoroutine(MoveToEnemy());
}
void NextFloor()
{
StartCoroutine(MoveSceneDelay());
}
IEnumerator MoveSceneDelay()
{
DataManage.instance.gi.floor++;
DataManage.instance.SaveData();
yield return new WaitForSeconds(2f);
SceneManager.LoadScene(0);
}
}
포탈이 정상적으로 작동한다.
9) 이제 씬이 시작될 때마다 GameInfo의 floor 변수에서 현재 몇 층인지
정보를 받고, 그 층 수에 따라 몬스터를 생성할 것이다.
일단 지금은 층이 1씩 올라갈 때마다 몬스터들의 hp를 1씩 늘릴 것이다.
그리고 생성되는 일반몹도 1마리씩 늘어난다.
10) 일반 몹 다음으로는 보스몹을 생성해보자
(작성 중 날라가서 사진 대신 글로 대체합니다... 소스코드는 깃허브에 있슴!)
보스몹을 생성하기 위해 게임매니져에 보스몹들을 담을 배열을 만들었다.
그 배열에 보스몹 프리팹을 넣었고,
게임매니져의 SpawnEnemies()메서드 안에
2층, 4층, 6층, 8층에만 생성되도록 하는 코드를 짰다.
11) 보스몹을 처치하면 아이템이 나오도록 해보자
각 보스몹들은 아이템을 가지고 있다가 죽을 때 뱉는다.
뱉는 기능은 따로 스크립트를 짜서 보스몹 모두에게 줬다.
그리고 필드에 드랍된 아이템은 드랍되고 몇 초후에
플레이어를 향해 움직인다. (자석처럼)
그럼 플레이어가 아이템의 태그를 감지하고 태그와 동일한 무기데이타의 id를 찾아서
그 무기를 자신의 착용중인 무기로 바꾼다.
이제 모작이 모두 완성되었다.
UI 구현 설명은 생략했다.
[버그 픽스]
1) 씬에 몬스터를 2마리 이상 넣자, 1번째 몬스터를 처치하고 2번째 몬스터에게 다가가는 과정에서
버그가 생겼다. 당연히 캐릭터 이동관련 코드에 문제있을 줄 알았지만, 고쳐지지 않았고
결국 이것저것 만져보다가 전혀 상관없어보이는 코드를 바꾸자 고쳐졌다.
버그가 왜 생긴지도 모르겠고 어떻게 해결한건지도 몰라서 영 찜찜하지만, 아무튼 고쳐졌다.
[셀프 피드백]
1) 게임 첫 시작 때 DataManager에서 생성할 모든 몬스터를 배열에 손수 옮겨 넣기 때문에,
몬스터의 종류가 다양해지면 골치 아파질 듯.
2) 모든 몬스터의 기본 hp는 public으로 에디터 상에서 손수 할당해주기 때문에,
1번과 같은 이유로 골치 아파질 듯.
[느낀 점]
1) 데이터에 기반해서 몬스터와 아이템을 생성해야 할 필요를 느꼈다.
(그렇지 않으면 거의 수작업이 됨)
2) 병맛 물리엔진 게임이 아닌 이상은 콜라이더로 공격 피해 판정을 하는 것보다
임팩트 타임을 쓰는 게 오브젝트의 모든 상호작용 면에서 정확하다.
[공부가 필요한 것]
1) 애니메이터에서 레이어는 뭔지?
'Unity3D > 수업 과제' 카테고리의 다른 글
22.02.09 유저 인터페이스 완성하기 (0) | 2022.02.09 |
---|---|
22.01.20 MiniTest (0) | 2022.01.20 |
22.01.19 Cat Escape Two (0) | 2022.01.19 |
22.01.18 Cat Escape 피하기 게임 만들기 (0) | 2022.01.18 |
22.01.17 [홈워크] 표창 던지기 (0) | 2022.01.17 |