멀티플레이어 게임이라면 채팅이 꼭 있어야지
채팅 시스템을 만들어봤다.
채팅 메시지 한 줄 한 줄에 해당하는 메시지 블록이다.
생성하고 채팅창에 AddChild
할 것이므로 많은 기능이 필요하진 않다.
간단히 텍스트 블록만 하나 만들면 된다.
UCLASS()
class BLASTER_API UChatMessage : public UUserWidget
{
GENERATED_BODY()
public:
UPROPERTY(meta = (BindWidget))
class UTextBlock* ChatMessageTextBlock;
void SetChatMessage(const FString& Message);
};
#include "ChatMessage.h"
#include "Components/TextBlock.h"
void UChatMessage::SetChatMessage(const FString& Message)
{
if (ChatMessageTextBlock)
{
ChatMessageTextBlock->SetText(FText::FromString(Message));
ChatMessageTextBlock->Font.Size = 16;
}
}
텍스트 메시지를 만드는 것이 끝
채팅창 UI에 해당한다.
채팅을 입력하는 텍스트 박스와 사용자들의 채팅이 표시되는 채팅창이 포함된다.
보더로 음영을 표시하고, 버티컬 박스를 만들고 그 안에 채팅창과 입력창을 나란히 배치했다.
채팅창은 스크롤 박스로 채팅이 스크롤로 쭉 표시될 수 있게 하였다.
입력창은 보더로 음영 영역을 표시했으며 그 안에 에디터블 텍스트박스를 만들었다.
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
: 엔터키 재차 입력 시 채팅 보냄
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();
}
}
캐릭터 인터페이스, 알림 메시지 등 모든 HUD 요소들을 관리하는 HUD 클래스를 사용한다.
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
: 메시지를 채팅창에 표시
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;
}
}
}
채팅창에서 만들어진 메시지를 모든 플레이어에게 보낸다. 아래 순서로 진행된다.
따라서 플레이어 컨트롤러에서 구현할 기능은, 게임모드로 메시지를 보내는 서버 RPC와, 게임모드로부터 메시지를 받아서 HUD에 표시하는 클라이언트 RPC이다.
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 표시를 담당하는 클래스를 거쳐서 구현할 계획이다.
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
를 호출한다.
캐릭터에서는 단순히 엔터 키 입력에 대한 인풋만 바인딩하여 플레이어 컨트롤러 콜백 함수를 연결해 준다.
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;
}
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();
}
}
설명 생략
서버에서 채팅 메시지를 관리할 수 있도록 게임모드 하에 구현했다.
싱글톤으로 동작하는 어떠한 클래스여도 상관없을 것이며, 기능들이 많이 추가되어 볼륨이 커지면 아예 ChattingManager
등 별도의 클래스로 빼는 것이 나을 듯
앞서 플레이어 컨트롤러와 연계되어, 서버 RPC를 받아서 게임 내 모든 클라이언트에게 메시지를 표시하는 클라이언트 RPC를 호출하는 함수 하나만 구현하면 된다.
UCLASS()
class BLASTER_API ABlasterGameMode : public AGameMode
{
GENERATED_BODY()
public:
void SendChatMessage(const FString& Message);
};
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값으로 구분되지만 스팀 멀티플레이 환경에서는 스팀 프로필 네임으로 표시된다.