개발/유니티

[유니티] 강화버튼 누르면, 실제로 쎄지게

kimchangmin02 2025. 8. 9. 12:18

근데 궁금한게 유니티에서, 아이템 만들때, 단순히 즉시 적용되는것도 있는데
다른 아이템끼리 상호작용하는 아이템도 있는데 그러면 지금까지 얻은 아이템을 리스트에 저장해둬야하나
그런걸하려면

유니티에서 아이템 상호작용 구현: 아이템 리스트 저장은 필수!

유니티(Unity)에서 다른 아이템과 상호작용하는 아이템을 만들려면, 플레이어가 지금까지 획득한 아이템을 리스트(List)나 배열(Array)과 같은 데이터 구조에 저장해 두는 것이 필수적입니다. 이는 아이템 조합, 특정 아이템 소지 시 발동하는 패시브 효과 등 다양한 상호작용 시스템을 구현하기 위한 기본적인 단계입니다.

왜 아이템 리스트가 필요한가?

단순히 획득 시 즉시 효과가 적용되는 아이템(예: 체력 회복 포션)과 달리, 다른 아이템과의 상호작용은 현재 플레이어가 어떤 아이템들을 가지고 있는지를 알아야만 가능하기 때문입니다.

예를 들어, '레시피'에 따라 여러 재료 아이템을 조합하여 새로운 아이템을 만드는 제작 시스템을 구현한다고 가정해 봅시다. 플레이어가 제작을 시도할 때, 시스템은 플레이어의 인벤토리를 확인하여 레시피에 필요한 모든 재료 아이템이 있는지 검사해야 합니다. 이때, 획득한 아이템들이 리스트 형태로 저장되어 있어야 이 검사 과정을 효율적으로 처리할 수 있습니다.

효율적인 아이템 시스템 구현 방법

효과적인 아이템 상호작용 시스템을 구축하기 위해 다음과 같은 방법을 고려할 수 있습니다.

1. 스크립터블 오브젝트 (Scriptable Object)를 활용한 아이템 데이터 관리

각각의 아이템 데이터를 ScriptableObject를 사용하여 관리하는 것이 매우 효율적입니다. ScriptableObject는 아이템의 이름, 설명, 아이콘, 능력치 등 고유한 데이터를 에셋(Asset) 파일로 저장할 수 있게 해주는 기능입니다.

  • 장점:
    • 데이터와 로직의 분리: 게임 로직과 아이템 데이터를 분리하여 코드를 더 깔끔하고 유지보수하기 쉽게 만듭니다.
    • 재사용성 및 확장성: 아이템 '템플릿'을 만들어두고, 새로운 아이템을 추가할 때 이 템플릿을 복사하여 값만 수정하면 되므로 생산성이 크게 향상됩니다.
    • 메모리 효율: ScriptableObject는 씬(Scene)의 메모리 사용량에 포함되지 않아 메모리 부담을 줄일 수 있습니다.

2. 인벤토리 매니저 클래스 구현

플레이어가 획득한 아이템을 관리하는 InventoryManager와 같은 싱글톤(Singleton) 클래스를 만드는 것이 일반적입니다. 이 매니저는 내부에 아이템 리스트를 가지고 있으며, 아이템을 추가, 제거, 검색하는 기능을 제공합니다

C#
// 간단한 인벤토리 매니저 예시
public class InventoryManager : MonoBehaviour
{
    public static InventoryManager Instance;
    public List<ItemData> items = new List<ItemData>(); // 아이템 데이터를 저장할 리스트

    void Awake()
    {
        Instance = this;
    }

    // 아이템 추가
    public void AddItem(ItemData item)
    {
        items.Add(item);
    }

    // 아이템 제거
    public void RemoveItem(ItemData item)
    {
        items.Remove(item);
    }
}

3. 상호작용 인터페이스(Interface) 활용

IInteractable과 같은 인터페이스를 만들어 아이템 줍기, 문 열기, NPC와 대화하기 등 다양한 상호작용을 일관된 방식으로 처리할 수 있습니다플레이어가 특정 키를 눌렀을 때, 바라보는 대상이 IInteractable 인터페이스를 가지고 있다면 해당 객체의 Interact() 메소드를 호출하는 방식으로 구현할 수 있습니다.

결론

질문하신 내용처럼, 아이템 간의 상호작용을 구현하기 위해서는 획득한 아이템을 리스트에 저장하고 관리하는 과정이 반드시 필요합니다. ScriptableObject로 아이템 데이터를 정의하고, InventoryManager를 통해 아이템 리스트를 관리하며, IInteractable 인터페이스로 상호작용을 표준화하는 방식을 사용하면 확장 가능하고 효율적인 아이템 시스템을 구축할 수 있습니다.

 

 

일단, 즉각적으로 쎄지는 효과들만 구현해보자

 


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class maxHp5 : MonoBehaviour
{
    PlayerStats playerStats;

    // Start is called before the first frame update
    void Start()
    {
        //한번만 호출하면 되는거겟지
        //아 그리고, 만약에 배열에 추가해야한다면, start에서 추가하는게 좋으려나,
        //아니면 새로운 스크립트를 만들어서, 아니야, 그러면  instanceOf를 계속 호출해야하니
        //차라리 태그를 만들어두는것도?
        //그래서 근데 태그로 접근해도 이게 무슨 태그인지에 따라 조건문으로 구분하면 어지러울것같은데 방법
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}



자바에서는 이 문제를 오버라이딩으로 해결햇던것같기도한데

 


현재ㅡㄴㄴ 각 업그레이드 항목 별로 스크립트를 만들어서, 즉각적으로 적용되게햇는데
만약, 이떄까지 선택한 체력 증가의 갯수만큼, 체력 추가로 증가 같은 아이템도 있다고 가정해보자
그러면 이떄까지 몇개의 체력 관련 아이템을 선택햇는지를 알아야하는데
그러면, 내가 아이템 선택햇을떄, 이게 어떤 아이템 종류인지 확인한다음
그 리스트에 추가해줘야하는데,
이떄 어떤 아이템 종류인지 확인할떄는 어쩔수없이 조건문을 써야하나

 

이 문제를 해결하기 위해 if switch 같은 조건문을 길게 늘어놓는 것은 아이템이 많아질수록 코드를 지저분하게 만들고 확장성을 해치는 주범이 됩니다.

이 문제의 핵심 해결책은 아이템 데이터 자체에 '종류(Type)' 정보를 포함시키는 것입니다. 지난번 답변에서 사용했던 ScriptableObject를 조금 더 확장하면 이 문제를 아주 깔끔하게 해결할 수 있습니다.

해결 방법: 열거형(Enum)을 사용한 아이템 타입 명시

조건문을 사용하지 않고 아이템의 종류를 구분하는 가장 좋은 방법은 **열거형(Enum)**을 사용하는 것입니다.

Step 1: 아이템 종류를 위한 열거형(Enum) 정의

먼저 어떤 종류의 아이템들이 있는지 열거형으로 정의합니다. 이렇게 하면 문자열을 직접 쓰는 것보다 오타를 방지하고 코드를 명확하게 만들 수 있습니다.

ItemType.cs (새 스크립트 파일 생성)

C#
// 아이템의 종류를 미리 정의해 둡니다.
public enum ItemType
{
    Health,     // 체력 관련
    Damage,     // 공격력 관련
    Speed,      // 속도 관련
    Utility,    // 유틸리티 관련
    Special     // 특수 효과 관련
}

Step 2: ItemData 스크립터블 오브젝트에 타입 변수 추가

이제 이전에 만들었던 ItemData 스크립트에 방금 정의한 ItemType 변수를 추가합니다.

ItemData.cs (기존 스크립트 수정)

C#
using UnityEngine;

[CreateAssetMenu(fileName = "New ItemData", menuName = "Item/Data")]
public class ItemData : ScriptableObject
{
    public string itemName;
    public Sprite icon;
    public int value;

    // 여기에 아이템 타입을 지정할 수 있는 변수를 추가합니다!
    public ItemType itemType; 
}

이제 유니티 에디터에서 maxHp5 아이템의 ItemData 에셋을 선택하면, Inspector 창에 'Item Type' 이라는 드롭다운 메뉴가 생깁니다. 여기서 이 아이템을 'Health' 타입으로 지정해주면 됩니다. '공격력 증가' 아이템은 'Damage' 타입으로 지정하면 되겠죠.

Step 3: 아이템 종류별 카운트를 위한 인벤토리 시스템

이제 플레이어의 인벤토리(또는 아이템 관리자)가 아이템을 획득할 때, 이 itemType을 보고 종류별로 개수를 세도록 만들면 됩니다. 이때 Dictionary 자료구조를 사용하면 매우 효율적입니다.

PlayerInventory.cs (예시)

C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerInventory : MonoBehaviour
{
    // 획득한 모든 아이템의 데이터를 저장하는 리스트
    public List<ItemData> acquiredItems = new List<ItemData>();

    // <아이템 타입, 개수> 를 저장하는 딕셔너리
    public Dictionary<ItemType, int> itemCounts = new Dictionary<ItemType, int>();

    // 아이템을 추가하는 메소드
    public void AddItem(ItemData itemData)
    {
        // 1. 전체 리스트에 아이템 데이터 추가
        acquiredItems.Add(itemData);

        // 2. 종류별 카운트 딕셔너리 업데이트 (핵심 로직)
        ItemType type = itemData.itemType;

        if (itemCounts.ContainsKey(type))
        {
            // 이미 해당 타입의 아이템을 획득한 적이 있다면, 개수만 1 증가
            itemCounts[type]++;
        }
        else
        {
            // 해당 타입의 아이템을 처음 획득했다면, 딕셔너리에 새로 추가하고 개수를 1로 설정
            itemCounts.Add(type, 1);
        }

        Debug.Log(type + " 타입 아이템 획득! 현재 " + type + " 타입 아이템 개수: " + itemCounts[type]);
    }

    // 특정 타입의 아이템 개수를 가져오는 메소드
    public int GetItemCount(ItemType type)
    {
        int count = 0;
        itemCounts.TryGetValue(type, out count); // 딕셔너리에 해당 타입이 없으면 count는 0이 됨
        return count;
    }
}

 AddItem 메소드 안에는 if (type == ItemType.Health) 같은 조건문이 전혀 없습니다! 어떤 아이템이 들어오든 그저 데이터에 적힌 itemType을 키(Key)로 삼아 딕셔너리를 업데이트할 뿐입니다.

Step 4: 특별 아이템 효과 적용하기

이제 "지금까지 얻은 체력 아이템 개수만큼 체력 추가 증가" 아이템을 만들어 봅시다. 이 아이템은 자신의 효과를 적용할 때, PlayerInventory에 "Health 타입 아이템 몇 개야?" 라고 물어보기만 하면 됩니다.

BonusHealthPerHealthItem.cs```csharp
using UnityEngine;

public class BonusHealthPerHealthItem : MonoBehaviour, IItemEffect
{
public ItemData itemData;
private PlayerInventory playerInventory; // 플레이어 인벤토리 참조
private PlayerStats playerStats; // 플레이어 스탯 참조

Code
void Start()
{
    // 게임 시작 시 플레이어의 인벤토리와 스탯을 찾아 연결해 둡니다.
    playerInventory = FindObjectOfType<PlayerInventory>();
    playerStats = FindObjectOfType<PlayerStats>();
}

public void ApplyEffect()
{
    // 1. 인벤토리에 체력 타입 아이템이 몇 개인지 물어봅니다.
    int healthItemCount = playerInventory.GetItemCount(ItemType.Health);

    // 2. 그 개수만큼 추가로 체력을 증가시킵니다.
    int bonusHp = healthItemCount; // 예: 1개당 1씩 추가
    playerStats.maxHp += bonusHp;

    Debug.Log("특별 아이템 효과 발동! 기존에 획득한 체력 아이템 " + healthItemCount + "개 만큼, 최대 체력 " + bonusHp + " 추가 증가!");

    // 3. 이 특별 아이템 자체도 인벤토리에 추가합니다.
    playerInventory.AddItem(itemData);
    
    Destroy(gameObject);
}

}

Code
### 정리하자면

1.  **아이템 종류는 `Enum`으로 정의합니다.** (실수 방지, 명확성)
2.  **`ScriptableObject` (ItemData) 안에 `public ItemType itemType;` 변수를 추가**하여 각 아이템이 자신의 타입을 알도록 합니다.
3.  **인벤토리는 `Dictionary<ItemType, int>`를 사용**하여 아이템을 획득할 때마다 `if`문 없이 자동으로 종류별 카운트를 업데이트합니다.
4.  **특별 아이템**은 이 딕셔너리에 특정 타입의 아이템 개수를 **물어보기만** 하고 자신의 로직을 수행합니다.

이렇게 하면 아이템을 획득하는 로직이나, 개수를 세는 로직을 전혀 건드리지 않고도 새로운 타입의 아이템, 새로운 방식의 상호작용 아이템을 무한정 추가할 수 있는 매우 유연하고 확장 가능한 시스템이 완성됩니다.

 

 


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class maxHp5 : MonoBehaviour
{
    PlayerStats playerStats;

    // Start is called before the first frame update
    void Start()
    {
        //한번만 호출하면 되는거겟지
        //아 그리고, 만약에 배열에 추가해야한다면, start에서 추가하는게 좋으려나,
        //아니면 새로운 스크립트를 만들어서, 아니야, 그러면  instanceOf를 계속 호출해야하니
        //차라리 태그를 만들어두는것도?
        //그래서 근데 태그로 접근해도 이게 무슨 태그인지에 따라 조건문으로 구분하면 어지러울것같은데 방법
        playerStats.maxHealth += 5;
    }

    // Update is called once per frame
    // void Update()
    // {
        
    // }
}

가장 큰 문제는 playerStats 변수를 초기화하지 않고 바로 사용하려고 했다는 점입니다

 


 

using UnityEngine;

public class MaxHp5 : MonoBehaviour
{
    // 효과를 적용할 대상을 담을 변수
    private PlayerStats playerStats;

    // Start is called before the first frame update
    void Start()
    {
        // "Player" 태그를 가진 게임 오브젝트를 찾습니다.
        GameObject playerObject = GameObject.FindGameObjectWithTag("Player");

        // 만약 "Player" 태그를 가진 오브젝트를 찾았다면
        if (playerObject != null)
        {
            // 해당 오브젝트에 붙어있는 PlayerStats 컴포넌트를 가져옵니다.
            playerStats = playerObject.GetComponent<PlayerStats>();

            // PlayerStats 컴포넌트를 성공적으로 가져왔다면
            if (playerStats != null)
            {
                // 최대 체력을 5 증가시킵니다.
                playerStats.maxHealth += 5;
                Debug.Log("최대 체력이 5 증가했습니다! 현재 최대 체력: " + playerStats.maxHealth);
            }
            else
            {
                Debug.LogError("Player 오브젝트에서 PlayerStats 컴포넌트를 찾을 수 없습니다.");
            }
        }
        else
        {
            Debug.LogError("'Player' 태그를 가진 게임 오브젝트를 찾을 수 없습니다.");
        }
    }
}

playerStats = playerObject.GetComponent<PlayerStats>();  왜 이 부분이 필요한거지?
아 player스크립트를 여러명이 가지고있을수있어서인가
만약에 한명이라도 이 부분이 필요한가 왜지??

 

이해가 안되
플레이어 스크립트안에 체력 변수가 있는데

 

설계도 vs 실제 제품

여기서 핵심 개념은 **'클래스(Class)'**와 **'인스턴스(Instance)'**의 차이입니다.

  • PlayerStats.cs 스크립트 파일 (클래스 Class):
    이것은 '플레이어'라는 캐릭터를 만들기 위한 설계도와 같습니다. 이 설계도에는 "플레이어는 maxHealth라는 정수 값(체력)을 가져야 한다"라고 적혀 있습니다. 이 파일 자체는 그냥 설계도, 즉 텍스트 파일일 뿐입니다.
  • 게임 씬(Scene)에 있는 실제 플레이어 캐릭터 (인스턴스 Instance):
    Unity 에디터에서 Player라는 게임 오브젝트를 만들고, 거기에 PlayerStats.cs 스크립트를 끌어다 놓는 순간, Unity는 그 설계도를 사용해서 실제 작동하는 PlayerStats 부품을 하나 만들어냅니다. 이것이 바로 '인스턴스'입니다.
  • 플레이어가 게임 세상에 단 한 명뿐이라도, 그 한 명은 설계도가 아니라 설계도로 만들어진 '실제 제품'인 셈입니다.

MaxHp5 스크립트의 입장

이제 MaxHp5 스크립트의 입장에서 생각해 봅시다. 이 스크립트도 별개의 '설계도'로 만들어진 '실제 부품'입니다. 이 부품의 임무는 "플레이어의 체력을 5 올리는 것"입니다.

이 임무를 수행하려면, MaxHp5는 두 가지를 알아야 합니다.

  1. 누구의 체력을 올려야 하는가? (어느 게임 오브젝트에 붙어있는가?)
  2. 그 대상의 어떤 부품에 접근해야 하는가? (체력 변수는 어떤 컴포넌트가 들고 있는가?)

이것을 코드로 풀어보면 이렇습니다.

1단계: GameObject playerObject = GameObject.FindGameObjectWithTag("Player");

  • MaxHp5가 외치는 겁니다: "이 게임 세상에서 'Player'라는 이름표를 달고 있는 **실제 물체(게임 오브젝트)**를 찾아주세요!"
  • 이 단계를 통해 MaxHp5는 체력을 올려줘야 할 대상, 즉 플레이어라는 '실제 제품'을 찾았습니다. 하지만 아직 그 제품의 '체력 부품'을 손에 쥔 것은 아닙니다. 그냥 제품의 겉모습만 찾은 상태입니다.

2단계: playerStats = playerObject.GetComponent<PlayerStats>();

  • MaxHp5가 다시 말합니다: "좋아, 방금 찾은 그 'Player' 물체에 붙어있는 수많은 부품들 중에서, PlayerStats라는 설계도로 만들어진 바로 그 '실제 부품'을 저에게 주세요!"
  • 이 단계를 통해 비로소 MaxHp5는 플레이어의 체력(maxHealth) 변수를 직접 가지고 있는 PlayerStats 컴포넌트 '인스턴스'에 대한 연결고리(참조)를 얻게 됩니다.

만약 2단계(GetComponent)가 없다면?

playerStats라는 변수는 그냥 텅 빈 상자와 같습니다. MaxHp5는 "체력을 올려야지!"라고 생각은 하지만, 그 체력 정보가 담긴 부품을 손에 들고 있지 않습니다. 그래서 텅 빈 상자에게 "네 안에 있는 체력을 5 올려!"라고 명령하는 셈이고, 컴퓨터는 "상자가 비어있어서 아무것도 할 수 없습니다! (NullReferenceException)"라고 오류를 내뱉는 것입니다.

핵심 요약

PlayerStats 스크립트 파일은 '체력'이라는 변수를 정의하는 설계도일 뿐, 실제 게임에서 체력 값을 가지고 있는 것은 그 설계도로 만들어진 '컴포넌트 인스턴스'입니다. GetComponent는 바로 그 실제 컴포넌트 인스턴스를 찾아오는 필수적인 과정입니다.

플레이어가 한 명이든 백 명이든, 코드가 특정 대상과 상호작용하려면 반드시 그 대상의 **'실체(인스턴스)'**를 찾아와야 합니다.

 

 

 

 

스크립트와 게임 오브젝트 이해<나중에

근데 중요한드쇼