UE5多人MOBA+GAS 54、用户登录和会话创建请求

文章目录


创建主菜单需要的

创建主菜单游戏模式

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("连接协调服务器成功!"))
}
相关推荐
雨白6 小时前
Drawable 与 Bitmap 的区别、互转与自定义
android
程序员江同学6 小时前
Kotlin 技术月报 | 2025 年 8 月
android·kotlin
nju永远得不到的男人6 小时前
关于virtual camera
android
雨白9 小时前
Android 自定义 View:属性动画和硬件加速
android
hellokai10 小时前
React Native新架构源码分析
android·前端·react native
真西西10 小时前
Koin:Kotlin轻量级依赖注入框架
android·kotlin
CYRUS_STUDIO12 小时前
手把手教你改造 AAR:解包、注入逻辑、重打包,一条龙玩转第三方 SDK!
android·逆向
CYRUS_STUDIO13 小时前
Android 源码如何导入 Android Studio?踩坑与解决方案详解
android·android studio·源码阅读
前端赵哈哈14 小时前
初学者入门:Android 实现 Tab 点击切换(TabLayout + ViewPager2)
android·java·android studio
一条上岸小咸鱼17 小时前
Kotlin 控制流(二):返回和跳转
android·kotlin