文章目录
- 创建主菜单需要的
- 创建等待界面
- [配置和获取协调器 URL](#配置和获取协调器 URL)
- 撰写和发送会话创建请求
创建主菜单需要的
创建主菜单游戏模式
MainMenuGameMode
创建主菜单游戏控制器
MainMenuPlayerController
cpp
#pragma once
#include "CoreMinimal.h"
#include "MenuPlayerController.h"
#include "MainMenuPlayerController.generated.h"
/**
*
*/
UCLASS()
class CRUNCH_API AMainMenuPlayerController : public AMenuPlayerController
{
GENERATED_BODY()
public:
AMainMenuPlayerController();
};
cpp
#include "MainMenuPlayerController.h"
AMainMenuPlayerController::AMainMenuPlayerController()
{
// 禁用自动管理相机目标
bAutoManageActiveCameraTarget = false;
}
分别创建两个的蓝图
创建一个摄像机,自动启用玩家0
创建主菜单界面UI
MainMenuWidget
cpp
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Components/WidgetSwitcher.h"
#include "Framework/MGameInstance.h"
#include "MainMenuWidget.generated.h"
class UButton;
/**
*
*/
UCLASS()
class CRUNCH_API UMainMenuWidget : public UUserWidget
{
GENERATED_BODY()
public:
virtual void NativeConstruct() override;
/******************************/
/* Main */
/******************************/
private:
UPROPERTY(meta = (BindWidget))
TObjectPtr<UWidgetSwitcher> MainSwitcher;
// 游戏实例
UPROPERTY()
TObjectPtr<UMGameInstance> MGameInstance;
/******************************/
/* Login */
/******************************/
private:
// 登录界面的根组件
UPROPERTY(meta = (BindWidget))
TObjectPtr<UWidget> LoginWidgetRoot;
// 登录按钮
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> LoginButton;
// 登录按钮点击事件
UFUNCTION()
void OnLoginButtonClicked();
/**
* 登录完成时的回调函数
* @param bWasSuccessful 是否登录成功
* @param PlayerNickname 登录成功时的玩家昵称
* @param ErrorMsg 登录失败时的错误信息
*/
void LoginCompleted(bool bWasSuccessful, const FString& PlayerNickname, const FString& ErrorMsg);
};
cpp
#include "MainMenuWidget.h"
#include "Components/Button.h"
void UMainMenuWidget::NativeConstruct()
{
Super::NativeConstruct();
// 获取游戏实例
MGameInstance = GetGameInstance<UMGameInstance>();
if (MGameInstance)
{
}
// 绑定登录按钮点击事件
LoginButton->OnClicked.AddDynamic(this, &UMainMenuWidget::OnLoginButtonClicked);
}
void UMainMenuWidget::OnLoginButtonClicked()
{
// 登录按钮点击时触发
UE_LOG(LogTemp, Warning, TEXT("Longing In!"))
}
void UMainMenuWidget::LoginCompleted(bool bWasSuccessful, const FString& PlayerNickname, const FString& ErrorMsg)
{
if (bWasSuccessful)
{
UE_LOG(LogTemp, Warning, TEXT("登录成功: %s"), *PlayerNickname)
}else
{
UE_LOG(LogTemp, Warning, TEXT("登录失败: %s"), *ErrorMsg)
}
}
创建蓝图版本
然后修改对应命名
设置切换器的锚点
设置一下登录
添加一个尺寸框包裹
添加一个背景模糊
把UI添加到主菜单玩家控制器中
实现登录游戏实例
UMGameInstance
cpp
/**
* 登录完成委托
* 参数:
* - bWasSuccessful:登录是否成功
* - PlayerNickName:玩家昵称
* - ErrorMsg:错误信息
*/
DECLARE_MULTICAST_DELEGATE_ThreeParams(FOnLoginCompleted, bool /*bWasSuccessful*/, const FString& /*PlayerNickName*/, const FString& /*ErrorMsg*/);
cpp
/*************************************/
/* 登录功能 */
/*************************************/
public:
// 检查是否已登录
bool IsLoggedIn() const;
// 检查是否正在登录中
bool IsLoggingIn() const;
// 客户端通过账户门户登录
void ClientAccountPortalLogin();
// 登录完成委托
FOnLoginCompleted OnLoginCompleted;
private:
// 客户端登录实现
void ClientLogin(const FString& Type, const FString& Id, const FString& Token);
// 登录完成回调
void LoginCompleted(int NumOfLocalPlayer, bool bWasSuccessful, const FUniqueNetId& UserId, const FString& Error);
// 登录委托句柄
FDelegateHandle LoggingInDelegateHandle;
cpp
bool UMGameInstance::IsLoggedIn() const
{
if (IOnlineIdentityPtr IdentityPtr = UTNetStatics::GetIdentityPtr())
{
// 查询本地玩家0的登录状态,是否为已登录
return IdentityPtr->GetLoginStatus(0) == ELoginStatus::LoggedIn;
}
// 如果IdentityPtr无效,则默认未登录
return false;
}
bool UMGameInstance::IsLoggingIn() const
{
// 如果登录委托句柄有效,说明还在等待登录回调
return LoggingInDelegateHandle.IsValid();
}
void UMGameInstance::ClientAccountPortalLogin()
{
// 调用统一的ClientLogin接口,使用AccountPortal方式
ClientLogin("AccountPortal", "", "");
}
void UMGameInstance::ClientLogin(const FString& Type, const FString& Id, const FString& Token)
{
if (IOnlineIdentityPtr IdentityPtr = UTNetStatics::GetIdentityPtr())
{
// 如果已经有一个登录委托在监听,先移除它,避免重复绑定
if (LoggingInDelegateHandle.IsValid())
{
IdentityPtr->OnLoginCompleteDelegates->Remove(LoggingInDelegateHandle);
LoggingInDelegateHandle.Reset();
}
// 绑定登录完成回调
LoggingInDelegateHandle = IdentityPtr->OnLoginCompleteDelegates->AddUObject(this, &UMGameInstance::LoginCompleted);
// 调用OnlineSubsystem的登录函数(异步)
if (!IdentityPtr->Login(0,FOnlineAccountCredentials(Type, Id, Token)))
{
UE_LOG(LogTemp, Warning, TEXT("登录失败!"))
if (LoggingInDelegateHandle.IsValid())
{
IdentityPtr->OnLoginCompleteDelegates->Remove(LoggingInDelegateHandle);
LoggingInDelegateHandle.Reset();
}
// 通知外部,登录失败
OnLoginCompleted.Broadcast(false, "", "登录失败!");
}
}
}
void UMGameInstance::LoginCompleted(int32 NumOfLocalPlayer, bool bWasSuccessful, const FUniqueNetId& UserId,
const FString& Error)
{
if (IOnlineIdentityPtr IdentityPtr = UTNetStatics::GetIdentityPtr())
{
// 移除登录完成委托
if (LoggingInDelegateHandle.IsValid())
{
IdentityPtr->OnLoginCompleteDelegates->Remove(LoggingInDelegateHandle);
LoggingInDelegateHandle.Reset();
}
FString PlayerNickname = "";
if (bWasSuccessful)
{
// 获取玩家昵称
PlayerNickname = IdentityPtr->GetPlayerNickname(UserId);
UE_LOG(LogTemp, Warning, TEXT("登录成功: %s"), *(PlayerNickname))
}
else
{
UE_LOG(LogTemp, Warning, TEXT("登录失败: %s"), *(Error))
}
OnLoginCompleted.Broadcast(bWasSuccessful, PlayerNickname, Error);
}else
{
OnLoginCompleted.Broadcast(false, "", "无法找到身份指针");
}
}
UMainMenuWidget
到主菜单UI中绑定委托
cpp
void UMainMenuWidget::NativeConstruct()
{
Super::NativeConstruct();
// 获取游戏实例
MGameInstance = GetGameInstance<UMGameInstance>();
if (MGameInstance)
{
MGameInstance->OnLoginCompleted.AddUObject(this, &UMainMenuWidget::LoginCompleted);
}
// 绑定登录按钮点击事件
LoginButton->OnClicked.AddDynamic(this, &UMainMenuWidget::OnLoginButtonClicked);
}
void UMainMenuWidget::OnLoginButtonClicked()
{
// 登录按钮点击时触发
UE_LOG(LogTemp, Warning, TEXT("Longing In!"))
if (MGameInstance && !MGameInstance->IsLoggedIn() && !MGameInstance->IsLoggingIn())
{
// 触发登录
MGameInstance->ClientAccountPortalLogin();
}
}
编译运行后点击登录会跳转到网页登录
创建等待界面
WaitingWidget
cpp
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Components/Button.h"
#include "Components/TextBlock.h"
#include "WaitingWidget.generated.h"
/**
* 等待界面控件
* 用于在需要玩家等待(如连接、加载、匹配)时显示提示信息,
* 并可选提供"取消"按钮。
*/
UCLASS()
class CRUNCH_API UWaitingWidget : public UUserWidget
{
GENERATED_BODY()
public:
virtual void NativeConstruct() override;
// 获取 Cancel 按钮的点击事件引用,并清空之前绑定的所有回调。
FOnButtonClickedEvent& ClearAndGetButtonClickedEvent();
/**
* 设置等待提示信息,并控制"取消"按钮是否可见。
* @param WaitInfo 等待提示文本
* @param bAllowCancel 是否允许取消(决定按钮显隐)
*/
void SetWaitInfo(const FText& WaitInfo, bool bAllowCancel = false);
private:
// 等待提示文本
UPROPERTY(meta = (BindWidget))
TObjectPtr<UTextBlock> WaitInfoText;
// 取消按钮
UPROPERTY(meta = (BindWidget))
TObjectPtr<UButton> CancelButton;
};
cpp
#include "WaitingWidget.h"
void UWaitingWidget::NativeConstruct()
{
Super::NativeConstruct();
}
FOnButtonClickedEvent& UWaitingWidget::ClearAndGetButtonClickedEvent()
{
// 清空按钮已有的点击事件
CancelButton->OnClicked.Clear();
return CancelButton->OnClicked;
}
void UWaitingWidget::SetWaitInfo(const FText& WaitInfo, bool bAllowCancel)
{
// 设置取消按钮的可见性
if (CancelButton)
{
CancelButton->SetVisibility(bAllowCancel ? ESlateVisibility::Visible : ESlateVisibility::Hidden);
}
// 更新提示文字
if (WaitInfoText)
{
WaitInfoText->SetText(WaitInfo);
}
}
回到主菜单界面UMainMenuWidget
cpp
/******************************/
/* Main */
/******************************/
// 切换到主界面
void SwitchToMainWidget();
// 主界面的根组件
UPROPERTY(meta = (BindWidget))
TObjectPtr<UWidget> MainWidgetRoot;
/******************************/
/* 等待 */
/******************************/
private:
// 等待界面
UPROPERTY(meta = (BindWidget))
TObjectPtr<UWaitingWidget> WaitingWidget;
// 切换到等待界面
FOnButtonClickedEvent& SwitchToWaitingWidget(const FText& WaitInfo, bool bAllowCancel = false);
cpp
void UMainMenuWidget::NativeConstruct()
{
Super::NativeConstruct();
// 获取游戏实例
MGameInstance = GetGameInstance<UMGameInstance>();
if (MGameInstance)
{
MGameInstance->OnLoginCompleted.AddUObject(this, &UMainMenuWidget::LoginCompleted);
if (MGameInstance->IsLoggedIn())
{
SwitchToMainWidget();
}
}
// 绑定登录按钮点击事件
LoginButton->OnClicked.AddDynamic(this, &UMainMenuWidget::OnLoginButtonClicked);
}
void UMainMenuWidget::SwitchToMainWidget()
{
if (MainSwitcher)
{
MainSwitcher->SetActiveWidget(MainWidgetRoot);
}
}
void UMainMenuWidget::OnLoginButtonClicked()
{
// 登录按钮点击时触发
UE_LOG(LogTemp, Warning, TEXT("Longing In!"))
if (MGameInstance && !MGameInstance->IsLoggedIn() && !MGameInstance->IsLoggingIn())
{
// 触发登录
MGameInstance->ClientAccountPortalLogin();
// SwitchToWaitingWidget(FText::FromString("正在登录..."));
SwitchToWaitingWidget(FText::FromString(FString(TEXT("正在登录..."))));
}
}
void UMainMenuWidget::LoginCompleted(bool bWasSuccessful, const FString& PlayerNickname, const FString& ErrorMsg)
{
if (bWasSuccessful)
{
UE_LOG(LogTemp, Warning, TEXT("登录成功: %s"), *PlayerNickname)
}
else
{
UE_LOG(LogTemp, Warning, TEXT("登录失败: %s"), *ErrorMsg)
}
SwitchToMainWidget();
}
FOnButtonClickedEvent& UMainMenuWidget::SwitchToWaitingWidget(const FText& WaitInfo, bool bAllowCancel)
{
// 切换到等待界面
MainSwitcher->SetActiveWidget(WaitingWidget);
// 设置等待信息
WaitingWidget->SetWaitInfo(WaitInfo, bAllowCancel);
return WaitingWidget->ClearAndGetButtonClickedEvent();
}
创建等待UI蓝图版本
设置按钮锚点和位置
可以添加等待动画
主界面菜单中添加该UI
主菜单界面UMainMenuWidget
中创建用于创建会话的按钮已经输入
cpp
/******************************/
/* 会话 */
/******************************/
// 创建会话按钮
UPROPERTY(meta=(BindWidget))
TObjectPtr<UButton> CreateSessionButton;
// 输入房间(会话)的名称的文本框
UPROPERTY(meta=(BindWidget))
TObjectPtr<UEditableText> NewSessionNameText;
// 点击"创建会话"按钮时调用
UFUNCTION()
void CreateSessionBtnClicked();
// 取消房间创建(如等待界面点击取消时触发)
UFUNCTION()
void CancelSessionCreation();
// 房间(会话)名称输入框文字变更时调用(用于校验是否可创建)
UFUNCTION()
void NewSessionNameTextChanged(const FText& NewText);
cpp
void UMainMenuWidget::NativeConstruct()
{
Super::NativeConstruct();
// 获取游戏实例
MGameInstance = GetGameInstance<UMGameInstance>();
if (MGameInstance)
{
MGameInstance->OnLoginCompleted.AddUObject(this, &UMainMenuWidget::LoginCompleted);
if (MGameInstance->IsLoggedIn())
{
SwitchToMainWidget();
}
}
// 绑定登录按钮点击事件
LoginButton->OnClicked.AddDynamic(this, &UMainMenuWidget::OnLoginButtonClicked);
// 绑定创建会话按钮点击事件
CreateSessionButton->OnClicked.AddDynamic(this, &UMainMenuWidget::CreateSessionBtnClicked);
// 绑定新会话名称输入框内容改变事件
NewSessionNameText->OnTextChanged.AddDynamic(this, &UMainMenuWidget::NewSessionNameTextChanged);
}
void UMainMenuWidget::CreateSessionBtnClicked()
{
// 确保玩家已登录
if (MGameInstance && MGameInstance->IsLoggedIn())
{
// 请求创建并加入一个新的房间(会话)
MGameInstance->RequestCreateAndJoinSession(FName(NewSessionNameText->GetText().ToString()));
// 切换到等待界面,提示"Creating Lobby",并绑定取消操作
SwitchToWaitingWidget(FText::FromString(FString(TEXT("创建大厅"))), true)
.AddDynamic(this, &UMainMenuWidget::CancelSessionCreation);
}
}
void UMainMenuWidget::CancelSessionCreation()
{
if (MGameInstance)
{
MGameInstance->CancelSessionCreation();
}
SwitchToMainWidget();
}
void UMainMenuWidget::NewSessionNameTextChanged(const FText& NewText)
{
// 创建按钮是否可用
CreateSessionButton->SetIsEnabled(!NewText.IsEmpty());
}
游戏实例UMGameInstance
中补充函数
cpp
/*************************************/
/* 客户端会话创建和搜索 */
/*************************************/
public:
// 请求创建并加入新会话
void RequestCreateAndJoinSession(const FName& NewSessionName);
// 取消会话创建
void CancelSessionCreation();
cpp
void UMGameInstance::RequestCreateAndJoinSession(const FName& NewSessionName)
{
UE_LOG(LogTemp, Warning, TEXT("请求创建并加入会话: %s"), *(NewSessionName.ToString()))
}
void UMGameInstance::CancelSessionCreation()
{
UE_LOG(LogTemp, Warning, TEXT("取消会话创建"))
}
添加控件
再修改一下
配置和获取协调器 URL
cpp
/**
* 获取协调器URL键值
* @return 协调器服务URL的FName键
*/
static FName GetCoordinatorURLKey();
/**
* 获取协调器URL地址
* @return 协调器服务地址字符串
*/
static FString GetCoordinatorURL();
/**
* 获取默认协调器URL
* @return 默认的协调器服务地址
*/
static FString GetDefaultCoordinatorURL();
cpp
FName UTNetStatics::GetCoordinatorURLKey()
{
// 返回用于解析命令行参数的键值(这里是固定字符串 "COORDINATOR_URL")
return FName("COORDINATOR_URL");
}
FString UTNetStatics::GetCoordinatorURL()
{
// 优先从命令行中获取 Coordinator URL
FString CoordinatorURL = GetCommandlineArgAsString(GetCoordinatorURLKey());
if (CoordinatorURL != "")
{
// 如果命令行中有值,则直接返回
return CoordinatorURL;
}
// 如果命令行参数为空,则使用配置文件中的默认值
return GetDefaultCoordinatorURL();
}
FString UTNetStatics::GetDefaultCoordinatorURL()
{
FString CoordinatorURL = "";
// 从配置文件 [Crunch.Net] 节点中读取键 "CoordinatorURL"
// 目标配置文件为 DefaultGame.ini
GConfig->GetString(TEXT("Crunch.Net"), TEXT("CoordinatorURL"), CoordinatorURL, GGameIni);
// 打印日志,方便调试,输出获取到的默认 URL
UE_LOG(LogTemp, Warning, TEXT("Getting Default Coordinator URL as: %s"), *CoordinatorURL)
// 返回默认配置中的 URL
return CoordinatorURL;
}
填写本地主机号,这里的 127.0.0.1
表示 本机
ini
[Crunch.Net]
CoordinatorURL="127.0.0.1"
撰写和发送会话创建请求
UMGameInstance
cpp
#include "Interfaces/IHttpResponse.h"
#include "Interfaces/IHttpRequest.h"
/*************************************/
/* 客户端会话创建和搜索 */
/*************************************/
private:
// 会话创建请求完成回调
void SessionCreationRequestCompleted(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bConnectedSuccessfully, FGuid SessionSearchId);
cpp
void UMGameInstance::RequestCreateAndJoinSession(const FName& NewSessionName)
{
UE_LOG(LogTemp, Warning, TEXT("请求创建并加入会话: %s"), *(NewSessionName.ToString()))
// 创建HTTP请求对象,准备向协调器发起会话创建请求
FHttpRequestRef Request = FHttpModule::Get().CreateRequest();
// 生成唯一会话搜索ID用于后续查询
FGuid SessionSearchId = FGuid::NewGuid();
// 获取协调器服务的基础URL地址
FString CoordinatorURL = UTNetStatics::GetCoordinatorURL();
// 拼接目标 API 地址:<CoordinatorURL>/Sessions
FString URL = FString::Printf(TEXT("%s/Sessions"), *CoordinatorURL);
UE_LOG(LogTemp, Warning, TEXT("发送会话创建请求到 URL:%s"), *URL)
// 配置 HTTP 请求:目标地址 + 请求方式 POST
Request->SetURL(URL);
Request->SetVerb("POST");
// 设置 HTTP 请求头,指定请求体为 JSON 格式
Request->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
// 构造 JSON 请求体,包含 SessionName 和 SessionSearchId
TSharedPtr<FJsonObject> JsonObject = MakeShareable(new FJsonObject);
JsonObject->SetStringField(UTNetStatics::GetSessionNameKey().ToString(), NewSessionName.ToString());
JsonObject->SetStringField(UTNetStatics::GetSessionSearchIdKey().ToString(), SessionSearchId.ToString());
// 将 JSON 对象转换为字符串
FString RequestBody;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&RequestBody);
FJsonSerializer::Serialize(JsonObject.ToSharedRef(), Writer);
// 设置 HTTP 请求体
Request->SetContentAsString(RequestBody);
// 绑定会话创建完成回调
Request->OnProcessRequestComplete().BindUObject(this, &UMGameInstance::SessionCreationRequestCompleted, SessionSearchId);
if (!Request->ProcessRequest())
{
UE_LOG(LogTemp, Warning, TEXT("会话创建请求失败"))
}
}
void UMGameInstance::SessionCreationRequestCompleted(FHttpRequestPtr Request, FHttpResponsePtr Response,
bool bConnectedSuccessfully, FGuid SessionSearchId)
{
if (!bConnectedSuccessfully)
{
UE_LOG(LogTemp, Warning, TEXT("连接协调服务器失败,网络连接未成功!"))
return;
}
UE_LOG(LogTemp, Warning, TEXT("连接协调服务器成功!"))
}