midas+son의 크리에이티브(creative) 이야기

Unreal4로 3~4개월 만든 시뮬레이션(Windows PC용)을

Web에서 돌리고 싶다는 Needs가 있어서(왜 있는지 모르겠지만)

시도는 해보았다.


여러가지 문제가 발생 하였는데

정리를 하자면

1. 윈도우용 lib, exe, #include <Windows.h> 등 사용 불가 Error

2. #pragma once 가 cpp에 있으면 Error(h헤더 파일은 당연히 OK)

3. TEXT("한글") Error

4. Runtime으로 메시를 지원해주는 ProceduralMeshComponent 사용불가

5. Plugin으로 쓰고 있던 RamaSaveComponent 사용불가

6. struct 내부에 있는 생성자나 함수 앞에 struct명:: 이 있으면 Error(예로 들면 struct A{ A::A(){} } 이건 Error, struct A{ A(){} } 이건 OK)

7. strtok_s 사용 불가


위의 문제는 어떻게 해서 cpp문제는 다 잡았다.


프로젝트 셋팅에서 타겟 디바이스를 모바일용으로 바꾸고 퀄리티도 낮추었다.


하지만 결국은 LogPlayLevel: BUILD FAILED

PackagingResults:Error: 오류 실행 실패! Unknown Error


눈에 띄는 출력 로그는 아래와 같다.

LogPlayLevel: UnrealBuildTool: INFO:root:Closure compiler (C:\Program Files\Epic Games\UE_4.15\Engine\Extras\ThirdPartyNotUE\emsdk\emscripten\incoming\third_party\closure-compiler\compiler.jar) does not exist, check the paths in C:\Program Files\Epic Games\UE_4.15\Engine\Intermediate\Build\HTML5\.emscripten

LogPlayLevel: UnrealBuildTool: INFO:root:closure compiler will not be available

LogPlayLevel: UnrealBuildTool: error: Linking globals named '_Z14UELinkerFixupsv': symbol multiply defined!

LogPlayLevel: UnrealBuildTool: Traceback (most recent call last):

LogPlayLevel: UnrealBuildTool:   File "C:\Program Files\Epic Games\UE_4.15\Engine\Extras\ThirdPartyNotUE\emsdk\emscripten\incoming\emcc", line 13, in <module>

LogPlayLevel: UnrealBuildTool:     emcc.run()

LogPlayLevel: UnrealBuildTool:   File "C:\Program Files\Epic Games\UE_4.15\Engine\Extras\ThirdPartyNotUE\emsdk\emscripten\incoming\emcc.py", line 1531, in run

LogPlayLevel: UnrealBuildTool:     final = shared.Building.llvm_opt(final, link_opts, DEFAULT_FINAL)

LogPlayLevel: UnrealBuildTool:   File "C:\Program Files\Epic Games\UE_4.15\Engine\Extras\ThirdPartyNotUE\emsdk\emscripten\incoming\tools\shared.py", line 1633, in llvm_opt

LogPlayLevel: UnrealBuildTool:     assert os.path.exists(target), 'Failed to run llvm optimizations: ' + output

LogPlayLevel: UnrealBuildTool: AssertionError: Failed to run llvm optimizations:

LogPlayLevel: UnrealBuildTool: ERROR: UBT ERROR: Failed to produce item: C:\Users\apex\Desktop\gcsvc2_WebGL_\Binaries\HTML5\gcsvc.js

LogPlayLevel: UnrealBuildTool: Total build time: 353.94 seconds (Local executor: 0.00 seconds)


구글링 해보니 환경변수 얘기도 있고 추측글들이 난무하는데

다른 글을 찾아보니 언리얼에서 버그 fix된 것을 발견했다.

https://issues.unrealengine.com/issue/UE-36717

4.16에 고칠 예정이라니...(현재 최신 버전 4.15.1)

찾아보니 더 전에도 같은 버그 report가 있었는데

재현이 안된다고 안 넘어갔었나보다.


==================================

//결론

어짜피 언리얼이나 유니티나 Web용은 아직 최적화가 안되고 신경도 많이 쓰고 있지 않아서

안 될 줄 알고 있었다.

==================================

Pawn을 움직이는 방법은 여러가지가 있다.

대표적으로 Actor를 강제로 움직이거나,

MovementComponent(CharacterMovementComponent)를 이용하는 방법.

전자는 물리 적용이 되지 않고 좌표에 맞게 움직이는 것이다.

SetActorRelativeLocation(FVector)를 보통 사용한다.(World도 있다.)


하지만 기본적으로 물리가 적용안되니 떨어지거나 언덕을 오르는데 무리가 있다.

후자인 MovementComponent를 사용하는 것은

간편하지만 고려되어야 할게 좀 있다.

이 글은 어떠한 점을 고려해야 하는지에 대한 글이다.


1.

튜토리얼이나 docs를 보면 간단하게 MovementComponent를 가지는 Pawn에서

AddMovementInput(Direction, Val)함수를 호출하면 알아서 이동하는 것을 볼 수 있다.

튜토리얼들은 기본적으로 PlayerController가 그 Pawn에 들어가 있기에 가능한 것이다.


문제는 캐릭터 시점에서 컨트롤 하지 않거나 여러 캐릭을 컨트롤해야 할 경우

즉, 해당 Pawn에 Possess하지 않거나 DefaultPawn으로 설정 하지 않을 경우 AddMovementInput가 동작 안한다.


이럴 경우에는 설정을 하나 더 해주어야 한다.

블루프린트에서는 위와 같은 디테일 창에서 찾으면 된다.

Auto Possess AI가 기본적으로는 Palced in World로 되어있다.

이 옵션을 SpawnedPlaced in World or Spawned로 바꾸어 주자.

그러면 이제서야 AddMovementInput이 동작하는 것을 볼 수 있다.


2.

AddMovementInput 함수의 인자값에 대한 얘기를 해보자.

(FVector WorldDirection, float ScaleValue, bool bForce /*=false*/)

이렇게 3가지 인자가 들어간다.

1번째는 방향 벡터, 

2번째는 스케일값, 

3번째는 false 일 경우 !IsMoveInputIgnored()를 같이 체크하고, true일경우 ControlInputVector에 바로 움직일 값이 들어간다.


문제는 1, 2번째 인자 값이다.

1번째는 방향 Direction인데 어떠한 큰 값을 적용해도

나중에는 크기 1의 단위 벡터로 되어버린다.

2번째 인자값 스케일도 -1.0f~1.0f 값 사이만 적용이 되어야 한다.

아무리 크게 주어도 더 빨리 움직이지 않는다.

여기서 중요한 규칙이 있다.

단위 벡터화 되는 것이 1번째와 2번째 인자를 곱하고 나서 나중이기 때문에

1번째가 단위 벡터가 아니고 2번째가 -1.0f~1.0f이 아니라면

나중에 계산될 단위 벡터가 스케일을 제대로 따라 가지 않게 된다.

이는 중요하게 입력값 제한을 두어야 한다.

설정에 따른 속도 변화와 부드러운 움직임을 원한다면

1번째는 단위벡터, 2번째는 -1.0f~1.0f를 넣자.(큰값을 넣어봤자 의미도 없고 나중 계산이 달라지므로...)

따라야 한다.


3.

2번 문제처럼 값을 받는다면 도대체 어디에서 속도를 제어 할 수 있는가?

그 값은 CharacterMovementComponent에 있다.

컴포넌트 내부에 MaxWalkSpeed, MaxAcceleration 등등의 float값이 있고

아래처럼 초기값이 들어간다.

...

MaxFlySpeed = 600.0f;

MaxWalkSpeed = 600.0f;

MaxSwimSpeed = 300.0f;

MaxCustomMovementSpeed = MaxWalkSpeed;

...

MaxAcceleration = 2048.0f;

BrakingFrictionFactor = 2.0f; // Historical value, 1 would be more appropriate.

BrakingDecelerationWalking = MaxAcceleration;

...

위의 float값들을 직접 변경하면 속도를 컨트롤 할 수 있다.

Character의 움직임 속도는 가속도를 따른다.

Max Speed를 넘기지 않는 선에서 가속도에 따라 속도가 차츰 올라간다.

적절히 변경해 원하는 속도대로 가게 만들자.


이상... 

여러 캐릭터를 움직이게 프로그래밍하면서 알게 된 내용을 정리해보았다.

나름 시간을 들여 알아보았는데 단순하다.

위와 같은 단순하고 필요한 내용들이 튜토리얼이나 docs에 없었다는게 아쉬울 따름이다.



UGameViewportClient* Viewport = GetWorld()->GetGameViewport();

FVector2D pos = Viewport->GetWindow()->GetPositionInScreen();    //시작점. 전체 화면일 경우 -8, -8로 나왔다.(Win10)

FVector2D WH = Viewport->GetWindow()->GetViewportSize();    //실행창 크기. 위에 타이틀 바가 있다면 그 크기도 포함


ffmpeg라는 동영상 프로그램을 사용하기 위해 위의 값들이 필요했다.

원하는 위치부터 원하는 사이즈 만큼 

인자 값들을 넣기 위해 아래처럼 스트링을 만들어 영상 캡쳐가 성공했다.

int X = 0;    //ffmpeg에 들어갈 인자 값들이 int가 아니면 작동 안한다.

int Y = 0;

int Width = 0;

int Height = 0;


//가로축 컨트롤 + width

if (pos.X < 0)    //인자값 중 -offset_x 값이 -(음수)여도 동작 안한다.

{

X = 0;

Width = WH.X + pos.X * 2;    //더해진 만큼 좌우 값을 빼준다.  pos.X가 -이므로 

}

else

{

X = pos.X;

Width = WH.X;

}

//세로축 컨트롤 + Height

if (pos.Y < 0)    //인자값 중 -offset_y 값이 -(음수)여도 동작 안한다.

{

Y = 0; 

Height = WH.Y + pos.Y * 2;    //더해진 만큼 좌우 값을 빼준다.  pos.Y가 -이므로 

}

else

{

Y = pos.Y; 

Height = WH.Y;

}


//아래 코드는 ffmpeg를 실행하기 위한 코드

FString FilePath = FPaths::ConvertRelativePathToFull(FPaths::Combine(*FPaths::GameDir(), TEXT("Plugins/ThirdParty/ffmpeg/"), TEXT("ffmpeg.exe")));

FString Args = FString::Printf(TEXT("-f gdigrab -offset_x %d -offset_y %d -video_size %dx%d -i desktop -r 24000/1001 -q 1 -vf crop %s"), X, Y, Width, Height, *Filename);    //Filename은 인자로 받아온 저장될 파일 이름, 해상도는 -video_size 1024x768 이런 식으로 들어가야 함.

FString Cmd = FString::Printf(TEXT("%s %s %s"), *FilePath, *Args, *Filename);


TRACE("%s", *Cmd);

if (FPaths::FileExists(FilePath))

{

RecordHandle = FPlatformProcess::CreateProc(*FilePath, *Args, false, true, false, nullptr, 0, nullptr, nullptr);

}



//PlayerInput 제거 - Controller = GetWorld()->GetFirstPlayerController();

UPlayerInput* PlayerInput = Controller->PlayerInput;

PlayerInput->AxisMappings.Empty();    //컨트롤러 인풋에 있는 Axis맵핑 비우기

PlayerInput->ActionMappings.Empty();    //콘트롤러 인풋에 있는 Action맵핑 비우기

// InputComponent 제거 - InputComponent = Character->GetInputComponent();

InputComponent->ClearBindingValues();    //소유한 AxisValue 초기화

InputComponent->ClearActionBindings();    //바인딩된 Action값 비우기

InputComponent->AxisBindings.Empty();    //바인딩된 Axis값 TArray 비우기


키맵핑을 하던 컨트롤러를 다른 곳으로 Pawn으로 옮겨 재사용하기 위해서는

그 플레이어 컨트롤에 들어있는 Input값을 초기화 해주어야 깔끔하다.

이동하기 전에 가지고 있던 InputComponent까지 지울 필요는 없었겠지만

깔끔하게 해보자고 해서 알아봤다.


혹시 키맵핑(바인딩) 설정 방법을 못보고 왔다면 아래 링크에 들어가 보자.

http://midason.tistory.com/418

/*

UPlayerInput::AddEngineDefinedAxisMapping은 더이상 바꿀 수 없는 단일 엔진 키맵핑 설정이다. 

캐릭터 하나나 컨트롤 값을 단일로만 사용할 경우 사용한다.

*/

//UPlayerInput::AddEngineDefinedAxisMapping(FInputAxisKeyMapping("NpcMoveForward", EKeys::W, 1.f));

//UPlayerInput::AddEngineDefinedAxisMapping(FInputAxisKeyMapping("NpcMoveForward", EKeys::S, -1.f));

//UPlayerInput::AddEngineDefinedAxisMapping(FInputAxisKeyMapping("NpcMoveRight", EKeys::D, 1.f));

//UPlayerInput::AddEngineDefinedAxisMapping(FInputAxisKeyMapping("NpcMoveRight", EKeys::A, -1.f));

//UPlayerInput::AddEngineDefinedAxisMapping(FInputAxisKeyMapping("NpcLookUp", EKeys::E, 1.f));

//UPlayerInput::AddEngineDefinedAxisMapping(FInputAxisKeyMapping("NpcLookUp", EKeys::Q, -1.f));

//UPlayerInput::AddEngineDefinedActionMapping(FInputActionKeyMapping("NpcRightMouse", EKeys::Zero));

//UPlayerInput::AddEngineDefinedActionMapping(FInputActionKeyMapping("NpcDelete", EKeys::Delete));


/*

컨트롤러에 있는 PlayerInput에 있는 키맵핑수정 가능한 설정이다.

캐릭마다 여러 컨트롤 값을 사용할 경우 유용하다.

*/

UPlayerInput* PlayerInput = GetWorld()->GetFirstPlayerController()->PlayerInput;

PlayerInput->AddAxisMapping(FInputAxisKeyMapping("NpcMoveForward", EKeys::W, 1.f));

PlayerInput->AddAxisMapping(FInputAxisKeyMapping("NpcMoveForward", EKeys::S, -1.f));

PlayerInput->AddAxisMapping(FInputAxisKeyMapping("NpcMoveRight", EKeys::D, 1.f));

PlayerInput->AddAxisMapping(FInputAxisKeyMapping("NpcMoveRight", EKeys::A, -1.f));

PlayerInput->AddAxisMapping(FInputAxisKeyMapping("NpcLookUp", EKeys::E, 1.f));

PlayerInput->AddAxisMapping(FInputAxisKeyMapping("NpcLookUp", EKeys::Q, -1.f));

PlayerInput->AddActionMapping(FInputActionKeyMapping("NpcUnpossess", EKeys::Zero));

PlayerInput->AddActionMapping(FInputActionKeyMapping("NpcDelete", EKeys::Delete));


//인풋컴포넌트에 함수 바인딩

InputComponent->BindAxis("NpcMoveForward", this, &ANpcCharacter::MoveForward);

InputComponent->BindAxis("NpcMoveRight", this, &ANpcCharacter::MoveRight);

InputComponent->BindAxis("NpcLookUp", this, &APawn::AddControllerPitchInput);

InputComponent->BindAction("NpcUnpossess", IE_Pressed, this, &ANpcCharacter::NpcUnpossess);

InputComponent->BindAction("NpcDelete", IE_Pressed, this, &ANpcCharacter::NpcDelete);

프로젝트 설정에서 하나하나 지정하는 것 말고 

C++에서 처리 하는 방식을 찾아 정리하였다.


키맵핑을 삭제 하는 방법은 다음 글로 포스팅 하겠다.

http://midason.tistory.com/420

GetWorld()->GetFirstPlayerController()->Possess(this); //현재 pawn으로 빙의. this는 pawn을 상속받은 클래스

Possess는 소유하다, 지니다는 뜻으로 Controller를 지니게되는 Pawn을 정해준다.

APawn*을 인자로 받는다.(APawn을 상속받은 하위 클래스 포함)

인자로 들어온 pawn으로 Controller와 카메라 시점을 이동할 수 있다.


이것에 대해 아래와 같은 연관되게 되는 함수가 있다.

//현재 Pawn에 대해

//SetupPlayerInputComponent(GetWorld()->GetFirstPlayerController()->InputComponent); //키맵핑 하는 함수

//Possess()함수를 실행하면서 SetupPlayerInputComponent 함수도 같이 실행됨. 그래서 따로 함수 호출을 안해도 된다.


//과거 Pawn에 대해

//GetWorld()->GetFirstPlayerController()->UnPossess();

//이전의 pawn에 있던 Controller가  nullptr이 된다.

위의 함수들은 Possess할 때 자동으로 불리우며

과거와의 연을 끊고 새로운 연을 잇는다고 생각하면 편하다.


정리

Possess()함수를 호출하면 

이전에 Controller가 빙의해있던 pawn은 UnPossess()가 실행 되면서 Controller가 nullptr이 된다.

현재 이동할 pawn이 Controller를 가지게 되고

자동으로 SetupPlayerInputComponent(UInputComponent*)함수를 호출한다.


SetupPlayerInputComponent()함수에서 키맵핑 하는 법은 다음 글에 이어서 하겠다.

어느 액터(그외 하위 클래스)가 가지고 있는 Component를

검색하기 위해 아래와 같은 코드를 많이 사용한다.

UMyComponent* MyComp = MyActor->FindComponentByClass<UMyComponent>();

하지만 당연히 될 줄 알았던게 안됐을 때의 통수란...많은 시간 낭비를 하게 만든다.(하루를 날렸다.)


다른 컴포넌트에서 Owner가 되는 액터의 내부 컴포넌트를 찾기 위해 사용했었는데

설마 여기서 nullptr이 나올 줄은 몰랐다.


FindComponentByClass에 대해서 찾아보니 설명에 떡하니 이렇게 나와있었다.

Searches components array and returns first encountered component of the specified class.

first encountered...처음 발견된 것만...가져온다...

그래서 상속을 계속 받다보니 할아버지, 증조 할아버지격 부모 클래스에서 

통일한 Component의 이름을 가진게 있었고

그 컴포넌트에는 비어있었기에 문제가 생겼었던 것이다.


이 문제를 해결하기 위해 find보다는 특정 변수에 값을 담아

Get함수로 가져오게 하였다.


엔진에서 제공해주는 함수라도 다시 알아보고 사용하자.

확실히 어떤 기능인지 알아보지 않고 사용하면 

실수하는 건 자신이라는 걸 생각하자.



https://wiki.unrealengine.com/Iterators:_Object_%26_Actor_Iterators,_Optional_Class_Scope_For_Faster_Search

참고 사이트


UWorld* myWorld = nullptr;

UDataSingleton* dataSingleton = Cast<UDataSingleton>(GEngine->GameSingleton);

if (dataSingleton)

{

AGameMode* gameMode = dataSingleton->gameMode;

if (gameMode)

{

myWorld = gameMode->GetWorld();

}

}


if (myWorld == nullptr) return;


for (TActorIterator<AItemActor> ActorItr(myWorld); ActorItr; ++ActorItr)

{

AItemActor *Mesh = *ActorItr;

if (Mesh && Mesh->inventoryIndex != -1)

{

this->ApplyItemInInventory(Mesh, Mesh->inventoryIndex);

}

}

UWorld를 받아와서 TActorIterator<액터클래스명>을 받아와 반복문을 돌리는 형태이다.

현재 레벨의 World에서 Spwan된 모든 Actor를 검사하여 <AItemActor>로 캐스팅 되는 것을 찾아

이터레이터 형식으로 변환된다고 생각된다.(내부 코드를 보지 않아 정확하진 않다.)

//4.12.5에서 사용한 코드

//FString path;//인자값

//std::string filename(TCHAR_TO_UTF8(*path));

//인코딩 문제 발생 : https://docs.unrealengine.com/latest/INT/Programming/UnrealArchitecture/StringHandling/CharacterEncoding/


const wchar_t* ptr = *path;

int csize = 1024; //글자 바이트 제한

char* tempFileName = new char[csize];

char def = '?';


//함수 참고 Url : https://msdn.microsoft.com/en-us/library/windows/desktop/dd374130(v=vs.85).aspx

WideCharToMultiByte(CP_ACP, 0, ptr, -1, tempFileName, csize, &def, NULL);


std::string fileName(tempFileName);

delete[] tempFileName;

4.12.5 버전에서는 #include <Windows.h>를 하여 

WideCharToMultiByte를 사용하였었다.

하지만 4.15.1로 오면서 #include <Windows.h> 를 사용하면

Winnt.h 에서 TEXT 매크로 재정의 Warning이 발생하는 등

여러 문제가 발생하여 #include <Windows.h>를 사용하지 않게 바꾸기로 했다.


wchar_t를 char*나 string으로 바꾸는 방법을 구글링으로 열심히 찾았으나

다 의미없는 소스와 블로그 글들 뿐이었다.

한글에 대한 처리가 고려되지 않는 글들도 많았다.

공식 MSDN이나 언리얼에서도 명확한 해결법을 주지 않았다.


UTF-16과 UTF-8에 대한 컨버팅 구조는 찾았으나

CP949로 가는 구조는 못찾았다.


2일째 고민하던 중 찾은 방법은 codecvt을 사용한 방법이었다.

저 헤더는 처음 봤다.(자세한 설명은 맨 위의 참고사이트 링크에 들어가보자.)

컨버트용 헤더로 생각된다.


//include해야 하는 헤더들

#include <string> 

#include <vector> 

#include <codecvt> 

#include <locale>

//4.15.1에서 새로 작성한 코드

std::wstring str = *FilePath;

typedef std::codecvt<wchar_t, char, std::mbstate_t> codecvt_t;

//std::locale loc = std::locale("ko_KR.UTF-8"); // windows 에서는 사용이 안됨

std::locale loc = std::locale("Korean"); // windows 용

codecvt_t const& codecvt = std::use_facet<codecvt_t>(loc);

std::mbstate_t state = std::mbstate_t();

std::vector<char> buf((str.size() + 1) * codecvt.max_length());

wchar_t const* in_next = str.c_str();

char* out_next = &buf[0];

std::codecvt_base::result r = codecvt.out(state,

str.c_str(), str.c_str() + str.size(), in_next,

&buf[0], &buf[0] + buf.size(), out_next);

if (r == std::codecvt_base::error)

throw std::runtime_error("can't convert wstring to string");    //실제 에러가 난다. 언리얼 로그로 바꾸어도 된다.

std::string fileName(&buf[0]);

위와 같이 바꾸니 Warning없이 정상적으로 동작한다.

복잡해보이니 아래와 같이 함수로 바꾸어 사용했다.(참고사이트에 있는 것 복붙 + 약간 수정)

std::string CommonFunc::WCStoMBCS(std::wstring const& str, std::locale const& loc /*= std::locale("")*/)

{

//loc에 std::locale("")로 들어오면 자동으로 OS의 locale을 찾는것 같다.(확인 필요), std::locale()로 하면 에러 난다.

typedef std::codecvt<wchar_t, char, std::mbstate_t> codecvt_t;

codecvt_t const& codecvt = std::use_facet<codecvt_t>(loc);

std::mbstate_t state = std::mbstate_t();

std::vector<char> buf((str.size() + 1) * codecvt.max_length());

wchar_t const* in_next = str.c_str();

char* out_next = &buf[0];

std::codecvt_base::result r = codecvt.out(state,

str.c_str(), str.c_str() + str.size(), in_next,

&buf[0], &buf[0] + buf.size(), out_next);

if (r == std::codecvt_base::error)

throw std::runtime_error("can't convert wstring to string");    //실제 에러가 난다. UELOG로 바꾸어도 된다.

return std::string(&buf[0]);

}

마이그레이션 하던 것 중 제일 골치 아팠던 게 해결 되었다.

굿굿!!

cpp클래스는 마이그레이션과 리팩토링이 완료되어 
BP에서만 건드리면 될 줄 알았는데
싱글톤 클래스가 BP에서 불러올때 nullptr로 받아오는 문제가 발생했다. 

무엇이 문제인가 봤더니 
프로젝트 설정에 빠진 부분이 있었다.


일반 설정에서 기본 클래스에 싱글톤 클래스를 잡는 부분이 있다.

그래야지 cpp에서 GEngine->GameSingleton으로 받아와 캐스팅 할 수 있다.


VS에서 바로 수정하려면 Config 디렉터리에 DefaultEngine.ini 파일을 열어서 아래와 같이 하면 된다.

[/Script/Engine.Engine]

GameSingletonClassName=/Script/프로젝트이름(or상위티렉터리이름).싱글톤클래스파일이름

이름쓰다가 실수할 수도 있고 잘 모를 수도 있으니

실수 방지를 위해서 UE 에디터 열어 프로젝트 설정에서 바꾸자.

자세한건 참고 사이트를 확인하자.