유니티 사화
조선을 공격하는 마법 세력을 물리치고, 침식된 조선의 사방신을 해방시키는 여정을 담은 게임입니다.
적을 공격해 ‘덧칠’을 축적하고, 일정량의 덧칠이 쌓인 적을 처형할 수 있습니다.

프로젝트 개요
장르 및 플랫폼
- - 3D 액션 어드벤처
- - PC(Windows)
- - Mobile(Android)
엔진 및 언어
- - Unity 2022.3
- - C#
개발 기간 및 인원
- - 2024.06 ~ 2025.06 (개발 시작 ~ 출시)
- - 프로그래밍(2) / 기획(1) / 아트(4)
- - 플레이어 · UI · 최적화 담당
출시
- - Steam
- - Google Play Store
출시 실적
- - 실행 유저 Steam 1,718명 / Google Play 399명
- - 해외 유저 Steam 94.5%(유럽·중남미 중심) / Google Play 93%(동남아 중심)
주요 활동
최적화
PC 타겟 게임을 모바일로 전환하면서 메모리 부족·프레임 드롭 발생
- ⇒ Galaxy S10 기준 40~60 FPS 유지 및 순간 프레임 드롭 개선
문제
Google Play 출시를 위한 모바일 플랫폼 전환 중 문제 발생
모바일 성능 문제
- 폴리곤이 밀집된 구역과 다수 캐릭터가 등장하는 전투 상황에서 프레임 드롭 발생
- 새로운 지역 진입 시 리소스 로딩으로 인한 순간적인 병목 발생
- 모바일 사양 대비 과도한 오브젝트 퀄리티, 수량으로 GPU 병목 발생
작업 방식 문제
- 모바일 전환이 개발 후반에 결정됐으나, 이미 PC 기준 고퀄리티 에셋으로 작업이 진행된 상태
- 잦은 레벨 디자인 변경으로 단일 씬 방식을 채택
- 단일 씬 탓에 레벨 용량은 물론 카메라에 렌더링되는 오브젝트까지 과도해짐
문제 분석
Unity Profiler로 병목 원인을 하나씩 특정
몬스터 리소스 로딩 — 첫 활성화 시 텍스처를 한 번에 읽으면서 발생
관측
- 게임 시작 후 기믹으로 몬스터가 처음 활성화되는 프레임에서 CPU 시간이 튀는 스파이크 발생
- Profiler Hierarchy에서 그 프레임의 대부분을
Texture.AwakeFromLoad가 차지 - 호출 주체는
Enemy.Awake()→Loading.ReadObject경로 - 같은 몬스터가 다시 등장할 때는 재현되지 않음

원인
- 몬스터 프리팹의 텍스처를 활성화 시점에 처음 읽어 GPU에 올리는 비용이 한 프레임에 몰림
대용량 사운드 로딩 — 지역 진입 시 동기 읽기로 메인 스레드가 멈추면서 발생
관측
- 새로운 지역에 진입해 배경음악이 바뀌는 순간 프레임 드롭 발생
- 해당 프레임의 CPU 시간이
SoundManager.LoadFMODSound아래File.Read에 묶여 있음 - 비동기가 아닌
AsyncReadManager.SyncRequest라 메인 스레드가 파일 읽기를 그대로 대기 - 지역 경계를 넘을 때마다 반복 재현

원인
- 대용량 사운드 파일을 런타임에 동기 읽기로 메모리에 올리며 메인 스레드가 대기
GPU 연산 대기 — 렌더링 완료까지 CPU가 멈춰 서면서 발생
관측
- 특정 이벤트가 아니라 맵 이동, 전투 등 여러 상황에서 반복적으로 등장
- 프레임 시간의 상당 부분이
Gfx.WaitForPresentOnGfxThread→Semaphore.WaitForSignal - 스크립트, 물리 등 CPU 작업량은 적은데 프레임 시간만 길어지는 형태
- CPU가 일을 못 해서가 아니라 GPU를 기다려서 늘어난 시간

원인
- 카메라에 잡히는 오브젝트의 폴리곤 수, 셰이더, 텍스처가 모바일 GPU 처리 능력을 초과
- GPU가 프레임을 끝낼 때까지 CPU가 대기 상태로 묶임
메시 스키닝 — 다수 캐릭터가 동시에 움직이며 연산이 급증해 발생
관측
- 전투 중 여러 캐릭터가 동시에 움직이는 구간에서만 프레임 시간이 치솟음
- 렌더 루프 안쪽
MeshSkinning.UpdateImmediate→SkinOnGPU→ComputeSkinningDispatch구간이 급증 - 동시에 움직이는 캐릭터 수에 비례해 부하가 증가
- 캐릭터가 멈춰 있으면 같은 구간에서 부하가 잡히지 않음

원인
- 뼈대 움직임에 맞춰 메시를 변형하는 스키닝 연산이 캐릭터마다 매 프레임 수행됨
렌더링 전반의 부하 — 그림자, 오브젝트, 후처리가 겹치면서 발생
관측 1 — 불투명 오브젝트 렌더링
DrawOpaqueObjects아래RenderLoop.Draw→QuadTreeNode.Render에서 시간이 몰림- 카메라가 불투명 오브젝트를 QuadTree 기반으로 그리는 구간 자체가 병목
- 화면에 들어오는 오브젝트 수가 많은 구역일수록 시간이 증가

관측 2 — 렌더 패스 전반
ScriptableRenderer.Execute아래 그림자, 불투명, 반투명, 후처리 패스가 나란히 잡힘- 어느 한 패스가 압도적인 게 아니라 네 패스가 고르게 시간을 차지
- 특정 오브젝트를 지워도 프레임이 크게 회복되지 않음

원인
- 특정 패스 하나가 아니라 렌더링 요소 전반이 조금씩 초과된 형태
- 개별 수정이 아닌 전체 부하 감축이 필요하다고 판단
카메라 렌더링 거리 — 맵 뒤에 가려진 오브젝트까지 그리면서 발생
관측
- 지형이 이어진 오픈형 맵이라 카메라 시야 안에 다른 구역까지 함께 들어옴
- Frame Debugger 기준, 화면에서 보이지 않는 오브젝트도 드로우 콜에 그대로 포함
원인
- 카메라 렌더링 거리가 길게 잡혀 있어 맵 뒤에 숨겨진 오브젝트까지 모두 렌더링
- 단일 씬 구조라 Occlusion Culling 같은 카메라 컬링 적용도 제한적
PC 기준 해상도 — 모바일에서 과도한 GPU 연산을 유발
관측
- 모바일 빌드가 PC와 동일한 1920 × 1080 해상도로 동작
- 픽셀 처리 단계가 프레임 시간을 지배하는 형태로 확인
원인
- 해상도가 클수록 3D 정보를 2D 픽셀로 변환하는 연산량이 늘어 GPU 부담이 커짐
- 모바일 GPU에는 픽셀 처리량 자체가 과도한 설정
개선
GPU 렌더링 부하를 줄이는 방향으로, 학습 비용이 적고 효과가 큰 순서대로 적용
목표 기준
- 한국 기준 저사양 기기(Galaxy S10)에서 50~60 FPS 유지
- 체감 품질 저하가 없는 선까지만 적용
게임 해상도 조정 — 연산량 약 56% 감소
조치
- 모바일 타겟 해상도를 1280 × 720으로 조정
- 디바이스가 안드로이드로 감지되면 타겟 해상도를 자동 적용
- 화면 비율은 유지하면서 타겟 픽셀 수에 맞춰 재계산
private void AdjustResolution()
{
int targetWidth;
int targetHeight;
#if UNITY_ANDROID // 모바일
targetWidth = 1280;
targetHeight = 720;
#else // PC
targetWidth = 1920;
targetHeight = 1080;
#endif
float deviceAspectRatio = (float)Screen.width / Screen.height;
int targetPixelCount = targetWidth * targetHeight;
float adjustedHeight = Mathf.Sqrt(targetPixelCount / deviceAspectRatio);
float adjustedWidth = adjustedHeight * deviceAspectRatio;
Screen.SetResolution((int)adjustedWidth, (int)adjustedHeight, true);
}결과
- 2,073,600 → 921,600 픽셀, 약 56% 연산량 감소
- 체감 품질 차이는 확인되지 않음
로우폴리곤화 — 폴리곤 수 최대 1/10
조치
- 하이폴리곤 오브젝트와 다수 배치된 자연물을 대상으로 진행 (기획자 담당)
- LOD 적용도 검토했으나 용량 증가 문제로 보류 (3D 모델러 부재)



결과
- 대상 오브젝트의 폴리곤 수를 최대 1/10까지 감소
- 버텍스 연산이 줄어 GPU 부담 감소
씬 분리 — 다른 맵 오브젝트의 동시 렌더링 차단
조치
- 맵 디자인이 계속 바뀌던 상황이라, 하나의 씬에서 맵 제작을 완성한 뒤 분리하는 방식으로 진행
- 오브젝트를 맵별로 그룹화해 씬을 분리하고 재배치
- 씬 전환 로직은 별도 구현






결과
- 화면에 보이지 않는 다른 맵의 오브젝트가 렌더링 대상에서 제외됨
그 외 — 그림자, 텍스처, 사운드 프리로딩
그림자 렌더링 최적화
- 간단한 그림자를 인위적으로 만들거나, 그림자가 필요한 오브젝트를 분류해 설정
- Shadow Max Distance 설정으로 원거리 그림자 렌더링 차단
- Light Probe 베이킹도 테스트했으나 학습 비용, 베이킹 시간 문제로 보류

텍스처 크기 표준화
- 배경 에셋의 텍스처 해상도가 1024~8192로 제각각인 상태
- 1024, 2048로 통일 (시각적 품질 차이 없음)

사운드 프리로딩
- 런타임 중 대용량 오디오 파일을 로딩하며 CPU 병목이 발생
- 게임 시작 시 볼륨 0으로 미리 재생해 로딩을 앞당기고 원래 상태로 복구
- 이펙트, 몬스터, 환경 오브젝트도 같은 방식으로 미리 활성화해 렌더링 병목을 예방
[SerializeField] private AudioSource[] audios;
[SerializeField] private GameObject[] playerEffects;
[SerializeField] private GameObject[] environmentObjects;
[SerializeField] private GameObject[] Enemies;
private IEnumerator AudioPreload()
{
if (audios.Length <= 0)
{
yield break;
}
yield return null;
bool[] isInitiallyActive = new bool[audios.Length];
float[] initialVolumes = new float[audios.Length];
for (int i = 0; i < audios.Length; i++)
{
isInitiallyActive[i] = audios[i].gameObject.activeSelf;
initialVolumes[i] = audios[i].volume;
audios[i].gameObject.SetActive(true);
audios[i].volume = 0;
audios[i].Play();
}
yield return new WaitForSeconds(1f);
for (int i = 0; i < audios.Length; i++)
{
audios[i].gameObject.SetActive(isInitiallyActive[i]);
audios[i].volume = initialVolumes[i];
}
}- 같은 방식으로
PlayerPreload(),EnvironmentPreload(),EnemyPreload()를 구현
결과
Galaxy S10 기준 40~60 FPS 유지, 순간 프레임 드롭 해소
- 해상도 조정, 폴리곤 감소, 씬 분리 적용 후 모든 지역에서 40~60 FPS 유지
- 새로운 지역, 이벤트 진입 시 발생하던 순간적인 프레임 드롭은 프리로딩으로 개선
빌드 디버깅
빌드 발표 당일 실행 파일에서 플레이어 입력이 안되는 문제 발생
- ⇒ Resources 폴더로 경로 변경하여 당일 해결
- ⇒ 이후 재분석으로 에디터 전용 코드가 버그를 가렸던 구조적 문제 파악
문제
베타 빌드 발표 당일 오전, 실행 파일에서 플레이어 이동·스킬 입력이 전부 먹통이 됨
- 유니티 엔진에서는 정상 작동하나, 빌드에서만 키 입력이 되지 않음
- ESC·Space는 정상 작동, Idle 애니메이션도 실행 중
- 몬스터 타겟 UI가 몬스터가 없어도 활성화된 채 유지됨
문제 분석
엔진에서 재현이 불가능해 빌드 환경에서 직접 디버깅
1단계 — 화면 Text 출력으로 범위 좁히기
- 정상 작동하는 UI·카메라 기능은 제외하고 입력·이동 문제에 집중
- 관련 클래스 메서드 중간중간에 디버그 코드를 심어 화면에 Text로 출력하도록 구현
- 특정 메서드들이 매 프레임 반복되다 중간에 끊기는 것을 확인
- 특정 지점에서 코드 실행이 멈추고 있다고 판단
2단계 — Log Viewer로 지점 특정
- 빌드 환경에서 콘솔처럼 로그를 띄우는 Log Viewer 에셋으로 문제 발생 위치를 특정
- 해당 위치의 수치 데이터(ScriptableObject) 참조 코드가 참조에 실패하고 있었음
Resources.Load로 파일을 불러오도록 구현했으나, 정작 파일은 다른 경로에 생성되도록 되어 있었음- 처음부터 빌드에서는 참조할 수 없는 구조
문제가 된 수치 데이터 코드
환경별 동작 차이
- 엔진에서 —
Resources.Load실패 →#if UNITY_EDITOR블록 실행 →GameResource폴더에서 파일을 찾거나 생성 → instance 할당 성공 - 빌드에서 —
Resources.Load실패 →#if UNITY_EDITOR블록 제외 → instance null →NullReferenceException
결론
- 에디터 전용 코드가 버그를 가리고 있었음
public partial class PlayerCommonData : ScriptableObject
{
private const string SettingFileDirectory = "Assets/GameResource/ControlData";
private const string SettingFilePath = "Assets/GameResource/ControlData/Player Common Mask Data.asset";
private static PlayerCommonMaskData instance;
public static PlayerCommonMaskData Instance
{
get
{
if (instance != null)
{
return instance;
}
instance = Resources.Load<PlayerCommonMaskData>("Player Common Mask Data");
#if UNITY_EDITOR
if (instance == null)
{
if (!AssetDatabase.IsValidFolder(SettingFileDirectory))
{
AssetDatabase.CreateFolder("GameResource", "ControlData");
}
instance = AssetDatabase.LoadAssetAtPath<PlayerCommonMaskData>(SettingFilePath);
if (instance == null)
{
instance = CreateInstance<PlayerCommonMaskData>();
AssetDatabase.CreateAsset(instance, SettingFilePath);
}
}
#endif
return instance;
}
}
}개선
Resources 폴더에 수치 데이터 파일을 저장하도록 경로 변경
선택 근거
- 경로 변경만으로 파일 누락 문제를 가장 빠르게 해결할 수 있음
- 수치 데이터라 빌드 시 용량이 작음 (1~3KB)
- Resources 폴더에 수치 데이터 외의 파일이 존재하지 않음
- 초기화 시
Resources.Load이후 instance로 계속 참조하는 구조라, 런타임 중 Resources 폴더에 접근하는 일이 없음
수정한 수치 데이터 코드
public partial class PlayerCommonData : ScriptableObject
{
private const string SettingFileDirectory = "Assets/Resources";
private const string SettingFilePath = "Assets/Resources/PlayerCommonMaskData.asset";
private static PlayerCommonData instance;
public static PlayerCommonData Instance
{
get
{
if (instance != null)
{
return instance;
}
instance = Resources.Load<PlayerCommonData>("PlayerCommonMaskData");
// 에디터 타임에 자동 생성되도록. 런타임에는 무조건 존재해야 함
#if UNITY_EDITOR
if (instance == null)
{
if (!AssetDatabase.IsValidFolder(SettingFileDirectory))
{
AssetDatabase.CreateFolder("Assets", "Resources");
}
instance = AssetDatabase.LoadAssetAtPath<PlayerCommonData>(SettingFilePath);
if (instance == null)
{
instance = CreateInstance<PlayerCommonData>();
AssetDatabase.CreateAsset(instance, SettingFilePath);
}
}
#endif
return instance;
}
}
}결과 및 회고
빌드 발표는 실패했으나, 문제 발견 즉시 당일 해결 완료
회고
- 엔진에서 돌아가고 빌드가 만들어진다고 게임이 제대로 돌아가는 것이 아님을 체감함
- 현재 상황에서 가장 합리적인 선택을 하려면 많은 문제를 직접 겪어봐야 한다고 생각하게 됨
- 시각적 디버깅(인게임 텍스트, Log Viewer)이 직접 접근이 어려운 환경에서 문제를 이해하는 데 효과적임을 확인
먹 이펙트 구현
카메라 각도에 따라 2D 트레일 이펙트가 평면으로 노출되는 문제 발생
- ⇒ 빌드 발표 기한 내 1주일 만에 개선 및 적용
문제
2D 트레일 이펙트로는 ‘먹으로 칠한다’는 연출을 살릴 수 없었음
- 카메라 각도와 애니메이션 동작에 따라 2D 트레일 이펙트가 평면으로 노출되는 문제 발생
- ‘먹으로 칠한다’는 느낌을 살리려면 입체감과 표현력을 모두 갖춘 이펙트가 필요한 상황
개선
3D 메시 + Shader Graph로 전환해 입체감과 먹의 흐름을 함께 표현
텍스처를 활용한 먹의 흐름 표현
- 텍스처 표현 방법에 대해 외부 FX 아티스트(Zeng Ryan)에게 직접 메일로 자문을 구하고, 텍스처를 받아 활용
- 셰이더 그래프에서 텍스처의 R·G 채널을 시간에 따라 이동시켜 물결치는 흐름을 표현
- B 채널과 파티클 시스템 Custom Data로 알파값을 제어해 서서히 사라지는 먹 느낌을 표현
- 파티클 시스템 Custom Data로 시간에 따라 텍스처의 Tiling을 제어해, 메시 경로대로 텍스처가 흘러가는 잉크 느낌을 표현
3D 메시를 적용해 입체감 형성
- Blender로 임시 메시를 직접 제작하며 이펙트를 다듬음
- 모델러·원화·기획 인원이 해당 이펙트를 기반으로 최종 메시 형태와 활용 방향을 논의
- 임시 메시로 먼저 움직인 덕에 일주일 내 완성 (2024.08.05 ~ 08.11)
결과 및 회고
빌드 발표 기한 내 1주일 만에 개선 및 적용 완료
회고
- 이펙트, 셰이더, Blender 모두 처음이었지만 직접 부딪혀 보니 낯선 분야 자체는 큰 장벽이 아님을 확인
- 막히면 전문가에게 직접 물어보는 것이 생각보다 강력한 무기임을 경험했고, 모르면 두려워하지 말고 물어보면 된다는 자세를 갖게 됨
- 메시 작업이 지연되는 상황에서 기다리는 대신 임시 메시를 직접 만들어 먼저 움직였고, 일정이 걸린 상황에서는 기다리기보다 내가 먼저 움직이는 것이 팀에도 나에게도 낫다는 것을 배움
출시
6개월로 잡은 출시 일정을 넘기면서, 기간을 조건으로 합류했던 팀원 대부분이 이탈
- ⇒ 실행 유저 Steam 1,718 / Google Play 399 / 해외 유저 94%
- ⇒ 피드백 반영 중 확인한 결합 문제가 리팩토링 프로젝트의 출발점이 됨
문제
6개월로 잡았던 출시 일정을 지키지 못한 상태
일정이 밀린 원인
- 기획이 개발 도중 계속 바뀌면서 구현과 수정이 반복됨
- 기획 명세가 세부적이지 않아, 기능을 만들 때마다 요구가 붙고 그때마다 버그 대응이 따라옴
- 외부에서 가져온 에셋과 팀 내부 제작 에셋의 퀄리티 차이로 재작업 발생
- 실사에 가까운 퀄리티로 기획이 잡혀 목표 사양이 계속 올라감
남은 조건
- 6개월을 조건으로 합류했던 팀원 대부분이 기간 종료 시점에 이탈, 기획자와 둘만 남음
- 남은 인원과 기간으로 기획된 범위를 그대로 완성하는 것은 불가능
- 중단하면 여기까지 들인 시간도, 검증도 회고도 남지 않음
판단
완성도가 아니라 ‘끊기지 않는 한 바퀴’를 기준으로 범위를 잘라 출시
출시를 택한 이유
- 여기까지 들인 시간을 결과물 없이 끝내고 싶지 않았음
- 개발부터 스토어 게시까지 전 과정을 직접 겪어 보고 싶었음
- 지인 피드백이 아니라, 모르는 사람의 냉정한 반응을 받아 볼 자리가 필요했음
지킨 것
- 시작부터 엔딩까지 끊기지 않는 플레이 흐름
- 목표 기기(Galaxy S10)에서의 프레임 유지
- PC와 모바일 양쪽의 조작 대응 (입력 방식, 카메라 회전)
버린 것
- 미구현 상태로 남아 있던 후반 콘텐츠와 확장 요소
- 실사 지향으로 잡혀 있던 리소스 퀄리티 목표
- PC 단독 출시 계획 (범위를 줄이는 대신 모바일까지 확장)
결과 및 회고
출시로만 얻을 수 있는 데이터를 확보
- 실행 유저 Steam 1,718 / Google Play 399 (해외 유저 94%)
- Steam 중앙값 플레이 시간 24시간 52분, 위시리스트 196
- 유저 플레이 영상 10+ 및 리뷰, 버그 리포트를 근거로 업데이트 진행
회고
- 개발자가 익숙해진 상태로 테스트해서 놓친 버그를 유저들이 먼저 찾아냄. 제3자 QA 절차가 필요했음
- 리뷰에서 지적된 버그와 사용성을 고치려 코드를 건드리자 다른 클래스가 줄줄이 깨짐. 결합이 심해 한 곳만 고치는 것이 불가능한 구조였음
- 이 경험으로 ‘기능만 작동하는 코드’의 한계를 확인, 전체 코드를 분석해 규모에 맞는 구조로 리팩토링하기로 결정
- 기능과 리소스가 늘수록 목표 프레임이 무너짐. 해외 저사양 기기 비중을 보면 최적화는 상시 항목으로 둬야 함
- 일정이 밀린 근본 원인은 인원이 아니라 명세와 범위 관리였음. 다음부터는 기능 단위로 범위와 완료 조건을 먼저 확정하고 시작하기로 함
그 외 활동
플레이어 입력 개선
스킬 실행 중 입력이 차단되는 구간 때문에 입력 누락이 발생해 조작감 저하 → 입력을 Stack에 저장했다가 적절한 타이밍에 자동 실행하는 방식으로 변경
⇒ 타이밍이 늦어도 다음 스킬로 자연스럽게 이어지는 조작감 확보
수치 데이터 조정
수치 변수가 씬 곳곳의 오브젝트에 흩어져 하나하나 찾아다니며 수정해야 하는 번거로움 발생 → ScriptableObject 에셋으로 통합하고, 변수를 구조체로 묶어 그룹화
⇒ 에셋 한 곳에서 바로 테스트·수정 가능, 변수명 가독성 개선
문제
입력 차단 구간 때문에 입력 누락이 발생해 조작감이 나빴음
기존 구현
- 스킬 실행 중 특정 구간에서 bool 변수로 입력을 차단해 스킬이 끊어지지 않도록 구현
- 입력이 차단된 구간을 피해서 입력해야 하는 구조
- 타이밍이 정확하지 않으면 입력 누락이 발생 → 조작감 악화 및 유저 피로도 증가
관련 코드
PlayerController.cs — 입력 감지 및 행동 함수 호출
private void Update()
{
// 상태에 따른 행동 함수 호출 제어
if (playerState.doNotAct || player.IsPerformingHitAction)
{
return;
}
if (Input.GetKeyDown(saveManager.InputKeys[KeyAction.ATTACK_NORMAL]))
{
// 행동 함수(스킬) 호출
humanMaskSkill.NormalAttack();
}
}HumanMaskSkill.cs — 사람탈 캐릭터 스킬
// CoFirstAttack()을 실행
public void NormalAttack() {}
public IEnumerator CoFirstAttack()
{
while (isPerformingFirstAttack)
{
// bool 변수 'doNotAct'로 행동 함수 호출을 제어
playerState.RestrictPlayer(humanData.firstNormalAttackRestrict, firstAttackStartTime);
}
}PlayerState.cs — 행동 함수 호출을 제어하는 상태 클래스
public void RestrictPlayer(RestrictStruct restrictStruct, float skillStartTime)
{
// 일정 시간 동안 doNotAct를 true로 하여 다른 행동의 호출을 방지
if (restrictStruct.actRestrictDuration != 0)
{
if (Time.time >= skillStartTime + restrictStruct.actRestrictWaitTime + restrictStruct.actRestrictDuration)
{
doNotAct = false;
}
else if (Time.time >= skillStartTime + restrictStruct.actRestrictWaitTime)
{
doNotAct = true;
}
}
}개선
《갓 오브 워: 고스트 오브 스파르타》의 입력 방식을 참고해 입력 저장·자동 실행 구조로 전환
적용 방식
- 스킬 실행 시 코루틴으로 추가 입력을 저장할 수 있는 시간 범위를 활성화
- 해당 범위 안에서 입력된 스킬을 Stack에 저장해 적절한 타이밍에 자동 실행
- 타이밍이 다소 늦어도 다음 스킬로 자연스럽게 이어지는 조작감 구현
관련 코드
PlayerController.cs — 입력을 저장하도록 변경
private void Update()
{
if (Input.GetKeyDown(saveManager.InputKeys[KeyAction.ATTACK_NORMAL]))
{
// 입력을 저장
playerSkillInput.StoreInput(PlayerStateType.HUMAN_NORMALATTACK);
// 스킬 실행 중이 아니라면 바로 실행
if (!playerState.isPerfomingSklill) humanMaskSkill.NormalAttack();
}
}HumanMaskSkill.cs — 저장 가능 구간 활성화
// CoFirstAttack()을 실행
public void NormalAttack() {}
public IEnumerator CoFirstAttack()
{
// 지정된 시간 동안 입력된 스킬을 저장
playerSkillInput.ProcessInput(humanData.firstNormalAttackInput, firstAttackStartTime);
while (isPerformingFirstAttack)
{
playerState.RestrictPlayer(humanData.firstNormalAttackRestrict, firstAttackStartTime);
}
}PlayerSkillInput.cs — 입력 저장 및 실행
public void StoreInput(PlayerStateType inputValue)
{
// 저장 가능할 때만 Stack<PlayerStateType>에 저장
if (canStoreInputValue)
{
playerSkillInputValue.Push(inputValue);
}
}// CoProcessInput() 실행
public void ProcessInput(PlayerSkillInputStruct skillInput, float startTime) {}
public IEnumerator CoProcessInput(PlayerSkillInputStruct skillInput, float startTime)
{
while (!stopCoroutine)
{
// 지정된 타이밍 이후 마지막으로 저장된 스킬값 실행
else if (Time.time >= startTime + skillInput.executeWaitTime)
{
if (playerSkillInputValue.Count > 0)
{
ExecuteStoredInput(playerSkillInputValue.Pop());
yield break;
}
}
// bool 제어로 지정된 시간 동안만 입력이 저장되도록
if (Time.time >= startTime + skillInput.storeWaitTime + skillInput.storeDuration)
{
canStoreInputValue = false;
}
else if (Time.time >= startTime + skillInput.storeWaitTime)
{
canStoreInputValue = true;
}
}
}결과 및 회고
타이밍이 늦어도 다음 스킬로 이어지는 조작감 확보
회고
- 스킬 커맨드 기능 추가를 고려해 Stack으로 저장하도록 구현했으나, 해당 기능이 게임에 포함되지 않으면서 불필요한 구현이 됨. 확정된 요구사항에 집중해야 한다는 것을 반성
- 스킬 입력과 저장을 담당하는 클래스들이 강하게 결합되어 있어, 스킬 추가 시 여러 클래스를 동시에 수정해야 하는 번거로움이 있음
- 인터페이스 기반으로 재설계해 스킬을 클래스 단위로 분리하면 확장성을 개선할 수 있을 것으로 판단
문제
수치 데이터가 씬 곳곳의 오브젝트에 흩어져 조정이 번거로워짐
- 수치 데이터 변수를 클래스에
[SerializeField]로 선언하고 Inspector에서 조정하며 테스트하는 방식으로 구현 - 개발이 진행될수록 수치 데이터가 많아지면서 씬 곳곳의 오브젝트에 흩어짐
- 오브젝트를 하나하나 찾아다니며 수정해야 하는 번거로움이 생겼고, 비슷한 기능의 변수가 많아지면서 변수명 가독성도 떨어짐
개선
ScriptableObject 에셋을 통한 수치 데이터 통합 관리
- 기획 데이터 테이블과 개발 중 추가된 수치 변수를 통합 관리
- Hierarchy에서 수치 데이터를 찾을 필요 없이 ScriptableObject 에셋에서 바로 테스트·수정 가능
- 변수를 구조체 타입으로 구성해 세부 그룹화 → 변수명 정리 및 가독성 개선
결과 및 회고
에셋 한 곳에서 모든 수치를 테스트·수정할 수 있게 됨
회고
- 변수가 많아질수록 찾기 번거로운 문제는 여전히 존재. Google Sheet의 행렬 구조, 탭 분류, 검색 기능 등을 참고해 구현한다면 개발 편의성을 더 높일 수 있을 것으로 판단