ScriptableObject
ScriptableObject는 GameObject에 붙지 않는 독립 데이터 컨테이너로 .asset으로 저장되며 여러 객체가 데이터를 공유합니다. CreateAssetMenu·공유 스탯·Event Channel·Play Mode 편집 주의점을 배우는 입문 가이드입니다.
상상해 보세요...
ScriptableObject는 식당 메뉴와 같습니다. 메뉴(SO)에 요리 정보——이름, 가격, 설명——가 있습니다. 모든 테이블(GameObject)이 같은 메뉴를 참조하고, 각자 복사본을 두지 않습니다. 마스터 메뉴에서 가격을 바꾸면 모든 테이블이 즉시 봅니다. 메뉴는 누가 읽는지 모르고 데이터만 저장합니다.
개념 자세히
ScriptableObject는
UnityEngine.Object를 상속합니다(MonoBehaviour 아님).
[CreateAssetMenu]로 만들면 Assets → Create에 나타납니다. 파일은 Assets 아래
.asset으로 저장됩니다.
왜 static이나 [SerializeField]가 아닌가? Static은
직렬화되지 않고 Inspector에 불편합니다. MonoBehaviour의 [SerializeField]는
인스턴스마다 데이터를 복사해 메모리·동기화가 어렵습니다. SO: 하나의 소스, 많은 참조.
용도: Item/Enemy 스탯(WeaponData, EnemyStats), 게임 설정(AudioSettings, InputConfig), 이벤트 채널(느슨한 결합), 상태 컨테이너 (GameStateSO). SO는 Update/Awake를 돌리지 않지만 OnEnable/OnDisable은 받습니다.
Editor vs Runtime: Edit Mode 편집은 유지됩니다. Play Mode 편집도 Editor에서는 유지됩니다(MonoBehaviour 필드와 다름). 플레이어 빌드에서는 게임 재시작 시 에셋 값으로 리셋됩니다.
SO vs MonoBehaviour 데이터
❌ [SerializeField] on MonoBehaviour
Enemy_01 (MonoBehaviour)
hp: 100, speed: 3
← 개별 복사본
Enemy_02 (MonoBehaviour)
hp: 100, speed: 3
← 개별 복사본
Enemy_03 (MonoBehaviour)
hp: 100, speed: 3
← 개별 복사본
복사본 3개 × N바이트 = 메모리 낭비
✅ ScriptableObject
GoblinData.asset (SO)
hp: 100, speed: 3
Enemy_01
→ ref SO
Enemy_02
→ ref SO
Enemy_03
→ ref SO
단 하나 — SO 수정 → 모두 갱신
실습 단계
ScriptableObject 클래스 만들기
[CreateAssetMenu(menuName="Game/ItemData")]를 추가해 Assets → Create에 나타나게 합니다.
Project에서 Asset 생성
우클릭 → Assets → Create → Game → ItemData. 데이터 이름으로 지정(예: Sword_Data.asset).
Inspector에서 필드 채우기
.asset 선택 → Inspector에 필드 표시 → 스탯, 스프라이트, 사운드… 입력.
MonoBehaviour에서 참조
[SerializeField] private ItemData _data; → Inspector에 에셋 드래그.
인터랙티브 시뮬레이터
ScriptableObject 에셋을 고르고 필드를 편집하면 모든 소비자가 즉시 갱신되는 모습을 보세요.
Inspector (ScriptableObject)
이 SO를 쓰는 오브젝트
📌 Shared Reference
위 오브젝트는 모두 같은 에셋을 참조. 한 번 수정 → 모두 갱신.
코드 예제
기본WeaponData ScriptableObject를 만들고 MonoBehaviour에서 사용합니다.
using UnityEngine;
// Menu: Assets → Create → Game/Data → Weapon Data
[CreateAssetMenu(menuName = "Game/Data/Weapon Data", fileName = "New Weapon")]
public class WeaponData : ScriptableObject
{
public string weaponName;
public int damage;
public float attackSpeed;
public Sprite icon;
public AudioClip swingSound;
}
public class PlayerWeapon : MonoBehaviour
{
// Drag the .asset from Project onto this field
[SerializeField] private WeaponData currentWeapon;
public void Attack()
{
Debug.Log($"Attacking with {currentWeapon.weaponName}: {currentWeapon.damage} dmg");
AudioSource.PlayClipAtPoint(currentWeapon.swingSound, transform.position);
}
public void EquipWeapon(WeaponData newWeapon)
{
currentWeapon = newWeapon; // instant swap — only the reference changes
}
}코드 예제
고급SO를 Event Channel로——브로드캐스터와 리스너를 분리합니다.
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
// Event Channel — Project asset, independent of scene hierarchy
[CreateAssetMenu(menuName = "Events/Void Event")]
public class VoidEventSO : ScriptableObject
{
private List<UnityAction> _listeners = new();
public void Raise()
{
for (int i = _listeners.Count - 1; i >= 0; i--)
_listeners[i]?.Invoke();
}
public void Subscribe(UnityAction listener) => _listeners.Add(listener);
public void Unsubscribe(UnityAction listener) => _listeners.Remove(listener);
}
// Broadcaster: does not know who is listening
public class PlayerDeath : MonoBehaviour
{
[SerializeField] private VoidEventSO onPlayerDied;
public void Die() => onPlayerDied.Raise();
}
// Listener: does not know who raised the event
public class GameOverUI : MonoBehaviour
{
[SerializeField] private VoidEventSO onPlayerDied;
void OnEnable() => onPlayerDied.Subscribe(ShowGameOver);
void OnDisable() => onPlayerDied.Unsubscribe(ShowGameOver);
void ShowGameOver() => gameObject.SetActive(true);
}📌 빠른 정리
- ▸SO = 씬에 묶이지 않은 데이터 컨테이너
- ▸많은 객체가 하나의 SO를 참조——데이터 복사 없음
- ▸Play Mode에서 SO 편집은 Editor에 유지됨
- ▸SO Event Channel = 씬 참조 없이 분리된 pub/sub
⚠️ 흔한 실수
❌ 프로덕션 빌드에서 런타임에 SO를 변경
공유 SO——변경이 그걸 쓰는 모든 객체에 영향
✅ 인스턴스별 데이터가 필요하면 복제: Instantiate(soAsset)
❌ [CreateAssetMenu] 잊음
Editor 메뉴에서 .asset을 만들 수 없음
✅ 특성을 추가하거나 ScriptableObject.CreateInstance<T>()