컴포넌트 패턴 = 기능 블록 조립.
"원하는 동작에 대한 기능을
블록 단위로 나누고
필요할 때 가져와서 사용하고 수정하고 하는 방식"
❌ 상속 방식
Player → Character → Entity (수정 지옥)
✅ 컴포넌트 방식
GameObject + Move + Attack + Health (조립/교체 자유)
Unity = 컴포넌트 패턴 엔진.
Transform, Rigidbody, Collider 전부 컴포넌트다.
상속 대신 GameObject에 Move/Attack/Health 같은 기능을 붙여서 조합으로 만든다.
목표: 이동/공격/체력을 분리해서 플레이어 만들기
// 1. 이동 컴포넌트
public class MoveComponent : MonoBehaviour
{
[SerializeField] float speed = 5f;
void Update()
{
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
transform.Translate(new Vector3(h, 0, v) * speed * Time.deltaTime);
}
}
// 2. 체력 컴포넌트
public class HealthComponent : MonoBehaviour
{
[SerializeField] int maxHp = 100;
int currentHp;
void Start() => currentHp = maxHp;
public void TakeDamage(int damage)
{
currentHp -= damage;
if (currentHp <= 0) Die();
}
void Die() => gameObject.SetActive(false);
}
// 3. 공격 컴포넌트
public class AttackComponent : MonoBehaviour
{
[SerializeField] int damage = 10;
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
Attack();
}
void Attack()
{
// 레이캐스트로 타겟 찾아서 데미지
Debug.Log("공격!");
}
}
조립
목표: 컴포넌트끼리 느슨하게 통신
// 1. 인터페이스 정의
public interface IDamageable
{
void TakeDamage(int damage);
}
// 2. 체력 컴포넌트 (이벤트 추가)
public class HealthComponent : MonoBehaviour, IDamageable
{
[SerializeField] int maxHp = 100;
int currentHp;
public event System.Action<int> OnDamaged; // hp 변경 알림
public event System.Action OnDied; // 사망 알림
void Start() => currentHp = maxHp;
public void TakeDamage(int damage)
{
currentHp = Mathf.Max(0, currentHp - damage);
OnDamaged?.Invoke(currentHp);
if (currentHp == 0)
{
OnDied?.Invoke();
Die();
}
}
void Die() => gameObject.SetActive(false);
}
// 3. 공격 컴포넌트 (인터페이스 사용)
public class AttackComponent : MonoBehaviour
{
[SerializeField] int damage = 10;
[SerializeField] float attackRange = 2f;
[SerializeField] LayerMask targetLayer;
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
Attack();
}
void Attack()
{
Collider[] hits = Physics.OverlapSphere(transform.position, attackRange, targetLayer);
foreach (var hit in hits)
{
if (hit.TryGetComponent<IDamageable>(out var target))
target.TakeDamage(damage);
}
}
}
// 4. UI 연결 컴포넌트
public class HealthUI : MonoBehaviour
{
[SerializeField] HealthComponent health;
[SerializeField] Slider hpBar;
void Start()
{
health.OnDamaged += UpdateUI;
health.OnDied += ShowDeadScreen;
}
void UpdateUI(int hp) => hpBar.value = hp;
void ShowDeadScreen() => Debug.Log("You Died");
}
핵심
목표: 수치는 SO로 분리, 재사용/확장 극대화
// 1. 데이터 정의 (ScriptableObject)
[CreateAssetMenu(menuName = "Stats/AttackData")]
public class AttackData : ScriptableObject
{
public int damage = 10;
public float cooldown = 1f;
public float range = 2f;
public LayerMask targetLayer;
}
[CreateAssetMenu(menuName = "Stats/HealthData")]
public class HealthData : ScriptableObject
{
public int maxHp = 100;
public float regenRate = 0f;
}
// 2. 체력 컴포넌트 (데이터 주입)
public class HealthComponent : MonoBehaviour, IDamageable
{
[SerializeField] HealthData data;
int currentHp;
public event System.Action<int, int> OnDamaged; // (current, max)
public event System.Action OnDied;
void Start()
{
currentHp = data.maxHp;
if (data.regenRate > 0)
InvokeRepeating(nameof(Regen), 1f, 1f);
}
public void TakeDamage(int damage)
{
currentHp = Mathf.Max(0, currentHp - damage);
OnDamaged?.Invoke(currentHp, data.maxHp);
if (currentHp == 0)
{
OnDied?.Invoke();
gameObject.SetActive(false);
}
}
void Regen()
{
if (currentHp < data.maxHp)
{
currentHp = Mathf.Min(data.maxHp, currentHp + Mathf.RoundToInt(data.regenRate));
OnDamaged?.Invoke(currentHp, data.maxHp);
}
}
}
// 3. 공격 컴포넌트 (데이터 주입)
public class AttackComponent : MonoBehaviour
{
[SerializeField] AttackData data;
float cooldownTimer;
void Update()
{
cooldownTimer -= Time.deltaTime;
if (Input.GetKeyDown(KeyCode.Space) && cooldownTimer <= 0)
{
Attack();
cooldownTimer = data.cooldown;
}
}
void Attack()
{
Collider[] hits = Physics.OverlapSphere(transform.position, data.range, data.targetLayer);
foreach (var hit in hits)
{
if (hit.TryGetComponent<IDamageable>(out var target))
target.TakeDamage(data.damage);
}
}
}
// 4. 버프 시스템 (데이터 교체)
public class BuffComponent : MonoBehaviour
{
[SerializeField] AttackComponent attack;
[SerializeField] AttackData normalAttack;
[SerializeField] AttackData buffedAttack;
[SerializeField] float buffDuration = 5f;
public void ApplyBuff()
{
// 데이터만 교체 (리플렉션 또는 public setter 필요)
StartCoroutine(BuffRoutine());
}
System.Collections.IEnumerator BuffRoutine()
{
// buffedAttack으로 교체
yield return new WaitForSeconds(buffDuration);
// normalAttack으로 복구
}
}
핵심
Q. 전역 상수 방식으로 사용하면 되지 않을까? 왜 굳이 Scriptable Object를 사용할까?
1️⃣ 전역 상수 방식
public class BossHealth : MonoBehaviour
{
const int MAX_HP = 10000; // ← 이거 수정하려면?, 각 오브젝트마다 복사
}
// 100개 생성 → 100개의 damage 변수 (메모리 낭비)
작업 과정:
1. 코드 열기
2. 10000 → 8000 수정
3. 저장 후 리컴파일 대기 (10초~1분)
4. 플레이 버튼 눌러서 테스트
5. 또 조정 필요? → 1번부터 반복
문제점:
프로그래머: 코드 수정 중...
기획자: "슬라임 체력 올려주세요!"
프로그래머: "지금 다른 작업 중이라 30분 후에요"
기획자: 대기...
기획자: "고블린도요!"
프로그래머: 또 코드 열고 수정...
→ 반복 (병목 발생)
2️⃣ ScriptableObject 방식
[CreateAssetMenu(menuName = "Data/BossHealth")]
public class BossHealthData : ScriptableObject
{
public int maxHp = 10000;
}
public class BossHealth : MonoBehaviour
{
[SerializeField] BossHealthData data; // ← 에셋 참조만 저장 (8바이트)
int currentHp;
void Start() => currentHp = data.maxHp;
}
작업 과정:
1. Project 창에서 에셋 클릭
2. Inspector에서 10000 → 8000 입력
3. 즉시 플레이 버튼 (리컴파일 없음)
4. 테스트
5. 또 조정? → 1번부터 (3초 컷)
이점:
프로그래머: 코드 수정 중...
기획자: Slime.asset 클릭 → 체력 수정 → 즉시 테스트 ✅
기획자: Goblin.asset 클릭 → 체력 수정 → 즉시 테스트 ✅
→ 프로그래머 방해 없음

1. 참조 캐싱
// ❌ 매번 검색
void Update() => GetComponent<Rigidbody>().AddForce(Vector3.up);
// ✅ 캐싱
Rigidbody rb;
void Awake() => rb = GetComponent<Rigidbody>();
void Update() => rb.AddForce(Vector3.up);
2. Update 최소화
// ❌ 매 프레임
void Update() => CheckHealth();
// ✅ 이벤트 기반
public event Action OnHealthChanged;
3. 오브젝트 풀링
// 총알/이펙트는 생성/파괴 대신 활성화/비활성화
public class BulletPool : MonoBehaviour
{
Queue<GameObject> pool = new();
public GameObject Get()
{
if (pool.Count > 0)
{
var obj = pool.Dequeue();
obj.SetActive(true);
return obj;
}
return Instantiate(bulletPrefab);
}
public void Return(GameObject obj)
{
obj.SetActive(false);
pool.Enqueue(obj);
}
}
⚠️ 실습
🎯 목표
시작은 작게: Move + Health만 분리해도 효과가 보인다.