[내일 배움 캠프 8주차] 팀 과제 WIL - Scriptable Object & Singleton

하얀요니콘·2025년 9월 8일
0

너무 늦었나 싶지만 일단 이번주도 트러블 슈팅작성한다. 주간작업내용이라 Scriptable Object와 Singleton 두 주제로 나누었다.

우선 영상부터 보자. 메탈슬러그 에셋들로 제작한 횡스크롤 pinball 게임이다.

시연 영상

이부분에서 몬스터를 생성로직을 작업하였고, 이 부분에서 Scriptable Object가 여러모로 충돌하였다.

1-1. Scriptable Object 변동문제

구현

우선 적의 아이템 드롭 확률표를 제작하였다. 우선 확률 계산로직은 다음과 같다.
Fig1

우선 확률 보면 연산테이블은 앞에있는 원소들을 합한 부분합이된다. 그래서 합이 1이 되는것을 계산하여 랜덤값이 어디 사이에 있는지를 확인하여 예시로 0.6이 들어가면 결과는 1번 인덱스가 된다.

문제 상황

자 눈치를 챘을수도 있겠지만, 이 로직에는 큰 하자가 있다. 이제 아래 연산을 여러번 하는 상황을 보도록 하자.
Fig2

Scriptable오브젝트에서 값을 변경하다보니 다음 참조를 하면 해당 변경된, 즉 합이 된 확률표가 들어간다! 기존 생각은 정규화를 한번만 하자라는 의도로 출발한 점이 변동이 되다보니 해당 값이 변동되어 후순위로 갈수록 마지막 인덱스의 확률이 높아질 수 밖에 없다. 특히 모든 적들이 같이 사용하는 SO기 때문에, 마지막 인덱스가 될 확률이 기하급수적으로 늘어나게 된다.

원인 파악

자 그러면 우리는 Scriptable Object를 건들면 안된다. 왜냐하면 Scriptable Object자체는 가변데이터가 아니기 때문이다. 우리는 몇가지 방법이 있을 수 있다.

기각 방식

가장 간단한 방식은 확률표 자체를 메소드에서 깊은복사해서 Scriptable Object의 기존 확률표를 두고 연산을 하는것이다. 하지만 이 방식은 확률표 내용이 많아질수록 깊은복사를 하는데도 많은 시간이 소요될 것이기 때문에 바로 기각하였다.

해결 방식

다시 생각해보면 우리는 Scriptable Object만 안건들이면 된다. 그러면 변경되도 되는 값을 찾아보자. 위에있는 Random값은 적이 생성될 때마다 만들어 준다. 즉 SO 는 그대로 두고, 랜덤값을 조절하여 확률을 계산하는 로직으로 만들어 주면 된다.

해당 방식은 다음과 같다.

  • 우선 확률표는 정규화를 시킨 후 랜덤값을 만든다.
  • 랜덤값에서 확률표에 있는 값을 하나씩 빼 주고, 만약 랜덤값이 음수라면 해당 인덱스를 반환한다.

이제 위의 0.6을 예시로 들면

  • 0.6 - 0.2 = 0.4 -> 계속 진행
  • 0.4 - 0.5 = -0.1 -> 1번 인덱스 반환

이런 방식으로 진행된다고 보면 된다.

결론

Scriptable Object는 변동되지 않을 고정데이터만 넣어줘야한다.
만약 변동될 값이 필요하다면, 이를 Scriptable Object가 아닌 외부에서, 즉 각 생성된 Monobehaviour에서 복사를 받아 처리를 해 줄 필요가 있다.

예를들어 적의 체력 변수가 있다고 하자. 그러면 Scriptable Object에는 inital health값을 변하지 않게 저장한다. 그 후 Enemy에 내부 변수 curhelath와 maxhealth의 값을 inital helath값으로 설정 한 후, 만약 현제 체력이 변했다면 curhealth를 변경시키는 방식으로, Scriptable Object의 데이터를 유지한체로 관리를 하는 편이 좋다.

1-2.Scriptable Object 참조문제

구현

우선 위와 비슷하게 스킬 중 랜덤하게 하나를 동일확률로 구한다고 해보자. 그래서 생각해낸 방식은 Scriptable Object에서 Array에 시작 시 확률표를 만들어 작업을 해 주면 된다고 생각했다.

문제

문제는 반복되서 생성되는 적들이, 해당 array를 참조하게된다. 즉 하나가 파괴되면 해당 array또한 같이 파괴되어 참조를 못하는 현상이 발생했다. (파괴 참조)

원인

이또한 위와 비슷하다. 우선 ScriptableObject내부에서 값을 변경하는 방식은 올바르지 않기 때문에 다른 방식을 찾아야 한다.

해결

동일 확률표는 개수만이 중요하기 때문에, 따로 외부 매소드에서 처리를 하도록 하자. Scriptable Object의 원소 개수를 받고, 이를 확률에 따라 하나의 인덱스만 반환하는 메소드를 다른 Monobehaviour등 스크립트에 넣어서 그 메소드 내부에서 생성된 확률표를 만들고 지우고를 한다면 해결이 되는 문제였다.


우선 이부분까지는 스스로 작업한 부분중에 문제가 발생했던 부분을 해결한것이다. 이제 통합을 하면서 다른분들에 이상이 생긴 부분들은 거의 Singleton 부분이다. 한번 보도록 하자.

2. 싱글톤 참조

상황

다른 팀원분의 스크립트를 보니, 싱글톤을 사용하신 부분이 많았다. 특히 UI등 관리를 하는 부분에도 씬마다 적용이 되는거에 반해 DontDestroyOnLoad를 상요해서 싱글톤을 유지하였다.

문제

이제 DontDestroyOnLoad를 사용하다보니, 발생한 문제는 하이러키 창에서 참조를 못하는것이다.

예를들어 처음 MainMenu에서 Battle로 갔다고 하자. 여기까지는 정상 동작 하지만, 만약 이 상황에서 MainMenu로 돌아왔을 시 DontDestroyOnLoad된 내용들은 해당 위치의 참조를 받지 못한다.

원인

이는 간단하게 싱글톤 남용이다. 한 씬에서 처리할 내용들은 한 씬 내부에서만 처리 해 주면 되기 때문에, 이를 위해 싱글톤을 분리할 필요가 있었다.

해결

싱글톤이지 않아도 되는 부분들은 싱글톤이 아닌 일반적인 Monobehaviour로 사용하고, 싱글톤 중 하나의 씬에만 해당되는 내용들은 씬 전환시 파괴되도록 설정하니 문제가 해결되었다.

부가

이제 앞으로 정리르 할 때 나는 이렇게 정리 할 방침이다. 아무래도 이름이 매니저로 통일되다보니 싱글톤을 써야한다는 압박이 다들 오시는것 같아 매니저를 상황에 따라 두가지로 나눌것이다.

  • Manager : 매니저이다. 모든 씬에서 관리가 들어가는 큰 매니저이다. 이는 씬이 전환되도 파괴되면 안된다는 성질을 가진다.
  • Control : 컨트롤이다. 이제 매니저와 기능은 같지만, 특정 씬에서만 작동을 하는 관리체계로, 씬이 전환될 시 파괴되어야 한다.

예시들을 적어보자.

  • Manger 예시 : GameManager, SaveManager, SoundManager 등 전역
  • Control 예시 : BattleControl(배틀씬에서만 사용), InventoryControl(카드 선택 씬에서만 사용) 등 특정 씬에서만 사용

이 두개를 이름을 따로 지정만 해 주어도 어떤것만 매니저로 남겨야 할지 파악이 빨리 될것이라고 생각하여 선택한 방법이다.


하고싶은 이야기는 많지만, 일단 트러블 슈팅은 여기까지였다. 개발 틀을 잘못 잡아서 여러 부분에 동일한 내용을 사용해주는 부분도 있었지만 (Singleton class 혹은 SceneChanger) 결국 이는 리펙토링 과정에서 거친 작업이라서 대화가 필요한 내용이지, 트러블 슈팅감은 아니긴 한거 같다.

profile
코딩공부용

0개의 댓글