[UE5] C++ 멀티플레이어 채팅 시스템 구현

kkado·2024년 8월 13일
0

UE5

목록 보기
53/63

멀티플레이어 게임이라면 채팅이 꼭 있어야지
채팅 시스템을 만들어봤다.

1. UserWidget 생성

1-1. ChatMessage

채팅 메시지 한 줄 한 줄에 해당하는 메시지 블록이다.
생성하고 채팅창에 AddChild 할 것이므로 많은 기능이 필요하진 않다.

1) WBP_ChatMessage

간단히 텍스트 블록만 하나 만들면 된다.

2) ChatMessage.h

UCLASS()
class BLASTER_API UChatMessage : public UUserWidget
{
	GENERATED_BODY()

public:
	UPROPERTY(meta = (BindWidget))
	class UTextBlock* ChatMessageTextBlock;

	void SetChatMessage(const FString& Message);
};

3) ChatMessage.cpp

#include "ChatMessage.h"
#include "Components/TextBlock.h"

void UChatMessage::SetChatMessage(const FString& Message)
{
	if (ChatMessageTextBlock)
	{
		ChatMessageTextBlock->SetText(FText::FromString(Message));
		ChatMessageTextBlock->Font.Size = 16;
	}
}

텍스트 메시지를 만드는 것이 끝

1-2. Chatting

채팅창 UI에 해당한다.
채팅을 입력하는 텍스트 박스와 사용자들의 채팅이 표시되는 채팅창이 포함된다.

1) WBP_Chatting

보더로 음영을 표시하고, 버티컬 박스를 만들고 그 안에 채팅창과 입력창을 나란히 배치했다.
채팅창은 스크롤 박스로 채팅이 스크롤로 쭉 표시될 수 있게 하였다.
입력창은 보더로 음영 영역을 표시했으며 그 안에 에디터블 텍스트박스를 만들었다.

2) Chatting.h

UCLASS()
class BLASTER_API UChatting : public UUserWidget
{
	GENERATED_BODY()

public:
	virtual void NativeConstruct() override;

	UFUNCTION()
	void ActivateChatText();

	UPROPERTY(meta = (BindWidget))
	class UEditableText* ChatText;

	UPROPERTY(meta = (BindWidget))
	class UScrollBox* ChatScrollBox;

protected:
	UFUNCTION()
	void OnTextCommitted(const FText& Text, ETextCommit::Type CommitMethod);
};

ActivateChatText : 엔터키 입력 시 채팅창 활성화
OnTextCommitted : 엔터키 재차 입력 시 채팅 보냄

3) Chatting.cpp


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

	// Text commit 콜백 함수 바인딩
	if (ChatText)
	{
		ChatText->OnTextCommitted.AddDynamic(this, &UChatting::OnTextCommitted);
	}

	ChatText->SetIsEnabled(false);
}

void UChatting::OnTextCommitted(const FText& Text, ETextCommit::Type CommitMethod)
{
	if (CommitMethod == ETextCommit::OnEnter)
	{
		if (ChatText)
		{
			// 좌우 공백 제거
			FText InputText = ChatText->GetText();
			FString TrimmedText = InputText.ToString().TrimStartAndEnd();

			if (!TrimmedText.IsEmpty())
			{
				ABlasterPlayerController* PlayerController = Cast<ABlasterPlayerController>(GetWorld()->GetFirstPlayerController());
				if (PlayerController)
				{
					// TrimmedText 앞에 UserName을 붙여 최종 Message 생성
					APlayerState* PlayerState = PlayerController->GetPlayerState<APlayerState>();
					FString Message = FString::Printf(TEXT("%s : %s"), *PlayerState->GetPlayerName(), *TrimmedText);
					// 채팅 메시지를 보내기 위한 Server RPC 호출
					PlayerController->ServerSendChatMessage(Message);

					// 다시 FInputModeGameOnly로 인풋모드 변경
					FInputModeGameOnly InputMode;
					PlayerController->SetInputMode(InputMode);

					// 채팅창 비우고 비활성화
					ChatText->SetText(FText::GetEmpty());
					ChatText->SetIsEnabled(false);
				}
			}
		}
	}
}

void UChatting::ActivateChatText()
{
	if (ChatText)
	{
		ChatText->SetIsEnabled(true);
		ChatText->SetFocus();
	}
}

2. HUD

캐릭터 인터페이스, 알림 메시지 등 모든 HUD 요소들을 관리하는 HUD 클래스를 사용한다.

HUD.h

UCLASS()
class BLASTER_API ABlasterHUD : public AHUD
{
public:
	UPROPERTY(EditAnywhere)
	TSubclassOf<UUserWidget> ChattingClass;

	UPROPERTY(EditAnywhere)
	TSubclassOf<UUserWidget> ChatMessageClass;
    
	void AddChatting();
	void AddChatMessage(const FString& Message);
}

AddChatting : 채팅창 UI를 화면에 표시(add to viewport)
AddChatMessage : 메시지를 채팅창에 표시

HUD.cpp


void ABlasterHUD::PostInitializeComponents()
{
	Super::PostInitializeComponents();

	APlayerController* PlayerController = GetOwningPlayerController();
	if (PlayerController)
	{
		if (ChattingClass)
		{
			Chatting = CreateWidget<UChatting>(PlayerController, ChattingClass);
		}
	}
}

void ABlasterHUD::AddChatting()
{
	if (Chatting)
	{
		Chatting->AddToViewport();
	}
}

void ABlasterHUD::AddChatMessage(const FString& Message)
{
	OwningPlayer = OwningPlayer == nullptr ? GetOwningPlayerController() : OwningPlayer;
	Chatting = Chatting == nullptr ? CreateWidget<UChatting>(OwningPlayer, ChattingClass) : Chatting;

	if (OwningPlayer && ChattingClass && Chatting && ChatMessageClass)
	{
    	// 메시지 한 줄에 해당하는 위젯을 만듦
		UChatMessage* ChatMessageWidget = CreateWidget<UChatMessage>(OwningPlayer, ChatMessageClass);
		if (ChatMessageWidget)
		{
        	// Scroll box에 AddChild 부착
			ChatMessageWidget->SetChatMessage(Message);
			Chatting->ChatScrollBox->AddChild(ChatMessageWidget);
			Chatting->ChatScrollBox->ScrollToEnd();
			Chatting->ChatScrollBox->bAnimateWheelScrolling = true;
		}
	}
}

3. PlayerController & Character

3-1. PlayerController

채팅창에서 만들어진 메시지를 모든 플레이어에게 보낸다. 아래 순서로 진행된다.

  1. 플레이어 컨트롤러에서 서버RPC를 호출하고
  2. 이 서버 RPC에서 게임모드에 접근해서
  3. 모든 플레이어에게 채팅을 표시하는 클라이언트 RPC를 호출

따라서 플레이어 컨트롤러에서 구현할 기능은, 게임모드로 메시지를 보내는 서버 RPC와, 게임모드로부터 메시지를 받아서 HUD에 표시하는 클라이언트 RPC이다.

1) PlayerController.h

UCLASS()
class BLASTER_API ABlasterPlayerController : public APlayerController
{
public:
	UFUNCTION()
	void ActivateChatBox();

	UFUNCTION(Server, Reliable)
	void ServerSendChatMessage(const FString& Message);

	UFUNCTION(Client, Reliable)
	void ClientAddChatMessage(const FString& Message);

private:
	UPROPERTY()
	class ABlasterHUD* BlasterHUD;
}

서버, 클라이언트 RPC를 만들었다. HUD 표시를 담당하는 클래스를 거쳐서 구현할 계획이다.

2) PlayerController.cpp

void ABlasterPlayerController::ActivateChatBox()
{
	BlasterHUD = BlasterHUD == nullptr ? Cast<ABlasterHUD>(GetHUD()) : BlasterHUD;
	if (BlasterHUD && BlasterHUD->Chatting)
	{
		BlasterHUD->Chatting->ActivateChatText();
	}
}

void ABlasterPlayerController::ServerSendChatMessage_Implementation(const FString& Message)
{
	ABlasterGameMode* GameMode = GetWorld()->GetAuthGameMode<ABlasterGameMode>();
	if (GameMode)
	{
		GameMode->SendChatMessage(Message);
	}
}

void ABlasterPlayerController::ClientAddChatMessage_Implementation(const FString& Message)
{
	BlasterHUD = BlasterHUD == nullptr ? Cast<ABlasterHUD>(GetHUD()) : BlasterHUD;
	if (BlasterHUD)
	{
		BlasterHUD->AddChatMessage(Message);
	}
}

서버 RPC의 경우 게임모드 측에 구현된 SendChatMessage를 호출하며
클라이언트 RPC의 경우 실질적으로 채팅창에 채팅 메시지를 표시하는 AddChatMessage 를 호출한다.

3-2. Character

캐릭터에서는 단순히 엔터 키 입력에 대한 인풋만 바인딩하여 플레이어 컨트롤러 콜백 함수를 연결해 준다.

Character.h


UCLASS()
class BLASTER_API ABlasterCharacter : public ACharacter
{
protected:
	void ChatButtonPressed(const FInputActionValue& Value);
private:
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
	class UInputMappingContext* DefaultMappingContext;
    
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input, meta = (AllowPrivateAccess = "true"))
	class UInputAction* ChatAction;
}

Character.cpp

void ABlasterCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
	if (UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(PlayerInputComponent))
    {
		EnhancedInputComponent->BindAction(ChatAction, ETriggerEvent::Triggered, this, &ABlasterCharacter::ChatButtonPressed);
	}
}

void ABlasterCharacter::ChatButtonPressed(const FInputActionValue& Value)
{
	BlasterPlayerController = BlasterPlayerController == nullptr ? Cast<ABlasterPlayerController>(Controller) : BlasterPlayerController;
	if (BlasterPlayerController)
	{
		BlasterPlayerController->ActivateChatBox();
	}
}

설명 생략


4. GameMode

서버에서 채팅 메시지를 관리할 수 있도록 게임모드 하에 구현했다.
싱글톤으로 동작하는 어떠한 클래스여도 상관없을 것이며, 기능들이 많이 추가되어 볼륨이 커지면 아예 ChattingManager 등 별도의 클래스로 빼는 것이 나을 듯

앞서 플레이어 컨트롤러와 연계되어, 서버 RPC를 받아서 게임 내 모든 클라이언트에게 메시지를 표시하는 클라이언트 RPC를 호출하는 함수 하나만 구현하면 된다.

GameMode.h

UCLASS()
class BLASTER_API ABlasterGameMode : public AGameMode
{
	GENERATED_BODY()
    
public:
	void SendChatMessage(const FString& Message);
};

GameMode.cpp

void ABlasterGameMode::SendChatMessage(const FString& Message)
{
	// 모든 플레이어 컨트롤러에 대한 Iterator을 이용
	for (FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It)
	{
		ABlasterPlayerController* BlasterPlayerController = Cast<ABlasterPlayerController>(*It);
		if (BlasterPlayerController)
		{
			BlasterPlayerController->ClientAddChatMessage(Message);
		}
	}
}

결과

에디터 환경에서는 ID값으로 구분되지만 스팀 멀티플레이 환경에서는 스팀 프로필 네임으로 표시된다.

profile
베이비 게임 개발자

0개의 댓글