게임 개발/유니티 엔진

유니티의 스크립터블 오브젝트(Scriptable Object)란 무엇일까

술단고 2025. 4. 6. 20:35

스크립터블 오브젝트를 활용한, 공유 자원 데이터 컨테이너

 

 

Scriptable Object가 무엇일까?

 

좋은 설계의 기준에는 항상 쉬운 유지보수가 따라붙는다.

게임을 만들 때도 항상 추후에 밸런스 조절이나 테스트를 위해 값을 조정해야 하는 일이 많다.

 

지금까지 우리는 단순히 그런 값을 로직 안에 명세해두거나,

SerializeField로 직렬화해서 인스펙터 내에서 적어두는 것이 한계였다.

 

로직 내에 HP나 공격력을 적고, 인스펙터에서 값을 조정한다

 

 

캐주얼한 게임이나 가벼운 게임, 유일한 객체 등에서는 이렇게 만드는 것이 좋을 수 있다.

그러나 만약 플레이어블 캐릭터가 여러 개이거나,

게임 내 등장하는 무기가 수십개가 될 경우, 이는 매우 좋지 않은 선택이 될 수 있다.

 

밸런스 조절이나 테스트를 할 때 일일히 해당 클래스로 가서 값을 수정해야 하고,

이는 하드코딩적인 성질을 띄게 되며 기획자나 아티스트와의 협업도 매우 힘들다.

 

누가 객체지향 프로그래밍에서 그렇게 만들어요.
무기가 여러개라면, 부모 무기 클래스를 만들어서 자식 클래스가 값을 오버라이딩하면 되죠.

 

 

아주 훌륭하고 일반적인 접근이다.

무기가 수십개라면, 무기 베이스 클래스를 만들어서 값을 미리 정의해두고,

자식 무기 클래스들이 Start() 메서드 등에서 값을 지정하게끔 할 수 있다.

public class Weapon : WeaponBase
{
    // 게임 내에 등장하는 특정 무기 클래스
    void Start()
    {
        this.damage = 10;
        this.fireRate = 2;
        this.range = 5;
    }
}

 

구조적으로는 훨씬 개선되었지만,

사실 여전히 Start() 메서드 문 안에서 직접 값을 하드하게 명시한다는 문제점은 그대로 남아있다.

 

이는 아까 말했던 밸런스 조절 및 테스트에서 해당 클래스를 일일히 방문해 수정하는 문제나, 기획자 아티스트와 협업이 어려운 문제점이 고스란히 남아있으며, 무기 간 공격력이나 공격속도 등을 비교할 때도 매우 시간이 오래 걸린다.

 

그럴 때 사용하는 것이 바로 스크립터블 오브젝트(Scriptable Object)이다!

ScriptableObject는 씬에 존재하지 않아도 되는 독립적인 데이터 컨테이너이며, “밸런스 조정이 자주 필요한 데이터”를 코드 바깥으로 빼서, 관리와 협업 및 밸런스 조절을 위한 값 변경을 훨씬 쉽게 만들어주는 도구다.

 

 

스크립터블 오브젝트로 코드를 효율적으로 변경하고 디버깅할 수 있도록 설계 | Unity

스크립터블 오브젝트로 코드를 설계하면 코드를 유연하고 간편하며 디버깅에 용이한 상태로 유지할 수 있습니다.

unity.com

 

게임을 기획하다보면, 이런 생각이 들 때가 있다.

무기별 공격력 등의 컬럼을 스크립트 내에서 명시하지 말고,

차라리 엑셀 파일 등에 데이터 테이블을 깔끔하게 정리해놓고, 거기서 참조해오는게 낫지 않을까?

 

이렇게 별도로 데이터 테이블을 만드는 것은 큰 규모의 게임을 만들 때 필수불가결한 접근법이다.

그러나 Unity에는 스크립터블 오브젝트가 있기에, 굳이 엑셀과 게임 로직의 통신 문법 및 파싱 방법을 힘들게 공부할 필요가 없어진다.

 

Scriptable Object 사용법

우선 Scriptable Object는 C# 스크립트이기 때문에, 이걸 사용한다는 것은 MonoBehaviour를 상속받지 않는다. 아니, 사용할 필요가 없다.

항목 MonoBehaviour ScriptableObject
씬에 존재해야 함 ✅ 존재해야 한다. ❌ 존재할 필요가 없다.
GameObject에 붙여야 작동 ✅ 컴포넌트로 작동한다. ❌ 컴포넌트에 붙일 필요 없다.
에디터에서 인스턴스 생성 ✅ CreateAssetMenu
데이터 재활용 어렵다 아주 쉬움
이벤트/로직 처리 주로 여기에 데이터 중심 처리에 적합

 

 

만약 FPS게임을 만든다고 치면, 수십~수백개의 총기가 존재하게 될 것이다.

우선 아래와 같이, 모든 총들이 사용하게 될 데이터 컨테이너를 만든다.

using UnityEngine;

[CreateAssetMenu(fileName = "NewGunData", menuName = "Data/Gun")]
public class GunData : ScriptableObject
{
    public float damage; // 공격력
    public float fireRate; // 공격 속도
    public float range; // 사거리
}

 

유의할 점은, MonoBehaviour를 상속받지 않는다는 것이다.

위에 있는 대괄호(리플렉션)은 이 컨테이너를 사용한 새로운 데이터 파일 이름과, 메뉴에 표시될 이름을 설정하는 것이다.

이 스크립트를 만들고 저장한 뒤, 유니티 에셋창을 우클릭해보자.

 

스크립터블 오브젝트에서 정의한대로, 새로운 에셋을 만들 수 있게 되었다!

 

이번 게임에서 쓸 예시로 '나무 총' 이라는 귀여운 아이템을 만들어보겠다.

 

 

해당 데이터를 만들고 WoodGunData라고 이름을 지으니 새로운 데이터가 만들어졌다.

이걸 클릭하고 인스펙터 창을 보면,

 

인스펙터에서 보이는 데이터 명시 장소

 

스크립터블 오브젝트에서 선언한 변수의 자료형에 맞게

직접 인스펙터에서 데이터를 명시할 수 있는 창이 등장했다!

그럼 이제 Wood Gun 클래스를 만들어서 이 정보를 따른다고 명시만 하면 완성이다.

 

using UnityEngine;

public class WoodGun : MonoBehaviour
{
    public GunData data;

    void Shoot()
    {
        Debug.Log($"공격력: {data.damage}, 발사속도: {data.fireRate}");
    }
}

 

위와 같은 총 클래스를 만들었다.

물론 Shoot() 등의 모든 총기가 사용할 수 있는 메서드가 있다면 부모 클래스에서 정의하는 편이 좋다.

그러나, 우선 쉬운 예시 코드를 위해 부모 클래스를 상속받는 구조로 코드를 작성하지는 않았다.

 

총기 오브젝트에 Wood Gun 스크립트를 컴포넌트로 넣고, 데이터를 삽입한다.

 

이제 해당 스크립트를 가졌다면 원하는 데이터 컨테이너를 삽입할 수 있게 된다!

이후 테스팅해본다면 제대로 결과가 나오는 것을 볼 수 있을 것이다.

 


 

진짜 Scriptable Object 사용법

 

똑똑한 독자라면 위에서 Wood Gun오브젝트에 Wood Gun 데이터를 삽입하는 부분에서,

뭔가 이상함을 감지하거나, 더 나은 개발 방법을 떠올릴 것이 분명하다.

 

잠깐... 생각해보니 이럴거라면
굳이 여러 종류의 총기 클래스를 하나하나 정의할 필요 없이,
하나의 총기 클래스만 만들어서 그때그때 데이터만 바꾸면 되는거 아냐?

 

 

바로 이것이 진짜 Scriptable Object를 사용하는 방법이자 목적이다!

 

class AK47 : Gun { ... }
class Shotgun : Gun { ... }
class Sniper : Gun { ... }

 

 이렇게 각종 총기를 만들어서 부모 총기도 따로 상속받고,

데이터도 하나하나 다 보유하고 있게 하면, 클래스가 너무 많아지고 유지보수가 너무나 힘들어진다.

 

그렇다면 플레이어가 장착한 총기를 다른 총기로 바꿀 때,

진짜 그 총기 클래스가 다른 총기 클래스로 바뀌는것이 아니라,

총기 클래스는 애초에 하나이고, 데이터와 외형만 바뀌는 것이라면?

 

클래스 하나로 다양한 총기를 전부 구현하게 될 수 있는 것이다!

 

public class Gun : MonoBehaviour
{
    public GunData data;

    public void Fire()
    {
        // 모든 총이 공통으로 사용하는 로직
        SpawnBullet();
        ApplyRecoil(data.recoil);
        DealDamage(data.damage);
    }
}

 

총기 클래스를 만들어서 공통으로 사용하는 로직을 정의하고,

총기를 바꿀 때 새로운 데이터만 사용하면 그만이다.

public void EquipGun(GunData newGunData)
{
    myGun.data = newGunData;
}

 

이렇게 만든다면 수많은 총기들을 손쉽게 관리할 수 있게 된다!

 

[CreateAssetMenu(fileName = "NewGunData", menuName = "Data/Gun")]
public class GunData : ScriptableObject
{
    public string gunName;

    [Header("Stat")]
    public float damage;
    public float fireRate;
    public float recoil;

    [Header("Prefab & Model")]
    public GameObject bulletPrefab;
    public GameObject gunModelPrefab;

    [Header("Effects")]
    public AudioClip fireSound;
    public AudioClip reloadSound;
    public ParticleSystem muzzleFlash;
    public AnimationClip reloadAnimation;

    [Header("UI")]
    public Sprite gunIcon;
    public string description;
}

 

총기 이름과 데미지, 공격속도, 반동값, 아이템 레어도 등은 물론이거니와

총 3D 모델부터 총알 프리팹까지,

각종 발사 효과음과 머즐에서 터지는 파티클, UI에 적용할 아이콘도 별도로 관리할 수 있게 된다!

 

언리얼 데이터 테이블에서 만들었던 유사한 기능

 

 

이렇게 핵심 로직은 그대로 두고, 변경하게 되는 데이터만 바꾸는 것을

데이터 중심 설계 (Data-Driven Design) 라고 한다.

 

혹은 행동의 파라미터, 속성값을 바꾸는 전략 패턴이라고도 볼 수 있으며,

데이터를 공유해 메모리를 아끼는 것이 플라이웨이트 패턴과도 유사성이 있다.

 


마무리

아이템 데이터 테이블 등으로 쉽게 응용할 수 있는 스크립터블 오브젝트에 대해 알아보았다.

하지만 스크립터블 오브젝트의 위대함은 여기서 멈추지 않는다.

 

이벤트 채널을 만들때도 쓸 수 있고, (이벤트 버스 패턴, 옵저버 패턴)

델리게이트 오브젝트를 만들어 메서드 자체를 들고 있을 수도 있다. (전략 패턴)

 

외에도 다양한 활용법이 있으니 공식 문서를 참조하길 바라며,

더 윤택한 확장성을 가진 게임을 개발하는 데에 잔뜩 사용하길 바란다.

 

 

협업과 코딩 측면에서 유용한 스크립터블 오브젝트 활용법 6가지

스크립터블 오브젝트는 데이터 컨테이너 외에도 다양한 방식으로 활용될 수 있습니다. 스크립터블 오브젝트를 통해 디자인 패턴을 구현하여 코드 아키텍처를 개선하고, Unity 워크플로를 단축하

unity.com