강의에서는 작은 점을 만들어서 화면에 Crosshair를 표시했지만
난 무료에셋을 사용해서 에임을 꾸며주고 싶었다.
하지만 그냥 바로 사용하려니 컴포넌트 값으로 드로그 앤 드랍이 안 됐다.

사용하고자 하는 이미지 인스펙터 창에서 텍스처 타입을 Sprite (2D and UI)로 변경해줬다.

사용할 수 있게 되었다. 😊

마음에 드는 이미지와 색상, 크기까지 적용!
이 에임과 아이템의 위치가 맞을 때 아이템 정보 출력, 획득하는 기능을 만들어볼 것이다.
이제 카메라의 정중앙(Crosshair)에서 Ray를 쏘게 하고 싶은데,
그 전에 Ray에 맞을 아이템을 만들어야 한다.

빈 오브젝트 생성 후 Log 프리팹 넣어주고 프리팹 언팩하기

안에 있는 캡슐 콜라이더 삭제

부모 오브젝트에 박스 콜라이더 추가 후 사이즈 조절하기

ItemObject 스크립트 생성 후 붙여준 다음 복붙!

아이템 도끼, 당근, 돌, 검 추가 생성

만든 아이템들 프리팹으로 만들어주기

아이템들의 데이터 세팅해줄 곳 생성
ItemData.cs
public enum ItemType
{
Equipable, // 장착 가능
Consumable, // 소비, 섭취 가능
Resource // 단순 자원
}
public enum ConsumableType // 어떤 종류의 소비, 섭취
{
Health,
Hunger
}
[Serializable]
public class ItemDataConsumable
{
public ConsumableType type; // ConsumableType 저장
public float value; // 아이템 사용 시 회복되는 값
}
// 에디터에서 쉽게 사용할 수 있도록 메뉴를 만듦
// ScriptableObject를 만들 때 빠르게 만들 수 있도록 메뉴창에 추가해 줌
[CreateAssetMenu(fileName = "Item", menuName = "New Item")]
public class ItemData : ScriptableObject
{
[Header("Info")] // 아이템에 들어갈 정보들
public string displayName; // 이름
public string description; // 설명
public ItemType type; // 활용 방법에 따른 타입, 체력 회복/공격용/채집용 등
public Sprite icon; // 아이콘 정보
public GameObject dropPrefab; // 프리팹 정보
[Header("Stacking")] // 단일 소유, 중복 소유 여부
public bool canStack; // 여부 체크
public int maxStackAmount; // 최대 보유값
[Header("Consumable")]
public ItemDataConsumable[] consumables;
}

Create - New Item

New Item으로 아이템 5개 세팅하기
ItemObject.cs
public class ItemObject : MonoBehaviour
{
public ItemData data;
}

아이템에 맞는 데이터 넣어주기

프리팹에도 적용될 수 있게 Apply All 하기
여기까지 했다면 아이템 상호작용에 필요한 사전 세팅 완료!
Player.cs
public class Player : MonoBehaviour
{
public PlayerController controller;
public PlayerCondition condition; // 새로운 기능(UI)이 추가됐으니 작성해주기
public ItemData itemData; // 현재 Interaction된 아이템 데이터를 넣어줄 변수
public Action addItem;
private void Awake()
{
CharacterManager.Instance.Player = this;
controller = GetComponent<PlayerController>();
condition = GetComponent<PlayerCondition>();
}
}
ItemObject.cs
// 어떤 아이템인지 하나하나 조건문으로 검사할 수 없으니까 인터페이스 만들기
public interface IInteractable
{
// 미리 정의해주기
public string GetInteractPrompt(); // 화면에 띄워줄 Prompt
public void OnInteract(); // Interact 됐을 때 어떤 효과를 발생시키게 할 것인지
}
public class ItemObject : MonoBehaviour, IInteractable
{
public ItemData data; // 아이템 데이터
/*
어떤 아이템이든 간에 일단 IInteractable이라고 하는 Component를 찾게 되면
없다면(Interaction이 가능하지 않은 아이템) 그냥 넘어가면 되고,
IInteractable이 GetComponent 됐다면 그 안에 있는 기능(아래에 있는 함수)들을 쓸 수 있는 것
*/
// 상호작용할 때 필요한 기능들
public string GetInteractPrompt()
{
// Prompt에 띄워줄 정보 보내기
string str = $"{data.displayName}\n{data.description}"; // 이름\n설명
return str;
}
// 상호작용 키를 누르면 함수 호출해주기
public void OnInteract()
{
// 정보를 Player에 직접적으로 넘겨줄 수 없기 때문에 CharacterManager를 통해서 Player의 변수 만들기
CharacterManager.Instance.Player.itemData = data; // Player itemData에 내가 가지고 있는 data 넣어주기
CharacterManager.Instance.Player.addItem?.Invoke();
Destroy(gameObject); // 상호작용 키를 누르면 맵에서 사라지게 하기
}
}
Interaction.cs
public class Interaction : MonoBehaviour
{
public float checkRate = 0.05f; // 얼마나 자주 Update 해서 검출할지(얼마나 자주 최신화할 것인지), 검출 주기
private float lastCheckTime; // 마지막으로 체크한 시간
public float maxCheckDistance; // 얼마나 멀리 있는 것을 체크할 것인지
public LayerMask layerMask; // 어떤 레이어가 달린 게임 오브젝트를 추출할 것인지
// 캐싱하는 자료가 담겨있는 변수들
public GameObject curInteractGameObject; // Interaction에 성공했다면 현재 Interaction된 게임 오브젝트의 정보
private IInteractable curInteractable; // 인터페이스 캐싱
// Prompt에 띄워주기
public TextMeshProUGUI promptText; // promptText 직접적으로 가지고 있기
private Camera camera; // 카메라 정보
void Start()
{
camera = Camera.main; // 메인 카메라
}
void Update()
{
// Update에 있으면 매 프레임마다 Ray를 쏘게 되므로
// if문으로 얼마나 자주 호출하게 할 것인지 정해주기
if (Time.time - lastCheckTime > checkRate)
{
lastCheckTime = Time.time; // 현재 시간 넣어주기
// 계속 Interactable이 가능할 수 있게 Ray 쏘기
// 카메라가 찍고 있는 그 방향 자체가 기본적으로 있기 때문에 중심점(Ray가 나가는 시작점)만 잡아주면 됨
Ray ray = camera.ScreenPointToRay(new Vector3(Screen.width / 2, Screen.height / 2)); // 화면의 정중앙
RaycastHit hit; // 부딪힌 오브젝트 정보를 담아놓을 RaycastHit
// ray 정보 담기, 충돌이 된 물체가 있다면 hit 변수에 정보 넘겨주기, 길이, layerMask
if (Physics.Raycast(ray, out hit, maxCheckDistance, layerMask))
{
// 충돌이 됐을 때
// 기존에 상호작용된 게임 오브젝트와 다를 때
if (hit.collider.gameObject != curInteractGameObject)
{
// 새로운 정보 넣어주기
curInteractGameObject = hit.collider.gameObject;
curInteractable = hit.collider.GetComponent<IInteractable>();
// prompt 출력
SetPromptText();
}
}
else
{
// 충돌이 안 됐을 때(빈 공간에 Ray 쏘기)
// 정보 없애주기
curInteractGameObject = null;
curInteractable = null;
promptText.gameObject.SetActive(false);
}
}
}
// prompt 세팅
private void SetPromptText()
{
promptText.gameObject.SetActive(true);
// 정보 넣어주기
promptText.text = curInteractable.GetInteractPrompt(); // 아이템 이름, 설명 문자열 text에 넣기
}
// 상호작용 키를 누르면
// Player Action에 이벤트 달아주기
public void OnInteractInput(InputAction.CallbackContext context)
{
// 키가 눌렸을 때 && curInteractable에 정보가 있을 때
if(context.phase == InputActionPhase.Started && curInteractable != null)
{
curInteractable.OnInteract();
curInteractGameObject = null;
curInteractable = null;
promptText.gameObject.SetActive(false);
}
}
}

ScreenToViewportPoint
터치, 눌렀을 때 기준
ScreenPointToRay
카메라 기준

아이템 프리팹들 레이어 Interactable로 변경

Player에 Interaction.cs 붙여주기
TextMeshPro 생성
한글 폰트 안 깨지게 폰트 에셋 파일로 만들어주기

Character Sequence
32-126: 알파벳
44032-55203: 한글 조합 글자
12593-12643: 한글 모음, 자음
8200-9900: 특수문자

만든 폰트 적용하기

아이템 이름, 설명 한글로 잘 뜨고
여기서 E 키를 눌러서 아이템을 먹으면!

Player의 Item Data에 정보가 들어가있는 것을 확인할 수 있다.
인벤토리 만들 때 저 값을 null로 만들어 주면 된다.
👀 실행 결과