미니맵과 캐릭터 선택 창까지는 만들었고 이번주에는 캐릭터에게 장비를 입히는 기능 위주로 개발했다.
Scriptable Object
개념
Scriptable Object는 Unity 내에서 오브젝트 각각의 값을 저장할 수 있는 저장소이다. 스크립트 내부에서 변경되지 않는 Prefab 데이터를 저장하기 유용한데, Prefab 내의 변수들은 Prefab을 인스턴스로 생성하면 매번 Prefab에 대한 사본이 만들어진다. 하지만 Scriptable Object를 사용하면 Scriptable Object의 사본을 만들고 이것을 참조하는 방식으로 작동해서 메모리 관리 측면에서 더 유리하다고.
개념은 베르의 프로그래밍 노트를 많이 참고했다.
https://wergia.tistory.com/189
[Unity3D] Scriptable Object - 스크립터블 오브젝트(Scriptable Object) 기본 사용법
Scriptable Object - 스크립터블 오브젝트(Scriptable Object) 기본 사용법 작성 기준 버전 :: 2019.1 ~ 2019.2 [이 포스트의 내용은 유튜브 영상으로도 시청하실 수 있습니다] 스크립터블 오브젝트(Scriptable Ob..
wergia.tistory.com
사용법
[CreateAssetMenu( fileName = "", menuName = "", order = "")]
새로 Scriptable Object를 생성할 때의 기본 이름을 정의할 수 있고, Scriptable 오브젝트를 Asset 폴더에서 생성할 때 찾을 메뉴 이름과 그 순서를 결정할 수 있다.
아래는 내가 옷 입히기를 위해 필요하다고 생각한 옵션들을 적어둔 것이다. [Header("")]를 사용하면 여러 옵션이 있을 때 주제별로 정리하기 조금 더 편하다.
itemImage는 인벤토리에 아이템을 집어넣고 보여줄 이미지를 넣기 위한 필드이고, 퀘스트 관련 아이템은 맘대로 소비하거나 버릴 수 없도록 하기 위해 퀘스트 아이템인지를 기록할 isQuestItem 필드를 만들었다. wearable 옵션을 통해 캐릭터가 입을 옷이나 장비는 소비할 수 있는 일반 아이템과는 따로 관리하기로 했다. 또한 wearType에 따라 장비가 손에 가서 붙을지, 발에 가서 붙을지 등을 결정하도록 했다.
using UnityEngine;
[CreateAssetMenu (fileName = "New Item", menuName ="ScriptableItem")]
public class ItemScriptable : ScriptableObject
{
//Create Scriptable Item in the Assets folder
//Make name and edit settings
public string itemName;
public Sprite itemImage;
public bool itemOff;
[Header("Quest")]
public bool isQuestItem;
[Header("Wearables")]
public bool wearable;
public int characterChangeNum = 999;
public string wearType;
public GameObject[] itemObjs;
}
Scriptable Object는 MonoBehaviour를 상속하지 않기 때문에 Hierarchy 내부의 오브젝트에 가서 직접 붙을 수 없다. 따라서 각 아이템에 Scriptable 오브젝트를 연결하기 위한 새로운 Script가 필요하다.
오브젝트에 Item.cs 같은 아이템 관련 스크립트를 만들고, 간단히 내용을 작성한다. 코드가 복잡한 것 같지만 제일 중요한 것은 public ItemScriptable item; 같이 내가 정의한 Scriptable Object 클래스의 이름으로 Scriptable Object를 받아올 수 있게 선언해주는 것이다. 이후에는 변수명.속성 등으로 Scriptable Object 내부의 값에 접근할 수 있다. 나의 경우 item.itemName, item.isQuestItem 등이 되겠다. 아이템이 Trigger로 설정되어 Player가 아이템이 닿았을 때를 체크하여 아이템에 대한 여러 처리가 일어나도록 스크립트를 작성했다.
이후 Scriptable Object를 만들고 Inspector 창에서 그 Scriptable Object를 받아 놓는다. 이를 통해 Item.cs에서 Scriptable Object의 여러 정보에 접근해 Item.cs가 붙어 있는 아이템을 관리할 수 있게 된다.
using UnityEngine;
using UnityEngine.UI;
public class Item : MonoBehaviour
{
// Attached to each Item
// Scriptable object 붙이기 위한 script
public ItemScriptable item;
/// 사실상 아랫줄부터는 다 그냥 내가 작성한 부분. ItemScriptable 창구만 만들어주고 Inspector 창에서 Scriptable Object를 잘 넣어주면 됨.
[SerializeField]private Inventory inven; //Inventory
private MeshRenderer meshRen;
private MeshCollider meshCol;
//QuestManager는 Singleton으로 작성했다.
private void Start()
{
inven = FindObjectOfType<Inventory>();
//itemEffect = FindObjectOfType<ItemEffect>();
meshRen = gameObject.GetComponent<MeshRenderer>();
meshCol = gameObject.GetComponent<MeshCollider>();
}
private void OnTriggerEnter(Collider other)
{
if (other.gameObject.tag == "Player" && item.isQuestItem)
{
// Quest items should be used only when the quest is activated
if (NpcMove.questMode)
{
if (QuestManager.Instance.currentQuestData.questObjects.Contains(gameObject))
{
gameObject.SetActive(false);
inven.GetItem(gameObject, item);
QuestManager.questItemCount++;
}
}
return;
}
if (other.gameObject.tag == "Player")
{
inven.GetItem(gameObject, item); // item
Debug.Log(item.itemName + " Got!");
if (item.itemOff)
{
meshRen.enabled = false;
meshCol.enabled = false;
}
}
}
}
캐릭터 옷 입히기
방법
장비를 크게 3종류로 나누어 생각했다.
1. 손에 붙는 장비
2. 발에 붙는 장비 (신발)
3. 옷
3가지 모두 캐릭터의 애니메이션에 따라 함께 움직여야 하므로, 1,2번은 캐릭터의 손과 발을 tag로 찾아 붙이고 3번은 너무 거대한 작업이라서 일단 옷을 입은 상태과 안 입은 상태 2가지로 캐릭터를 만들어 두고 둘 중 하나만 SetActive(true);가 되게끔 처리했다. 또한 처음 아이템을 습득해서 캐릭터의 관절에 아직 아이템이 부착되어 있지 않은 상태와 아이템이 관절에 이미 붙어 있는 상태는 다르기 때문에 Create Item을 해야 하는 상황, Put On, Off를 해야 하는 상황 2가지로 나누어 함수를 작성했다.
위에서 작성한 Scriptable Object의 wearType를 가지고 조건문을 나누어 작성했다.
1. 손에 붙는 장비 CreateHandItem(), PutOnHand(), PutOffHand()
왼손이나 오른손 끝 관절에 Tag를 설정해둔다. ex) LeftHand, RightHand로 설정.
GameObject.FindWithTag("RightHand"); 로 손 끝 관절을 찾아 거기에 장비를 붙여주고 적절히 위치와 회전 값을 조정한다.
2. 발에 붙는 장비
왼발이나 오른발 끝 관절에 Tag를 설정해둔다.
var rightFoot = GameObject.FindWithTag("RightToe");
var leftFoot = GameObject.FindWithTag("LeftToe");
각 발을 찾아놓고 발에 장비를 붙여주고 적절히 위치와 회전 값을 조정한다.
나는 발 처리가 은근 까다로웠는데, 신발 장비를 습득하기 위한 아이템(아이콘)과 실제로 신겨져야 하는 신발이 달랐기 때문이다. 그래서 위의 Scriptable Object를 참고하면 보이는 ItemObjs라는 배열에 새로 생성되어야 하는 오브젝트를 미리 넣어놓고 item.itemObjs[0] 을 Instantiate하겠다~ 이런 식으로 작성했다.
3. 옷
게임 내에서 움직일 캐릭터는 이미 3D 모델링을 통해 관절을 꽂고 애니메이션을 적용해둔 상태였다. 이 상태에서 각 부분에 옷만 입히면 옷은 가만히 있고 팔만 움직이는... 그런 불상사가 일어난다.
제대로 만들려면 옷 아이템도 관절을 꽂아서 플레이어의 애니메이션에 따라 움직이게 해야 하는 것 같은데 그렇게까지 처리할 자신은 없고 옷은 일단 SetActive()로 옵을 입은 형태의 캐릭터를 껐다 켰다 하는 것으로 처리했다.
작성
도움이 될진 모르겠으나, 대충 아래와 같이 작성했으니 참고 바람.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ItemEffect : MonoBehaviour
{
/// <summary>
/// Inventory, ItemEffect scripts are attached to the Inventory Canvas
///
/// </summary>
public GameObject player;
GameObject character;
public GameObject[] collectedWearableItems;
public Dictionary<GameObject, bool> isPutOn;
public int itemIndex;
private void Start()
{
collectedWearableItems = new GameObject[5];
isPutOn = new Dictionary<GameObject, bool>();
}
// Consumable type
public void UseItem(ItemScriptable _itemScriptable)
{
character = player.GetComponent<PlayerSelect>().activeCharacter;
//if (_itemComponent.itemName == "ChoonsikPants")
//{
// character.transform.localScale = new Vector3(3f, 3f, 3f);
//}
if(_itemScriptable.itemName == "SweetPotatoes")
{
character.transform.localScale = new Vector3(2f, 2f, 2f);
return;
}
}
// Wearable Items
public void CreateItem(GameObject _itemObj)
{
var playerSelect = player.GetComponent<PlayerSelect>();
var _itemComponent = _itemObj.GetComponent<Item>();
GameObject wearableItem = _itemObj;
if (_itemObj.transform.parent)
{
// if parent exist, put parent to the hand
wearableItem = _itemObj.transform.parent.gameObject;
}
print("Created Wearableitem is "+wearableItem);
// save wearable item into the list
collectedWearableItems[itemIndex] = wearableItem;
isPutOn[wearableItem] = true;
if (_itemComponent.item.wearType == "hand") // Items attached to hands
{
Debug.Log(_itemComponent.item.itemName + " is attached to the right hand.");
CreateHandItem(wearableItem);
isPutOn[wearableItem] = true;
}
else if (_itemComponent.item.wearType == "shoes") // Items attached to feet
{
Debug.Log(_itemComponent.item.itemName + " is attached to the feet.");
CreateFeetItem(_itemComponent);
// to push shoe object into the array
//collectedWearableItems[itemIndex] = _itemComponent.item.itemObjs[0];
//isPutOn[_itemComponent.item.itemObjs[0]] = true;
}
else if (_itemComponent.item.characterChangeNum < 999) // Character change type
{
Debug.Log("Your character has been changed by " + _itemComponent.item.itemName);
playerSelect.ChangePlayer(_itemComponent.item.characterChangeNum);
isPutOn[wearableItem] = true;
CheckItem();
}
itemIndex++;
}
public void PutOnOff(GameObject _itemObj, bool itemOn)
{
var playerSelect = player.GetComponent<PlayerSelect>();
var _itemComponent = _itemObj.GetComponent<Item>();
GameObject wearableItem = _itemObj;
if (_itemObj.transform.parent)
{
// if parent exist, put parent to the hand
wearableItem = _itemObj.transform.parent.gameObject;
}
foreach (var collectedItem in collectedWearableItems)
{
if(collectedItem.name == _itemObj.transform.parent.name)
{
print(collectedItem.name);
wearableItem = collectedItem;
break;
}
Debug.Log("still in forloop....");
}
if (itemOn)
{
isPutOn[wearableItem] = true;
if (_itemComponent.item.wearType == "hand") // Items attached to hands
{
Debug.Log(_itemComponent.item.itemName + " is attached to the right hand.");
PutOnHand(wearableItem);
}
else if (_itemComponent.item.wearType == "shoes") // Items attached to feet
{
Debug.Log(_itemComponent.item.itemName + " is attached to the feet.");
//PutOnFeet(_itemComponent);
PutOnFeet(wearableItem);
}
else if (_itemComponent.item.characterChangeNum < 999) // Character change type
{
Debug.Log("Your character has been changed by " + _itemComponent.item.itemName);
playerSelect.ChangePlayer(_itemComponent.item.characterChangeNum);
CheckItem();
}
}
else
{
isPutOn[wearableItem] = false;
// put off items
if (_itemComponent.item.wearType == "hand") // Items attached to hands
{
Debug.Log(_itemComponent.item.itemName + " is detached from the right hand.");
PutOffHand();
}
else if (_itemComponent.item.wearType == "shoes") // Items attached to feet
{
Debug.Log(_itemComponent.item.itemName + " is detached from the feet.");
PutOffFeet();
}
else if (_itemComponent.item.characterChangeNum < 999) // Character change type
{
Debug.Log("Your character has been changed by " + _itemComponent.item.itemName);
playerSelect.ChangePlayer(Data.charNum); // go back to original character
CheckItem();
}
}
}
public void CreateHandItem(GameObject wearableItem)
{
var hand = GameObject.FindWithTag("RightHand");
wearableItem.transform.parent = hand.transform;
wearableItem.transform.localPosition = hand.transform.localPosition + new Vector3(-0.2f, 0, 0);//hand.transform.localPosition;
wearableItem.transform.localEulerAngles = hand.transform.localEulerAngles;
}
public void PutOnHand(GameObject wearableItem)
{
var hand = GameObject.FindWithTag("RightHand");
wearableItem.transform.parent = hand.transform;
wearableItem.transform.localPosition = hand.transform.localPosition + new Vector3(-0.2f, 0, 0); // modify position
wearableItem.transform.localEulerAngles = hand.transform.localEulerAngles;
wearableItem.SetActive(true);
//hand.transform.GetChild(0).gameObject.SetActive(true);
}
public void PutOffHand()
{
var hand = GameObject.FindWithTag("RightHand");
hand.transform.GetChild(0).gameObject.SetActive(false);
}
public void CreateFeetItem(Item itemComponent)
{
var rightFoot = GameObject.FindWithTag("RightToe");
var leftFoot = GameObject.FindWithTag("LeftToe");
GameObject shoeRight = Instantiate(itemComponent.item.itemObjs[0], rightFoot.transform.position, Quaternion.Euler(0, 0, 0), rightFoot.transform);
GameObject shoeLeft = Instantiate(itemComponent.item.itemObjs[0], leftFoot.transform.position, Quaternion.Euler(0, 0, 0), leftFoot.transform);
shoeRight.transform.localEulerAngles = new Vector3(270, 180, 0);
shoeLeft.transform.localEulerAngles = new Vector3(270, 180, 0);
shoeRight.transform.localPosition = new Vector3(0, 0, -0.2f);
shoeLeft.transform.localPosition = new Vector3(0, 0, -0.2f);
}
public void PutOnFeet(GameObject wearableItem)
{
var rightFoot = GameObject.FindWithTag("RightToe");
var leftFoot = GameObject.FindWithTag("LeftToe");
try{
//
rightFoot.transform.GetChild(0);
}
catch
{
Debug.Log(wearableItem);
Debug.Log(wearableItem.GetComponentInChildren<Item>());
// nothing found, create shoes
CreateFeetItem(wearableItem.GetComponentInChildren<Item>());
}
wearableItem = rightFoot.transform.GetChild(0).gameObject;
wearableItem.transform.parent = rightFoot.transform;
wearableItem.SetActive(true);
wearableItem = leftFoot.transform.GetChild(0).gameObject;
wearableItem.transform.parent = leftFoot.transform;
wearableItem.SetActive(true);
//rightFoot.transform.GetChild(0).gameObject.SetActive(true);
//leftFoot.transform.GetChild(0).gameObject.SetActive(true);
}
public void PutOffFeet()
{
var rightFoot = GameObject.FindWithTag("RightToe");
var leftFoot = GameObject.FindWithTag("LeftToe");
rightFoot.transform.GetChild(0).gameObject.SetActive(false);
leftFoot.transform.GetChild(0).gameObject.SetActive(false);
}
public void CheckItem()
{
for (int i = 0; i < collectedWearableItems.Length; i++)
{
if (collectedWearableItems[i])
{
var item = collectedWearableItems[i];
Item itemComponent = item.GetComponentInChildren<Item>();
Debug.Log("CheckItem: " + item.name + isPutOn[item]);
if(itemComponent == null)
{
// character라는 뜻
return;
}
if (itemComponent.item.wearType == "hand")
{
PutOnHand(item);
if (!isPutOn[item])
{
PutOffHand();
}
//try
//{
// hand.transform.GetChild(0);
//}
//catch
//{
// CreateHandItem(item); // if item is not under hand, make new item and attach to the hand
//}
}
else if (itemComponent.item.wearType == "shoes")
{
//item = itemComponent.item.itemObjs[0];
PutOnFeet(item);
if (!isPutOn[item])
{
PutOffFeet();
}
}
}
}
}
}
후기
이번주는 중간 학습 상담에, 발표에 일이 많았다. 유데미를 관리하는 웅진씽크빅이 교육 회사이다보니 멘토링 느낌으로 중간 점검 상담을 받았다. 이전에 보았던 알고리즘 테스트와 기획 개발 테스트에서의 내 수준, 그리고 현재 내 학습 태도가 어떠한지 중간 점검을 받을 수 있었다. 앞으로 내가 어떤 부분을 보완하면 좋을지 이야기를 들었고, 현재 수업에 대한 건의 사항에 대해서도 이야기를 나눴다. 앞으로 준비해야 할 내용과 보완해야 할 점에 대해서 한 번 더 생각하는 기회가 되었다.
지난주부터 작업한 내용을 금요일 오후에 각자 발표했다. 나는 정말 옷 입히기, 인벤토리 장비 관리를 완성해보겠다는 일념으로 여기에 매달렸는데, 퀘스트나 게임 요소를 적절하게 배치해 재미가 느껴졌던 다른 사람의 작품을 보면서 내 게임이 조금 아쉽게 느껴지기도 했다. 하지만 인벤토리나 옷 입히기는 다른 강좌 같은 데 없는 거니깐;;;; (있는데 내가 못 찾은 건가...? ㅎ)
재미있게 만들었고, 여러 에러를 해결하면서 여기까지 온 거라 뿌듯뿌듯. 깃허브로 중간에 커밋하면서 작업했으면 훨씬 좋았을 텐데 깃헙에 올리는 건 생각을 못했다;;; 일단 이번 숙제는 한 번에 올린 뒤 자잘하게 수정하고 다음 숙제부터는 github에 버전 관리하면서 작업해야겠다.
유데미코리아 바로가기 : https://bit.ly/3b8JGeD
본 포스팅은 유데미-웅진씽크빅 취업 부트캠프 유니티 1기 과정 후기로 작성되었습니다.
새로운 가능성의 시작, 유데미 x 웅진씽크빅
글로벌 최신 IT 기술과 실무 교육을 입문부터 심화까지! 프로그래밍, 인공지능, 데이터, 마케팅, 디자인 등 세계 최고의 강의를 경험하세요.
www.udemykorea.com
댓글