꾸준실습 3주차 분석 문제

woollim·2024년 11월 3일
1

내일배움캠프TIL

목록 보기
27/36
post-thumbnail

■ Q1 분석

○ 입문 주차와 비교해서 입력 받는 방식의 차이와 공통점을 비교해보세요.(Input System)

  • 입문주차 : Send Messages
    • [설명] : SendMessage는 컴포넌트에서 특정 메서드를 호출하는 방식. 입력 이벤트가 발생할 때 특정 메서드 이름을 사용하여 해당 메서드를 호출함
    • [장점] : 빠르고 간단하게 특정 메서드를 호출할 수 있어 간단한 프로젝트에 적합
    • [단점] : 메서드 이름을 문자열로 호출하기 때문에 오타가 발생해도 컴파일 오류가 발생하지 않으며, 성능상 다소 비효율적일 수 있음.
      또한, 메서드 호출을 확인하기 어렵기 때문에 유지보수가 어려움
  • 숙련주차 : Invoke Unity Events
    • [설명] : Unity Event 시스템을 활용해 입력 이벤트를 실행함. Unity 이벤트는 Unity의 Inspector에서 설정할 수 있으며, 다양한 메서드에 쉽게 연결할 수 있음
    • [장점] : Inspector를 통해 직관적으로 설정할 수 있어 코드 수정 없이도 다양한 메서드를 연결할 수 있음. 이벤트 시스템을 통해 더 나은 구조화와 유지보수가 가능함
    • [단점] : Send Message보다 설정이 복잡하며, 더 많은 설정을 요구할 수 있어 초기 세팅 시간이 길어질 수 있음
  • 요약
    • Send Messages는 간단하고 빠르지만 유지보수에 어려움이 있음
    • Invoke Unity Events는 관리와 유연성이 뛰어나지만 설정이 복잡할 수 있음
    • 프로젝트의 복잡도에 따라 두 방법 중 하나를 선택하면 됨

○ CharacterManager와 Player의 역할에 대해 고민해보세요.

  • CharacterManager 클래스
    • [역할] : 게임 내에서 캐릭터 정보를 중앙에서 관리하는 싱글톤 클래스
    • [상속] : Singletone 클래스를 상속하여, 전체 프로젝트에서 하나의 인스턴스만 존재하도록 설계되어 있음
    • [주요 기능]
      • 싱글톤 인스턴스 제공 : Instance 프로퍼티를 통해, CharacterManager의 인스턴스를 어디서든 접근할 수 있게 함
      • DontDestroyOnLoad 설정 : Awake 메서드에서 인스턴스가 이미 존재하면 DontDestroyOnLoad를 호출하여 씬 전환 시에도 파괴되지 않도록 함
      • Player 객체 관리 : _player라는 Player 객체를 CharacterManager를 통해 접근하고 설정할 수 있도록 합니다. 이를 통해 캐릭터와 관련된 모든 정보와 로직을 CharacterManager를 통해 접근하고 관리할 수 있음
  • Player 클래스
    • [역할] : 플레이어 캐릭터에 대한 정보를 관리하는 클래스
    • [주요 구성 요소]
      • 컴포넌트 : PlayerController, PlayerCondition, quipment 등과 같은 다른 컴포넌트를 가져와서 초기화함. 이들 컴포넌트는 캐릭터의 이동, 상태, 장비 등을 담당함
      • Item 관리 : 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)

  • Move 메서드

    • 목적 : 플레이어가 벽에 붙어 있을 때와 일반 이동 시 다른 움직임을 처리하도록 구현되어 있음
    • 구현
      - curMovementInput을 사용하여 이동 방향(dir)을 결정함. 방향은 앞/뒤(W/S)와 좌/우(A/D) 입력을 조합하여 계산됨
      - 벽에 붙어 있을 때
      - W를 누르면 curMovementInput.y > 0이 참이 되어 climbSpeed만큼 위로 이동함
      - S를 누르면 curMovementInput.y < 0이 되어 아래로 내려갈 수 있음
      - 움직임이 없으면 velocity를 Vector3.zero로 설정하여 움직임을 멈춤
      - 일반 이동 시
      - moveSpeed를 곱해 이동 속도를 설정
      - Y축 속도는 현재 Rigidbody의 Y축 속도를 그대로 유지하여 중력을 고려한 움직임을 보장함

  • CameraLook 메서드

    • 목적 : 플레이어가 마우스를 움직일 때 카메라 시점을 조절하는 역할
    • 구현
      - 상하 회전
      - mouseDelta.y에 lookSensitivity를 곱해 상하 움직임 민감도를 적용
      - camCurXRot은 minXLook과 maxXLook 사이에서 제한하여 덤블링과 같은 비정상적인 상하 움직임을 방지
      - 최종적으로 카메라의 X축 회전을 조절하여 상하 시점을 변경.
      - 좌우 회전
      - 마우스 좌우 움직임(mouseDelta.x)에 lookSensitivity를 곱해 캐릭터의 Y축 오일러 회전을 조절
      - 이를 통해 캐릭터가 좌우로 회전하여 시점을 조정할 수 있음

  • IsGrounded 메서드

    • 목적: 캐릭터가 바닥에 닿아 있는지 여부를 판단
    • 구현
      • 바닥에 가까운 4개의 위치에서 아래 방향으로 Raycast를 발사하여 착지 여부를 확인
      • 각 Ray는 캐릭터의 중심에서 약간 떨어진 지점(앞, 뒤, 좌, 우)에서 시작하며, 약간의 거리(0.1f) 내에서 groundLayerMask에 닿는지 검사
      • Ray 중 하나라도 groundLayer에 맞으면 착지한 것으로 판단하여 true를 반환하고, 그렇지 않으면 false를 반환

○ Move와 CameraLook 함수를 각각 FixedUpdate, LateUpdate에서 호출하는 이유에 대해 생각해보세요.

  • Move 함수 - FixedUpdate에서 호출
    • Unity에서 물리 연산은 FixedUpdate에서 수행되는 것이 권장됨
    • 일정한 시간 간격 : FixedUpdate는 매 프레임이 아닌 일정한 시간 간격(보통 0.02초)마다 호출되며, 물리적 연산이 프레임 속도와 무관하게 안정적으로 처리됨. 따라서, Rigidbody를 통한 물리적 움직임은 FixedUpdate에서 처리하는 것이 더 정확함
    • 물리 엔진과의 일관성 : Rigidbody의 속도나 힘을 조작할 때 FixedUpdate에서 호출하면, Unity 물리 엔진이 움직임을 일관되게 적용할 수 있음. 반면, Update에서 호출할 경우 프레임 속도에 따라 결과가 달라질 수 있음.

  • CameraLook 함수 - LateUpdate에서 호출
    • 모든 이동 연산 후 호출 : LateUpdate는 모든 Update 연산이 끝난 후 마지막에 호출되므로, 캐릭터의 이동이 완료된 후에 카메라를 따라가게 됨. 이를 통해 플레이어의 움직임을 기반으로 자연스럽게 카메라 시점을 설정할 수 있음
    • 프레임 속도에 맞춘 부드러운 시점 조정 : CameraLook은 프레임 속도에 따라 부드러운 시점 이동이 중요하므로, LateUpdate를 사용하면 자연스러운 카메라 움직임을 구현할 수 있음


■ Q2 분석

○ 별도의 UI 스크립트를 만드는 이유에 대해 객체지향적 관점에서 생각해보세요.

  • 요약
    • 별도의 UI 스크립트를 만드는 것은 객체지향 원칙을 적용하여 책임 분리, 코드 보호, 재사용성, 확장성, 낮은 의존성을 구현하는 데 중요한 역할을 함. 이는 개발 효율성을 높이고 유지보수를 용이하게 만들어 장기적으로 코드의 품질을 높임
  • 단일 책임 원칙 (Single Responsibility Principle)
    • UI 스크립트는 화면에 표시되는 정보와 사용자와의 상호작용에만 책임을 가지도록 설계됨
    • 게임 내 주요 로직과 UI 로직이 분리되면, UI 코드 변경이 게임 로직에 영향을 주지 않으며, 게임 로직을 수정할 때 UI 코드를 변경할 필요가 없기 때문에 각 클래스가 자신만의 역할에 집중할 수 있음
  • 캡슐화 (Encapsulation)
    • UI 관련 코드가 별도로 존재하면, UI와 관련된 데이터를 UI 스크립트 내부에 숨길 수 있음. 이렇게 하면 UI를 관리하는 코드가 외부에 노출되지 않으며, 외부 클래스가 UI 관련 내부 상태에 접근하지 못하게 함
    • 캡슐화를 통해 UI 변경이 필요한 경우에도 다른 게임 로직 코드에 영향을 주지 않고 UI 스크립트 내부에서만 수정이 가능해짐
  • 재사용성 (Reusability)
    • UI는 여러 씬이나 다른 게임 오브젝트에서 재사용될 가능성이 큼. 별도의 UI 스크립트를 만들면 특정 UI 기능을 모듈화하여 다른 게임이나 씬에서 손쉽게 재사용할 수 있음
    • 예를 들어, 체력바, 경험치 바, 인벤토리 창과 같은 UI 요소는 별도로 구성하면 다양한 캐릭터나 오브젝트에 일관되게 적용할 수 있음
  • 확장성 (Extensibility)
    • UI 요구사항은 프로젝트 진행 중에 추가되거나 변경될 가능성이 높음. UI 스크립트를 분리해 두면 새로운 UI 기능을 추가하는 것이 용이하며, 변경이 필요한 경우에도 해당 스크립트만 수정하면 됩니다.
    • 예를 들어, 플레이어 컨디션 UI를 확장해야 하는 경우 기존 UI 스크립트를 상속받아 확장할 수도 있고, 기존 기능을 재활용하여 새로운 UI를 손쉽게 추가할 수 있음
  • 의존성 감소 (Low Coupling)
    • UI 스크립트가 다른 게임 로직과 강하게 결합되어 있지 않기 때문에, UI가 다른 시스템에 미치는 영향이 최소화 됨. 게임 로직이 복잡해질수록, UI는 독립된 모듈로 작동하며 다른 모듈에 영향을 주지 않고 독립적으로 수정과 테스트가 가능함
    • 이를 통해 전체 코드베이스의 유연성이 높아지며, 특히 협업 환경에서 각 기능을 독립적으로 개발하고 유지보수할 수 있음


○ 인터페이스의 특징에 대해 정리해보고 구현된 로직을 분석해보세요.

○ 인터페이스의 주요 특징

  • 메서드와 속성의 시그니처만 정의 : 인터페이스는 구현을 포함하지 않고, 어떤 메서드와 속성이 필요한지 명시만 함
  • 다중 상속을 지원 : 한 클래스가 여러 인터페이스를 구현할 수 있으므로, 클래스의 기능을 유연하게 확장할 수 있음
  • 다형성 지원 : 같은 인터페이스를 구현한 여러 클래스 객체를 동일한 인터페이스 타입으로 다룰 수 있어, 코드의 유연성과 확장성이 향상됨
  • 강제성 부여 : 인터페이스를 상속받은 클래스는 반드시 해당 인터페이스의 모든 멤버를 구현해야 하므로, 특정 기능이 구현되도록 보장할 수 있음
  public interface IDamagable
{
    void TakePhysicalDamage(int damage);
}
  • [코드분석] - IDamagable 인터페이스와 PlayerCondition 클래스
    • IDamagable 인터페이스 : TakePhysicalDamage 메서드를 정의하여, 해당 인터페이스를 구현하는 클래스가 물리적 피해를 받을 수 있음을 명시함
    • PlayerCondition 클래스 구현
      - PlayerConditionIDamagable 인터페이스를 구현하고 있으며, 플레이어의 체력, 허기, 스태미너 상태를 관리하는 클래스
      - TakePhysicalDamage 메서드를 통해 피해를 받을 때 체력을 감소시키고, onTakeDamage 이벤트를 통해 피해 이벤트를 발생시킴
      - PlayerCondition 클래스는 체력 소모, 허기 감소, 체력 회복 등의 상태를 지속적으로 갱신하며 Die 메서드를 통해 사망 상태도 관리함


public interface IInteractable
{
    string GetInteractPrompt();
    void OnInteract();
    bool notGain();
}
  • [코드분석] - IInteractable 인터페이스와 ItemObject 클래스

    • IInteractable 인터페이스 : GetInteractPrompt, OnInteract, notGain 세 가지 메서드를 정의하여, 해당 인터페이스를 구현하는 클래스가 상호작용 가능한 오브젝트임을 명시함
    • ItemObject 클래스 구현
      - ItemObject 클래스는 IInteractable 인터페이스를 구현하여 아이템과의 상호작용을 가능하게 함
      - GetInteractPrompt 메서드를 통해 아이템의 이름과 설명을 반환하여 플레이어가 아이템과 상호작용할 때 정보를 표시할 수 있게 함
      - OnInteract 메서드는 아이템이 수집되었을 때 호출되며, CharacterManagerPlayer 객체의 itemData에 현재 아이템 데이터를 전달하고, addItem 이벤트를 실행한 후 아이템 오브젝트를 제거함
      - notGain 메서드는 아이템이 수집 가능 여부를 확인하는 로직을 제공하여, 특정 타입의 아이템은 획득하지 않도록 조건을 설정할 수 있음

  • 요약

    • IDamagable 인터페이스는 피해를 처리하는 메서드를 강제하여, 해당 기능을 가진 클래스들이 동일한 형태로 피해를 받도록 함
    • IInteractable 인터페이스는 상호작용 가능한 아이템을 정의하는 메서드를 강제하여, 각기 다른 아이템들이 동일한 형태로 플레이어와 상호작용하도록 함

○ 핵심 로직을 분석해보세요. (UI 스크립트 구조, CampFire, DamageIndicator)

  • [코드분석] - CampFire 클래스
    • CampFire는 트리거 충돌을 통해 범위 안에 있는 IDamagable 객체들을 things 리스트에 추가하며, DealDamage 메서드를 주기적으로 호출해 리스트에 있는 모든 객체에게 피해를 줌
    • 속성:
      • damage : 한 번에 가하는 피해량
      • damageRate : 피해를 가하는 주기 (초 단위)
      • things : IDamagable 인터페이스를 구현한 객체 목록으로, 범위 내에 들어온 피해를 받을 수 있는 객체들이 저장됨
      • 주요 메서드
        - DealDamage : things 목록에 있는 모든 객체에 피해를 입히는 메서드입니다.
        - InvokeRepeating을 통해 damageRate 간격으로 자동 호출됨
        - OnTriggerEnterOnTriggerExit
        Collider 범위 안에 들어온 객체를 감지하여, IDamagable 인터페이스가 구현된 객체는 things 리스트에 추가하고, 범위를 벗어나면 제거함
        - OnTriggerEnter : 범위 안에 들어오면 things에 추가.
        - OnTriggerExit : 범위 밖으로 나가면 things에서 제거.
void DealDamage()
{
    for(int i = 0; i < things.Count; i++)
    {
        things[i].TakePhysicalDamage(damage);
    }
}

  • [코드분석] - DamageIndicator 클래스
    • 플레이어가 피해를 받을 때 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;
}


■ Q3 분석

○ Interaction 기능의 구조와 핵심 로직을 분석해보세요.

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);
        }
    }
}
  • 속성 정의

    • checkRate : 얼마나 자주 오브젝트를 감지할지 설정
    • maxCheckDistance : 상호작용 가능한 오브젝트를 탐색하는 최대 거리
    • layerMask : 특정 레이어에만 반응하도록 설정해, 상호작용 가능한 오브젝트만 감지할 수 있게 함
    • curInteractGameObject와 curInteractable : 현재 상호작용할 수 있는 오브젝트와 인터페이스를 저장함
    • promptText : 상호작용 가능 여부와 해당 오브젝트의 정보를 표시하는 UI 텍스트
    • _camera : 상호작용 시, 중심에서 Ray를 쏘기 위한 카메라 객체

  • 메서드 구조

    • Start() : 시작 시 카메라 객체를 할당
    • Update() : 매 프레임마다 상호작용할 수 있는 오브젝트를 체크하는 주요 로직이 포함
    • SetPromptText() : 상호작용 가능한 오브젝트를 감지하면, 해당 오브젝트의 정보를 UI 텍스트에 표시
    • OnInteractInput() : 플레이어가 상호작용 입력을 했을 때 호출되는 메서드로, 감지된 오브젝트와의 상호작용을 수행

  • 상호작용 오브젝트 감지 (Update 메서드)

    • checkRate를 기준으로 주기적으로 상호작용 가능한 오브젝트를 감지하여 lastCheckTime을 갱신
    • Raycast 사용
      • Camera.main.ScreenPointToRay를 통해 화면 중앙에서 오브젝트를 향해 Ray를 발사하여 탐색
      • Physics.Raycast를 사용해 maxCheckDistance 이내에 레이어 마스크에 해당하는 오브젝트를 감지하고, hit 정보를 획득
    • 오브젝트 갱신
      • Ray가 새로운 오브젝트와 충돌하면, 해당 오브젝트를 curInteractGameObject에 저장하고, 오브젝트가 IInteractable을 구현하고 있는지 확인 후 curInteractable에 저장
      • SetPromptText 호출 : 상호작용 가능한 오브젝트가 감지되면 SetPromptText()가 호출되어, 해당 오브젝트의 정보를 UI에 표시
    • 오브젝트 감지 실패 시
      • Ray가 오브젝트와 충돌하지 않거나 감지된 오브젝트가 없으면 curInteractGameObject와 curInteractable을 null로 설정하고, promptText를 비활성화

  • 상호작용 텍스트 설정 (SetPromptText 메서드)

    • UI 텍스트 활성화 : promptText를 활성화하고, 현재 감지된 curInteractable 오브젝트의 GetInteractPrompt() 메서드를 호출하여 상호작용 정보를 표시
    • 목적 : 플레이어가 감지된 오브젝트와 상호작용할 수 있음을 직관적으로 보여줌

  • 상호작용 수행 (OnInteractInput 메서드)

    • 상호작용 입력(InputAction.CallbackContext)을 받아, curInteractable이 존재하고 상호작용이 가능한 상태(notGain() 메서드가 false)인지 확인
    • 조건이 충족되면, curInteractable의 OnInteract() 메서드를 호출하여 상호작용을 수행
    • 상호작용 후 상태 초기화
      - 상호작용이 완료되면 curInteractGameObject와 curInteractable을 null로 초기화하여 다음 상호작용 준비
      - promptText를 비활성화하여 UI에서 상호작용 텍스트를 제거


○ Inventory 기능의 구조와 핵심 로직을 분석해보세요.

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 요소 : slots 배열에는 인벤토리 아이템 슬롯들이 저장되고, inventoryWindow는 인벤토리 UI 창을 관리함. selectedItemName, selectedItemDescription, useButton 등의 UI 요소가 선택된 아이템의 정보를 표시하거나 버튼을 활성화하는 데 사용됨.
    • 플레이어 관련 정보 : controller, condition, dropPosition 속성은 플레이어 캐릭터의 컨트롤러와 상태, 아이템 드랍 위치와 연결됨
    • 아이템 관련 정보 : selectedItem은 선택된 아이템 데이터이며, curEquipIndex는 현재 장착된 아이템의 인덱스를 나타냄
  • 메서드 구조

    • 시작 설정 (Start) : 인벤토리 슬롯 배열을 초기화하고, 슬롯 인덱스를 설정하여 각각이 인벤토리와 연결
    • 아이템 추가 및 삭제 : AddItem, ThrowItem, RemoveSelectedItem 메서드는 아이템을 인벤토리에 추가, 드랍, 제거하는 로직을 포함
    • UI 업데이트 (UpdateUI) : 아이템의 상태를 업데이트하고, UI에 반영
    • 아이템 선택 및 버튼 동작 : SelectItem, OnUseButton, OnDropButton, OnEquipButton, OnUnEquipButton 메서드는 아이템을 선택하고, 사용할 때 버튼 동작을 수행함

  • 인벤토리 창 토글 및 UI 초기화 (Toggle, ClearSelectedItemWindow)

    • Toggle : inventoryWindow의 활성화 상태를 반전하여 인벤토리를 열고 닫음
    • ClearSelectedItemWindow : 선택된 아이템의 UI 정보를 초기화하고, 버튼을 비활성화함
  • 아이템 추가 (AddItem)

    • 스택 가능한 아이템 추가
      • 현재 아이템이 스택 가능(canStack)한 경우, GetItemStack 메서드를 통해 기존 슬롯을 확인하고, 해당 슬롯이 있으면 quantity 증가
    • 새로운 슬롯에 추가
      • 슬롯에 빈 공간이 없으면 GetEmptySlot을 사용하여 빈 슬롯을 찾고, 해당 슬롯에 아이템을 추가
    • 아이템 드랍
      • 모든 슬롯이 가득 찬 경우 ThrowItem 메서드를 호출해 아이템을 dropPosition에서 드랍
  • 아이템 UI 업데이트 (UpdateUI)

    • 슬롯 순회 및 UI 업데이트
      • slots 배열을 순회하면서 슬롯이 비어 있지 않다면 Set() 메서드를 호출해 슬롯을 설정하고, 그렇지 않으면 Clear()를 호출해 초기화
    • 이를 통해 인벤토리 UI의 아이템 정보를 최신 상태로 유지
  • 아이템 선택 및 UI 표시 (SelectItem)

    • 아이템 선택
      • slots[index].item에 데이터가 있으면 selectedItem으로 설정하고, selectedItemName, selectedItemDescription 등에 데이터를 반영
    • 버튼 활성화
      • selectedItem의 타입에 따라 useButton, equipButton, unequipButton, dropButton 등을 적절히 활성화하여 UI에서 사용할 수 있도록 설정
  • 아이템 사용 (OnUseButton)

    • 아이템 효과 적용
      • selectedItem이 Consumable 타입이면, ConsumableType에 따라 Health, Hunger, Doping 등의 효과를 condition에 적용하고, 사용 후 RemoveSelectedItem을 호출하여 제거
  • 아이템 장착 및 해제 (OnEquipButton, OnUnEquipButton)

    • 장착
      • 다른 장착된 아이템이 있으면 UnEquip 메서드를 호출해 해제하고, EquipNew 메서드를 호출해 selectedItem을 장착
    • 해제
      • UnEquip 메서드를 통해 장착 해제 후 UI를 업데이트하여 상태를 반영
  • 선택된 아이템 제거 (RemoveSelectedItem)

    • 아이템 수량 감소
      • selectedItemIndex의 quantity를 감소시키고, 수량이 0 이하일 경우 해당 슬롯을 초기화
    • UI 업데이트
      • UpdateUI를 호출해 슬롯 배열의 상태를 최신화

0개의 댓글