본문 바로가기
Unity/Undead Survivor

스크립터블 오브젝트(Scriptable Object) / 능력 업그레이드(Ability Upgrade)

by hwan91 2025. 3. 18.

Scriptable Object

ScriptableObject는 보통 MonoBehaviour를 상속받아 게임 오브젝트와 함께 동작하는 스크립트와 달리 씬에 존재하지 않아도 되는 데이터만 저장하는 객체이다.

게임에서 반복적으로 사용되는 데이터(아이템, 능력치 등)의 설정을 줄이고, 저장 및 효율적으로 관리하는 데 사용된다.

 

왜 사용할까?

- 데이터 재사용의 편리성 : 한 번 생성해두면 여러 오브젝트에서 공유 가능

- 메모리 절약 : MonoBehaviour 기반 스크립트는 씬이 로드될 때마다 새롭게 생성되지만, ScriptableObject는 같은 데이터라면 하나만 로드해서 공유 가능

- 유니티 에디터에서 편집 가능 : 에셋 파일로 저장되어 Unity 에디터에서 직접 값을 변경할 수 있음, CreateAssetMenu 속성을 사용하면 에디터에서 ScriptableObject를 쉽게 생성 가능

 

MonoBehaviour vs ScriptableObject 차이점

  MonoBehaviour ScriptableObject
목적 게임 오브젝트의 동작 관리 데이터 저장 및 공유
씬 내 존재 씬에 있어야 함 씬에 없어도 됨
수명 주기 씬이 바뀌면 사라짐 씬이 바뀌어도 유지
메모리 관리 씬이 변경되면 다시 생성 같은 데이터를 계속 사용 가능

 

사용 방법

1) ScriptableObject 클래스 생성

using UnityEngine;

// [CreateAssetMenu(...)] : Unity 에디터에서 새 ScriptableObject를 만들 수 있도록 메뉴 추가
// fileName.. -> 새 파일을 만들면 기본 이름이 "Item"
// menuName.. -> Unity에서 "ScriptableObject" 폴더 안에 "ItemData" 항목이 생김
[CreateAssetMenu(fileName = "Item", menuName = "ScriptableObject/ItemData")]
public class ItemData : ScriptableObject
{
    public string weaponName; // 무기 이름
    public int damage;        // 공격력
    public float attackSpeed; // 공격 속도
    public Sprite icon;       // 무기 아이콘
}

 

2) 필요한 스크립트에 ScriptableObject 데이터 적용

3) Assets 폴더에서 우클릭 -> "Create/ScriptableObject/IteamData" 선택하여 새로운 무기 데이터를 생성 후 값 입력

4) Item 스크립트가 추가된 무기 오브젝트를 Hierarchy에 생성 후 ItemData 필드에 만들어놓은 ScriptableObject 연결

 


ItemData.cs

ScriptableObject 클래스를 생성하는 역할을 한다.

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

[CreateAssetMenu(fileName = "Item", menuName = "ScriptableObject/ItemData")]
public class ItemData : ScriptableObject
{
    // enum은 열거형(Enumeration)으로 여러 가지 타입을 명확하게 구분할 때 사용
    public enum ItemType { Melee, Range, Glove, Shoe, Heal } // 아이템 타입(종류) 정의
    
    // 아이템의 기본 정보
    [Header("# Main Info")] // 인스펙터에서 변수들을 구분하기 위해 섹션 헤더 추가
    public ItemType itemType; // 아이템 타입
    public int itemId; // 아이템 고유 ID
    public string itemName; // 아이템 이름
    [TextArea] // 인스펙터에서 여러줄의 텍스트 작성 가능하게 변경
    public string itemDesc; // 아이템 설명(Description)
    public Sprite itemIcon; // 아이템 아이콘

    // 아이템 레벨업 시 강해지는 능력치
    [Header("# Level Data")]
    public float baseDamage; // 기본 데미지(레벨 0)
    public int baseCount; // 기본 공격 횟수(레벨 0) - 근거리 : 무기 개수, 원거리 : 관통력
    public float[] damages; // 레벨별 데미지 증가량
    public int[] counts; // 레벨별 공격 횟수 증가량

    // 실제 발사할 Prefab을 연결
    [Header("# Weapon")]
    public GameObject projectile; // 발사체 프리팹(총알, 화살 등)
    public Sprite hand; // 무기를 들고 있는 손 이미지
}

Item.cs

아이템 UI를 관리하고, 버튼을 눌렀을 때 아이템을 장착하거나 레벨업하는 역할을 한다.

 

주요 역할

- 아이템 데이터를 가져와 UI에 표시 (이름, 아이콘, 설명, 레벨)

- 버튼을 눌렀을 때 적절한 동작 수행 (무기 장착, 레벨업, 체력 회복)
- 레벨이 최대치에 도달하면 버튼 비활성화

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

public class Item : MonoBehaviour
{
    // 아이템 정보 저장을 위한 변수들
    public ItemData data; // 아이템 데이터(Scriptable Object)
    public int level; // 아이템 레벨
    public Weapon weapon; // 무기 정보
    public Gear gear; // 장비 정보

    Image icon; // 아이템의 아이콘 이미지
    Text textLevel; // 아이템 레벨을 표시하는 텍스트
    Text textName; // 아이템 이름을 표시하는 텍스트
    Text textDesc; // 아이템 설명을 표시하는 텍스트

    // UI 초기화
    void Awake()
    {
        // GetComponentsInChildren<Image>()는 자식 오브젝트의 Image 컴포넌트들을 가져와 배열로 반환
        icon = GetComponentsInChildren<Image>()[1]; // 두 번째 Iamge 컴포넌트를 아이콘으로 사용
        icon.sprite = data.itemIcon; // 아이콘 이미지 설정

        Text[] texts = GetComponentsInChildren<Text>();
        textLevel = texts[0]; // 레벨 표시
        textName = texts[1]; // 아이템 이름
        textDesc = texts[2]; // 아이템 설명
        textName.text = data.itemName; // 아이템 이름은 초기 설정 후 변경되지 않기 때문에 바로 초기화
    }

    // 오브젝트가 활성화될 때마다 자동 실행
    void OnEnable()
    {
        textLevel.text = "Lv." + (level + 1); // 레벨 표시(Lv. 1부터 시작)

        switch (data.itemType) {
            case ItemData.ItemType.Melee:
            case ItemData.ItemType.Range:
                // string.Format(...) -> 문자열을 만들 때 변수를 삽입할 수 있도록 도와주는 함수
                /* data.damages[level] 값은 0.1, 0.2처럼 소수(배율) 형태로 저장됨
                 * UI에서 백분율료 표기하기 위해 100을 곱해 정수로 변환 */
                textDesc.text = string.Format(data.itemDesc, data.damages[level] * 100, data.counts[level]); // 데이터에 맞는 아이템 설명 설정
                break;
            case ItemData.ItemType.Glove:
            case ItemData.ItemType.Shoe:
                textDesc.text = string.Format(data.itemDesc, data.damages[level] * 100);
                break;
            default:
                textDesc.text = string.Format(data.itemDesc);
                break;
        }
    }

    // 버튼 클릭 이벤트와 연결
    public void OnClick()
    {
        switch (data.itemType)
        {
            // 동일한 로직으로 작동하기 때문에 2개의 case를 붙임
            // 무기 아이템 클릭 시
            case ItemData.ItemType.Melee:
            case ItemData.ItemType.Range:
                if (level == 0) { // 처음 선택하면(level == 0) 무기를 새로 생성
                    GameObject newWeapon = new GameObject();
                    // AddComponent<T> = 오브젝트에 T 컴포넌트를 추가하는 함수
                    // 반환 값을 미리 선언한 변수에 저장
                    weapon = newWeapon.AddComponent<Weapon>();
                    weapon.Init(data);
                }
                else { // 처음 선택이 아니라면
                    float nextDamage = data.baseDamage;
                    int nextCount = 0;

                    nextDamage += data.baseDamage * data.damages[level]; // 기존 데미지에 레벨별 데미지(백분율)를 곱해 레벨업 시 데미지를 설정
                    nextCount += data.counts[level];

                    weapon.LevelUp(nextDamage, nextCount); // 레벨업 함수 호출
                }
                level++;
                break;
            // 장비 아이템 클릭 시
            case ItemData.ItemType.Glove:
            case ItemData.ItemType.Shoe:
                if (level == 0) {
                    GameObject newGear = new GameObject();
                    gear = newGear.AddComponent<Gear>();
                    gear.Init(data);
                }
                else {
                    float nextRate = data.damages[level];
                    gear.LevelUp(nextRate);
                }
                level++;
                break;
            case ItemData.ItemType.Heal:
                GameManager.instance.health = GameManager.instance.maxHealth; // 최대 체력으로 회복
                break;
        }

        if (level == data.damages.Length) // Scriptable Object에 작성한 최대 레벨로 제한
        {
            GetComponent<Button>().interactable = false; // interactable(상호작용) 비활성화
        }
    }
}

Gear.cs

아이템 중에서도 장비(Glove, Shoe 등)를 관리하는 역할을 합니다.

 

주요 역할

- 장비(Glove, Shoe)의 정보를 저장

- 장비가 장착될 때 효과를 적용 (ApplyGear())

- 장비 레벨이 올라갈 때 능력 증가 (LevelUp())

- Glove는 무기 속도를 증가, Shoe는 플레이어 이동 속도를 증가

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

public class Gear : MonoBehaviour
{
    public ItemData.ItemType type; // 장비의 타입
    public float rate; // 능력의 수치 증가율(%)

    // 장비 초기화(처음 장착될 때)
    public void Init(ItemData data)
    {
        // 기본 설정
        name = "Gear " + data.itemId; // 장비의 이름 설정
        transform.parent = GameManager.instance.player.transform; // 플레이어에게 장착(부모를 플레이어로 설정)
        transform.localPosition = Vector3.zero; // 오브젝트 위치를 플레이어에의 중심(0, 0, 0)에 위치하도록 설정

        // 속성 설정
        type = data.itemType;
        rate = data.damages[0];

        ApplyGear();
    }

    // 장비 레벨업
    public void LevelUp(float rate)
    {
        this.rate = rate; // 능력의 새로운 수치를 저장

        ApplyGear(); // 레벨업 효과 반영
    }

    // 장비 효과 적용
    void ApplyGear()
    {
        // 장비의 타입에 따라 로직을 적용
        switch (type) {
            case ItemData.ItemType.Glove: // Glove면 공격 속도를 증가
                RateUP();
                break;
            case ItemData.ItemType.Shoe: // Shoe면 이동 속도를 증가
                SpeedUp();
                break;
            default:
                break;
        }
    }

    // Glove의 기능(공격 속도 증가)
    void RateUP()
    {
        Weapon[] weapons = transform.parent.GetComponentsInChildren<Weapon>();

        float speed;

        // 장비의 타입에 따라 변경되는 속도 설정
        foreach (Weapon weapon in weapons) {
            switch (weapon.id) {
                case 0: // 근거리 무기
                    // 여기서 150은 게임 밸런스를 맞추기 위해 개발자가 설정한 기본 무기 속도
                    speed = 150 * Character.WeaponSpeed; // 기본 공격 속도 설정
                    /* 고정값(speed + rate)을 더하면 속도 증가량이 일정하여 모든 레벨에서 동일하게 추가됨
                    * 비율(speed + speed * rate)을 적용하면 속도가 속도에 비례해 증가해 현재 속도가 높을수록 더 크게 증가함
                    * 즉, 장비 장착 시 속도가 기존 속도의 일정 비율(%)만큼 증가하도록 설계 */
                    weapon.speed = speed + speed * rate;
                    break;
                default: // 원거리 무기
                    speed = 0.5f * Character.WeaponRate; // 기본 발사 간격 설정
                    // 빼기로 점점 작은값을 만들어 발사 간격을 줄임(연사력 증가)
                    weapon.speed = speed * (1f - rate);
                    break;
            }
        }
    }

    // Shoe의 기능(이동 속도 증가)
    void SpeedUp()
    {
        float speed = 3 * Character.Speed; // 기본 이동 속도를 가져와
        GameManager.instance.player.speed = speed + speed * rate; // rate% 만큼 증가시킴
    }
}