85. Unity 최종 프로젝트 9주차(4)

이규성·2024년 3월 6일
0

TIL

목록 보기
94/106

03/06

📌기능 명세서

Android build

https://velog.io/@leedlrbtjd/68.-Unity-최종-프로젝트-4주차6

Weapon

기능의 구조

  • 각 무기들의 정보를 ScriptableObject에 저장한다. (이하 SO)
  • 무기의 정보에 따라 PlayerState가 결정되고 알맞은 애니메이션이 동작한다.
  • 공격 시 무기의 SO에 저장된 데미지 정보를 가져와서 동작한다.
  1. PlayerState의 전환
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로 진입합니다.

  1. 플레이어의 공격 구현
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;
        }
    }
}

Fade object system

기능의 구조

  • 카메라와 플레이어 사이에 Raycast를 쏴서 hit한 객체의 정보를 가지고 온다.
  • 해당 객체가 설정한 layer와 일치하면 재질을 투명화 시키는 메서드를 호출한다.
  • 객체가 더이상 hit하지 않는다면 재질을 원상복구하는 메서드를 호출한다.
  1. ChangeMaterials class
    재질을 바꿔주는 메서드를 가진 class이다.
    재질을 바꿔야하는 객체에 컴포넌트로 추가한다.
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;
    }
}

  1. RaycastToChangeMaterials class
    카메라에서 플레이어로 Raycast를 쏘고 데이터 처리를 하는 class이다.
    MainCamera에 컴포넌트로 추가한다.
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();
        }
    }
}

RaycastNonAlloc의 사용 이유


RaycastAll이 아닌 RaycastNonAlloc을 사용한 이유는, RaycastAll은 호출되면서 내부적으로 배열을 생성 후 반환함. 반면에 RaycastNonAlloc은 미리 생성된 배열을 out 키워드로 재사용할 수 있어서 가비지 생성을 막을 수 있기 때문에 사용하였음.

카메라와 플레이어 사이에 Ray를 쏴서 설정해둔 layer와 일치하는 객체를 hits에 할당

hits에 할당된 객체에 컴포넌트로 추가해둔 ChangeMaterials class를 GetComponent로 가져온 뒤, currentMaterials HashSet에 추가한 후 투명화 재질로 바꿔주는 메서드를 호출하여 투명화

투명화 후 prevChangeMaterials에서 currentChangeMaterals와 일치하는 객체를 삭제. Ray에 현재 충돌하는 객체가 없다면 prevChangeMaterials 자료 구조에 존재하는 객체에 기존 재질로 바꿔주는 메서드를 호출하여 투명화를 해제

아래는 예시 이미지

Armor system

기능의 구조

  • 각 방어구의 정보를 ScriptableObject에 저장한다. (이하 SO)
  • 현재 방어구는 투구, 갑옷 두 종류이며 SO에서 enum Part 값을 지정하여 구분한다.
  • 방어구 슬롯이 생성될 때 enum Part 값을 지정하여 알맞게 장착되게 한다.
  • 플레이어의 피격 발생 시 장착된 방어구의 Defense를 모두 합산하여 전달, Damage를 경감한다.

아래는 예시 이미지

  1. ArmorSystem class
    방어구 장착과 데이터 처리의 기능을 가진 class이다.
    Player에게 컴포넌트로 추가한다.
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;
}
  1. UIArmorSlotContainer class
    UIArmorSlot을 생성과 enum 값 지정 등의 기능을 하는 class
// 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;
}
  1. UIArmorSlot class
    UIArmorSlot의 UI요소를 그리는 기능이 담긴 class
    UIArmorSlot prefab에 컴포넌트로 추가한다.
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;
    }
}

Knockback system

현재는 리팩토링 되었습니다.

기능의 구조

  • Weapon과 Monster의 충돌 시 Monster를 타격 방향으로 밀어냅니다.
  • monster가 navmesh로 동작하여 단순히 rigibody.AddForce를 할 경우 제대로 동작하지 않아, 충돌이 발생하면 rigidbody의 Kinematic을 껐다 켰다하며 knockback을 구현합니다.

아래는 예시 이미지

  1. KnockbackSystem class
    Monster prefab에 컴포턴트로 추가
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;
        }
    }
}

0개의 댓글