1. Mass : 질량 값으로 중력에 영향을 받지 않고 Rigidbody 컴포넌트끼리 충동했을 때 영향을 결정함.
2. Drag : 공기 저항값, 0에 가까울수록 무거워 보이고 크면 가벼워 보임.
3. Angular Drag : 회전을 할 때의 공기 저항 값
4. Is Kinemetic : 물리 엔진이 아닌 게임 오브젝트의 로직에 따라 이동할 것인가를 결정
5. Interpolate : 물리 엔진에서의 애니메이션의 자연스러운 보간 여부
6. Collision Detection : 충돌 처리 조건 여부( 조건, 연속 등 등)
7. Freeze Position, Rotation : 해당 옵션을 킬 경우 물리 엔진에서 처리 되는 값들을 사용하지 않고 고정 시킬 수 있음.
게임을 제작하면서 데이터를 저장은 필수불가결입니다. 특히 RPG의 경우라면 많은 아이템이 필요할 겁니다. 이때 스크립트 가능한 오브젝트를 하나의 방법으로 사용할 수 있습니다. 스크립트 가능한 오브젝트는 대량의 데이터를 저장하기 위해 사용되는 데이터 컨테이너입니다.
일반적으로 게임 내에 플레이하며 사용되는 데이터들은 일반적으로 클래스로 구현하게 됩니다. 예를 들어 대량의 몬스터 스폰(뱀파이어 서바이벌)이나 대량의 아이템의 드롭(RPG 게임의 재료 아이템) 등의 경우 복사된 사본만큼 메모리를 소모하게 됩니다. 하지만 스크립트 가능한 오브젝트를 통해 구현을 하게 된 경우 메모리에 스크립트 오브젝트의 원본 데이터를 저장하고 이 데이터를 참조하여 메모리 소모를 줄일 수 있습니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "New Card", menuName = "Scriptable Object/Card", order = 0)]
public class Card : ScriptableObject
{
public new string name;
public string description;
public Sprite artwork;
public int manaCost;
public int attack;
public int health;
public void Print()
{
Debug.Log(name + " : " + description + " The card Costs : " + manaCost);
}
}
스크립트 가능한 오브젝트를 사용하는 방법은 간단합니다. 위의 예제와 같이 Monobehavior 대신 ScriptableObject 클래스를 상속받고 해당 클래스를 에셋의 형태로 제작할 수 있게 만들어주는 CreateAssetMenu 속성을 추가하고 원하는 데이터를 추가해주면 됩니다.
이때 주의할 점은 스크립트 오브젝트는 하나의 에셋 데이터를 서로 참조하는 형태이기 때문에 현재 체력이나 마나 같은 변할 수 있는 값을 사용해서는 안됩니다. 또한 에디터 상에서는 스크립트 오브젝트의 생성과 데이터 저장이 자유롭지만 배포된 빌드에서는 새로 생성이 불가능하고 기존의 만들어둔 에셋만 사용이 가능합니다. 마지막으로 스크립트 오브젝트는 게임 빌드에 포함되기 때문에 사용자에 의해 악의적으로 데이터 변조가 가능할 수도 있기 때문에 멀티 플레이 게임에서는 사용에 주의가 필요합니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class CardDisplay : MonoBehaviour
{
public Card card;
public Text nameText;
public Text descriptionText;
public Image artworkImage;
public Text manaText;
public Text attackText;
public Text healthText;
private void Start()
{
nameText.text = card.name;
descriptionText.text = card.description;
artworkImage.sprite = card.artwork;
manaText.text = card.manaCost.ToString();
attackText.text = card.attack.ToString();
healthText.text = card.health.ToString();
}
}
https://youtu.be/9xz_kYJZ_zw 디아블로 형식의 인벤토리로 해당 유튜브 강좌를 보고 만들었습니다. 강좌에서는 (0, 0) 기준점인 첫 슬롯에서만 아이템 추가가 가능했는데 여기서 간단하게 모든 슬롯을 검사하여 아이템을 추가할 수 있도록 변경하고 마무리하였습니다.
코드는 전체적으로 어려움 없이 해석할 수 있었습니다. 기존에는 인벤토리에서 아이템을 제작할 때는 에디터 상에서 일일이 UI를 제작해서 사용했는데 해당 강좌에서는 코드로 UI를 다루는 법에 대해서 많이 배웠습니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public abstract class BaseItem
{
public bool isSearched;
public Texture2D image;
/* 아이템이 차지하는 슬롯의 시작 위치 */
public int x;
public int y;
/* 아이템이 차지하는 슬롯의 크기 */
public int width;
public int height;
public abstract void PerformAction();
}
Base Item
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class Item : BaseItem
{
public override void PerformAction()
{
}
}
Item
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class Slot
{
public bool occupied; /* 슬롯 사용중 여부 */
public Rect rectPosition;
public Slot(Rect rectPosition)
{
this.rectPosition = rectPosition;
}
}
Slot
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Inventory : MonoBehaviour
{
private static Inventory inventoryInstance = null;
public static Inventory InventoryInstance()
{
if(inventoryInstance == null)
{
Debug.LogError("오브젝트가 존재하지 않습니다.");
}
return inventoryInstance;
}
/* 인벤토리 이미지 */
public Texture2D image; /* 321 x 434 px */
/* 인벤토리 Rect 값 */
public Rect rectPosition;
public List<Item> items = new List<Item>();
/* 인벤토리 슬롯의 총 크기 10 * 4 */
public int slotWidthSize = 10;
public int slotHeightSize = 4;
public Slot[,] slots;
/* slotX, slotY : 인벤토리 전체 이미지(0, 0)을 기준으로 첫 슬롯(0, 0)의 까지의 거리 */
public int slotX;
public int slotY;
/* 슬롯의 너비와 높이 */
public int width = 29;
public int height = 29;
/* 마우스의 눌리고 떼졌을 때의 Vector2 값 */
private Vector2 mouseDown;
private Vector2 mouseUp;
/* 아이템의 이동을 위한 간이 아이템 */
private Item tempItem;
private void Awake()
{
inventoryInstance = this;
}
private void Start()
{
SetSlots();
}
/* 슬롯 초기화 */
private void SetSlots()
{
slots = new Slot[slotWidthSize, slotHeightSize];;
for(int x = 0; x < slotWidthSize; ++x)
{
for(int y = 0; y < slotHeightSize; ++y)
{
slots[x, y] = new Slot(new Rect((slotX + width * x), (slotY + height * y), width, height));
}
}
}
private void OnGUI()
{
DrawInventory();
DrawItems();
DetectMouseOnGUI();
DrawTempItem();
}
/* 인벤토리 이미지 출력 */
private void DrawInventory()
{
rectPosition.x = Screen.width - rectPosition.width;
rectPosition.y = Screen.height - rectPosition.height - Screen.height * 0.2f;
GUI.DrawTexture(rectPosition, image);
}
/* 전체 아이템 이미지 출력 */
private void DrawItems()
{
for(int i = 0; i < items.Count; ++i)
{
GUI.DrawTexture(
/* slotX, Y : 인벤토리 이미지(0,0)을 기준으로 첫 슬롯(0, 0)까지의 거리,
rectPosition.X, Y: 해상도 Width - rectPosition Width,
items[i].x, y : 그려질 아이템의 시작 위치
width, height : 아이템의 너비와 높이 */
new Rect( slotX + rectPosition.x + items[i].x * width + 4,
slotY + rectPosition.y + items[i].y * height + 4,
items[i].width * width - 8,
items[i].height * height - 8),
items[i].image);
}
}
/* 전체 인벤토리 순회 */
public bool AddItem(Item item)
{
for(int x = 0; x < slotWidthSize; ++x)
{
for(int y = 0; y < slotHeightSize; ++y)
{
if(!OccupiedSlotSearch(x, y, item.width, item.height))
{
AddItem(x, y, item);
return true;
}
}
}
return false;
}
/* 들어온 Rect를 기준으로 점거중인 슬롯이 있는지 확인 */
private bool OccupiedSlotSearch(int x, int y, int widthX, int heightY)
{
for(int oX = x; oX < x + widthX; ++oX)
{
for(int oY = y; oY < y + heightY; ++oY)
{
if(oX >= slotWidthSize || oY >= slotHeightSize)
{
return true;
}
if(slots[oX, oY].occupied)
{
return true;
}
}
}
return false;
}
/* 원하는 위치만 검색 후 아이템을 추가 */
public bool AddItem(int x, int y, Item item)
{
/* 슬롯에서 검색을 시도한 위치부터 아이템의 크기만큼 점거중인 아이템이 있는지 확인 */
for(int sX = x; sX < item.width + x; ++sX)
{
for(int sY = y; sY < item.height + y; ++sY)
{
if(slots[sX, sY].occupied)
{
Debug.Log("이미 다른 아이템이 점거 중인 슬롯입니다.");
return false;
}
}
}
/* 배열 범위 오류 검색 */
if( x + item.width > slotWidthSize)
{
Debug.Log("Out of X bounds");
return false;
}
else if( y + item.height > slotHeightSize)
{
Debug.Log("Out of Y bounds");
return false;
}
/* 추가한 아이템의 시작 위치 설정 */
item.x = x;
item.y = y;
items.Add(item);
/* 슬롯의 점거 상태 변경 */
for(int sX = x; sX < item.width + x; ++sX)
{
for(int sY = y; sY < item.height + y; ++sY)
{
slots[sX,sY].occupied = true;
}
}
Debug.Log("아이템 추가 완료.");
return true;
}
/* 아이템을 제거하는 함수 */
private void RemoveItem(Item item)
{
/* 아이템의 점거 상태를 false로 변경 */
for(int x = item.x; x < item.x + item.width; ++x)
{
for(int y = item.y; y < item.y + item.height; ++y)
{
slots[x, y].occupied = false;
}
}
items.Remove(item);
}
/* 마우스가 인벤토리 UI안에 있는지 확인 */
private void DetectMouseOnGUI()
{
if(Input.mousePosition.x > rectPosition.x && Input.mousePosition.x < rectPosition.x + rectPosition.width)
{
if(Screen.height - Input.mousePosition.y > rectPosition.y && Screen.height - Input.mousePosition.y < rectPosition.y + rectPosition.height)
{
DetectMouseAction();
return;
}
}
}
/* 마우스 클릭 감지 */
private void DetectMouseAction()
{
for(int x = 0; x < slotWidthSize; ++x)
{
for(int y = 0; y < slotHeightSize; ++y)
{
Rect slot = new Rect(rectPosition.x + slots[x,y].rectPosition.x, rectPosition.y + slots[x,y].rectPosition.y, width, height);
/* Point를 받아와 Rect안에 존재하는 지 판단 */
if(slot.Contains(new Vector2(Input.mousePosition.x, Screen.height - Input.mousePosition.y)))
{
if(Event.current.isMouse && Input.GetMouseButtonDown(0))
{
mouseDown.x = x;
mouseDown.y = y;
/* 총 아이템 개수 만큼 반복 */
for(int index = 0; index < items.Count; ++index)
{
/* 아이템의 크기(x, y)만큼 반복, countX, Y : 아이템의 시작 위치 */
for(int countX = items[index].x; countX < items[index].x + items[index].width; ++countX)
{
for(int countY = items[index].y; countY < items[index].y + items[index].height; ++countY)
{
/* 클릭한 위치와 아이템의 위치가 동일한 경우 */
if(countX == x && countY == y)
{
tempItem = items[index];
RemoveItem(tempItem);
return;
}
}
}
}
}
else if(Event.current.isMouse && Input.GetMouseButtonUp(0))
{
mouseUp.x = x;
mouseUp.y = y;
/* 시작 클릭 위치와 종료 클릭 위치가 동일하지 않은 경우 == 아이템 이동을 시동한 경우 */
if(mouseUp.x != mouseDown.x || mouseUp.y != mouseDown.y)
{
if(tempItem != null)
{
/* mouseUp 위치에 아이템을 놓을 수 있으면 위치 변경 */
if(AddItem((int)mouseUp.x, (int)mouseUp.y, tempItem))
{
}
/* 변경 불가능한 경우 원래 위치로 이동 */
else
{
AddItem(tempItem.x, tempItem.y, tempItem);
}
tempItem = null;
}
}
/* 아이템 이동을 시작하지 않은 경우 */
else
{
if(tempItem != null)
{
AddItem(tempItem.x, tempItem.y, tempItem);
tempItem = null;
}
}
}
//Debug.Log(mouseDown + " " + mouseUp);
return;
}
}
}
}
/* 임시 아이템을 그리는 함수 */
private void DrawTempItem()
{
if(tempItem != null)
{
GUI.DrawTexture(new Rect(Input.mousePosition.x, Screen.height - Input.mousePosition.y, tempItem.width * width, tempItem.height * height), tempItem.image);
}
}
}
Inventory System
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ItemDatabase : MonoBehaviour
{
public List<Item> itemInspector;
private static List<Item> itemList;
private void Start()
{
itemList = itemInspector;
}
public static Item GetItem(int id)
{
Item item = new Item();
item.image = itemList[id].image;
item.width = itemList[id].width;
item.height = itemList[id].height;
return item;
}
}
ItemDatabase
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/* 아이템 드롭 */
public class PhysicalItem : MonoBehaviour
{
public static Inventory inventory;
public int itemCode;
private void OnMouseDown()
{
inventory = Inventory.InventoryInstance().GetComponent<Inventory>();
inventory.AddItem(ItemDatabase.GetItem(itemCode));
}
}
해당 함수를 사용하면 자식 오브젝트 뿐만 아니라 본인 오브젝트에 포함된 컴포넌트도 가져옵니다. 따라서 사용시에 주의해서 사용해야 합니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/* Smf : Stat Modifier Feature */
public class Smf : MonoBehaviour
{
public string itemName;
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Consume : MonoBehaviour
{
public Smf[] smfList;
void Start()
{
smfList = gameObject.GetComponentsInChildren<Smf>();
}
}
유니티에서는 변수를 선언할 때 public, private와 같은 다양한 접근 제한을 설정할 수 있습니다. public의 경우 인스펙터 창과, 다른 스크립트에서 접근 가능하다는 특징을 가지고 있습니다. 반대로 private의 경우 양 쪽 모두 접근이 불가능합니다.
그런데 외부 스크립트에서 수정, 참조를 불가능하게 하면서 동시에 인스펙터 창에서는 자주 사용되는 변수가 있을 수 있습니다. 이때 private 변수에 SerializeField를 사용하여 인스펙터 상에서 접근이 가능하도록 만들어 줍니다. 사용법도 간단한데, 원하는 변수 윗 라인에 [SerializeField]를 붙여주면 됩니다.
Scriptable Object는 클래스의 인스턴스와는 별개로 자료를 저장할 수 있는 데이터 컨테이너입니다. Scrpitable Object는 주로 값의 사본이 생성되는 것을 방지하여 메모리 사용을 줄여줍니다. 예를 들어서 MonoBehaviour 스크립트에 변경되지 않는 데이터를 저장하는 프리팹이 있는 프로젝트의 경우 유용하게 사용됩니다. 프리팹을 인스턴스화 할 때마다 프리팹에 이 데이터 자체의 사본이 생성되는데, 이와 같은 방법을 사용하여 중복 데이터를 저장하는 대신에 Scriptable Object를 이용하여 데이터를 저장한 다음 모든 프리팹을 참조하여 접근할 수 있습니다. 즉, 메모리에 데이터 사본을 하나만 저장하게 되는 것입니다.
Scriptable Object는 유니티 오브젝트에서 파생되지만 MonoBehavior와 달리 게임 오브젝트에 연결할 수 없고 프로젝트의 에셋으로 사용하게 됩니다.
에디터 사용 시, Scriptable Object에 데이터를 저장하는 작업은 편집할 때나 런타임에 가능합니다. 이는 Scriptable Object가 에디터 스크립팅을 사용하기 때문입니다. 빌드 단계에서는 Scriptable Object를 사용하여 데이터를 저장할 수 없고 사용만 가능합니다. 에디터 툴에서 에셋 형태로 저장한 Scirptable Object 데이터는 디스크에 저장되서 세션 간에도 유지됩니다.
이제 간단한 예제로 이해를 해보겠습니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "Data", menuName = "ScriptableObject/SpawnManagerScriptableObject", order = 1)]
public class SpawnManagerScriptableObject : ScriptableObject
{
public string prefabName;
public int numberOfPrefabsToCreate;
public Vector3[] spawnPoints;
}
우선 데이터 컨테이너를 만들어주기 위해 MonoBehaviour 대신 Scirptable Object를 상속합니다. 이후 간편한 데이터 편집을 위해서 Create Asset Menu 속성을 사용해 Assets > Create > ScriptableObjects > SpawnManagerScriptableObject 경로로 인스턴스를 생성할 수 있게 만들어줍니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Spawner : MonoBehaviour
{
public GameObject entityToSpawn;
public SpawnManagerScriptableObject spawnManagerValues;
int instanceNumber = 1;
private void Start()
{
SpawnEntities();
}
private void SpawnEntities()
{
int currentSpawnPointIndex = 0;
for (int i = 0; i < spawnManagerValues.numberOfPrefabsToCreate; i++)
{
GameObject currentEntity = Instantiate(
entityToSpawn,
spawnManagerValues.spawnPoints[currentSpawnPointIndex],
Quaternion.identity);
currentEntity.GetComponentInChildren<Text>().text = spawnManagerValues.prefabName;
currentEntity.name = spawnManagerValues.prefabName + instanceNumber;
/* Scriptable Object의 데이터 수정 환경 테스트 */
spawnManagerValues.prefabName = "TEST";
currentSpawnPointIndex = (currentSpawnPointIndex + 1) % spawnManagerValues.spawnPoints.Length;
instanceNumber++;
}
}
}
이제 만들어둔 Scriptable Object 데이터를 사용할 수 있는 스크립트를 만들어 줍니다. SpawnManagerScriptableObject 인스턴스에서 설정해둔 Prefab을 설정한 위치에서 원하는 개수만큼 생성하는 스크립트입니다. 중간에 Scriptable Object의 수정 환경 테스트를 위해서 spawnManagerValues.prefabName = "TEST"; 의 코드를 삽입했습니다.
Scriptable Object 인스턴스 생성
이제 실제로 Scriptable Object 인스턴스를 생성하고 테스트를 진행하겠습니다. 처음은 Scriptable Object의 인스턴스를 생성하고 원하는 대로 데이터를 저장하면 됩니다. (에디터 상의 데이터 저장)
에디터 상의 데이터 수정 및 저장
이후 플레이 버튼을 눌러 테스트를 진행하면 아래와 같은 형태로 오브젝트들이 구성됩니다.
런타임 상의 데이터 수정런타임 상의 데이터 저장데이터 수정 후의 테스트
위에서 코드 중간에 Scriptable Object의 변수인 prefabName을 런타임 도중 수정하도록 코드를 삽입했었습니다. 실제로 테스트를 해보면 런타임 도중 수정이 가능하고 저장 또한 가능하다는 것을 알 수 있습니다.
마지막으로 빌드 단계에서는 데이터의 사용만 가능하고 수정이 불가능한데 실제로 그렇습니다. 테스트를 해보시면 게임 실행 중에는 Cube - Test - Test로 데이터의 일시적인 변경이 가능하나, 다시 게임을 시작하면 Test - Test - Test가 아닌 Cube - Test - Test의 형태로 출력됩니다. 즉, 빌드 단계에서의 데이터의 수정이 불가능합니다.