개발/Unity·C#

[유니티/C#] Scriptable Object(스크립터블 오브젝트) 활용하기

은성. 2025. 1. 26. 11:30

Scriptable Object 활용하기


유니티의 스크립트는 기본적으로 MonoBehaviour(모노 비헤이비어)라는 클래스를 사용한다.
하지만 MonoBehaviour는 동작과 로직 중심의 클래스이기 때문에, 데이터를 관리하는 데에는 부적합하다.
예를 들어 캐릭터의 능력치, 아이템의 정보같은 것들 말이다.

오늘은 유니티에서 데이터 관리에 최적화된 클래스인 Scriptable Object가 무엇인지 살펴보고, 그 활용 방법을 찾아내보고자 한다.

 


Scriptable Object란?

Scriptable Object는 유니티에서 제공하는 데이터를 저장하고 공유하기 위한 객체이다.
MonoBehaviour와는 달리, Scriptable Object는 게임 오브젝트와 연결되지 않은 채로도 존재하며 데이터 중심적으로 설계되어 있다.

Scriptable Object의 장점


▶ 데이터 재사용성

여러 오브젝트에서 같은 데이터를 참조할 수 있어 반복 작업을 줄여준다.

▶ 메모리 효율성

데이터를 인스턴스화하지 않고 공유하기 때문에 메모리 사용량을 절감할 수 있다.

▶ 테스트 용이성
플레이 모드에서 데이터를 실시간으로 수정하고 확인할 수 있어 빠르게 테스트가 가능하다.

▶ 유지보수 간소화
중앙에서 데이터를 관리하므로 수정 사항이 모든 관련 오브젝트에 즉시 반영된다.

 

Scriptable Object 사용 사례


MonoBehaviour와 비교해서 Scriptable Object를 언제 사용하느냐 한다면, 이렇게 비교해볼 수 있다.

  • MonoBehaviour: 캐릭터 이동, 전투 시스템 구현
  • Scriptable Object: 캐릭터 스탯 데이터 관리, 적 스탯 데이터 관리

 


Scriptable Object 생성하기

Scriptable Object 생성하기


새로운 Scriptable Object 클래스를 생성하려면 [Project] 패널을 우클릭한 뒤 [Create] - [Scripting] - [ScriptableObject Script] 를 선택해 생성해주면 된다.

새로운 Scriptable Object


그러면 이렇게 새로운 Scriptable Object가 생성된다.

클래스 위쪽의 [CreateAssetMenu] 부분은 유니티에서 Scriptable Object를 에셋 파일로 생성할 수 있도록 에디터에 메뉴 항목을 추가해주는 역할이다.

  • fileName: Scriptable Object 에셋 생성 시 기본 파일 이름
  • menuName: 에디터에서 [Create] 메뉴에 표시되는 항목 이름
  • order: (선택) 메뉴 내에서 항목이 표시되는 순서 결정, 숫자가 낮을 수록 위쪽에 나타난다. (기본값 1000)

 

Project 패널에서 Scriptable Object 생성하기


[CreateAssetMenu] 어트리뷰트 덕분에,
이렇게 Project 패널에서 우클릭 후 [Create] - [Scriptable Objects] - [(클래스명)] 으로 위에서 생성한 클래스의 새로운 Scriptable Object 에셋을 생성할 수 있다.

이하에서는 혼선을 방지하기 위해 다음과 같이 구분하여 칭하고자 한다.

  • Scriptable Object 형식을 저장하는 스크립트는 'Scriptable Object 클래스'
  • Scriptable Object 클래스로 생성된 에셋은 'Scriptable Object 에셋'

 


활용 예시: 캐릭터 스탯 데이터 생성하기

1) "CharacterStat" Scriptable Object 클래스 생성하기

using UnityEngine;

[CreateAssetMenu(fileName = "CharacterStat", menuName = "Scriptable Objects/CharacterStat")]
public class CharacterStat : ScriptableObject
{
    public string characterName;
    public int hp;
    public int attackPower;
}


Scriptable Object 클래스로 캐릭터 스탯의 구조를 생성한다. 캐릭터 이름, HP, 공격력 정보를 들고 있다.
예제를 위해 간소화했지만, 실제 RPG 게임이라면 방어력, 크리티컬 확률, 크리티컬 데미지 퍼센트 등 다른 스탯도 가지고 있을 수 있다.

2) "CharacterStatA", "CharacterStatB" Scriptable Object 에셋 생성하고 설정하기

Inspector에서 편집 가능하다.


CharacterStat 클래스로 CharacterStatA, CharacterStatB 라는 Scriptable Object 에셋들을 생성한다.

생성된 에셋은 [Inspector] 패널에서 편집할 수 있다.
각각의 에셋에 캐릭터 이름, HP, 공격력 정보를 입력해준다.

 

3) MonoBehaviour 스크립트로 Scriptable Object 데이터 사용하기

1. "CharacterSimulator" MonoBehaviour 스크립트 생성하기

MonoBehaviour 스크립트 생성 방법


Scriptable Object를 활용하려면 먼저 동작을 구현할 MonoBehaviour 스크립트를 생성해야 한다.
[Project] 패널 우클릭 후, [Create] - [Scripting] - [MonoBehaviour Script] 를 눌러 MonoBehaviour 스크립트를 생성할 수 있다.

2. 프로퍼티로 에셋을 할당하고 Instantiate로 사본 인스턴스 생성하기

using UnityEngine;

public class Character : MonoBehaviour
{
    public CharacterStat charStatData; // 원본 데이터
    private CharacterStat charStat; // 사본

    void Start()
    {
        // Scriptable Object 복사본 생성
        charStat = Instantiate(charStatData);
    }
}


Character 스크립트에 위에서 만든 ScriptableObject 에셋을 할당해 줄 변수를 선언해주었다.

다만 할당된 ScriptableObject('charStatData')를 직접 사용하여 로직을 작동할 경우 해당 데이터를 참조하고 있는 다른 오브젝트가 함께 영향을 받기 때문에, 원본 데이터를 보존하기 위해 실제로는 에셋을 인스턴스화한 'charStat' 프로퍼티를 사용할 것이다.

이렇게 하면 예를 들어 "CharacterStatA"를 사용하는 'charA1', 'charA2' 캐릭터가 있을 경우 charA1이 공격받았는데 charA2의 체력도 깎이는 등의 불상사를 면할 수 있다.

 

Inspector 설정하기


이후 [Hierarchy]에 새로운 빈 게임 오브젝트를 생성하고, [Add Component]로 스크립트를 추가해준 뒤
[Inspector]에서 CharStatData 에 해당하는 Scriptable Object 에셋을 할당해주면 된다.

MonoBehaviour 스크립트의 동작과 연계하는 활용법은 아래의 예제를 통해 구체적으로 살펴보도록 하자.


예제: 전투 시뮬레이터 구현하기

[개발/Unity·C#] - [유니티/C#] 주기적으로 반복되는 로직 구현하기 w. 코루틴(Coroutine)

 

[유니티/C#] 주기적으로 반복되는 로직 구현하기 w. 코루틴(Coroutine)

게임을 만들다 보면, 시간마다 반복적으로 특정 함수를 실행해야 하는 경우가 있다.예를 들어 1초마다 방치형 골드를 획득하게 한다거나, 독 디버프에 걸려 3초마다 HP를 잃어야 한다거나..이런

hes527u.tistory.com

이런 코루틴의 시스템을 활용한다면 간단한 싸움 시뮬레이터를 만들 수도 있을 것이다.
예시 구문은 첨부하지 않겠지만.. 아이디어를 첨부하자면 이런 식이다.
- A는 hp가 100, 3초에 한 번 10의 hp를 회복, 2초에 한 번 30의 데미지로 공격
- B는 hp가 120, 4초에 한 번 15의 hp를 회복, 1초에 한 번 25의 데미지로 공격
(누가 이길까? 계산을 안 해봐서 누가 더 유리한지 모르겠다.)


오늘의 예제는 위의 코루틴 포스팅에서 첨부한 아이디어를 기반으로 실행해보려 한다.
(다만 원본대로 하면 밸런스가 안 맞아서 무한 전투가 이루어지니 공격력을 좀 더 높이도록 하겠다.)

 

1. CharacterStat ScriptableObject 클래스 설정하기

이 예제를 실행하려면 "CharacterStat" ScriptableObject 클래스를 다음과 같이 약간 변경해주어야 한다.
주석을 달아둔 부분이 추가된 부분이다.

using UnityEngine;

[CreateAssetMenu(fileName = "CharacterStat", menuName = "Scriptable Objects/CharacterStat")]
public class CharacterStat : ScriptableObject
{
    public string characterName;
    public int hp; 
    public int hpRecoveryCool; // HP 회복 쿨
    public int hpRecoveryAmount; // HP 회복량
    public int attackCool; // 공격 쿨
    public int attackPower;    
}

 

에셋 내용 변경


프로퍼티가 추가되었기 때문에 "CharacterStatA", "CharacterStatB" Scriptable Object 에셋도 이렇게 바꿔 주었다.

2. 공격 시뮬레이터 코루틴 만들기

using System.Collections;
using UnityEngine;

public class Character : MonoBehaviour
{
    public CharacterStat charStatData; // 원본 데이터
    [HideInInspector]
    public CharacterStat charStat; // 사본
    public Character target; // 공격 대상

    void Start()
    {
        // Scriptable Object 복사본 생성
        charStat = Instantiate(charStatData);

        // 코루틴 시작
        StartCoroutine(Recovery());
        StartCoroutine(Attack());
    }

    IEnumerator Recovery()
    {
        while(true)
        {
            yield return new WaitForSeconds(charStat.hpRecoveryCool);
            
            if (charStat.hp < charStatData.hp && charStat.hp > 0)
            {
                // hp가 Max 체력보다 낮고 hp > 0일 때에만 회복
                charStat.hp += charStat.hpRecoveryAmount;
                Debug.Log($"{gameObject.name} Recovery {charStat.hpRecoveryAmount} : {charStat.hp}");
            }
        }
    }

    IEnumerator Attack()
    {
        while(true)
        {
            yield return new WaitForSeconds(charStat.attackCool);

            if (charStat.hp > 0)
            {
                // hp > 0일 때에만 공격 가능
                target.charStat.hp -= charStat.attackPower;        
                Debug.Log($"{gameObject.name} Attacked {target.gameObject.name} {charStat.attackPower} : {target.charStat.hp}");

                if (target.charStat.hp <= 0)
                {
                    // 타겟의 hp가 0이 되면 게임 종료
                    Debug.Log($"Game End! {gameObject.name} Defeated {target.gameObject.name} : {target.charStat.hp}");
                    break;
                }
            }
        }        
    }
}


우선 코드 전문이다.

코루틴 Recovery, Attack으로 회복, 공격 로직을 구현했다.
간소화한 구현으로 따로 '죽음' 상태가 있는 것은 아니라, hp <= 0을 기준으로 죽음을 체크했다.

조금 다른 점은 타겟의 charStat을 다른 스크립트에서 사용해야 하기 때문에,
charStat 프로퍼티를 public으로 바꿔준 대신 [HideInInspector] 어트리뷰트를 붙여주었다.
이렇게 되면 실제로는 public이지만 private처럼 [Inspector] 패널에서 할당해주지 않아도 된다.

3. 씬 세팅하기

캐릭터 A와 B 설정하기


[Hierarchy]에 "CharacterA"와 "CharacterB" 오브젝트를 생성한다.
나는 캡슐로 생성해주었는데 [Hierarchy] 우클릭 - [2D Object] - [Sprite] - [Capsule] 을 누르면 동일하게 생성할 수 있다.
적당한 크기와 위치로 잡아주면 된다.

CharacterA와 B에 각각 Character 스크립트를 할당해주고,
각각 타입에 맞는 CharStatData와 Target을 할당해준다. (Target은 본인이 아니라 상대 오브젝트를 넣어주면 된다.)

 

4. 실행 결과

(스포주의)


이렇게 실행하면 로그로 전투 상황과 결과가 남는다.
전투가 종료되었을 때 "Game End!" 로그가 찍히며 누가 이겼는지 보여준다.