Data Table Asset
1. 전체 코드
2부에서 했던 것처럼 asset_generator.py 파일을 생성하고 아래 코드를 작성한다.
import unreal
import os
import csv
import sys
# 프로젝트 명
project_name = "RottenPotato"
# 데이터 테이블 클래스
asset_class = unreal.DataTable
# 데이터 테이블 에셋 저장 경로
asset_path = "/Game/Table"
# CSV 파일이 존재하는 폴더 경로
csv_folder = unreal.SystemLibrary.get_project_directory() + "CSV"
# struct_path : ex) "/Script/RottenPotato.TestTable"
# 데이터 테이블 에셋 생성 함수
def create_data_table_asset(csv_path):
# 파일명
file_name = str(os.path.basename(csv_path)).split('.')[0]
# 데이터 테이블 파일명
asset_name = "DT_" + file_name
# base struct 스크립트 경로
unreal_struct_path = "/Script/" + project_name + "." + file_name
print("--------- Creating data table asset..." + " Struct path : " + unreal_struct_path + " ----------")
print("-")
# 데이터 테이블 구조체
asset_factory = unreal.DataTableFactory()
asset_factory.struct = unreal.load_object(None, unreal_struct_path)
if asset_factory.struct is None:
unreal.log_error("Asset factory struct is none.")
return
# CSV 를 추출해서 순 데이터만 존재하는 임시파일 생성
origin_rows = []
with open(csv_path, 'r') as origin:
csv_reader = csv.reader(origin)
id_row_index = sys.maxsize
for index, row in enumerate(csv_reader):
origin_rows.append(row)
if str(row[0]).lower() == "id":
id_row_index = index
if id_row_index == -1:
unreal.log_error("Cannot found Id column.")
return
raw_data_rows = []
for index, row in enumerate(origin_rows):
if index >= id_row_index:
raw_data_rows.append(row)
temp_csv_path = unreal.SystemLibrary.get_project_directory() + "Temp_TableGenerator.csv"
with open(temp_csv_path, 'w') as temp_csv:
for row in raw_data_rows:
for index, data in enumerate(row):
temp_csv.write(data)
if index != len(row):
temp_csv.write(",")
temp_csv.write("\n")
csv_factory = unreal.CSVImportFactory()
csv_factory.automated_import_settings.import_row_struct = asset_factory.struct
task = unreal.AssetImportTask()
task.filename = temp_csv_path
task.destination_name = asset_name
task.destination_path = asset_path
task.replace_existing = True
task.automated = True
task.save = True
task.factory = csv_factory
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
try:
os.remove(temp_csv_path)
except FileNotFoundError:
return
except Exception as e:
unreal.log_error(e)
# 시작 함수
def start():
print("####### Data Table Asset 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 : " + 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)
create_data_table_asset(csv_file_path)
# 실행 부분
start()
print("********* Asset Generator Closed. **********")
2. 코드 설명
2부의 struct 생성 스크립트와 유사한 부분이 있다.
CSV 파일을 탐색하고 불러오는 과정이 동일하다.
def start():
...(생략)...
# 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)
...(생략)...
for file in csv_file_list:
print("-")
print("::::::::::::: Start making [" + file + "] ::::::::::::::")
# csv 파일 경로 추출
csv_file_path = os.path.join(csv_folder, file)
create_data_table_asset(csv_file_path)
create_data_table_asset 이 달라지는 함수인데 이 부분을 살펴보겠다.
def create_data_table_asset(csv_path):
# 파일명
file_name = str(os.path.basename(csv_path)).split('.')[0]
# 데이터 테이블 파일명
asset_name = "DT_" + file_name
# base struct 스크립트 경로
unreal_struct_path = "/Script/" + project_name + "." + file_name
제일 처음에는 에셋을 생성하기 위해 필요한 것들을 선언하고 있다.
file_name 은 CSV 파일 경로에서 확장자를 제외한 파일명을 추출한 값이다.
asset_name 은 저장될 에셋의 이름이다. Data Table 이므로 약어 DT_ 를 사용했다.
unreal_struct_path 는 2부에서 만든 struct 스크립트의 경로이다.
이 경로는 상대경로이며 아래와 같은 형식을 가진다.
ex) "/Script/ProjectName.ScriptName"
/Script/프로젝트명.스크립트명
스크립트가 어느 하위 폴더 구조를 가지고 있는지는 중요하지 않다.
# 데이터 테이블 구조체
asset_factory = unreal.DataTableFactory()
asset_factory.struct = unreal.load_object(None, unreal_struct_path)
if asset_factory.struct is None:
unreal.log_error("Asset factory struct is none.")
return
asset_factory 는 unreal 의 DataTableFactory() 를 통해 새로운 것을 생성하고
factory.struct 에 만들었던 구조체 스크립트를 load_object 로 불러온다.
load_class, load_asset 등의 함수가 있지만 무조건 load_object 로 불러와서 object 형태여야 한다.
불러오기에 실패했다면 종료하는 예외처리도 추가한다.
# CSV 를 추출해서 순 데이터만 존재하는 임시파일 생성
origin_rows = []
with open(csv_path, 'r') as origin:
csv_reader = csv.reader(origin)
id_row_index = -1
for index, row in enumerate(csv_reader):
origin_rows.append(row)
if str(row[0]).lower() == "id":
id_row_index = index
if id_row_index == -1:
unreal.log_error("Cannot found Id column.")
return
Id 이름을 가진 행을 찾아낸다.
테이블 규칙에 따라 이 과정이 필요 없을 수도 있다.
raw_data_rows = []
for index, row in enumerate(origin_rows):
if index >= id_row_index:
raw_data_rows.append(row)
raw_data_rows 는 CSV 파일의 행의 배열이다.
가공하지 않은 그대로의 정보이다. 역시 2부에서 발생한 이슈로 인해 캐싱한다.
temp_csv_path = unreal.SystemLibrary.get_project_directory() + "Temp_TableGenerator.csv"
CSV 를 가공해서 임시 CSV 파일을 재 작성 한 후에 저장할 것이므로 임시 경로 또한 생성해준다.
with open(temp_csv_path, 'w') as temp_csv:
for row in raw_data_rows:
for index, data in enumerate(row):
temp_csv.write(data)
if index != len(row):
temp_csv.write(",")
temp_csv.write("\n")
임시 CSV 파일을 쓰기 모드로 열고,
원본 CSV 의 행을 한 줄 씩 기록할 것이다.
사전에 테이블 규칙에 따라 엑셀의 윗 공간이 메모장으로 활용될 수도 있으므로
이 메모 또한 CSV에서 데이터로 추출되었을 것이다.
따라서 이 무의미한 값을 거르고
실 데이터가 존재하는 Id 이후 부터의 값들을 기록한 사본을 만든다고 생각하면 된다.
또한, 메모장 규칙이 없더라도
CSV 에는 int, float 와 같은 타입명을 적어주는 행이 존재하기 때문에
적어도 1번은 데이터 정제가 필요하다.
(데이터 테이블 에셋은 타입명이 필요없다 -> 이미 구조체를 받아서 알고 있기 때문)
위와 같은 형태의 테이블 데이터를
이렇게 정제하는 과정이라고 생각하면 된다.
csv_factory = unreal.CSVImportFactory()
csv_factory.automated_import_settings.import_row_struct = asset_factory.struct
task = unreal.AssetImportTask()
# 불러올 경로
task.filename = temp_csv_path
# 저장 파일명
task.destination_name = asset_name
# 저장 경로
task.destination_path = asset_path
# 덮어쓰기
task.replace_existing = True
task.automated = True
task.save = True
task.factory = csv_factory
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
csv_factory 변수에 unreal.CSVImportFactory() 를 선언한다.
그리고 import_setting 에 로드했던 구조체를 전달하여 포맷을 정해준다.
AssetImportTask() 를 생성하고
task 의 각 속성에 원하는 데이터를 입력한다.
마지막 코드로 task 를 실행하도록 해주면 과정은 완료된다.
try:
os.remove(temp_csv_path)
except FileNotFoundError:
return
except Exception as e:
unreal.log_error(e)
정제된 데이터를 생성하느라 임시파일을 작성했었다.
이 파일은 방치해도 그만이지만,
사용 후 삭제까지 해준다면 좋을 것 같다.
os.remove 를 통해 삭제할 수 있다.
3. 에디터에서 실행하기
실행 전 중요한 점은
2부에서 만들어 둔 C++ Struct 가 존재한다면
꼭 엔진에서 컴파일을 실행하여 새로운 스크립트를 인식할 수 있도록 해주어야 한다.
본인의 환경은 Live Coding 옵션을 사용하지 않으므로 수동 컴파일 버튼을 눌러서 해주지만
Live Coding 환경에서 자동으로 인식할 지는 미지수다. 웬만하면 수동으로 하는게 좋다.
2부와 마찬가지로 py 파일을 프로젝트 내부의 적당한 경로에 이동하고
Tools 메뉴를 통하거나 Cmd 명령어를 통해 py 파일을 실행한다.
실행이 완료되면 컨텐츠 브라우저에 에셋이 만들어진다.
이제 이 에셋은 런타임에 코드로 접근해서 값을 얻어올 수 있는
실 사용이 가능한 상태가 되었다.
5. 의문점
여기서 의문점이 발생할 수 있다.
애초에 Python 작성 시 py 파일을 분리하지 않고,
struct 작성이 끝나는 대로 바로 asset 생성까지 이어지게 하면 되지 않을까?
즉, 한번의 실행으로 할 수 있지 않을까?
본인도 처음에는 당연히 위 생각대로 한 파일에 작성했었으나,
자동으로 생성된 C++ 파일이 에디터에서 컴파일 되어 인식되는 시간이 없다면
asset 생성 시에 load_object() 에서 스크립트를 찾지 못하여 오류가 발생한다.
두 과정 호출 사이에 에디터 컴파일을 코드 상에서 Trigger 시킬 수 있는 방법을 연구했으나,
결국 찾지 못하였다.
따라서 과정을 따로 분리하여 중간에 에디터에서 컴파일 하는 시간을 주어야만 한다.
4부에서는 Tools 메뉴나 Cmd 를 거치지 않고
에디터에 커스텀한 버튼을 추가하여 py 파일을 실행하는 법을 작성할 예정이다.
'언리얼5 & C++' 카테고리의 다른 글
언리얼5 C++ | 서브 레벨 스트리밍, 맵 로드 하기 (1) | 2024.02.16 |
---|---|
언리얼5 C++ | 레벨 열기와 특정 게임모드로 레벨 열기 (0) | 2024.02.16 |
[언리얼5] Data Table 로더 만들기 *2부* (CSV) with Python / C++ 구조체 생성기 작성하기 (2) | 2024.01.24 |
[언리얼5] Data Table 로더 만들기 *1부* (CSV) with Python / 데이터 작성하기 (0) | 2024.01.24 |
[언리얼 파이썬 스크립팅] 언리얼 Pycharm 자동 완성하는법 (삽질 노가다 끝에 공유) (0) | 2024.01.22 |