C++ 테이블 구조체란?
- 언리얼에서 Data Table Asset 을 만들거나 CSV 혹은 JSON 형태의 데이터를 Import 시키기 위해서는 베이스가 될 구조체를 반드시 고르게 되어 있다.
- 엔진에서 기본적으로 제공하는 구조체는 원하는 게임 제작 방향에서 대부분 맞지 않을 것이므로 커스텀한 구조체 생성이 필요하다
- 이 구조체는 C++ 로 작성가능하고, USTRUCT 의 형태로 만들어지며 FTableRowBase 를 상속받는다.
- 해당 스크립트는 테이블에 선언된 타입의 변수들의 나열로 이루어진다.
먼저 완성본을 보고 시작하자.
1. Python Plugin 설치
먼저 언리얼 엔진에서 Python 플러그인을 설치해야 한다.
설치 방법과 Python 스크립트 작성 시 필요한 자동완성(Intellisense) 설정에 필요한 과정은
아래 링크에서 설명하였다.
자동완성이 필요없다면 바로 py 파일을 생성하고 코드를 작성해도 무방하다.
본인은 자동완성 세팅까지 한 후에 PyCharm 에디터를 이용하여 작성하였다.
2024.01.22 - [언리얼5 & C++] - [언리얼 파이썬 스크립팅] 언리얼 Pycharm 자동 완성하는법 (삽질 노가다 끝에 공유)
2. Python 스크립트 작성
PyCharm 에디터를 열고 빈 프로젝트를 만들고, struct_generator.py 파일을 생성한다.
어느 에디터든 상관없고, py 파일을 만들 수만 있다면 메모장도 상관없다.
어차피 여기서 작성한 내용은 사본을 만들거나 파일 이동을 해서 언리얼 프로젝트 내부로 이동 시킬 것이고
여기서 작성하는 이유는 오직 자동완성 사용을 위해서이다.
바로 언리얼 프로젝트 폴더에서 py 파일을 생성하고 작성해도 아무 문제가 없다.
이제 아래 코드를 작성한다.
기능은 하나씩 설명할 것이다.
import unreal
import os
import sys
import csv
# 현재는 따로 폴더 생성 기능 제공 안함.
# 프로젝트 명
project_name = "RottenPotato"
# CSV 파일이 존재하는 폴더 경로
csv_folder = unreal.SystemLibrary.get_project_directory() + "CSV"
# c++ struct 를 저장할 폴더 경로 ->
struct_save_folder = unreal.SystemLibrary.get_project_directory() + "Source/" + project_name + "/Public/Table"
# 개행 함수
def next_line(file):
file.write("\n")
# 타입 선별 함수
def get_unreal_type(type):
str_type = str(type).lower()
if str_type == "int" or str_type == "int32":
return "int32"
elif str_type == "float" or str_type == "float32":
return "float"
elif str_type == "string" or str_type == "fstring":
return "FString"
elif str_type == "bool" or str_type == "boolean":
return "bool"
elif str_type == "vector" or str_type == "vector3":
return "FVector"
elif str_type == "rotator" or str_type == "rotator":
return "FRotator"
elif str_type == "text":
return "FText"
elif str_type == "color" or str_type == "coloru8":
return "FLinearColor"
else:
unreal.log_error(str_type + " << This type is not allowed. It will change to \'FString\'.")
return "FString"
# 스크립트 작성 함수
def create_struct():
print("####### Data Table C++ Struct Generator Started! #######")
print("###### Target CSV Folder : " + csv_folder)
print("-")
# csv_folder 내부의 모든 파일 리스트 검출
file_list = os.listdir(csv_folder)
csv_file_list = []
# CSV 가 아닌 것 걸러내기
for file in file_list:
if file.endswith(".csv"):
csv_file_list.append(file)
if len(csv_file_list) == 0:
unreal.log_error("There's no CSV file in folder : " + path.csv_folder)
sys.exit(0)
print("----------- CSV File List ------------")
print("-")
# 반복문 시작 : 하나 씩 변환 시작
index = 1
for file in csv_file_list:
print("(" + str(index) + ") " + file)
index += 1
print("-")
for file in csv_file_list:
print("-")
print("::::::::::::: Start making [" + file + "] ::::::::::::::")
# csv 파일 경로 추출
csv_file_path = os.path.join(csv_folder, file)
# 먼저 C++ 부터 작성
print("---------- Writing C++ Struct row table... -------------")
print("-")
# CSV 를 행 별로 저장
rows = []
# 파일 열고 행 별로 rows 에 담는다
with open(csv_file_path, 'r') as csvfile:
csv_reader = csv.reader(csvfile)
for row in csv_reader:
rows.append(row)
# 행이 아무것도 없다면 종료
if len(rows) == 0:
unreal.log_error("CSV row count is 0")
continue
# Id 로 시작하는 행을 찾기 위해 초기값 -1 로 설정
column_name_row_index = -1
for data in rows:
if data[0] == "Id" or str(data[0]).lower() == "id":
column_name_row_index = rows.index(data)
if column_name_row_index == -1:
unreal.log_error("Cannot found Id column")
continue
type_name_list = []
column_name_list = []
# 타입 이름과 컬럼 이름 수집
column_name_row = rows[column_name_row_index]
for index, column_name in enumerate(column_name_row):
# '#' 으로 시작하는 칼럼은 추가하지 않는다.
if not column_name.startswith("#"):
# Id 칼럼 위 행은 타입 행이므로 -1 한 위치에서 타입 이름을 저장.
# rows[Id 칼럼의 윗 행 인덱스][현재 열 인덱스]
type_row = rows[column_name_row_index - 1]
type_name_list.append(type_row[index])
column_name_list.append(column_name)
# 타입 갯수와 칼럼 열 갯수가 다르면 경고 후 스킵
if len(type_name_list) != len(column_name_list):
print("Type name count and column name count is not correct : " + len(type_name_list) + "/" + len(
column_name_list))
continue
# 파일명 추출
file_name = os.path.basename(csv_file_path)
file_name = str(file_name).split('.')[0]
# 파일 작성 시작
with open(struct_save_folder + "/F" + file_name + ".h", 'w') as c_file:
c_file.writelines("// Copyright Parrot_Yong, MIT LICENSE\n")
c_file.writelines("// This file is auto generated by Parrot_Yong's table generator.\n")
next_line(c_file)
c_file.writelines("# pragma once\n")
next_line(c_file)
c_file.writelines("#include \"Engine/DataTable.h\"\n")
c_file.writelines("#include \"F" + file_name + ".generated.h\"\n")
next_line(c_file)
c_file.writelines("USTRUCT(Blueprintable)\n")
c_file.writelines("struct F" + file_name + " : public FTableRowBase\n")
c_file.writelines("{\n")
c_file.writelines("\tGENERATED_USTRUCT_BODY()\n")
next_line(c_file)
c_file.writelines("public:\n")
next_line(c_file)
# 변수 선언
for index, value in enumerate(column_name_list):
# id 변수는 선언하지 않는다 -> 기본적으로 Row Name 칼럼이 Id 역할을 해주기 때문.
if str(value).lower() == "id":
continue
c_file.writelines("\tUPROPERTY(EditAnywhere, BlueprintReadWrite)\n")
c_file.writelines("\t" + get_unreal_type(type_name_list[index]) + " " + str(value) + ";\n")
next_line(c_file)
c_file.writelines("};\n")
# struct_path : ex) "/Script/project_name.TestTable"
# unreal_struct_path = "/Script/" + project_name + "." + file_name
# 실행 부분
create_struct()
print("********* C++ Struct Generator Closed. **********")
기능 위주의 작성이므로 더 나은 구조나 문법등이 있다면 용도에 맞게 수정하면 된다.
3. 코드 설명
우선, 제일 아래 줄을 보면 실행 부가 있다.
# 실행 부분
create_struct()
print("********* C++ Struct Generator Closed. **********")
이 함수에서 모든 것이 이루어진다.
해당 함수로 이동해보자.
# csv_folder 내부의 모든 파일 리스트 검출
file_list = os.listdir(csv_folder)
csv_file_list = []
# CSV 가 아닌 것 걸러내기
for file in file_list:
if file.endswith(".csv"):
csv_file_list.append(file)
if len(csv_file_list) == 0:
unreal.log_error("There's no CSV file in folder : " + path.csv_folder)
sys.exit(0)
csv_folder 는 파일 최상단에서 정한 경로이다. 개인의 프로젝트에 맞게 수정하면 된다.
os 라이브러리를 통해 csv_folder 경로에 존재하는 파일명을 리스트화 시켜서 file_list 로 가져온다.
리스트를 순회하면서 CSV 형태가 아닌 파일들을 제거하고,
결과가 0개이면 프로그램을 종료한다.
최종적으로 csv_file_list 에는 경로에 존재하는 CSV파일들의 파일명 (Filename.csv) 이 남게 된다.
for file in csv_file_list:
print("-")
print("::::::::::::: Start making [" + file + "] ::::::::::::::")
# csv 파일 경로 추출
csv_file_path = os.path.join(csv_folder, file)
# 먼저 C++ 부터 작성
print("---------- Writing C++ Struct row table... -------------")
print("-")
# CSV 를 행 별로 저장
rows = []
# 파일 열고 행 별로 rows 에 담는다
with open(csv_file_path, 'r') as csvfile:
csv_reader = csv.reader(csvfile)
for row in csv_reader:
rows.append(row)
# 행이 아무것도 없다면 종료
if len(rows) == 0:
unreal.log_error("CSV row count is 0")
continue
이 코드 이전의 중간 내용은 CSV리스트에 무엇이 들어있는지 확인하는 정도이므로 건너뛰고 현재 코드를 설명하겠다.
검출된 CSV 리스트에서 CSV 파일을 하나 씩 열어서 행 별로 잘라 배열에 담을 것이다.
별도로 담지 않고 csv_reader 에서 바로 행 별로 접근해서 구현을 하려고 했으나
따로 캐싱하지 않으면 에러가 발생했다.
아직 정확한 이유는 모르겠으나 캐싱한 후에 해결되었다.
(데이터 타입의 문제로 추측되나 정확한 정보를 아시는 분을 알려주시면 감사하겠습니다)
rows 의 길이가 0 이라면 CSV는 파일만 존재했지 내용은 없다는 뜻이므로 다음 파일로 넘어간다.
# Id 로 시작하는 행을 찾기 위해 초기값 -1 로 설정
column_name_row_index = -1
for data in rows:
if data[0] == "Id" or str(data[0]).lower() == "id":
column_name_row_index = rows.index(data)
if column_name_row_index == -1:
unreal.log_error("Cannot found Id column")
continue
사전에 정한 룰에 따라 엑셀에서 첫 실 데이터 작성 윗 부분은 메모 공간으로 활용하도록 하였기에
유의미한 데이터가 존재하는 시작 행을 찾기 위한 과정이다.
여기서는 Id 를 기준으로 찾았다. Id 가 항상 int 라면 int 를 기준으로 찾아도 무방하지만,
여기서는 Id 를 기준으로 잡았다.
사전에 유효데이터 이외의 값을 엑셀에 기입하지 않겠다라고 약속이 정해지면
첫 행이 바로 실 데이터 일 것이므로 이 과정이 필요 없을 수 있다.
Id 로 시작하는 행을 찾기 위해 column_name_row_index = -1 로 설정하고,rows 를 순회하면서 행의 첫번째 열 값 data[0] 가 "Id" 인지를 검사한다.여러 예외에 대응하기 위해 문자열을 소문자로 전환(lower()) 하고 "id" 인지를 비교하면 편리하다.
만약, column_name_row_index 가 여전히 -1 이라면 해당 CSV 는 id 열이 존재하지 않는데이터이므로, 사전에 정한 규칙에 위배되는 파일이다. 따라서 해당 파일은 스킵한다.
type_name_list = []
column_name_list = []
# 타입 이름과 컬럼 이름 수집
column_name_row = rows[column_name_row_index]
for index, column_name in enumerate(column_name_row):
# '#' 으로 시작하는 칼럼은 추가하지 않는다.
if not column_name.startswith("#"):
# Id 칼럼 위 행은 타입 행이므로 -1 한 위치에서 타입 이름을 저장.
# rows[Id 칼럼의 윗 행 인덱스][현재 열 인덱스]
type_row = rows[column_name_row_index - 1]
type_name_list.append(type_row[index])
column_name_list.append(column_name)
# 타입 갯수와 칼럼 열 갯수가 다르면 경고 후 스킵
if len(type_name_list) != len(column_name_list):
print("Type name count and column name count is not correct : " + len(type_name_list) + "/" + len(
column_name_list))
continue
C++ 구조체를 작성하기 위해 필요한 것은 변수 타입과 변수명이다.이것을 CSV 로 부터 알아내는 과정이다.
아까 찾은 column_name_row_index 는 Id 로 시작하는 행이다.따라서 column_name_row_index - 1 은 Id 행의 윗 행이므로 int 로 시작하는 데이터 타입 행이 된다.(사전에 정한 테이블 작성 규칙에 따라서)
이를 통해 타입명만 모아진 배열과 Column 명만 모아진 배열을 얻어낼 수 있다.이 배열의 길이가 서로 다르다면데이터 혹은 데이터 수집 과정에서 문제가 있는 것이므로 해당 파일을 스킵한다.
# 파일명 추출
file_name = os.path.basename(csv_file_path)
file_name = str(file_name).split('.')[0]
작성할 C++ 스크립트의 파일명을 짓기 위해 CSV파일명 (Filename.csv) 에서
확장자를 제거한 파일 이름을 얻어낸다.
# 파일 작성 시작
with open(struct_save_folder + "/F" + file_name + ".h", 'w') as c_file:
c_file.writelines("// Copyright Parrot_Yong, MIT LICENSE\n")
c_file.writelines("// This file is auto generated by Parrot_Yong's table generator.\n")
next_line(c_file)
c_file.writelines("# pragma once\n")
next_line(c_file)
c_file.writelines("#include \"Engine/DataTable.h\"\n")
c_file.writelines("#include \"F" + file_name + ".generated.h\"\n")
next_line(c_file)
c_file.writelines("USTRUCT(Blueprintable)\n")
c_file.writelines("struct F" + file_name + " : public FTableRowBase\n")
c_file.writelines("{\n")
c_file.writelines("\tGENERATED_USTRUCT_BODY()\n")
next_line(c_file)
c_file.writelines("public:\n")
next_line(c_file)
# 변수 선언
for index, value in enumerate(column_name_list):
# id 변수는 선언하지 않는다 -> 기본적으로 Row Name 칼럼이 Id 역할을 해주기 때문.
if str(value).lower() == "id":
continue
c_file.writelines("\tUPROPERTY(EditAnywhere, BlueprintReadWrite)\n")
c_file.writelines("\t" + get_unreal_type(type_name_list[index]) + " " + str(value) + ";\n")
next_line(c_file)
c_file.writelines("};\n")
with open(파일 경로, 파일 모드) 를 통해 생성할 파일의 경로를 확장자 까지 넣어주고,
'w' 로 쓰기 모드로 열어준다. 같은 파일이 이미 있다면 덮어 씌운다.
다음은 정말 말 그대로 문자열을 작성해주면 된다.
글 맨 위에 있는 완성본 사진을 참고하면 된다.
코드에 있는 next_line(c_file) 은 c_file.write("\n) 을 대체하기 위한 함수이지만 가독성 외에 별 의미는 없다.
그냥 함수화 하지말고 써도 된다.
주목해야 할 것은 변수 작성 부분이다.
for 문을 돌면서 아까 저장했던 타입명 배열과 변수명 배열에 있던 내용들을 출력해주면 되는데,
중요한 점은 Id 열은 작성하지 않고 스킵한다는 점이다.
이유는 언리얼 Data Table Asset 에서는 이미 'Row Name' 이라는 고유한 열이 존재한다.
따로 이렇게 선언하지 않아도,
UPROPERTY()
int32 Id;
Id 에 해당하는 첫 열의 값은 언리얼 Data Table Asset 의 Row Name 열에 들어가게 된다.
# 타입 선별 함수
def get_unreal_type(type):
str_type = str(type).lower()
if str_type == "int" or str_type == "int32":
return "int32"
elif str_type == "float" or str_type == "float32":
return "float"
elif str_type == "string" or str_type == "fstring":
return "FString"
elif str_type == "bool" or str_type == "boolean":
return "bool"
elif str_type == "vector" or str_type == "vector3":
return "FVector"
elif str_type == "rotator" or str_type == "rotator":
return "FRotator"
elif str_type == "text":
return "FText"
elif str_type == "color" or str_type == "coloru8":
return "FLinearColor"
else:
unreal.log_error(str_type + " << This type is not allowed. It will change to \'FString\'.")
return "FString"
변수 선언 중 사용되는 이 함수는 테이블에서 입력한 데이터 타입이
언리얼에서 사용가능 타입으로 대응되게 하도록 하는 함수이다.
대다수의 경우 테이블을 작성하는 사람은 프로그래머가 아닐 것이기에
그들에게 데이터 타입을 암기하도록 요구하는 것보다 툴에서 변환하는 것이 현명한 방법이다.
만약 상호간에 합의가 잘 이루어진다면 그 룰에 맞추어도 된다.
이 함수에 필요에 따라 여러 타입들을 추가하면 된다.
여기에는 매우 기본적인 타입만 작성되어 있다.
4. 언리얼에서 실행해보기
작성한 파일을 잘 저장하고,
py 파일을 언리얼 프로젝트 내부 어디든 이동시키거나 사본을 만든다.
/Content 하위 경로를 추천한다.
그리고 CSV 파일도 본인이 설정한 경로에 위치시킨다.
필자의 경우 프로젝트 폴더 최상위에 CSV 폴더를 만들었고 이 하위에 배치하였다.
이제 언리얼에서 Tools 메뉴에 들어가 Execute 를 눌러 py 파일을 찾아 선택하거나,
에디터 하단의 Cmd 창에서 "py C:/My/Python/File/Path.csv" 같은 형식으로 절대 경로를 넣으면 된다.
그전에!
출력 경로가 제대로 설정되었는지 확인이 필요하다.
작성된 스크립트가 저장될 경로에 맞게 폴더 생성을 해준다.
코드로 폴더 유무를 체크해서 폴더 생성까지 해주면 좋지만 다 만들고 나니 생각나서 추가하지 않았다.
어차피 최초 1회만 생성한다면 그 뒤로는 할 필요가 없으니까..
여기까지 했으면 결과 폴더 내부에 이렇게 잘 생성되고, 맨 처음 완성본과 같은 스크립트가 만들어진다.
이제 3부에서 이 구조체를 기반으로 CSV 를 Import 한 Data Table Asset 을 만들 것이다.
2024.01.24 - [언리얼5 & C++] - [언리얼5] Data Table 로더 만들기 *3부* (CSV) with Python / 에셋 생성하기
'언리얼5 & C++' 카테고리의 다른 글
언리얼5 C++ | 레벨 열기와 특정 게임모드로 레벨 열기 (0) | 2024.02.16 |
---|---|
[언리얼5] Data Table 로더 만들기 *3부* (CSV) with Python / 에셋 생성하기 (1) | 2024.01.24 |
[언리얼5] Data Table 로더 만들기 *1부* (CSV) with Python / 데이터 작성하기 (0) | 2024.01.24 |
[언리얼 파이썬 스크립팅] 언리얼 Pycharm 자동 완성하는법 (삽질 노가다 끝에 공유) (0) | 2024.01.22 |
[Unreal 5, C++] 적 AI 생성하기 : 2 / 적이 나를 조준하게 하기 (0) | 2022.06.28 |