귀찮은 걸 못 버티는 사람이 성공한다
우리 게임 제노사이드는 시나리오를 아래처럼 관리한다.
그리고 이걸 바탕으로 SO를 생성한다.
학기 중엔 이걸 하나씩 복붙 후 이름 변경으로 만들었다. 어디 그뿐인가? 스토리의 시작 번호, 끝 번호도 일일히 적어줘야 했다.
스토리를 20개 정도 추가해서 에셋을 만들려고 봤더니, 너무 귀찮았다!
그래서 GPT에 어떤 방법이 있을지 물어봤다. 간단하게 에셋 이름을 넘버링해 여러 개를 한 번에 만드는 방법을 물어봤다.
그러자 스크립트를 작성해 이를 자동화하는 방법을 추천했다. 그리고 생각했다. 그럼, 인덱스 정돈 혼자 만들 수 있겠는데?
자, 그럼 직접 구현해보자!
시작은 늘 구조부터
다행히 구조가 복잡하진 않다.
- CSV를 불러와 만든다.
- 이미 에셋이 있다면 건들지 않는다. (연계 이벤트까지 다 연결한 애들일 테니...)
- 이에 따라 기존 스토리는 수동으로 고친다는 한계가 생겼는데, 이는 기능을 확장하며 해결하자.
- 0번 인덱스부터 검사, Event 열만 살펴본다.
- 빈 칸이라면 스킵한다.
- 내용이 있다면, 이미 있는지 검색한다.
- 이미 있다면 빈칸이 나올 때까지 스킵한다.
- 없다면, 에셋을 만들고 시작 인덱스를 적는다.
- 마찬가지로 빈칸이 나올 때까지 진행한다. 그리고 빈칸이 나왔을 때, 마지막 인덱스를 새긴다.
- 이를 끝까지 반복하면, 완성!
if문이 꽤나 중첩됐는데, 얼리 리턴을 고려하면 깊이는 얕다. 오케이. 가보자.
[MenuItem("Assets/Create Story/Create Sub Story")]
// 메인 스토리를 생성한다.
public static void CreateMainStory()
{
// 메인 스토리 CSV를 불러온다.
TextAsset subStory = AssetDatabase.LoadAssetAtPath<TextAsset>("Assets/NoShare2024-1/NoShare2024-1/04. Scenarios/MainStorys.csv");
List<Dictionary<string, object>> csvFile = CSVReader.Read(subStory);
// 파일을 생성할 폴더 경로
string destinationFolder = "Assets/02. Scripts/Story/EventData SO/Events/Main/";
string destinationPath; // 복사된 파일 경로
EventData currentAsset;
// 스킵할 아이디
string skipId = "";
// 현재 라인의 아이디
string currentId;
// 기존 이벤트의 끝에서, 마지막까지 탐색
for (int i = 0; i < csvFile.Count; ++i)
{
// 아이디를 가져온다.
currentId = csvFile[i]["Id"].ToString();
// 빈칸이거나 스킵 아이디라면
if (csvFile[i]["Id"].ToString() == "" || currentId == skipId)
{
// 넘어간다.
continue;
}
// 빈칸이 아니라면 새 경로를 만든다.
destinationPath = Path.Combine(destinationFolder, $"{currentId}.asset");
// 파일이 없다면 생성한다.
if (File.Exists(destinationPath) == false)
{
// 파일이 없다면 새로 만든다.
EventData asset = CreateInstance<EventData>();
AssetDatabase.CreateAsset(asset, destinationPath);
}
// 현재 가리키는 에셋 교체
currentAsset = AssetDatabase.LoadAssetAtPath<EventData>(destinationPath);
// 스킵 아이디로 설정한다.
skipId = currentId;
// 정보를 변경한다.
currentAsset.eventID = EventType.Main;
// 시작 인덱스를 적어준다.
currentAsset.startIndex = i;
// 빈칸이 나올 때까지 스킵
while (true)
{
if(i + 1 >= csvFile.Count)
{
break;
}
currentId = csvFile[i + 1]["Id"].ToString();
if(currentId == "")
{
break;
}
++i;
}
// 마지막 인덱스를 적어준다.
currentAsset.endIndex = i;
}
// 에셋 데이터베이스를 리프레시해서 새로 생성된 파일을 유니티가 인식하도록 함
AssetDatabase.Refresh();
Debug.Log("메인 스토리가 생성되었습니다!");
}
파일을 불러오는 방법, 파일을 생성하는 방법, 불러온 파일의 변수에 접근하는 방법을 각각 GPT에 질문해 함수를 알아내고, 이를 바탕으로 코드를 작성했다.
내가 이해한 내용을 바탕으로 코드를 짰으니, 적어도 내가 모르는 무언가로 인해 버그가 날 일은 없을 것이다.
잘 될까?
함수 사용은 이렇게, 우클릭 후 찾아서 선택하면 된다.
그럼 한 번, 실행시켜보면...
완벽하게 동작한다!!!
인덱스를 검증해봤는데, 이마저도 제대로 들어갔다.
이제 노가다 안 해도 된다. 야호!
1차 목표는 달성. 이제 디벨롭해보자!
하지만 아직 여기엔 단점이 있다. 바로 연계 이벤트를 직접 등록해줘야 한다는 것.
내가 설계한 구조엔, 저 에셋 내에 선택지 선택 시 이어질 이벤트 배열, 다음에 바로 이어질 이벤트, (언젠가 진행될)이벤트 리스트에 추가할 이벤트가 들어 있다.
쟤네만 등록할 수 있다면, 완전 자동화가 이뤄지는 것이다...!
이를 위해선 CSV가 해당 정보를 알고 있어야 한다. 이에 따라 CSV 구조를 변경해야 한다.
Choice Event 4개, Next Event, Add Event 열을 추가했다. 여기에 이벤트 이름을 적어주면 할당하게 할 것이다.
이제 코드를 짜보자.
이번 코드에서 가장 위험한 점은
가장 주의할 점은, 없는 이벤트를 할당할 수도 있다는 점이다. 더 쉽게 말하면 아직 생성하지 않은 이벤트를 연결하려 시도할 수도 있다.
또, 추가할 스토리의 위치도 중요하다. 메인, 서브, 선택지 스토리는 전부 따로 관리하기에, 알맞은 경로에 가서 가져와야 한다.
이를 위해, 모든 스토리를 만드는 통합 함수를 만들고, 차례대로 이벤트를 생성한 후 알맞게 할당하는 방식을 사용했다.
[MenuItem("Assets/Create Story/Create Story")]
public static void CreateStory()
{
// 먼저 스토리 이벤트를 전부 생성한다.
CreateMainStory();
CreateSubStory();
CreateRelationStory();
// 이후 이벤트를 할당한다.
AssignMainStory();
AssignSubStory();
AssignRelationStory();
}
함수 자체는 간단히 생성되었다. 이제 내부를 채워보자.
Main 폴더 내 모든 이벤트에, 알맞은 이벤트를 할당하려면 어떻게 해야 할까?
- 우선 모든 에셋을 순회해야 한다.
- endIndex에 연계 이벤트들을 적어두면, 바로 찾을 수 있다!
- 첫글자가 M이면 Main 경로에서, S면 Sub 경로에서, R이면 Relation 경로에서 찾는다.
- 전부 할당하면 끝!
// 메인 스토리 이벤트들 내에 연관 이벤트를 할당한다.
public static void AssignMainStory()
{
// 메인 스토리 CSV를 불러온다.
TextAsset subStory = AssetDatabase.LoadAssetAtPath<TextAsset>("Assets/NoShare2024-1/NoShare2024-1/04. Scenarios/MainStorys.csv");
List<Dictionary<string, object>> csvFile = CSVReader.Read(subStory);
// 작업할 폴더 경로
string folderPath = "Assets/02. Scripts/Story/EventData SO/Events/Main/";
// 폴더 내의 모든 에셋 파일 경로 가져오기
string[] assetGuids = AssetDatabase.FindAssets("", new[] { folderPath });
// 각 에셋을 로드하고 ScriptableObject로 캐스팅
foreach (string guid in assetGuids)
{
// 에셋 경로 가져오기
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
// 에셋 로드
EventData asset = AssetDatabase.LoadAssetAtPath<EventData>(assetPath);
if (asset != null)
{
// ScriptableObject의 내부 데이터 접근 예시
if (asset is EventData story)
{
int i = story.endIndex;
// 선택지 이벤트가 있다면
if(csvFile[i]["Choice Event Count"].ToString() != "")
{
// 개수만큼 만들고
int count = int.Parse(csvFile[i]["Choice Event Count"].ToString());
story.relationEvent = new EventData[count];
// 할당한다.
for (int j = 1; j <= count; ++j)
{
// 빈칸이 아니면 할당한다.
if (csvFile[i]["Choice Event" + j].ToString() != "")
{
story.relationEvent[j - 1] = FindEventData(csvFile[i]["Choice Event" + j].ToString());
}
}
}
// Next Event가 있다면 연결한다.
if(csvFile[i]["Next Event"].ToString() != "")
{
story.nextEvent = FindEventData(csvFile[i]["Next Event"].ToString());
}
// Add Event가 있다면 연결한다.
if (csvFile[i]["Add Event"].ToString() != "")
{
// 일단 하나만 가능하게 해보자.
story.addEvent = new EventData[] { FindEventData(csvFile[i]["Add Event"].ToString()) };
}
}
}
}
Debug.Log("메인 이벤트에 연계 이벤트를 모두 할당했습니다.");
}
이번에도 비교적 단순하게 구현되었다.
이번엔?
디버깅을 좀 하긴 했지만, 크게 헤매지 않고 성공했다!
이로써 CSV 파일만 작성하면 자동으로 에셋을 만들어주는 시스템이 완성되었다!
사담
그동안 스토리를 퇴고하며 번호를 일일히 맞춰주고, 새 에셋을 만들고, 인덱스를 연결하고, 눈치껏 연계 이벤트를 연결했는데, 솔직히 힘들었다. 쓸데없는 게 시간만 잡아먹어
그래서 속이 시원하다. 이제 버튼 한 번이면 모든 게 자동으로 생성되니까!!
여담으로 GPT를 어떻게 사용해야 정확성도 지키고, 공부도 하면서 효율적일까?는 늘 고민하게 되는 주제인 거 같다. 여기에 대해 나는 나름의 답을 내렸는데, 바로 발전된 구글링으로 쓰는 것이다.
이번 포스팅을 예로 들면
- 내가 원하는 기능을 구현할 방법이 존재하는가? (대략적인 아이디어 얻기)
- 구조는 내가 직접 짠다.
- 구조에 맞춰 구현하면서, 필요한 함수가 있는지 검색한다.
- 만약 해당 함수의 사용법을 모르겠다면, 예시를 요구하며 구체적으로 물어보고 이해한다.
경험상 'OO을 가능하게 하는 함수'를 찾거나, 'OO 함수에 대한 설명'을 요구하면 명확한 레퍼런스가 있어서인지 거의 맞게 대답해줬다. 그리고 틀리더라도 스크립트를 짜며 바로 알아낼 수 있다.
게다가 새 함수도 알고, 구조는 직접 짰으니 내 코드를 온전히 이해할 수 있다. 사실 GPT 코딩이 문제되는 부분이 바로 이 부분인 것 같다. 본인이 짠 코드를 본인이 몰라서 다른 사람이 이해하고 고치게 한다던지, 코드 동작 방식을 고민하지 않아 배워가는 게 없다던지.
혹여 다른 GPT 사용 방법을 알고 있다면 공유해주기 바란다. 그럼 이만!
'Unity > 로직 설계' 카테고리의 다른 글
[Unity] 마우스 호버링 시 강조 로직 설계 (2) | 2024.12.06 |
---|---|
[Unity] 책 넘기는 효과 구현하기 (Page Curl) - 3 (0) | 2024.06.01 |
[Unity] 책 넘기는 효과 구현하기 (Page Curl) - 2 (0) | 2024.06.01 |
[Unity] 책 넘기는 효과 구현하기 (Page Curl) - 1 (0) | 2024.05.31 |
[Unity] InputAction 내 이벤트 초기화 (0) | 2023.02.24 |