Humility

아무리 노력해도 최고가 되지 못할 수 있다⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀그럼에도 노력하는자가 가장 겸손한 것 아닌가

공부하는 블로그

유니티/문법

[Unity] 유니티 C#) ScriptableObject ( 스크립터블오브젝트 )

새벽_글쓴이 2025. 1. 2. 13:04
반응형

스크립터블오브젝트란?

유니티에서 제공하는 데이터 컨테이너 클래스로, 게임 내의 데이터를 관리하고 저장하는데 매우 유용하다

 

주요 특징

  • 프리팹처럼 에셋으로 저장됩니다. Project 창에서 직접 생성하고 관리할 수 있다
  • MonoBehaviour와 달리 GameObject에 부착할 필요가 없다
  • 메모리 관리가 효율적입니다. 같은 ScriptableObject를 여러 곳에서 참조해도 메모리에 한 번만 로드된다
  • Unity 인스펙터에서 직접 데이터를 수정할 수 있다

 

스크립터블 오브젝트 만드는 법

C# 스크립트 생성

// ItemData.cs
using UnityEngine;

[CreateAssetMenu(fileName = "New Item", menuName = "Inventory/Item")]
public class ItemData : ScriptableObject
{
    public string itemName;
    public Sprite itemIcon;
    public int itemPrice;
    public string description;
}

 

사용 방법

 

1. Project 창에서 우클릭

2. Create > Inventory > Item 선택

3. 생성된 아이템 데이터를 인스펙터에서 수정

4. 스크립트에서 참조하여 사용

 

주의사항

1. 런타임 데이터 변경 주의

// 권장되는 방법: 런타임용 복사본 생성
public class GameManager : MonoBehaviour 
{
    public GameData originalData;
    private GameData runtimeData;

    void Awake() 
    {
        // 실행 중 사용할 복사본 생성
        runtimeData = Instantiate(originalData);
    }
}

 

 

2. 순환 참조 방지

// 순환 참조가 발생할 수 있는 예시
public class ItemData : ScriptableObject
{
    public WeaponData weaponData;  // WeaponData를 참조
}

public class WeaponData : ScriptableObject
{
    public ItemData itemData;  // ItemData를 참조
    // 이런 순환 참조는 직렬화 문제와 메모리 누수를 일으킬 수 있음
}

 

3. 리소스 관리

  • Resources 폴더에 너무 많은 ScriptableObject를 넣으면 초기 로딩 시간이 길어짐
  • 큰 데이터셋은 적절히 분리하여 관리

 

4. 직렬화 제한 이해

public class DataContainer : ScriptableObject 
{
    // Dictionary 등 직렬화 불가능한 자료구조 사용 불가
    public Dictionary<string, int> data; // 작동하지 않음
    
    // 직렬화 가능한 구조로 변경
    [System.Serializable]
    public class SerializableKeyValuePair
    {
        public string key;
        public int value;
    }
    public List<SerializableKeyValuePair> serializableData;
}

 

데이터 변경

런타임 중 값을 변경하면 원본값이 변하게 된다

 

ScriptableObject의 기본 원리

ScriptableObject는 참조 타입(Reference Type)으로, 에셋으로 저장되는 데이터 컨테이너이다.
게임 오브젝트에서 ScriptableObject를 참조할 때, 실제로는 에셋 파일의 동일한 인스턴스를 참조하게 된다

 

예시

[CreateAssetMenu(fileName = "PlayerData", menuName = "Game/PlayerData")]
public class PlayerData : ScriptableObject
{
    public int health = 100;
}

// 여러 곳에서 사용
public class Player1 : MonoBehaviour
{
    public PlayerData playerData;  // 에셋 참조
    
    void DamagePlayer()
    {
        playerData.health -= 10;   // 원본 데이터 직접 수정
    }
}

public class Player2 : MonoBehaviour
{
    public PlayerData playerData;  // 같은 에셋 참조
    
    void CheckHealth()
    {
        // Player1이 수정한 값이 여기서도 반영됨
        Debug.Log(playerData.health);
    }
}

 

이유

메모리 효율성을 위해 Unity는 ScriptableObject를 싱글톤처럼 관리
프로젝트의 모든 참조가 동일한 메모리 주소를 가리킴
한 곳에서 수정하면 모든 참조에 영향을 미침

 

이는 의도적인 디자인이다
그렇지만 런타임 중 데이터 변경이 필요한 경우에는
복사본을 만들어 사용하는 것이 안전하다

 

값 변경 관련 주의사항

1. 참조 타입 데이터 처리

[CreateAssetMenu(fileName = "GameData", menuName = "Game/GameData")]
public class GameData : ScriptableObject
{
    public List<int> scores = new List<int>();  // 참조 타입
    public int currentLevel;  // 값 타입
}

 

2. 런타임 데이터 초기화 방법

public class GameDataManager : MonoBehaviour
{
    public GameData gameData;
    private GameData runtimeData;

    void Awake()
    {
        // 런타임용 복사본 생성
        runtimeData = Instantiate(gameData);
    }
}

 

3. 안전한 데이터 접근을 위한 래퍼 클래스 사용

[CreateAssetMenu(fileName = "ItemDatabase", menuName = "Game/ItemDatabase")]
public class ItemDatabase : ScriptableObject
{
    [SerializeField]
    private List<ItemData> items = new List<ItemData>();

    // 읽기 전용 프로퍼티로 제공
    public IReadOnlyList<ItemData> Items => items;
}

 

권장되는 사용 패턴

1. 읽기 전용 데이터로 사용

[CreateAssetMenu(fileName = "Constants", menuName = "Game/Constants")]
public class GameConstants : ScriptableObject
{
    [SerializeField] private int maxHealth;
    public int MaxHealth => maxHealth;  // 읽기 전용 프로퍼티
}

 

2. 런타임 데이터 리셋 구현

public class RuntimeDataResetter : MonoBehaviour
{
    [SerializeField] private GameData gameData;
    private GameData originalData;

    void Awake()
    {
        // 원본 데이터 백업
        originalData = Instantiate(gameData);
    }

    void OnDisable()
    {
        // 게임 종료시 원본 데이터로 복원
        gameData.ResetToDefault(originalData);
    }
}

 

3. 이벤트 기반 업데이트 사용

[CreateAssetMenu(fileName = "PlayerStats", menuName = "Game/PlayerStats")]
public class PlayerStats : ScriptableObject
{
    public event System.Action<int> OnHealthChanged;
    private int health;

    public void UpdateHealth(int newHealth)
    {
        health = newHealth;
        OnHealthChanged?.Invoke(health);
    }
}

 

결론

장점

1. 메모리 효율성

여러 곳에서 참조해도 메모리에 한번만 로드됨
프리팹 인스턴스마다 데이터 복제가 필요없음

 

2. 데이터 관리 용이성

중앙집중식 데이터 관리 가능
인스펙터에서 쉽게 수정 가능
씬 간에 데이터 공유가 쉬움

 

단점

1. 런타임 데이터 관리의 위험성

직접 참조하면 원본 데이터가 수정될 수 있음
의도치 않은 데이터 변경이 영구적으로 저장될 수 있음

 

2. 직렬화 제한

Dictionary 등 일부 자료구조 직렬화 불가
복잡한 데이터 구조 저장의 제한

 

스크립터블오브젝트는 적절한 상황에서 사용하면 매우 강력하지만,
데이터 변경 시 주의가 필요한 도구이다.
그렇기에, 런타임 데이터는 반드시 복사본을 사용하는 것이 안전하다

 

반응형