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();
}
}
String 데이터 타입은 immutable Type인데 코드 상에서는 대입 연산자가 오버 로딩되어 있어 마치 mutable Type처럼 사용이 가능합니다. 하지만 내부적으론 새로운 공간에 새로운 값을 할당하여 오버헤드가 발생을 하게 됩니다.
이때 버퍼를 가지고 있는 StringBuilder 클래스를 사용하면 mutable Type으로 사용이 가능합니다. 따라서 String과 StringBuilder의 대입 연산을 반복적으로 돌려서 서로의 오버헤드를 알아보고자 합니다.
using System.Text;
using System.Diagnostics;
private void OverheadTest()
{
string test1 = "";
Stopwatch time1 = new Stopwatch();
time1.Start();
for (int i = 0; i < 100000; i++)
{
test1 += i.ToString();
}
time1.Stop();
UnityEngine.Debug.Log("TEST 1 Time : " + time1.ElapsedMilliseconds);
StringBuilder test2 = new StringBuilder();
Stopwatch time2 = new Stopwatch();
time2.Start();
for (int i = 0; i < 100000; i++)
{
test2.Append(i.ToString());
}
time2.Stop();
UnityEngine.Debug.Log("TEST 2 Time : " + time2.ElapsedMilliseconds);
}
실행 결과 --------
TEST 1 Time : 59987
TEST 2 Time : 74
각 각 10만번의 반복문을 돌린 결과 생각보다 큰 차이가 있었습니다. 따라서 문자열의 할당과 관련된 작업을 지속적이고 대량으로 할 경우 StringBuilder를 사용하면 될 것 같습니다.
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));
}
}