기존 전략패턴 (Strategy Pattern)이 어떤 건지는 Strategy Pattern 이 링크로 들어가서 보면 될 것 같다.
그래도 간단하게 설명하면 런타임 중에 전략에 따라서 바뀌는 로직에 강점이 있는 패턴이다.
오늘은 유니티에서 다양한 스킬을 가지고 전략적으로 계속해서 바뀔 수 있는 로직을 짜보려고 한다.
총 2개의 스킬이 있고, 내가 장착 할 수 있는 스킬은 1개 라면 어떤 구조로 짜야할까?
우선 글쓴이는 3가지 방법을 생각해봤다.
1. GameObject 안에 해당 Skiill Component를 붙여서 Prefab을 만들어서 해당 Prefab을 소환하여 사용하는 방법
2. GameObject 안에는 아무것도 없고, 사용할 때 AddComponent로 Skill Component를 붙여서 소환하여 사용하는 방법
3. Base를 미리 붙여 놓고, 데이터만 변경시켜서 구현하는 방법
저는 여기서 1번을 자주 사용하였고, 2번은 다른 프로그래머 분이 사용하던 걸 봤었다.
여기서 1번의 문제는 Prefab으로 만들어서 소환을 하는 시스템이면 풀링을 한다고 하더라도 결국 필요없는 오브젝트가 맵에 생길 수 밖에 없는 형태가 된다. 그리고 2번 처럼 AddComponent로 붙인다는거는 런타임 중에 시작 시 는 상관이 없지만 계속해서 AddComponent를 사용하는 거는 오버헤드를 발생시킬 수 밖에 없는 구조가 될 것 이다. 그래서 어떤 방법으로 구현을 해야할까 고민을 하였다.
여기서 Base는 1개이고, 데이터 객체만 교체를 하면 될 것 같다고 생각했다. 데이터 객체는 결국 메모리에 올라가도 그렇게 많은 부담이 되지 않고, 클래스에 대한 분리까지 간편해 질 것 이라고 생각했다.
글쓴이의 코드를 봐보자
- BaseSkill : 캐릭터에게 붙어 있는 클래스
public class BaseSkill : MonoBehaviour
{
private Base_SkillData _baseSkillData;
public void SetSkillData(Base_SkillData baseSkillData)
{
_baseSkillData = baseSkillData;
}
public void SkillAttack()
{
_baseSkillData?.SkillBehaviour.Execute();
}
}
- Base_SkillData : 해당 스킬의 기본 데이터
public class Base_SkillData : ScriptableObject
{
public SkillType SkillType;
public float Base_SkillDamage;
public float Base_SkillCooltime;
public float Base_SkillDistance;
public float Base_SkillHitCount;
public float Base_SkillSpawnCount;
public SkillBehaviour SkillBehaviour;
}
- SkillBehaviour : 스킬 호출기
public class SkillBehaviour : ScriptableObject
{
public virtual void Execute()
{
}
}
- OrderSkill : 해당 스킬 데이터
[CreateAssetMenu(menuName = "Skill/OrderSkill")]
public class OrderSkill : SkillBehaviour
{
public override void Execute()
{
Debug.Log("Order Skill");
}
}
- Character : 예) 예시로 스킬 데이터를 장착 할 수 있게 하는 클래스
private void InputSetSkillData()
{
if (Input.GetKeyDown(KeyCode.L))
{
Base_SkillData skillData = ResourceManager.Instance.GetBaseSkillData(SkillType.CHAINLIGHTING);
_heroAttack.SetSkillData(skillData);
}
if (Input.GetKeyDown(KeyCode.P))
{
Base_SkillData skillData = ResourceManager.Instance.GetBaseSkillData(SkillType.ORDER);
_heroAttack.SetSkillData(skillData);
}
}
- ResourceManager : 스킬 데이터를 저장해놓은 클래스
public class ResourceManager : Singleton<ResourceManager>
{
private Dictionary<SkillType, Base_SkillData> _baseSkillDatas = new Dictionary<SkillType, Base_SkillData>();
protected override void Awake()
{
base.Awake();
SetBaseSkillDatas();
}
private void SetBaseSkillDatas()
{
Addressables.LoadAssetsAsync<Base_SkillData>("SkillData", (obj) => { }).Completed += handle => {
if (handle.Status == AsyncOperationStatus.Succeeded)
{
foreach (var obj in handle.Result)
{
_baseSkillDatas[obj.SkillType] = obj;
}
}
else
{
Debug.LogError("스킬 데이터 로드 실패!");
}
};
}
public Base_SkillData GetBaseSkillData(SkillType skillType) => _baseSkillDatas[skillType];
}
이 코드들을 보면 1개의 의문이 생길 수 있다.
해당 스킬 데이터를 왜 ScriptableObject로 만들었지? 라는 MonoBehaviour로 만들어도 되는거 아닌가? 라고 할 수 있을 것 같다.
우선 처음은 abstract로 만들어 볼려 했지만 순수 C# 클래스는 직렬화가 되지 않는다.
그래서 Unity에서 "직렬화(Serialize)"가 가능한 클래스는 이 세 가지가 있다.
MonoBehaviour | 씬 오브젝트에 붙이는 컴포넌트 |
ScriptableObject | 프로젝트 에셋 파일로 존재하는 데이터 객체 |
[System.Serializable] 일반 클래스 | 필드로만 존재할 때 직렬화 가능 (에셋으로 만들 수 없음) |
즉, SkillBehaviour를 그냥 클래스로 만들면 에셋에 연결할 수 없다.
SkillBehaviour를 ScriptableObject 또는 Monobehaviour 로 만들어야 한다.
항목 MonoBehaviour ScriptableObject
어디에 존재? | 씬 오브젝트에 붙음 | 프로젝트 에셋으로 존재 |
생성/사용 방식 | AddComponent, Instantiate | Asset 생성 후 참조 |
용도 | 실시간 오브젝트 제어 | 데이터 및 로직 저장 |
퍼포먼스 | Instantiate/Destroy 필요 | 메모리에 상주 |
추천 상황 | 움직이는 오브젝트(총알, 이펙트 등) | 설정 데이터, 단순 로직 |
여기서 나는 데이터만 교체해서 해당 로직을 호출을 하는 형태이다. 그렇다면 ScriptableObject가 효율적이다.
- 스킬은 데이터처럼 관리하고 싶다.
- 스킬은 따로 움직이거나, 씬에 존재할 필요가 없다.
- 단지 스킬 데이터를 저장하고, 발동 로직만 다르게 하고 싶다.
이러한 조건이 있었고, 그래서 MonoBehaviour로 할 필요가 없었다.
결론
스킬 데이터처럼 관리할 때는 ScriptableObject가 압도적으로 효율적이다.
스킬 오브젝트가 씬 안을 돌아다니고 직접 제어해야 하면 MonoBehaviour를 쓴다.
'디자인패턴' 카테고리의 다른 글
Decorator Pattern (0) | 2025.05.24 |
---|---|
pub/sub Pattern (0) | 2025.05.23 |
MVC, MVP, MVVM 비교 (0) | 2025.04.15 |
State Pattern (FSM) (0) | 2024.05.01 |
Observer Parttern (0) | 2024.04.30 |