아이템 드롭 시스템은 어떤 들어갈 수 밖에 없습니다. 아이템 시스템이 몬스터와 잘 분리돼 있지 않으면 플레이어와 몬스터가 긴밀하게 연결돼 있어 객체 지향적인 프로그래밍을 하지 못하여 버그나 생산성이 급격히 떨어지게 됩니다. 이번 시간에는 아이템 드랍 시스템의 기본적인 설계를 해보고 코드를 작성하는 시간을 갖겠습니다.

[아이템 드롭 시스템 데이터 기반으로 설계하기]

아이템은 특성 상 아이템의 드랍률, 아이템의 타입 등 여러가지 데이터로 이루어져 있습니다. 그렇기에 ScriptableObject나 json을 사용하여 아이템 드랍 테이블을 만들고 해당 데이터를 통해 아이템을 드롭하는 방식이 좋습니다.

[
    {"name": "Sword", "type": "Sword", "probability": 0.3},
    {"name": "Potion", "type": "Potion", "probability": 0.5},
    {"name": "Gold", "type": "Gold", "probability": 0.2}
]

위는 이번 시간에 사용할 json 데이터 입니다. items.json 파일 형태로 저장해 두시면 됩니다.

[코드 설계]

[아이템 클래스]

using UnityEngine;
using System;
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json;

// 아이템 클래스
public class Item
{
    public string Name { get; private set; }
    public ItemType Type { get; private set; }

    public Item(string name, ItemType type)
    {
        Name = name;
        Type = type;
    }
}

// 아이템 타입 열거형
public enum ItemType
{
    Sword,
    Potion,
    Gold
}

json을 담을 아이템 클래스 입니다.

[아이템 매니저 클래스]

// 아이템 매니저 클래스 (싱글톤)
public class ItemManager : MonoBehaviour
{
    private static ItemManager instance;
    private List<ItemData> itemDataList;
    private System.Random random;

    private ItemManager() { }

    public static ItemManager Instance
    {
        get
        {
            if (instance == null)
            {
                instance = FindObjectOfType<ItemManager>();
                if (instance == null)
                {
                    GameObject obj = new GameObject();
                    obj.name = "ItemManager";
                    instance = obj.AddComponent<ItemManager>();
                }
            }
            return instance;
        }
    }

    private void Awake()
    {
        random = new System.Random();
        LoadItemData();
    }

    private void LoadItemData()
    {
        string filePath = Application.dataPath + "/items.json";
        if (File.Exists(filePath))
        {
            string json = File.ReadAllText(filePath);
            itemDataList = JsonConvert.DeserializeObject<List<ItemData>>(json);
        }
        else
        {
            Debug.LogError("Item data file not found!");
        }
    }

    public Item GenerateRandomItem()
    {
        if (itemDataList == null || itemDataList.Count == 0)
        {
            Debug.LogError("No item data loaded!");
            return null;
        }

        float totalProbability = 0f;
        foreach (ItemData itemData in itemDataList)
        {
            totalProbability += itemData.probability;
        }

        float randomValue = (float)random.NextDouble() * totalProbability;
        float cumulativeProbability = 0f;
        foreach (ItemData itemData in itemDataList)
        {
            cumulativeProbability += itemData.probability;
            if (randomValue <= cumulativeProbability)
            {
                return new Item(itemData.name, itemData.type);
            }
        }

        return null;
    }

    public void DropItem(Vector3 position)
    {
        Item item = GenerateRandomItem();
        if (item != null)
        {
            Debug.Log("Dropped item: " + item.Name);
            // 여기서 아이템을 생성하고 떨어뜨릴 수 있습니다.
        }
    }
}

아이템 매니저 클래스는 json 데이터를 로드하고, 랜덤으로 아이템을 생성하고 드롭하는 코드가 있습니다.

아이템 매니저를 통하여 몬스터와 플레이어간 강한 결합을 막아 확장성과 유연성이 증대 됩니다.

[몬스터 클래스]

// 몬스터 클래스
public class Monster : MonoBehaviour
{
    public event Action<Vector3> OnMonsterDeath; // 몬스터가 죽을 때 발생하는 이벤트

    private void Die()
    {
        // 몬스터가 죽었을 때 실행되어야 할 작업들을 수행합니다.
        if (OnMonsterDeath != null)
        {
            OnMonsterDeath(transform.position);
        }
    }

    private void OnDestroy()
    {
        Die();
    }
}

몬스터가 죽었을 때 발생하는 이벤트를 통하여 아이템 매니저와 몬스터 클래스간 강한 결합을 방지합니다. 이렇게 사용할 경우 몬스터가 죽었을 때 몬스터 처치 퀘스트 진행도 등 이벤트를 구독만 하면 어디서든지 알림을 받아 사용할 수 있기에 확장성과 유연성이 증가 합니다.

[게임 매니저 클래스]

// 게임 매니저 클래스
public class GameManager : MonoBehaviour
{
    private void Start()
    {
        // 몬스터 생성
        Monster monster = new GameObject("Monster").AddComponent<Monster>();

        // 몬스터가 죽을 때 아이템을 드랍하도록 이벤트를 구독합니다.
        monster.OnMonsterDeath += HandleMonsterDeath;

        // 몬스터가 죽을 때마다 아이템을 드롭하는 함수를 호출합니다.
        monster.OnMonsterDeath += ItemManager.Instance.DropItem;
    }

    private void HandleMonsterDeath(Vector3 position)
    {
        Debug.Log("Monster died at position: " + position);
    }
}

게임 매니저에서 몬스터 처치 이벤트를 아이템 매니저가 구독하고 아이템을 생성하는 시스템 입니다.

[코드의 장점]

정말 핵심적인 시스템만을 설계하였기 때문에 어떤 게임이든 베이스로 사용이 가능합니다. 또한 많은 추가 기능이 없기에 이해가 쉬워 바로 사용이 가능합니다.

또한 아이템 매니저와 몬스터가 이벤트 시스템으로 서로 떨어진 관계가 되어 확장성과 유연성이 증가 합니다. 만약 아이템 드롭 로직이 몬스터 자체에 포함돼 있다면, 가독성도 떨어질 뿐더러, 확장성도 떨어집니다. 몬스터 뿐아니라 만약 나무 파괴 시 아이템이 떨어진다면, 그때마다 드롭 로직을 다시 작성해야 하니 좋은 코드 작성 방식이 아닙니다.

[전체 코드]

using UnityEngine;
using System;
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json;

// 아이템 클래스
public class Item
{
    public string Name { get; private set; }
    public ItemType Type { get; private set; }

    public Item(string name, ItemType type)
    {
        Name = name;
        Type = type;
    }
}

// 아이템 타입 열거형
public enum ItemType
{
    Sword,
    Potion,
    Gold
}

// 아이템 매니저 클래스 (싱글톤)
public class ItemManager : MonoBehaviour
{
    private static ItemManager instance;
    private List<ItemData> itemDataList;
    private System.Random random;

    private ItemManager() { }

    public static ItemManager Instance
    {
        get
        {
            if (instance == null)
            {
                instance = FindObjectOfType<ItemManager>();
                if (instance == null)
                {
                    GameObject obj = new GameObject();
                    obj.name = "ItemManager";
                    instance = obj.AddComponent<ItemManager>();
                }
            }
            return instance;
        }
    }

    private void Awake()
    {
        random = new System.Random();
        LoadItemData();
    }

    private void LoadItemData()
    {
        string filePath = Application.dataPath + "/items.json";
        if (File.Exists(filePath))
        {
            string json = File.ReadAllText(filePath);
            itemDataList = JsonConvert.DeserializeObject<List<ItemData>>(json);
        }
        else
        {
            Debug.LogError("Item data file not found!");
        }
    }

    public Item GenerateRandomItem()
    {
        if (itemDataList == null || itemDataList.Count == 0)
        {
            Debug.LogError("No item data loaded!");
            return null;
        }

        float totalProbability = 0f;
        foreach (ItemData itemData in itemDataList)
        {
            totalProbability += itemData.probability;
        }

        float randomValue = (float)random.NextDouble() * totalProbability;
        float cumulativeProbability = 0f;
        foreach (ItemData itemData in itemDataList)
        {
            cumulativeProbability += itemData.probability;
            if (randomValue <= cumulativeProbability)
            {
                return new Item(itemData.name, itemData.type);
            }
        }

        return null;
    }

    public void DropItem(Vector3 position)
    {
        Item item = GenerateRandomItem();
        if (item != null)
        {
            Debug.Log("Dropped item: " + item.Name);
            // 여기서 아이템을 생성하고 떨어뜨릴 수 있습니다.
        }
    }
}

// 아이템 데이터 클래스
[Serializable]
public class ItemData
{
    public string name;
    public ItemType type;
    public float probability;
}

// 몬스터 클래스
public class Monster : MonoBehaviour
{
    public event Action<Vector3> OnMonsterDeath; // 몬스터가 죽을 때 발생하는 이벤트

    private void Die()
    {
        // 몬스터가 죽었을 때 실행되어야 할 작업들을 수행합니다.
        if (OnMonsterDeath != null)
        {
            OnMonsterDeath(transform.position);
        }
    }

    private void OnDestroy()
    {
        Die();
    }
}

// 게임 매니저 클래스
public class GameManager : MonoBehaviour
{
    private void Start()
    {
        // 몬스터 생성
        Monster monster = new GameObject("Monster").AddComponent<Monster>();

        // 몬스터가 죽을 때 아이템을 드랍하도록 이벤트를 구독합니다.
        monster.OnMonsterDeath += HandleMonsterDeath;

        // 몬스터가 죽을 때마다 아이템을 드롭하는 함수를 호출합니다.
        monster.OnMonsterDeath += ItemManager.Instance.DropItem;
    }

    private void HandleMonsterDeath(Vector3 position)
    {
        Debug.Log("Monster died at position: " + position);
    }
}

[마무리 글]

해당 코드들을 잘 이해하셔서 추가적인 기능을 만드셔서 자신만의 코드로 가지고 계시면 어떤 프로젝트든 아이템 드랍 시스템을 빠르게 구현하실 수 있을 것 입니다. 또한 아이템 드랍의 랜덤 확률 같이 유틸성 코드는 어디서든 사용하실 수 있도록 유틸리티 코드로 따로 작성해 두시는 것을 추천 드립니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다