https://velog.io/@leedlrbtjd/68.-Unity-최종-프로젝트-4주차6
기능의 구조
public class PlayerGroundedState : PlayerBaseState
{
protected override void AddInputActionsCallbacks()
{
base.AddInputActionsCallbacks();
Managers.Game.Player.ToolSystem.OnEquip += OnEquipTypeOfTool;
Managers.Game.Player.ToolSystem.OnUnEquip += OnUnEquipTypeOfTool;
}
protected override void RemoveInputActionsCallbacks()
{
base.RemoveInputActionsCallbacks();
Managers.Game.Player.ToolSystem.OnEquip -= OnEquipTypeOfTool;
Managers.Game.Player.ToolSystem.OnUnEquip -= OnUnEquipTypeOfTool;
}
protected virtual void OnMove()
{
ToolItemData toolItemDate = (ToolItemData)Managers.Game.Player.ToolSystem.ItemInUse.itemData;
if (toolItemDate.isTwoHandedTool == true)
{
_stateMachine.ChangeState(_stateMachine.TwoHandedToolRunState);
}
else if (toolItemDate.isTwinTool == true)
{
_stateMachine.ChangeState(_stateMachine.TwinToolRunState);
}
else
{
_stateMachine.ChangeState(_stateMachine.RunState);
}
}
protected virtual void OnEquipTypeOfTool(QuickSlot quickSlot)
{
ChangeIdleState();
}
protected void ChangeIdleState()
{
ItemData equippedItemData = _stateMachine.Player.EquippedItem.itemData;
ToolItemData equippedToolItemDate = (ToolItemData)equippedItemData;
if (equippedToolItemDate.isWeapon == true && equippedToolItemDate.isTwoHandedTool == true)
{
_stateMachine.ChangeState(_stateMachine.TwoHandedToolIdleState);
}
else if (equippedToolItemDate.isWeapon == true && equippedToolItemDate.isTwinTool == true)
{
_stateMachine.ChangeState(_stateMachine.TwinToolIdleState);
}
else
{
_stateMachine.ChangeState(_stateMachine.IdleState);
}
}
protected virtual void OnUnEquipTypeOfTool(QuickSlot quickSlot)
{
_stateMachine.ChangeState(_stateMachine.IdleState);
}
}
플레이어가 현재 장착한 ItemData의 정보를 참조하여 조건문을 통해 알맞은 IdleState, RunState로 진입합니다.
public class PlayerBaseState : IState
{
protected virtual void OnInteractStarted(InputAction.CallbackContext context)
{
if (_stateMachine.Player.EquippedItem == null) return;
if (_stateMachine.IsFalling) return;
Debug.Log("Player Interact");
var tool = _stateMachine.Player.EquippedItem.itemData as ToolItemData;
if (tool.isArchitecture)
{
_stateMachine.ChangeState(_stateMachine.BuildState);
}
else if (tool.isWeapon)
{
_stateMachine.IsAttacking = true;
_stateMachine.ChangeState(_stateMachine.ComboAttackState);
}
}
protected virtual void OnInteractCanceled(InputAction.CallbackContext context)
{
_stateMachine.IsAttacking = false;
}
}
public class PlayerGroundedState : PlayerBaseState
{
public override void Update()
{
base.Update();
if (_stateMachine.IsAttacking)
{
OnAttack();
return;
}
}
protected virtual void OnAttack()
{
_stateMachine.ChangeState(_stateMachine.ComboAttackState);
}
}
InputCallback이 입력되면 플레이어가 장착한 장비의 정보를 조건문으로 검사하고 만약 무기라면 AttackState에 진입한다.
public class PlayerComboAttackState : PlayerAttackState
{
protected override void Rotate(Vector3 movementDirection)
{
// Player의 조이스틱 입력으로 인한 방향 전환을 배제한다.
}
private void RotateOfTarget()
{
var look = target.transform.position - _stateMachine.Player.transform.position;
look.y = 0;
var targetRotation = Quaternion.LookRotation(look);
_stateMachine.Player.transform.rotation = targetRotation;
}
private void ExitState(ToolItemData toolItemData)
{
if (toolItemData.isTwoHandedTool == true)
{
_stateMachine.ChangeState(_stateMachine.TwoHandedToolIdleState);
}
else if (toolItemData.isTwinTool == true)
{
_stateMachine.ChangeState(_stateMachine.TwinToolIdleState);
}
else
{
_stateMachine.ChangeState(_stateMachine.IdleState);
}
}
}
공격이 시작되면 몬스터의 방향으로 플레이어가 바라보게 구현하였다.
공격이 끝나면 조건문을 통해 해당 IdleState로 진입한다.
Animator의 bool값으로 Transition의 진입, 이탈한다.
아래는 예시 이미지
Weapon Prefab에 컴포넌트로 추가한 Weapon class
public class Weapon : MonoBehaviour, IAttack
{
private int _damage;
private QuickSlot _linkedSlot;
// AttackState 진입 시에만 무기의 BoxCollider를 켜서 충돌 처리를 한다.
private void Awake()
{
gameObject.GetComponentInChildren<BoxCollider>().enabled = false;
Managers.Game.Player.ToolSystem.OnEquip += DamageOfTheEquippedWeapon;
}
// 몬스터의 Collider과 무기가 충돌하면 Attack 메서드를 호출한다.
private void OnTriggerEnter(Collider other)
{
Attack(other.GetComponent<IHit>());
}
// 몬스터는 IHit 인터페이스를 상속 받고 있으며, 무기와 충돌 시 데미지를 받는 Hit 메서드를 호출한다.
public void Attack(IHit target)
{
if (target == null) return;
target.Hit(this, _damage);
Managers.Game.Player.Inventory.UseToolItemByIndex(_linkedSlot.targetIndex, 1);
}
// 공격 시 데미지는 무기의 SO 정보를 기반으로 결정한다.
public void DamageOfTheEquippedWeapon(QuickSlot quickSlot)
{
ItemData weapon = quickSlot.itemSlot.itemData;
ToolItemData toolItemDate = weapon is ToolItemData ? (ToolItemData)weapon: null;
if (toolItemDate == null) return;
_damage = toolItemDate.damage;
}
// 무기의 SO 정보를 참조하는 메서드
public void Link(QuickSlot slot)
{
_linkedSlot = slot;
if(_linkedSlot.itemSlot.itemData is ToolItemData tool)
{
_damage = tool.damage;
}
}
}
기능의 구조
public class ChangeMaterials : MonoBehaviour
{
// 투명화 해놓은 재질을 담은 배열
public Material[] fadeMaterials = new Material[2];
// 원상복구 용 재질을 담는 배열
public Material[] originMaterials = new Material[2];
// 재질을 변환하기 위한 중간 단계 배열
private Material[] _materials;
// 재질을 변화하기 위해 컴포넌트를 받아오기 위한 멤버 변수
private MeshRenderer _meshRenderer;
// 객체의 MeshRenderer 컴포넌트를 가지고 온다.
private void Awake()
{
_meshRenderer = GetComponentInChildren<MeshRenderer>();
_materials = _meshRenderer.materials;
}
// 반복문으로 투명화 재질로 바꾼다.
public void ChangeFadeMaterials()
{
for (int i = 0; i < _materials.Length; i++)
{
_materials[i] = fadeMaterials[i];
}
_meshRenderer.materials = _materials;
}
// 반복문으로 재질을 원상복구 시킨다.
public void ReturnMaterials()
{
for (int i = 0; i < _materials.Length; i++)
{
_materials[i] = originMaterials[i];
}
_meshRenderer.materials = _materials;
}
}
public class RaycastToChangeMaterials : MonoBehaviour
{
[SerializeField]
private LayerMask _layerMask;
[SerializeField]
private Camera _camera;
[SerializeField]
private Transform _player;
// RaycastNonAlloc 사용을 위한 배열
private RaycastHit[] _hits = new RaycastHit[10];
// Raycast에 hit된 객체를 저장하고 지정한 동작을 수행하기 위한 배열들
private HashSet<ChangeMaterials> _prevChangeMaterials = new();
private HashSet<ChangeMaterials> _curruentChangeMaterials = new();
// Player가 생성되면 동작하기 위해 Coroutine 사용
private void Start()
{
StartCoroutine(CheckPlayer());
}
private IEnumerator CheckPlayer()
{
while (Managers.Game.Player == null)
{
yield return null;
}
_player = Managers.Game.Player.ViewPoint;
}
// Player가 생성되면 메서드를 호출한다.
private void FixedUpdate()
{
if (_player != null)
{
CheckForObjects();
}
}
private void CheckForObjects()
{
_prevChangeMaterials = _curruentChangeMaterials;
_curruentChangeMaterials = new();
int hits = Physics.RaycastNonAlloc(_camera.transform.position, (_player.transform.position - _camera.transform.position).normalized, _hits,
Vector3.Distance(_camera.transform.position, _player.transform.position), _layerMask, QueryTriggerInteraction.Collide);
if (hits >= 0)
{
for (int i = 0; i < hits; i++)
{
ChangeMaterials changeMaterials = _hits[i].transform.gameObject.GetComponentInParent<ChangeMaterials>();
_curruentChangeMaterials.Add(changeMaterials);
changeMaterials.ChangeFadeMaterials();
}
_prevChangeMaterials.ExceptWith(_curruentChangeMaterials);
foreach (var hit in _prevChangeMaterials)
hit.ReturnMaterials();
}
}
}
RaycastAll이 아닌 RaycastNonAlloc을 사용한 이유는, RaycastAll은 호출되면서 내부적으로 배열을 생성 후 반환함. 반면에 RaycastNonAlloc은 미리 생성된 배열을 out 키워드로 재사용할 수 있어서 가비지 생성을 막을 수 있기 때문에 사용하였음.
카메라와 플레이어 사이에 Ray를 쏴서 설정해둔 layer와 일치하는 객체를 hits에 할당
hits에 할당된 객체에 컴포넌트로 추가해둔 ChangeMaterials class를 GetComponent로 가져온 뒤, currentMaterials HashSet에 추가한 후 투명화 재질로 바꿔주는 메서드를 호출하여 투명화
투명화 후 prevChangeMaterials에서 currentChangeMaterals와 일치하는 객체를 삭제. Ray에 현재 충돌하는 객체가 없다면 prevChangeMaterials 자료 구조에 존재하는 객체에 기존 재질로 바꿔주는 메서드를 호출하여 투명화를 해제
아래는 예시 이미지
기능의 구조
아래는 예시 이미지
private Player _player;
private int _defense; // 장착된 방어구들의 Defense 값을 저장할 변수
private QuickSlot[] _equippedArmors; // 방어구가 장착되면 ItemData를 저장할 배열
// 방어구 탈착이 이루어지면 event를 통해 다른 class에 정보를 전달
public event Action<QuickSlot> OnEquipArmor;
public event Action<QuickSlot> OnUnEquipArmor;
Awake, Start 메서드
private void Awake()
{
_player = GetComponentInParent<Player>();
_equippedArmors = new QuickSlot[2];
for (int i = 0; i < _equippedArmors.Length; i++)
{
_equippedArmors[i] = new QuickSlot();
}
GameManager.Instance.Player.OnHit += OnUpdateDurabilityOfArmor;
Load();
GameManager.Instance.OnSaveCallback += Save;
}
private void Start()
{
_player.Inventory.OnUpdated += OnInventoryUpdated;
}
다른 class에 구독하여 Update 목적의 메서드
// 방어구를 장착한 상태로 Player의 피격이 발생하면 방어구의 내구도가 감소한다.
public void OnUpdateDurabilityOfArmor()
{
for (int i = 0; i < _equippedArmors.Length; i++)
{
if (_equippedArmors[i] != null && _equippedArmors[i].targetIndex != -1)
{
_player.Inventory.TrySubtractDurability(_equippedArmors[i].targetIndex, 1);
}
}
}
// 방어구의 내구도가 다 닳아서 파괴되면 Inventory에 정보를 전달하여 업데이트 한다.
public void OnInventoryUpdated(int inventoryIndex, ItemSlot itemSlot)
{
if (itemSlot.itemData != null) return;
for (int i = 0; i < _equippedArmors.Length; i++)
{
if (_equippedArmors[i] != null)
{
if (_equippedArmors[i].targetIndex == inventoryIndex && itemSlot.itemData == null)
{
SubtractDefenseOfArmor(_equippedArmors[i]);
OnUnEquipArmor?.Invoke(_equippedArmors[i]);
_equippedArmors[i].Clear();
}
}
}
}
private void Load()
{
if (SaveGame.TryLoadJsonToObject(this, SaveGame.SaveType.Runtime, "ArmorSystem"))
{
for (int i = 0; i < _equippedArmors.Length; ++i)
{
_equippedArmors[i].itemSlot.LoadData();
}
}
}
private void Save()
{
var json = JsonUtility.ToJson(this);
SaveGame.CreateJsonFile("ArmorSystem", json, SaveGame.SaveType.Runtime);
}
방어구 탈착 기능을 하는 메서드
// 방어구가 장착되면 _equippedArmors 배열에 정보를 저장, 삭제한다.
public void Equip(int index, ItemSlot itemSlot)
{
var part = GetPart(itemSlot);
UnEquip(part);
itemSlot.SetEquip(true);
_equippedArmors[(int)part].Set(index, itemSlot);
AddDefenseOfArmor(_equippedArmors[(int)part]);
OnEquipArmor?.Invoke(_equippedArmors[(int)part]);
}
public void UnEquip(ItemParts part)
{
if (_equippedArmors[(int)part].itemSlot.itemData == null) return;
SubtractDefenseOfArmor(_equippedArmors[(int)part]);
_equippedArmors[(int)part].itemSlot.SetEquip(false);
OnUnEquipArmor?.Invoke(_equippedArmors[(int)part]);
_equippedArmors[(int)part].Clear();
}
// 방어구가 장착되면 Defense 값을 더하거나 뺀다.
private void AddDefenseOfArmor(QuickSlot quickSlot)
{
EquipItemData toolItemData = GetArmorItemData(quickSlot);
_defense += toolItemData.defense;
}
private void SubtractDefenseOfArmor(QuickSlot quickSlot)
{
EquipItemData toolItemData = GetArmorItemData(quickSlot);
_defense -= toolItemData.defense;
}
방어구의 정보를 참조하기 위해 변환하는 메서드
private EquipItemData GetArmorItemData(QuickSlot quickSlot)
{
EquipItemData toolItemDate = quickSlot.itemSlot.itemData as EquipItemData;
return toolItemDate;
}
private ItemParts GetPart(ItemSlot slot)
{
var itemData = slot.itemData as EquipItemData;
return itemData.part;
}
// UIArmorSlot이 생성되면 객체를 저장할 배열
private List<UIArmorSlot> _armorSlots = new List<UIArmorSlot>();
Awake 메서드
// ArmorSystem에서 이벤트가 호출되면 해당 메서드를 동작시킨다.
private void Awake()
{
GameManager.Instance.Player.ArmorSystem.OnEquipArmor += EquipArmor;
GameManager.Instance.Player.ArmorSystem.OnUnEquipArmor += UnEquipArmor;
}
UIArmorSlot의 생성과 관련된 메서드
// 이 class에서 직접 생성하는 것이 아닌 UIInventory class에서 참조하여 생성한다.
public void CreatArmorSlots<T>(GameObject slotPrefab, int count) where T : UIArmorSlot
{
for (int i = 0; i < count; i++)
{
var armorSlotPrefab = Instantiate(slotPrefab, this.transform);
var armorSlotUI = armorSlotPrefab.GetComponent<T>();
SetParts(armorSlotUI, i);
_armorSlots.Add(armorSlotUI);
}
}
// 현재 장착된 방어구 정보를 UIArmorSlot에 적용한다.
public void Init<T>(QuickSlot[] quickSlot) where T : UIArmorSlot
{
foreach (var armor in quickSlot)
{
EquipArmor(armor);
}
}
// UIArmorSlot이 생성될 때, enum 값을 지정한다.
private void SetParts(UIArmorSlot armorSlotUI, int index)
{
armorSlotUI.SetPart((ItemParts)index);
}
방어구 탈착 시 UI를 업데이트 시키는 메서드
public void EquipArmor(QuickSlot quickSlot)
{
if (quickSlot.itemSlot.itemData == null) return;
int parts = GetPart(quickSlot);
_armorSlots[parts].Set(quickSlot.itemSlot);
}
public void UnEquipArmor(QuickSlot quickSlot)
{
int parts = GetPart(quickSlot);
_armorSlots[parts].Clear();
}
private int GetPart(QuickSlot slot)
{
var itemData = slot.itemSlot.itemData as EquipItemData;
if (itemData == null) return -1;
return (int)itemData.part;
}
public class UIArmorSlot : UIItemSlot
{
public Sprite[] sprites;
public Color emptyState;
enum Images
{
Icon,
}
// 투구, 갑옷의 분리를 위한 enum 값
public ItemParts part;
// Image를 가지고 처리를 하기 위해 바인딩
public override void Initialize()
{
Bind<Image>(typeof(Images));
Clear();
Get<Image>((int)Images.Icon).raycastTarget = false;
}
private void Awake()
{
Initialize();
}
public void SetPart(ItemParts part)
{
this.part = part;
Clear();
}
// 방어구가 장착되면 Slot의 Image sprite를 업데이트 한다
public override void Set(ItemSlot itemSlot)
{
if (itemSlot.itemData == null)
{
Clear();
return;
}
Get<Image>((int)Images.Icon).color = Color.white;
Get<Image>((int)Images.Icon).sprite = itemSlot.itemData.iconSprite;
Get<Image>((int)Images.Icon).gameObject.SetActive(true);
}
// 방어구가 해제되면 Image sprite를 원상복구한다.
public override void Clear()
{
Get<Image>((int)Images.Icon).sprite = sprites[(int)part];
Get<Image>((int)Images.Icon).color = emptyState;
}
}
기능의 구조
아래는 예시 이미지
public class KnockbackSystem : MonoBehaviour
{
private Rigidbody _rigidbody;
private float _knockbackTime = 0f; // isKinematic을 끄고 있는 시간
// Monster의 rigidbody 컴포넌트를 가져온다.
private void Awake()
{
_rigidbody = GetComponent<Rigidbody>();
}
private void Start()
{
_rigidbody.isKinematic = true;
}
// _knockbackTime에서 Time.deltaTime을 계속 빼주며 그 사이에 넉백시킨다
private void FixedUpdate()
{
_knockbackTime -= Time.deltaTime;
if (_knockbackTime <= 0f)
{
_rigidbody.isKinematic = true;
}
}
// 무기와의 충돌 발생 시 AddForce로 뒤로 밀려난다.
private void OnTriggerEnter(Collider other)
{
if (other == null)
{
return;
}
if (other.gameObject.layer == 8 && _rigidbody.isKinematic == true)
{
_rigidbody.isKinematic = false;
_rigidbody.AddForce((transform.position - other.transform.position).normalized, ForceMode.Impulse);
_knockbackTime = 0.5f;
}
}
}