기타/Unreal

[Unreal C++] 리슨 서버 채팅 기능 만들기

푸쿠이 2021. 6. 2. 18:02

채팅 기능을 만들긴 했는데, 이거를 코드로 설명하면서 적으려니, 뭔가 많아보인다.

한 눈에 들어오도록 흐름을 이미지로 만들어봐야겠다.

 

+ 만들었는데 나는 이해가 되는데, 처음 보는 사람도 이해가 될라나..?

 

UMG 제작하고, 기능 구현하기

총 2가지를 제작했다.

채팅 기능을 담당하는 WB_Chat

전체 화면 UI 기능을 담당하는 WB_Main

WB_Chat에서 상속하는 UW_Chat 클래스

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "UW_Chat.generated.h"

UCLASS()
class MYRPG_API UUW_Chat : public UUserWidget
{
	GENERATED_BODY()

public:
	virtual void NativeConstruct() override;
    
public:
	void AddChatMessage(const FString& Message);
	FText GetChatInputTextMessage();
	TSharedPtr<class SWidget> GetChatInputTextObject(); // 나중에 Enter 누르면, 채팅에 포커싱하기 위해서.

private:
	UPROPERTY(Meta = (BindWidget))
	class UScrollBox* ChatHistoryArea;

	UPROPERTY(Meta = (BindWidget))
	class UEditableTextBox* ChatInputText;
    
private:
	// 이 함수를 쓰기 위해서는,
	// Build 파일에 "UMG" 모듈을 추가하고, "Slate", "SlateCore" 주석을 해제해야한다.
	UFUNCTION()
	void OnChatTextCommitted(const FText& Text, ETextCommit::Type CommitMethod);
};

 

#include "UW_Chat.h"
#include "Components/TextBlock.h"
#include "Components/ScrollBox.h"

void UUW_Chat::NativeConstruct()
{
	Super::NativeConstruct();

	ChatInputText->OnTextCommitted.AddDynamic(this, &UUW_Chat::OnChatTextCommitted);
}

void UUW_Chat::AddChatMessage(const FString& Message)
{
	// Text 오브젝트를 생성하고, ScrollBox에 추가한다.
	UTextBlock* NewTextBlock = NewObject<UTextBlock>(ChatHistoryArea);
	NewTextBlock->SetText(FText::FromString(Message));

	ChatHistoryArea->AddChild(NewTextBlock);
	ChatHistoryArea->ScrollToEnd(); // 가장 최근 채팅을 보기 위해, 스크롤을 가장 아래로 내린다.
}

void UUW_Chat::SetChatInputTextMessage(const FText& Text)
{
	ChatInputText->SetText(Text);
}

TSharedPtr<SWidget> UUW_Chat::GetChatInputTextObject()
{
	return ChatInputText->GetCachedWidget();
}

void UUW_Chat::OnChatTextCommitted(const FText& Text, ETextCommit::Type CommitMethod)
{
	AMain_PC* MyPC = Cast<AMain_PC>(UGameplayStatics::GetPlayerController(GetWorld(), 0));
	if (MyPC == nullptr) return;

	switch (CommitMethod)
	{
	case ETextCommit::OnEnter:
		if (Text.IsEmpty() == false)
		{
			MyPC->SendMessage(Text); // 메시지 보냄.
			SetChatInputTextMessage(FText::GetEmpty()); // 메세지 전송했으니, 비워줌.
		}
		MyPC->FocusGame(); // 다시 게임으로 포커싱.
		break;
	case ETextCommit::OnCleared:
		MyPC->FocusGame(); // 다시 게임으로 포커싱.
		break;
	}
}

WB_Main에서 상속하는 UW_Main 클래스

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "UW_Main.generated.h"

UCLASS()
class MYRPG_API UUW_Main : public UUserWidget
{
	GENERATED_BODY()
	
public:
	TSharedPtr<class SWidget> GetChatInputTextObject();
	void AddChatMessage(const FString& Message);
	
private:
	UPROPERTY(Meta = (BindWidget))
	class UUW_Chat* WB_Chat;
};
#include "UW_Main.h"
#include "UW_Chat.h"

TSharedPtr<SWidget> UUW_Main::GetChatInputTextObject()
{
	return WB_Chat->GetChatInputTextObject();
}

void UUW_Main::AddChatMessage(const FString& Message)
{
	WB_Chat->AddChatMessage(Message);
}

 

HUD에서 UI 기능 정리하기

UI 작업의 편리를 위해, UI 관련 처리는 모두 HUD를 거쳐서 실행된다.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/HUD.h"
#include "Main_HUD.generated.h"

UCLASS()
class MYRPG_API AMain_HUD : public AHUD
{
	GENERATED_BODY()
	
public:
	AMain_HUD();
	virtual void BeginPlay() override;

public:
	TSharedPtr<class SWidget> GetChatInputTextObject();
	void AddChatMessage(const FString& Message);

private:
	TSubclassOf<class UUW_Main> MainUIClass;
	class UUW_Main* MainUIObject;

private:
	bool CheckUIObject();
	bool CreateUIObject();
};
GetChatInputTextObject#include "Main_HUD.h"
#include "../UI/UW_Main.h"

AMain_HUD::AMain_HUD()
{
	static ConstructorHelpers::FClassFinder<UUW_Main> WB_Main(TEXT("WidgetBlueprint'/Game/Blueprints/UI/WB/Main/WB_Main.WB_Main_C'"));
	if (WB_Main.Succeeded())
	{
		MainUIClass = WB_Main.Class;
	}
}

void AMain_HUD::BeginPlay()
{
	Super::BeginPlay();

	CheckUIObject(); // 시작하면 UI를 생성한다.
}

TSharedPtr<SWidget> AMain_HUD::GetChatInputTextObject()
{
	return MainUIObject->GetChatInputTextObject();
}

void AMain_HUD::AddChatMessage(const FString& Message)
{
	// BeginPlay()가 실행되기 전에 이 함수가 먼저 실행 될 수도 있다.
	// UI가 생기기 전에 UI에 접근하면 오류가 나기 때문에 검사한다.
	if (!CheckUIObject()) return;

	MainUIObject->AddChatMessage(Message);
}

bool AMain_HUD::CheckUIObject()
{
	if (MainUIObject == nullptr) // UI가 없다면 생성.
	{
		return CreateUIObject();
	}
	return true; // 있다면 True.
}

bool AMain_HUD::CreateUIObject()
{
	if (MainUIClass)
	{
		MainUIObject = CreateWidget<UUW_Main>(GetOwningPlayerController(), MainUIClass);
		if (MainUIObject)
		{
			MainUIObject->AddToViewport();
			return true; // 만들었다면 true.
		}
	}
	return false; // 못 만들었다면 false.
}

 

GameMode에서 설정하기

게임 실행 시, 해당 HUD가 생성될 수 있도록 한다.

#include "Main_GM.h"
#include "../Player/Player_Base.h"
#include "Main_HUD.h"
#include "Main_PC.h"

AMain_GM::AMain_GM()
{
	DefaultPawnClass = APlayer_Base::StaticClass();
	PlayerControllerClass = AMain_PC::StaticClass();
	HUDClass = AMain_HUD::StaticClass();
}

 

PlayerController 에서 UI 기능 사용하기

언리얼 에디터에서 프로젝트 세팅->입력에서 채팅 입력 키를 바인딩해준다.

나는 Enter 키를 누르면 "Chat" 액션이 발동되도록 설정했다.

UCLASS()
class MYRPG_API AMain_PC : public APlayerController
{
	GENERATED_BODY()
	
public:
	virtual void BeginPlay() override;
	virtual void SetupInputComponent() override;

public:
	void SendMessage(const FText& Text);

public:
	UFUNCTION()
	void FocusChatInputText();

	UFUNCTION()
	void FocusGame();

private:
	UFUNCTION(Server, Unreliable)
	void CtoS_SendMessage(const FString& Message);

	UFUNCTION(Client, Unreliable)
	void StoC_SendMessage(const FString& Message);
};
#include "Main_PC.h"
#include "../MyGameInstance.h"
#include "Main_HUD.h"
#include "Kismet/GameplayStatics.h"

void AMain_PC::BeginPlay()
{
	Super::BeginPlay();

	SetShowMouseCursor(false);
	SetInputMode(FInputModeGameOnly());
}

void AMain_PC::SetupInputComponent()
{
	Super::SetupInputComponent();
	
	// 액션 키 바인딩.
	InputComponent->BindAction(TEXT("Chat"), EInputEvent::IE_Pressed, this, &AMain_PC::FocusChatInputText);
}

void AMain_PC::SendMessage(const FText& Text)
{
	// GameInstance에 저장해두었던 내 닉네임.
	// 게시글로는 안 적었다. 이거까지 설명하진 않겠다.
	UMyGameInstance* MyGI = GetGameInstance<UMyGameInstance>();
	if (MyGI)
	{
		FString UserName = MyGI->GetUserName();
		FString Message = FString::Printf(TEXT("%s : %s"), *UserName, *Text.ToString());

		CtoS_SendMessage(Message); // 서버에서 실행될 수 있도록 보낸다.
	}
}

void AMain_PC::FocusChatInputText()
{
	AMain_HUD* HUD = GetHUD<AMain_HUD>();
	if (HUD == nullptr) return;

	FInputModeUIOnly InputMode;
	InputMode.SetWidgetToFocus(HUD->GetChatInputTextObject());

	SetInputMode(InputMode);
}

void AMain_PC::FocusGame()
{
	SetInputMode(FInputModeGameOnly());
}

void AMain_PC::CtoS_SendMessage_Implementation(const FString& Message)
{
	// 서버에서는 모든 PlayerController에게 이벤트를 보낸다.
	TArray<AActor*> OutActors;
	UGameplayStatics::GetAllActorsOfClass(GetPawn()->GetWorld(), APlayerController::StaticClass(), OutActors);
	for (AActor* OutActor : OutActors)
	{
		AMain_PC* PC = Cast<AMain_PC>(OutActor);
		if (PC)
		{
			PC->StoC_SendMessage(Message);
		}
	}
}

void AMain_PC::StoC_SendMessage_Implementation(const FString& Message)
{
	// 서버와 클라이언트는 이 이벤트를 받아서 실행한다.
	AMain_HUD* HUD = GetHUD<AMain_HUD>();
	if (HUD == nullptr) return;

	HUD->AddChatMessage(Message);
}

 

구현 결과

들어오거나 나가는 유저가 있으면, "누구누구가 입장했습니다. / 퇴장했습니다." 이런 기능도 넣어봐야겠다.