본문 바로가기

볼봇의 코딩 생활/언리얼한 엔진

[UE4] HTTP 통신으로 파일 다운로드

언리얼 엔진으로 VR 리듬게임 프로젝트를 진행하고 있다.

 

리듬 게임에서 중요한건 당연 음악인데, 인게임에 음악은 동적으로 추가되고 제거되어야 한다.

(음악 파일 을 추가할때마다 업데이트를 할 수는 없기 때문)

 

즉, 오디오 에셋을 정적으로 로드시켜 둘 수 없고, 인터넷(서버) 에서 오디오 파일을 다운로드 받아야 하는 상황이다.

 

어떤 상황에서든 에셋파일을 프로젝트에 끼워 넣는 방식이 아니라 파일을 게임 플레이 시간 (런타임) 에서

동적으로 다운로드 해서 로드 하는 부분이 필요할때까 꽤 많다.

 

즉, 파일을 다운로드하는 부분을 제작 해야 한다.

다운로드에서도 많은 방법이 있는데, 스트리밍 서버를 따로 짜서 TCP 프로토콜로 연결, 패킷을 보내서

파일을 작성 하는 방법이 있고,

언리얼 엔진에서 제공하는 편리하고 간단하고 안정적인 Http 모듈이 있다.

 

여기서 우리는 http 모듈을 사용하여 파일을 다운로드 해 보겠다.

http 모듈 설치

당연한 말이지만, http 모듈은 c++ 전용으로 작성되어있기 때문에, 블루프린트에서 사용 할 수 없다.

 

즉, 코딩을 할줄 알아야 아래 강좌를 따라갈 수 있다는것 이다.

 

Object 를 부모로 상속받아서, 새로운 클래스를 만든다.

Actor 나 다른 클래스를 상속 받아도 되지만, Object 를 선택 하면 블루프린트 내부에서도 함께 쓰기 편하다.

(Actor 상속시 스폰 해야 사용 가능함)

 

이 강좌에서는 블루프린트에서도 쓸 수 있도록 하기 위해서 Object 클래스를 상속 받았다.

 

 

 

 

 

[프로젝트명].Build.cs

위 파일을 열어서, 의존성 배열에 "Http" 를 추가 한다.

추가하지 않으면 Http 모듈을 사용 할 수 없다.

 

 

 

코드 작성

 

이제 HttpDownloader.h 파일로 이동 하여 아래와 같이 작성 한다.

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "Runtime/Online/HTTP/Public/Http.h"
#include "HttpDownloader.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FHttpDownloadStartDelegate);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FHttpDownloadProcessDelegate, int32, RecvSize, int32, TotalSize, float, Percent);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FHttpDownloadFinishedDelegate, bool, bIsSuccess, FString, ContentPath);

/**
 * 
 */
UCLASS(BlueprintType)
class RXTX_API UHttpDownloader : public UObject
{
	GENERATED_BODY()

public:
	UHttpDownloader();

	UFUNCTION(BlueprintCallable, DisplayName = "ExecuteDownload", Category = "HttpDownloader")
	void ExecuteDownload(FString SourceURL, FString Path, FString Name);

	UPROPERTY(BlueprintAssignable)
	FHttpDownloadStartDelegate OnDownloadStartCallback;

	UPROPERTY(BlueprintAssignable)
	FHttpDownloadProcessDelegate OnDownloadProcessCallback;
	
	UPROPERTY(BlueprintAssignable)
	FHttpDownloadFinishedDelegate OnDownloadFinishedCallback;

private:
	FHttpModule* HttpModule;

	void HttpRequestProgressDelegate(FHttpRequestPtr RequestPtr, int32 SendBytes, int32 RecvBytes);

	void HttpRequestFinishedDelegate(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful);

private:
	// Ex: http://www.example.com/file.mp3
	UPROPERTY()
		FString SourceURL;

	// Ex: /Folder/Packs/
	UPROPERTY()
		FString Path;
	// Ex: MyVideo.mp4
	UPROPERTY()
		FString Name;
};

각각 코드 블럭에 대해 설명을 간략하게 남긴다.

 

 

 

 

델리게이트 선언

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FHttpDownloadStartDelegate);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FHttpDownloadProcessDelegate, int32, RecvSize, int32, TotalSize, float, Percent);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FHttpDownloadFinishedDelegate, bool, bIsSuccess, FString, ContentPath);

우선 다운로드 상태에 대한 정보를 전달하기 위한 델리게이트를 몇개 선언 했다.

각각 시작할 때, 진행중일 때 (1 틱에 한번씩 호출 됨), 다운로드가 완료됬을 때 실행 된다.

 

FHttpDownloadProcessDelegate 의 인자값에 대한 추가 설명 이다.

int32 RecvSize 받은 바이트 크기
int32 TotalSize 받아야 할 최종 크기
float Percent 진행율

 

FHttpDownloadFinishedDelegate 의 인자값에 대한 추가 설명 이다.

bool bIsSuccess 최종적으로 다운로드에 성공 했는가?
FString 파일의 FullPath 이다.

 

 

함수 선언

UFUNCTION(BlueprintCallable, DisplayName = "ExecuteDownload", Category = "HttpDownloader")
void ExecuteDownload(FString SourceURL, FString Path, FString Name);

블루프린트에서 사용 할 수 있도록 UFUNCTION() 메크로를 추가 했고, 다운로드를 시작 시킬 때

이 함수에 SourceURL 과 경로 (Path), 파일 이름 (Name) 을 인수로 주면 된다.

 

 

void HttpRequestProgressDelegate(FHttpRequestPtr RequestPtr, int32 SendBytes, int32 RecvBytes);
void HttpRequestFinishedDelegate(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful);

Http 모듈이 먼저 델리게이트를 통해 알려주고, 알림을 받은 우리의 c++ 클래스가

데이터를 사용하기 편하게 가공해서 다시 델리게이트로 던져주도록 설계 했다.

 

http 모듈이 델리게이트를 통해 호출할 함수들 이다.

 

 

 

변수 선언

// Ex: http://www.example.com/file.mp3
UPROPERTY()
FString SourceURL;

// Ex: /Folder/Packs/
UPROPERTY()
FString Path;

// Ex: MyVideo.mp4
UPROPERTY()
FString Name;

설명은 생략 한다.

 

 

 

 

몇몇 선언을 알아봤다.

HttpDownloader.cpp 파일로 이동해서 아래와 같이 코드를 작성 한다.

 

#include "HttpDownloader.h"
#include "Public/HAL/PlatformFilemanager.h"
#include "Public/GenericPlatform/GenericPlatformFile.h"
#include "Public/Misc/Paths.h"

UHttpDownloader::UHttpDownloader() {
	HttpModule = &FHttpModule::Get();
}

void UHttpDownloader::ExecuteDownload(FString SourceURL, FString Path, FString Name)
{
	this->SourceURL = SourceURL;
	this->Path = Path;
	this->Name = Name;

	TSharedPtr<IHttpRequest> HttpRequest = HttpModule->CreateRequest();
	HttpRequest->SetVerb("GET");
	HttpRequest->SetURL(this->SourceURL);
	HttpRequest->OnRequestProgress().BindUObject(this, &UHttpDownloader::HttpRequestProgressDelegate);
	HttpRequest->OnProcessRequestComplete().BindUObject(this, &UHttpDownloader::HttpRequestFinishedDelegate);

	HttpRequest->ProcessRequest();
}

void UHttpDownloader::HttpRequestProgressDelegate(FHttpRequestPtr RequestPtr, int32 SendBytes, int32 RecvBytes)
{
	int32 TotalSize = RequestPtr->GetResponse()->GetContentLength();
	float Percent = (float) RecvBytes / TotalSize;

	OnDownloadProcessCallback.Broadcast(RecvBytes, TotalSize, Percent);
}

void UHttpDownloader::HttpRequestFinishedDelegate(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful)
{
	FString BasePath = FPaths::ConvertRelativePathToFull(FPaths::GameSavedDir()) + Path;
	FString FileSavePath = BasePath + Name;		// 경로를 지정 한다.

	// Save File.
	if (Response.IsValid() && EHttpResponseCodes::IsOk(Response->GetResponseCode())) {
		// 해당 플렛폼에 맞는 File 클래스 만들기.
		IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();

		// Dir tree 를 만든다.
		PlatformFile.CreateDirectoryTree(*BasePath);
		IFileHandle* FileHandler = PlatformFile.OpenWrite(*FileSavePath);

		if (FileHandler) {
			// 파일을 새로 쓴다.
			FileHandler->Write(Response->GetContent().GetData(), Response->GetContentLength());
			FileHandler->Flush();		// 저장.

			delete FileHandler;
			bWasSuccessful = true;
		}
		else {
			bWasSuccessful = false;
		}
	}
	else {
		bWasSuccessful = false;
	}

	OnDownloadFinishedCallback.Broadcast(bWasSuccessful, FileSavePath);
}

 

아랫 부분은 코드에 대한 간략한 설명 이다.

 

 

 

UHttpDownloader::UHttpDownloader() {
	HttpModule = &FHttpModule::Get();
}

HttpDownloader 가 생성 될때 HttpModule 을 만든다.

저 작업은 생성자에서 해도 좋다.

 

 

 

TSharedPtr<IHttpRequest> HttpRequest = HttpModule->CreateRequest();
HttpRequest->SetVerb("GET");
HttpRequest->SetURL(this->SourceURL);
HttpRequest->OnRequestProgress().BindUObject(this, &UHttpDownloader::HttpRequestProgressDelegate);
HttpRequest->OnProcessRequestComplete().BindUObject(this, &UHttpDownloader::HttpRequestFinishedDelegate);
HttpRequest->ProcessRequest();

1. HttpRequest (요청) 을 만든다.

2. 요청 방식은 "GET" 으로 설정 한다.

3. 요청 URL 은 인자값으로 받은 URL 을 사용하도록 설정 한다.

4. 중요한데, 요청 후 한 프레임 마다 받은 바이트의 크기를 리턴 해준다. 위에서 만들어둔 함수를 연결 한다.

5. 요청이 완료됬을때 호출할 함수를 연결 한다.

6. 요청을 날린다.

 

 

 

 

void UHttpDownloader::HttpRequestProgressDelegate(FHttpRequestPtr RequestPtr, int32 SendBytes, int32 RecvBytes)
{
	int32 TotalSize = RequestPtr->GetResponse()->GetContentLength();
	float Percent = (float) RecvBytes / TotalSize;

	OnDownloadProcessCallback.Broadcast(RecvBytes, TotalSize, Percent);
}

다운로드 작업이 진행 될 때 호출되는 함수 이다.

요청의 최종 크기를 구하고, 받은 크기로 나누어 진행율을 구한다.

 

만약 백분율로 나타내고 싶다면 Percent 에 * 100 을 해주면 된다.

 

값을 구하고 우리가 만든 클래스에 선언된 델리게이트를 통해 데이터를 뿌려주면 된다.

 

 

 

void UHttpDownloader::HttpRequestFinishedDelegate(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful)
{
	FString BasePath = FPaths::ConvertRelativePathToFull(FPaths::GameSavedDir()) + Path;
	FString FileSavePath = BasePath + Name;		// 경로를 지정 한다.

	// Save File.
	if (Response.IsValid() && EHttpResponseCodes::IsOk(Response->GetResponseCode())) {
		// 해당 플렛폼에 맞는 File 클래스 만들기.
		IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();

		// Dir tree 를 만든다.
		PlatformFile.CreateDirectoryTree(*BasePath);
		IFileHandle* FileHandler = PlatformFile.OpenWrite(*FileSavePath);

		if (FileHandler) {
			// 파일을 새로 쓴다.
			FileHandler->Write(Response->GetContent().GetData(), Response->GetContentLength());
			FileHandler->Flush();		// 저장.

			delete FileHandler;
			bWasSuccessful = true;
		}
		else {
			bWasSuccessful = false;
		}
	}
	else {
		bWasSuccessful = false;
	}

	OnDownloadFinishedCallback.Broadcast(bWasSuccessful, FileSavePath);
}

 

이 코드는 상당히 중요하다. 왜냐하면 파일을 모두 다운로드하고 적당한 경로에 파일을 써야 하기 때문이다.

 

BasePath 를 선언하는 부분을 보도록 하자.

우리가 인자로 받은 경로에 게임의 Saved 경로를 붙여서 기본 경로로 사용 한다.

이렇게 작업하지 않으면 파일을 쓰지 못한다.

 

FileHandler 라는 해당 파일에 대한 스트림을 열도록 한다. (만약 파일이 없다면 새로운 파일을 만들고, 존재하면 덮어씌운다.)

파일 핸들러를 통해 다운로드 받은 데이터를 작성 해주고 Flush 를 호출하여 저장 한다.

 

이 작업이 모두 끝났다면, 다운로드 완료 델리게이트를 호출해 준다.

 

 

 

 

우선 중요한 부분만 설명을 약간 붙여보았다.

오류가 있다면 댓글로 알려주시면 감사하겠습니다. ^^

추가설명은 댓글로 달아주시면 남겨두도록 하겠습니다!

 

 

 

 

사용 하기

C++ 코드에서 사용 하는 방법은 쉬우므로 생략 하고, 블루프린트에서 사용 하는 방법을 알아보겠다.

 

HttpDownloader 클래스를 인스턴스로 생성하고, 델리게이트를 두개 바인딩 한다.

그다음 ExecuteDownload 함수를 인자값과 함께 호출해주면 다운로드는 진행 된다.

 

 

 

 

다운로드 진행 중.

다운로드를 진행한다...

 

 

 

 

프로젝트 폴더로 이동해보면, 음악 파일이 잘 다운로드 됨을 확인 할 수 있다.

 

이상으로 포스팅을 마치도록 한다.