PlayerController
, PlayerCondition
, quipment
등과 같은 다른 컴포넌트를 가져와서 초기화함. 이들 컴포넌트는 캐릭터의 이동, 상태, 장비 등을 담당함itemData
는 플레이어가 소유한 아이템 정보를, addItem는 아이템을 추가하는 액션을 정의함dropPosition
을 통해 아이템 드롭 위치를 설정함CharacterManager
와 연결 : Awake 메서드에서 CharacterManager.Instance에 Player 객체 자신을 등록하여, CharacterManager
를 통해 플레이어 정보를 접근할 수 있게 합니다.using UnityEngine;
public class Singletone<T> : MonoBehaviour where T : Component
{
private static T _instance;
public static T Instance
{
get
{
if (_instance == null)
{
_instance = (T)FindObjectOfType(typeof(T));
if (_instance == null)
{
string tName = typeof(T).ToString(); // 오브젝트 이름 정하기
var singletoneObj = new GameObject(tName); // 타입 이름대로 지정되어 생성됨
_instance = singletoneObj.AddComponent<T>();
}
}
return _instance;
}
}
private void Awake()
{
if (_instance != null)
DontDestroyOnLoad(_instance);
}
}
public class CharacterManager : Singletone<CharacterManager>
{
public Player _player;
public Player Player
{
get { return _player; }
set { _player = value; }
}
}
using System;
using UnityEngine;
public class Player : MonoBehaviour
{
public PlayerController controller;
public PlayerCondition condition;
public Equipment equip;
public ItemData itemData;
public Action addItem;
public Transform dropPosition;
private void Awake()
{
// 1. 캐릭터 매니저에 접근. 없는 것을 확인하고 매니저 생성
CharacterManager.Instance.Player = this;
controller = GetComponent<PlayerController>();
condition = GetComponent<PlayerCondition>();
equip = GetComponent<Equipment>();
}
}
Move 메서드
CameraLook 메서드
IsGrounded 메서드
○ 인터페이스의 주요 특징
- 메서드와 속성의 시그니처만 정의 : 인터페이스는 구현을 포함하지 않고, 어떤 메서드와 속성이 필요한지 명시만 함
- 다중 상속을 지원 : 한 클래스가 여러 인터페이스를 구현할 수 있으므로, 클래스의 기능을 유연하게 확장할 수 있음
- 다형성 지원 : 같은 인터페이스를 구현한 여러 클래스 객체를 동일한 인터페이스 타입으로 다룰 수 있어, 코드의 유연성과 확장성이 향상됨
- 강제성 부여 : 인터페이스를 상속받은 클래스는 반드시 해당 인터페이스의 모든 멤버를 구현해야 하므로, 특정 기능이 구현되도록 보장할 수 있음
public interface IDamagable
{
void TakePhysicalDamage(int damage);
}
IDamagable
인터페이스 : TakePhysicalDamage
메서드를 정의하여, 해당 인터페이스를 구현하는 클래스가 물리적 피해를 받을 수 있음을 명시함PlayerCondition
은 IDamagable
인터페이스를 구현하고 있으며, 플레이어의 체력, 허기, 스태미너 상태를 관리하는 클래스TakePhysicalDamage
메서드를 통해 피해를 받을 때 체력을 감소시키고, onTakeDamage 이벤트를 통해 피해 이벤트를 발생시킴PlayerCondition
클래스는 체력 소모, 허기 감소, 체력 회복 등의 상태를 지속적으로 갱신하며 Die 메서드를 통해 사망 상태도 관리함public interface IInteractable
{
string GetInteractPrompt();
void OnInteract();
bool notGain();
}
[코드분석] - IInteractable 인터페이스와 ItemObject 클래스
ItemObject
클래스는 IInteractable
인터페이스를 구현하여 아이템과의 상호작용을 가능하게 함GetInteractPrompt
메서드를 통해 아이템의 이름과 설명을 반환하여 플레이어가 아이템과 상호작용할 때 정보를 표시할 수 있게 함OnInteract
메서드는 아이템이 수집되었을 때 호출되며, CharacterManager
의 Player
객체의 itemData
에 현재 아이템 데이터를 전달하고, addItem 이벤트를 실행한 후 아이템 오브젝트를 제거함요약
IDamagable
인터페이스는 피해를 처리하는 메서드를 강제하여, 해당 기능을 가진 클래스들이 동일한 형태로 피해를 받도록 함IInteractable
인터페이스는 상호작용 가능한 아이템을 정의하는 메서드를 강제하여, 각기 다른 아이템들이 동일한 형태로 플레이어와 상호작용하도록 함CampFire
는 트리거 충돌을 통해 범위 안에 있는 IDamagable
객체들을 things
리스트에 추가하며, DealDamage
메서드를 주기적으로 호출해 리스트에 있는 모든 객체에게 피해를 줌damage
: 한 번에 가하는 피해량damageRate
: 피해를 가하는 주기 (초 단위)things
: IDamagable 인터페이스를 구현한 객체 목록으로, 범위 내에 들어온 피해를 받을 수 있는 객체들이 저장됨DealDamage
: things
목록에 있는 모든 객체에 피해를 입히는 메서드입니다.InvokeRepeating
을 통해 damageRate
간격으로 자동 호출됨OnTriggerEnter
와 OnTriggerExit
OnTriggerEnter
: 범위 안에 들어오면 things에 추가.OnTriggerExit
: 범위 밖으로 나가면 things에서 제거.void DealDamage()
{
for(int i = 0; i < things.Count; i++)
{
things[i].TakePhysicalDamage(damage);
}
}
Flash
메서드가 호출되어, 빨간색 이미지가 잠시 깜빡이는 효과를 주고, FadeAway
코루틴을 통해 서서히 사라지도록 만듬image
: 피해를 입었을 때 화면에 표시할 이미지 객체flashSpeed
: 이미지가 화면에서 사라지는 속도를 설정coroutine
: 이미지 페이드 효과를 위한 코루틴을 관리하는 변수Flash
: 피해를 받을 때 호출되며, 이미지를 활성화하고 빨간색을 설정한 뒤 페이드 효과를 위해 FadeAway 코루틴을 실행함. 만약 이전에 실행 중이던 코루틴이 있다면 중지한 후 새로운 코루틴을 시작함FadeAway
: 이미지를 점점 투명하게 만드는 코루틴으로, flashSpeed에 따라 이미지의 알파 값을 줄이면서 서서히 사라지게 함. 알파 값이 0이 되면 이미지를 비활성화 함Start
메서드에서 CharacterManager.Instance.Player.condition.onTakeDamage
이벤트에 Flash
메서드를 연결하여, 플레이어가 피해를 받을 때 자동으로 Flash
가 호출되도록 설정함 public void Flash()
{
if(coroutine != null)
{
StopCoroutine(coroutine);
}
image.enabled = true;
image.color = new Color(1f, 100f / 255f, 100f / 255f);
coroutine = StartCoroutine(FadeAway());
}
private IEnumerator FadeAway()
{
float startAlpha = 0.3f;
float a = startAlpha;
while (a > 0)
{
a -= (startAlpha / flashSpeed) * Time.deltaTime;
image.color = new Color(1f, 100f / 255f, 100f / 255f, a);
yield return null;
}
image.enabled = false;
}
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
public class Interaction : MonoBehaviour
{
public float checkRate = 0.05f; // 얼마나 자주 체크하는지
private float lastCheckTime; // 마지막 체크 시간
public float maxCheckDistance;
public LayerMask layerMask;
public GameObject curInteractGameObject;
private IInteractable curInteractable;
public TextMeshProUGUI promptText;
private Camera _camera;
private void Start()
{
_camera = Camera.main;
}
private void Update()
{
if(Time.time - lastCheckTime > checkRate)
{
lastCheckTime = Time.time;
Ray ray = _camera.ScreenPointToRay(new Vector3(Screen.width / 2, Screen.height / 2));
// 카메라 기준으로 ray를 쏜다
RaycastHit hit; // 레이와 부딪힌 오브젝트 정보를 담음
if (Physics.Raycast(ray, out hit, maxCheckDistance, layerMask))
{
if (hit.collider.gameObject != curInteractGameObject)
{
curInteractGameObject = hit.collider.gameObject;
curInteractable = hit.collider.GetComponent<IInteractable>();
SetPromptText();
}
}
else
{
curInteractGameObject = null;
curInteractable = null;
promptText.gameObject.SetActive(false);
}
}
}
private void SetPromptText()
{
promptText.gameObject.SetActive(true);
promptText.text = curInteractable.GetInteractPrompt();
}
public void OnInteractInput(InputAction.CallbackContext context)
{
if(context.phase == InputActionPhase.Started && curInteractable != null && !curInteractable.notGain())
{
curInteractable.OnInteract();
curInteractGameObject = null;
curInteractable = null;
promptText.gameObject.SetActive(false);
}
}
}
속성 정의
메서드 구조
상호작용 오브젝트 감지 (Update 메서드)
상호작용 텍스트 설정 (SetPromptText 메서드)
상호작용 수행 (OnInteractInput 메서드)
using TMPro;
using UnityEngine;
using static UnityEditor.Timeline.Actions.MenuPriority;
public class UIInventory : MonoBehaviour
{
public ItemSlot[] slots;
public GameObject inventoryWindow;
public Transform slotPanel;
public Transform dropPosition;
[Header("Select Item")]
public TextMeshProUGUI selectedItemName;
public TextMeshProUGUI selectedItemDescription;
public TextMeshProUGUI selectedItemStatName;
public TextMeshProUGUI selectedItemStatValue;
public GameObject useButton;
public GameObject equipButton;
public GameObject unequipButton;
public GameObject dropButton;
private PlayerController controller;
private PlayerCondition condition;
ItemData selectedItem;
int selectedItemIndex = 0;
int curEquipIndex;
void Start()
{
controller = CharacterManager.Instance.Player.controller;
condition = CharacterManager.Instance.Player.condition;
dropPosition = CharacterManager.Instance.Player.dropPosition;
controller.inventory += Toggle;
CharacterManager.Instance.Player.addItem += AddItem;
inventoryWindow.SetActive(false);
slots = new ItemSlot[slotPanel.childCount]; // 슬롯 판낼 자식의 갯수, 14개
for (int i = 0; i < slots.Length; i++)
{
slots[i] = slotPanel.GetChild(i).GetComponent<ItemSlot>();
slots[i].index = i;
slots[i].inventory = this;
}
ClearSelectedItemWindow();
}
void Update()
{
}
void ClearSelectedItemWindow()
{
selectedItem = null;
selectedItemName.text = string.Empty;
selectedItemDescription.text = string.Empty;
selectedItemStatName.text = string.Empty;
selectedItemStatValue.text = string.Empty;
useButton.SetActive(false);
equipButton.SetActive(false);
unequipButton.SetActive(false);
dropButton.SetActive(false);
}
public void Toggle()
{
if (IsOpen())
{
inventoryWindow.SetActive(false);
}
else
{
inventoryWindow.SetActive(true);
}
}
public bool IsOpen()
{
return inventoryWindow.activeInHierarchy;
}
public void AddItem()
{
ItemData data = CharacterManager.Instance.Player.itemData;
// 현재 슬롯에 아이템이 스택 가능한지
if (data.canStack)
{
ItemSlot slot = GetItemStack(data);
if (slot != null)
{
slot.quantity++;
UpdateUI();
CharacterManager.Instance.Player.itemData = null;
return;
}
}
// 현재 슬롯이 꽉차있다면,
// 다른 비어 있는 슬롯 가져온다.
ItemSlot emptySlot = GetEmptySlot();
// 있다면, 슬롯에 아이템 추가
if (emptySlot != null)
{
emptySlot.item = data;
emptySlot.quantity = 1;
UpdateUI();
CharacterManager.Instance.Player.itemData = null;
return;
}
// 자리가 아예 없다면, 버려야지
ThrowItem(data);
CharacterManager.Instance.Player.itemData = null;
}
public void ThrowItem(ItemData data)
{
Instantiate(data.dropPrefab, dropPosition.position, Quaternion.Euler(Vector3.one * Random.value * 360));
// 360도 각도 중 랜덤으로 회전하여 떨어짐
}
public void UpdateUI()
{
// 슬롯 배열을 순회
for (int i = 0; i < slots.Length; i++)
{
if (slots[i].item != null)
{
// 데이터가 있다면 세팅해라
slots[i].Set();
}
else
{
slots[i].Clear();
}
}
}
ItemSlot GetItemStack(ItemData data)
{
for (int i = 0; i < slots.Length; i++)
{
// 슬롯에 아이템이 있고 양이 최대양보다 적다면
if (slots[i].item == data && slots[i].quantity < data.maxStackAmount)
{
// 슬롯을 반환
return slots[i];
}
}
return null;
}
ItemSlot GetEmptySlot()
{
for (int i = 0; i < slots.Length; i++)
{
if (slots[i].item == null)
{
// 비어있는 슬롯 반환
return slots[i];
}
}
return null;
}
public void SelectItem(int index)
{
if (slots[index].item == null) return;
selectedItem = slots[index].item;
selectedItemIndex = index;
selectedItemName.text = selectedItem.displayName;
selectedItemDescription.text = selectedItem.description;
selectedItemStatName.text = string.Empty;
selectedItemStatValue.text = string.Empty;
for (int i = 0; i < selectedItem.consumables.Length; i++)
{
selectedItemStatName.text += selectedItem.consumables[i].type.ToString() + "\n";
selectedItemStatValue.text += selectedItem.consumables[i].value.ToString() + "\n";
}
useButton.SetActive(selectedItem.type == ItemType.Consumable);
equipButton.SetActive(selectedItem.type == ItemType.Equipable && !slots[index].equipped);
unequipButton.SetActive(selectedItem.type == ItemType.Equipable && slots[index].equipped);
dropButton.SetActive(true);
}
public void OnUseButton()
{
if(selectedItem.type == ItemType.Consumable)
{
for(int i = 0; i < selectedItem.consumables.Length; i++)
{
switch(selectedItem.consumables[i].type)
{
case ConsumableType.Health:
condition.Heal(selectedItem.consumables[i].value);
break;
case ConsumableType.Hunger:
condition.Eat(selectedItem.consumables[i].value);
break;
case ConsumableType.Doping:
condition.Doping(selectedItem.consumables[i].value, selectedItem.consumables[i].duration);
break;
}
}
RemoveSelectedItem();
}
}
public void OnDropButton()
{
ThrowItem(selectedItem);
RemoveSelectedItem();
}
void RemoveSelectedItem()
{
slots[selectedItemIndex].quantity--;
if(slots[selectedItemIndex].quantity <= 0 )
{
selectedItem = null;
slots[selectedItemIndex].item = null;
selectedItemIndex = -1;
ClearSelectedItemWindow();
}
UpdateUI();
}
public void OnEquipButton()
{
if (slots[curEquipIndex].equipped)
{
UnEquip(curEquipIndex);
}
slots[selectedItemIndex].equipped = true;
curEquipIndex = selectedItemIndex;
CharacterManager.Instance.Player.equip.EquipNew(selectedItem);
UpdateUI();
SelectItem(selectedItemIndex);
}
void UnEquip(int index)
{
slots[index].equipped = false;
CharacterManager.Instance.Player.equip.UnEquip();
UpdateUI();
if(selectedItemIndex == index)
{
SelectItem(selectedItemIndex);
}
}
public void OnUnEquipButton()
{
UnEquip(selectedItemIndex);
}
}
속성 정의
메서드 구조
인벤토리 창 토글 및 UI 초기화 (Toggle, ClearSelectedItemWindow)
아이템 추가 (AddItem)
아이템 UI 업데이트 (UpdateUI)
아이템 선택 및 UI 표시 (SelectItem)
아이템 사용 (OnUseButton)
아이템 장착 및 해제 (OnEquipButton, OnUnEquipButton)
선택된 아이템 제거 (RemoveSelectedItem)