UE5多人MOBA+GAS 55、基于 Python 协调器与 EOS 的会话编排

发布于:2025-08-29 ⋅ 阅读:(15) ⋅ 点赞:(0)


创建和测试协调器 HTTP 服务器

补充一下.gitignore文件中添加下列部分

###########################################################
#                        PYTHON                           #
###########################################################
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
#   For a library or package, you might want to ignore these files since the code is
#   intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
#   install all needed dependencies.
#Pipfile.lock

# UV
#   Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
#   This is especially recommended for binary packages to ensure reproducibility, and is more
#   commonly ignored for libraries.
#uv.lock

# poetry
#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
#   This is especially recommended for binary packages to ensure reproducibility, and is more
#   commonly ignored for libraries.
#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock

# pdm
#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
#   in version control.
#   https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/

# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# PyCharm
#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
#  and can be added to the global gitignore or merged into this file.  For a more nuclear
#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/

# Visual Studio Code
#  Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore 
#  that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
#  and can be added to the global gitignore or merged into this file. However, if you prefer, 
#  you could uncomment the following to ignore the entire vscode folder
# .vscode/

# Ruff stuff:
.ruff_cache/

# PyPI configuration file
.pypirc

# Cursor
#  Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
#  exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
#  refer to https://docs.cursor.com/context/ignore-files
.cursorignore
.cursorindexingignore

项目下创建一个Coordinator文件夹,然后文件夹下输入powershell回车
在这里插入图片描述
输入python -m venv .venv
在这里插入图片描述

PS D:\ue_texiao\Crunch\Coordinator> python -m venv .venv

👉 用 Python 自带的 venv 模块,在当前目录(D:\ue_texiao\Crunch\Coordinator)下创建一个 虚拟环境,名字叫 .venv

虚拟环境的作用

  • 隔离项目依赖,不会污染全局 Python 环境
  • 每个项目可以有自己独立的第三方库版本(比如 requests==2.28 vs requests==2.32
  • 开发 UE 插件、工具或服务时,不会和系统里的其他 Python 项目冲突

创建完成后,你会在 D:\ue_texiao\Crunch\Coordinator 目录下看到一个 .venv 文件夹,里面有独立的 python.exepip.exe 以及 Lib/site-packages/ 等。
在这里插入图片描述

下一步通常需要激活它:

# 在 PowerShell 里执行
.\.venv\Scripts\activate

在这里插入图片描述
激活后命令行前面会多一个 (.venv) 标识,表示你现在就在虚拟环境里。然后你安装的库(pip install xxx)就只会安装到 .venv 里,不影响全局环境。
在这里插入图片描述

code .直接打开
在这里插入图片描述

在这里插入图片描述
安装flask库

pip install flask

在这里插入图片描述
然后发现我下了,失误了老铁
在这里插入图片描述
然后就可以发现文件下有flask
在这里插入图片描述

创建一个consts

# 会话名称字段的 key(用于标识游戏房间/会话的名字)
SESSION_NAME_KEY = "SESSION_NAME"

# 会话搜索 ID 的 key(用于唯一标识某次会话搜索)
SESSION_SEARCH_ID_KEY = "SESSION_SEARCH_ID"

# 端口号的 key(用于返回或指定服务器运行的端口)
PORT_KEY = "PORT"

创建一个coordinator.py

# 导入 Flask 框架中的 Flask 类、request(处理请求)、jsonify(返回 JSON 响应)
from flask import Flask, request, jsonify
import subprocess
from consts import SESSION_NAME_KEY, SESSION_SEARCH_ID_KEY, PORT_KEY
import re

# 创建 Flask 应用
app = Flask(__name__)

# TODO: 将来使用 Docker 时移除该变量
# 当前用作测试的可用端口(后续可以根据需求动态分配)
nextAvailablePort = 7777

# 定义路由,当客户端以 POST 请求访问 /Sessions 时触发该函数
@app.route('/Sessions', methods=['POST'])
def CreateServer():
    # 打印请求头信息(调试用,可以看到客户端传过来的数据)
    print(dict(request.headers))

    # 这里简单返回固定的端口
    port = nextAvailablePort
    
    # 返回 JSON 响应,其中包含状态(success)和分配的端口号
    # 状态码 200 表示请求成功
    return jsonify({"status": "success", PORT_KEY: port}), 200


# 启动 Flask Web 服务
# host="0.0.0.0" 表示允许外部访问
# port=80 表示监听 80 端口(标准 HTTP 端口)
if __name__ == "__main__":
    app.run(host="0.0.0.0", port=80)

运行
在这里插入图片描述
运行引擎,开始游戏然后登录账号,然后创建大厅
在这里插入图片描述
可以看到信息
在这里插入图片描述

根据请求使用 HTTP 服务器启动和实例化服务器

添加用于测试的函数

# 用于本地测试时创建服务器进程
# 参数:
#   sessionName: 会话名称
#   sessionSearchId: 会话搜索ID
def CreateServerLocalTest(sessionName, sessionSearchId):
    # 使用全局变量 nextAvailablePort
    global nextAvailablePort
    # 启动一个新的进程
    subprocess.Popen([
                # UnrealEditor.exe 的路径(指定引擎可执行文件)
        "D:/UnrealSource/UnrealEngine/Engine/Binaries/Win64/UnrealEditor.exe",

        # 工程文件路径(告诉引擎要启动哪个项目)
        "D:/ue_texiao/Crunch/Crunch.uproject" ,

        # 以服务器模式运行(而不是客户端/编辑器模式)
        "-server",

        # 打开日志输出
        "-log",

        # 指定 Epic 应用 ID(可用于标识不同的运行实例)
        '-epicapp="ServerClient"',

        # 传递会话名称参数(作为命令行参数给引擎使用)
        f'-SESSION_NAME="{sessionName}"',

        # 传递会话搜索 ID 参数
        f'-SESSION_SEARCH_ID="{sessionSearchId}"',

        # 指定使用的端口号
        f'-PORT={nextAvailablePort}'
    ])
    # 记录当前使用的端口号
    usedPort = nextAvailablePort
    nextAvailablePort += 1
    # 返回当前使用的端口号
    return usedPort


# 定义路由,当客户端以 POST 请求访问 /Sessions 时触发该函数
@app.route('/Sessions', methods=['POST'])
def CreateServer():
    # 打印请求头信息(调试用,可以看到客户端传过来的数据)
    print(dict(request.headers))
    # 获取请求体中的会话名称和搜索 ID
    sessionName = request.get_json().get(SESSION_NAME_KEY)
    sessionSearchId = request.get_json().get(SESSION_SEARCH_ID_KEY)
    # 创建服务器并获取分配的端口号
    port = CreateServerLocalTest(sessionName, sessionSearchId)

    # 返回 JSON 响应,其中包含状态(success)和分配的端口号
    # 状态码 200 表示请求成功
    return jsonify({"status": "success", PORT_KEY: port}), 200

然后运行,创建大厅,就会弹出服务器
在这里插入图片描述

设置查找会话定时器,并加入会话

UMGameInstance

// 全局会话搜索完成委托
DECLARE_MULTICAST_DELEGATE_OneParam(FOnGlobalSessionSearchCompleted, const TArray<FOnlineSessionSearchResult>& /*SearchResults*/)
// 加入会话失败委托
DECLARE_MULTICAST_DELEGATE(FOnJoinSesisonFailed);


	/*************************************/
	/*      客户端会话创建和搜索			 */
	/*************************************/
public:
	// 开始全局会话搜索
	void StartGlobalSessionSearch();
	
	// 通过会话ID加入会话
	bool JoinSessionWithId(const FString& SessionIdStr);
	
	// 加入会话失败委托
	FOnJoinSesisonFailed OnJoinSessionFailed;
	
	// 全局会话搜索完成委托
	FOnGlobalSessionSearchCompleted OnGlobalSessionSearchCompleted;
private:
	// 开始查找已创建的会话
	void StartFindingCreatedSession(const FGuid& SessionSearchId);
	
	// 停止所有会话查找
	void StopAllSessionFindings();
	
	// 停止查找已创建的会话
	void StopFindingCreatedSession();
	
	// 停止全局会话搜索
	void StopGlobalSessionSearch();

	// 查找全局会话
	void FindGlobalSessions();
	
	// 全局会话搜索完成回调
	void GlobalSessionSearchCompleted(bool bWasSuccessful);

	// 定时器句柄:查找已创建会话
	FTimerHandle FindCreatedSessionTimerHandle;
	
	// 定时器句柄:查找已创建会话超时
	FTimerHandle FindCreatedSessionTimeoutTimerHandle;

	// 定时器句柄:全局会话搜索
	FTimerHandle GlobalSessionSearchTimerHandle;
	
	// 全局会话搜索间隔
	UPROPERTY(EditDefaultsOnly, Category = "Session Search")
	float GlobalSessionSearchInterval = 2.f;

	// 查找已创建会话间隔
	UPROPERTY(EditDefaultsOnly, Category = "Session Search")
	float FindCreatedSessionSearchInterval = 1.f;

	// 查找已创建会话超时时间
	UPROPERTY(EditDefaultsOnly, Category = "Session Search")
	float FindCreatedSessionTimeoutDuration = 60.f;

	// 查找已创建会话
	void FindCreatedSession(FGuid SessionSearchId);
	
	// 查找已创建会话超时
	void FindCreatedSessionTimeout();
	
	// 查找已创建会话完成回调
	void FindCreateSessionCompleted(bool bWasSuccessful);
	
	// 通过搜索结果加入会话
	void JoinSessionWithSearchResult(const class FOnlineSessionSearchResult& SearchResult);

	// 加入会话完成回调
	void JoinSessionCompleted(FName SessionName, EOnJoinSessionCompleteResult::Type JoinResult, int64 Port);
	
	// 会话搜索对象
	TSharedPtr<class FOnlineSessionSearch> SessionSearch;


void UMGameInstance::CancelSessionCreation()
{
	UE_LOG(LogTemp, Warning, TEXT("取消会话创建"))
	// 停止所有会话查找
	StopAllSessionFindings();

	// 清理会话委托
	if (IOnlineSessionPtr SessionPtr = UTNetStatics::GetSessionPtr())
	{
		SessionPtr->OnFindSessionsCompleteDelegates.RemoveAll(this);
		SessionPtr->OnJoinSessionCompleteDelegates.RemoveAll(this);
	}
}

void UMGameInstance::StartGlobalSessionSearch()
{
	UE_LOG(LogTemp, Warning, TEXT("开始全局会话搜索"))
}

bool UMGameInstance::JoinSessionWithId(const FString& SessionIdStr)
{
	// 暂时放着还没写,不让他报错丢一个false
	return false;
}

void UMGameInstance::SessionCreationRequestCompleted(FHttpRequestPtr Request, FHttpResponsePtr Response,
	bool bConnectedSuccessfully, FGuid SessionSearchId)
{
	if (!bConnectedSuccessfully)
	{
		UE_LOG(LogTemp, Warning, TEXT("连接协调服务器失败,网络连接未成功!"))
		return;
	}

	UE_LOG(LogTemp, Warning, TEXT("连接协调服务器成功!"))

	// 获取 HTTP 响应状态码
	int32 ResponseCode = Response->GetResponseCode();
	if (ResponseCode != 200)
	{
		UE_LOG(LogTemp, Warning, TEXT("会话创建失败,服务器返回错误的状态码: %d"), ResponseCode)
		return;
	}
	// 获取 HTTP 响应内容
	FString ResponseContent = Response->GetContentAsString();

	// 解析响应内容(JSON)
	TSharedPtr<FJsonObject> JsonObject;
	TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(ResponseContent);
	int32 Port = 0;
	// 如果成功解析,则获取端口号
	if (FJsonSerializer::Deserialize(Reader, JsonObject) && JsonObject.IsValid())
	{
		// 获取端口号字段(Key 从 UCNetStatics 获取,确保与服务端一致)
		// Port = JsonObject->GetIntegerField(*(UTNetStatics::GetPortKey().ToString()));
		if (JsonObject->TryGetNumberField(UTNetStatics::GetPortKey().ToString(), Port))
		{
			UE_LOG(LogTemp, Warning, TEXT("连接协调服务器成功,新创建的会话端口为: %d"), Port)
		}else
		{
			UE_LOG(LogTemp, Warning, TEXT("会话创建成功,但未找到端口号字段"))
		}
	}

	// 开始查找并加入刚刚创建的会话
	StartFindingCreatedSession(SessionSearchId);
}

void UMGameInstance::StartFindingCreatedSession(const FGuid& SessionSearchId)
{
	if (!SessionSearchId.IsValid())
	{
		UE_LOG(LogTemp, Warning, TEXT("会话搜索ID无效,无法开始查找!"))
		return;
	}
	// 停止所有查找
	StopAllSessionFindings();

	UE_LOG(LogTemp, Warning, TEXT("开始查找新创建的会话,ID: %s"), *(SessionSearchId.ToString()))

	// 创建一个定时器,用于定期查找已创建的会话
	GetWorld()->GetTimerManager().SetTimer(
		FindCreatedSessionTimerHandle,
		FTimerDelegate::CreateUObject(this, &UMGameInstance::FindCreatedSession, SessionSearchId),
		FindCreatedSessionSearchInterval,
		true, 0.f
		);

	// 超时定时器
	GetWorld()->GetTimerManager().SetTimer(
		FindCreatedSessionTimeoutTimerHandle,
		this,
		&UMGameInstance::FindCreatedSessionTimeout,
		FindCreatedSessionTimeoutDuration
		);
}

void UMGameInstance::StopAllSessionFindings()
{
	UE_LOG(LogTemp, Warning, TEXT("停止所有会话查找"))
	StopFindingCreatedSession();
	StopGlobalSessionSearch();
}

void UMGameInstance::StopFindingCreatedSession()
{
	UE_LOG(LogTemp, Warning, TEXT("停止查找已创建的会话"))

	GetWorld()->GetTimerManager().ClearTimer(FindCreatedSessionTimerHandle);
	GetWorld()->GetTimerManager().ClearTimer(FindCreatedSessionTimeoutTimerHandle);

	// 清理会话委托
	if (IOnlineSessionPtr SessionPtr = UTNetStatics::GetSessionPtr())
	{
		// 移除查找会话完成委托
		SessionPtr->OnFindSessionsCompleteDelegates.RemoveAll(this);
		// 移除加入会话完成委托
		SessionPtr->OnJoinSessionCompleteDelegates.RemoveAll(this);
	}
}

void UMGameInstance::StopGlobalSessionSearch()
{
	UE_LOG(LogTemp, Warning, TEXT("停止全局会话查找"))
}

void UMGameInstance::FindGlobalSessions()
{
}

void UMGameInstance::GlobalSessionSearchCompleted(bool bWasSuccessful)
{
}

void UMGameInstance::FindCreatedSession(FGuid SessionSearchId)
{
	UE_LOG(LogTemp, Warning, TEXT("尝试查找已创建的会话"))
	
	// 获取在线子系统的 Session 接口指针
	IOnlineSessionPtr SessionPtr = UTNetStatics::GetSessionPtr();
	if (!SessionPtr.IsValid())
	{
		UE_LOG(LogTemp, Warning, TEXT("会话接口无效,无法查找已创建的会话"))
		return;
	}
	// 创建会话搜索对象
	SessionSearch = MakeShareable(new FOnlineSessionSearch);
	if (!SessionSearch.IsValid())
	{
		UE_LOG(LogTemp, Warning, TEXT("无法创建会话搜索对象,取消查找"))
		return;
	}
	// 配置搜索参数
	SessionSearch->bIsLanQuery = false;		// 设置为非局域网查询(搜索在线会话)
	SessionSearch->MaxSearchResults = 1;	// 只搜索一个结果
	// 设置搜索条件: 匹配 SessionSearchId
	SessionSearch->QuerySettings.Set(
		UTNetStatics::GetSessionSearchIdKey(), // 键名(和创建时保持一致)
		SessionSearchId.ToString(),            // 转换成字符串存储
		EOnlineComparisonOp::Equals            // 等于匹配
	);

	// 清理并重新绑定会话搜索结果回调
	SessionPtr->OnFindSessionsCompleteDelegates.RemoveAll(this);
	SessionPtr->OnFindSessionsCompleteDelegates.AddUObject(this,
		&UMGameInstance::FindCreateSessionCompleted);

	// 开始查找会话
	if (!SessionPtr->FindSessions(0, SessionSearch.ToSharedRef()))
	{
		UE_LOG(LogTemp, Warning, TEXT("查找已创建的会话失败"))
		// 移除回调
		SessionPtr->OnFindSessionsCompleteDelegates.RemoveAll(this);
	}
}

void UMGameInstance::FindCreatedSessionTimeout()
{
	UE_LOG(LogTemp, Warning, TEXT("查找已创建会话超时"))
	StopFindingCreatedSession();
}

void UMGameInstance::FindCreateSessionCompleted(bool bWasSuccessful)
{
	if (!bWasSuccessful || SessionSearch->SearchResults.Num() == 0)
	{
		UE_LOG(LogTemp, Warning, TEXT("未找到已创建的会话"))
		return;
	}
	// 停止查找
	StopFindingCreatedSession();
	// 加入已创建的会话
	JoinSessionWithSearchResult(SessionSearch->SearchResults[0]);
}

void UMGameInstance::JoinSessionWithSearchResult(const class FOnlineSessionSearchResult& SearchResult)
{
	UE_LOG(LogTemp, Warning, TEXT("尝试加入会话..."))

	// 获取会话接口
	IOnlineSessionPtr SessionPtr = UTNetStatics::GetSessionPtr();
	if (!SessionPtr.IsValid())
	{
		UE_LOG(LogTemp, Warning, TEXT("会话接口无效,无法加入会话"))
		return;
	}

	// 从搜索结果中获取会话名称
	FString SessionName = "";
	SearchResult.Session.SessionSettings.Get<FString>(UTNetStatics::GetSessionNameKey(), SessionName);

	// 从搜索结果中提取端口号(端口号默认为:7777)
	const FOnlineSessionSetting* PortSetting = SearchResult.Session.SessionSettings.Settings.Find(UTNetStatics::GetPortKey());
	int64 Port = 7777;
	if (PortSetting)
	{
		PortSetting->Data.GetValue(Port);
	}
	UE_LOG(LogTemp, Warning, TEXT("尝试加入会话: %s,端口: %lld"), *(SessionName), Port)

	// 清理旧加入会话委托
	SessionPtr->OnJoinSessionCompleteDelegates.RemoveAll(this);
	SessionPtr->OnJoinSessionCompleteDelegates.AddUObject(this, &UMGameInstance::JoinSessionCompleted, Port);

	// 尝试正式加入会话
	if (!SessionPtr->JoinSession(0, FName(SessionName), SearchResult))
	{
		// 如果加入会话失败,打印错误并广播失败事件
		UE_LOG(LogTemp, Warning, TEXT("加入会话失败!"))
		SessionPtr->OnJoinSessionCompleteDelegates.RemoveAll(this);
		OnJoinSessionFailed.Broadcast();
	}
}

void UMGameInstance::JoinSessionCompleted(FName SessionName, EOnJoinSessionCompleteResult::Type JoinResult, int64 Port)
{
	IOnlineSessionPtr SessionPtr = UTNetStatics::GetSessionPtr();
	if (!SessionPtr)
	{
		UE_LOG(LogTemp, Warning, TEXT("加入会话完成,但未找到会话接口"))
		OnJoinSessionFailed.Broadcast();
		return;
	}
	// 加入会话的结果类型是否为加入成功的类型
	if (JoinResult == EOnJoinSessionCompleteResult::Success)
	{
		// 停止所有查找
		StopAllSessionFindings();
		UE_LOG(LogTemp, Warning, TEXT("成功加入会话: %s,端口: %d"), *(SessionName.ToString()), Port)

		// 获取服务器连接字符串
		FString TravelURL = "";
		SessionPtr->GetResolvedConnectString(SessionName, TravelURL);

#if WITH_EDITOR
		// 在编辑器模式下,允许测试用 URL 覆盖
		FString TestingURL = UTNetStatics::GetTestingURL();
		if (!TestingURL.IsEmpty())
		{
			TravelURL = TestingURL;
			UE_LOG(LogTemp, Warning, TEXT("使用测试URL覆盖: %s | Using testing URL override: %s"), *TravelURL, *TravelURL);
		}
#endif

		// 实际的 URL
		UTNetStatics::ReplacePort(TravelURL, Port);

		UE_LOG(LogTemp, Warning, TEXT("跳转到会话地址: %s"), *TravelURL)

		// 客户端执行跳转,进入目标会话地图
		GetFirstLocalPlayerController(GetWorld())->ClientTravel(TravelURL, ETravelType::TRAVEL_Absolute);

	}else
	{
		// 广播失败事件
		OnJoinSessionFailed.Broadcast();
	}

	SessionPtr->OnJoinSessionCompleteDelegates.RemoveAll(this);
}

项目设置中修改一下地图模式
在这里插入图片描述
UTNetStatics添加获取URL的函数

	/**
	 * 获取测试用URL地址
	 * @return 测试环境的服务URL
	 */
	static FString GetTestingURL();

	/**
	 * 获取测试URL键值
	 * @return 测试URL的FName键
	 */
	static FName GetTestingURLKey();

	/**
	 * 替换URL中的端口号
	 * @param OutURLStr 输入输出的URL字符串
	 * @param NewPort 新的端口号
	 */
	static void ReplacePort(FString& OutURLStr, int NewPort);
FString UTNetStatics::GetTestingURL()
{
	FString TestURL = GetCommandlineArgAsString(GetTestingURLKey());
	UE_LOG(LogTemp, Warning, TEXT("获取测试的 URL: %s"), *TestURL)
	return TestURL;
}

FName UTNetStatics::GetTestingURLKey()
{
	return FName("TESTING_URL");
}

void UTNetStatics::ReplacePort(FString& OutURLStr, int NewPort)
{
	FURL URL(nullptr, *OutURLStr, ETravelType::TRAVEL_Absolute);
	URL.Port = NewPort;
	OutURLStr = URL.ToString();
}

launchGame.bat添加测试用URL

%UNREAL_EDITOR% ^
%~dp0../Crunch.uproject ^
-game ^
-log ^
-epicapp="GameClient" ^
-TESTING_URL="127.0.0.1:7777"

python中运行监听
然后运行脚本
在这里插入图片描述
创建房间后可以进入队伍选择界面
在这里插入图片描述

实施全局会话搜索

/**
 * 尝试通过 SessionId 加入会话
 * @param SessionIdStr 目标会话的唯一 SessionId 字符串
 * @return 是否成功找到对应的会话并发起加入
 */
bool UMGameInstance::JoinSessionWithId(const FString& SessionIdStr)
{
	// 确认当前是否已经有一次有效会话搜索结果(SessionSearch 保存了上次搜索的数据)
	if (SessionSearch.IsValid())
	{
		// 在搜索结果中查找是否存在与传入的 SessionIdStr 匹配的会话
		const FOnlineSessionSearchResult* SessionSearchResult = SessionSearch->SearchResults.FindByPredicate(
			[=](const FOnlineSessionSearchResult& Result)
			{
				// 比较搜索结果中的 SessionId 与目标 Id 是否相同
				return Result.GetSessionIdStr() == SessionIdStr;
			}
		);

		// 如果找到了匹配的会话
		if (SessionSearchResult)
		{
			// 调用已有的函数,使用搜索结果尝试加入该会话
			JoinSessionWithSearchResult(*SessionSearchResult);
			return true; // 返回 true 表示找到了并已开始加入流程
		}
	}

	// 如果搜索无效,或者没找到对应 SessionId 的会话,则返回 false
	return false;
}


void UMGameInstance::CancelSessionCreation()
{
	UE_LOG(LogTemp, Warning, TEXT("取消会话创建"))
	// 停止所有会话查找
	StopAllSessionFindings();

	// 清理会话委托
	if (IOnlineSessionPtr SessionPtr = UTNetStatics::GetSessionPtr())
	{
		SessionPtr->OnFindSessionsCompleteDelegates.RemoveAll(this);
		SessionPtr->OnJoinSessionCompleteDelegates.RemoveAll(this);
	}

	// 重新开始全局会话搜索
	StartGlobalSessionSearch();
}

void UMGameInstance::StartGlobalSessionSearch()
{
	UE_LOG(LogTemp, Warning, TEXT("开始全局会话搜索"))
	
	// 设置定时器定期搜索会话
	GetWorld()->GetTimerManager().SetTimer(
		GlobalSessionSearchTimerHandle, 
		this, 
		&UMGameInstance::FindGlobalSessions, 
		GlobalSessionSearchInterval, 
		true, 
		0.f
	);
}
void UMGameInstance::StopGlobalSessionSearch()
{
	UE_LOG(LogTemp, Warning, TEXT("停止全局会话查找"))
	// 停止全局会话查找定时器
	if (GlobalSessionSearchTimerHandle.IsValid())
	{
		GetWorld()->GetTimerManager().ClearTimer(GlobalSessionSearchTimerHandle);
	}

	// 清理会话搜索完成的回调委托
	if (IOnlineSessionPtr SessionPtr = UTNetStatics::GetSessionPtr())
	{
		SessionPtr->OnFindSessionsCompleteDelegates.RemoveAll(this);
	}
}

void UMGameInstance::FindGlobalSessions()
{
	UE_LOG(LogTemp, Warning, TEXT("----- 重试全局会话查找 -----"))
	IOnlineSessionPtr SessionPtr = UTNetStatics::GetSessionPtr();
	if (!SessionPtr)
	{
		UE_LOG(LogTemp, Warning, TEXT("无法找到Session接口,等待下一次全局会话查找"))
		return;
	}

	// 创建会话搜索对象
	SessionSearch = MakeShareable(new FOnlineSessionSearch);
	SessionSearch->bIsLanQuery = false;		// 设置为非局域网查询
	SessionSearch->MaxSearchResults = 100;	// 最多搜索100个结果

	// 重新添加会话搜索完成委托
	SessionPtr->OnFindSessionsCompleteDelegates.RemoveAll(this);
	SessionPtr->OnFindSessionsCompleteDelegates.AddUObject(
		this, 
		&UMGameInstance::GlobalSessionSearchCompleted
	);
	// 搜索会话
	if (!SessionPtr->FindSessions(0, SessionSearch.ToSharedRef()))
	{
		UE_LOG(LogTemp, Warning, TEXT("全局会话搜索失败!"))
		SessionPtr->OnFindSessionsCompleteDelegates.RemoveAll(this);
	}
}

void UMGameInstance::GlobalSessionSearchCompleted(bool bWasSuccessful)
{
	if (bWasSuccessful)
	{
		// 搜索成功,广播会话搜索结果
		OnGlobalSessionSearchCompleted.Broadcast(SessionSearch->SearchResults);
		// 遍历搜索会话名称
		for (const FOnlineSessionSearchResult& OnlineSessionSearchResult : SessionSearch->SearchResults)
		{
			FString SessionName = "Name_None";
			OnlineSessionSearchResult.Session.SessionSettings.Get<FString>(UTNetStatics::GetSessionNameKey(), SessionName);

			UE_LOG(LogTemp, Warning, TEXT("发现会话: %s (全局搜索结果)"), *SessionName)
		}
	}else
	{
		UE_LOG(LogTemp, Warning, TEXT("全局会话搜索失败!"))
	}
	// 搜索完成移除会话搜索完成委托
	if (IOnlineSessionPtr SessionPtr = UTNetStatics::GetSessionPtr())
	{
		SessionPtr->OnFindSessionsCompleteDelegates.RemoveAll(this);
	}
}

添加选房功能

创建会话条目小部件(存房间的)
SessionEntryWidget
在这里插入图片描述

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Components/Button.h"
#include "Components/TextBlock.h"
#include "SessionEntryWidget.generated.h"


// 定义一个多播委托,当玩家点击某个会话条目时触发
// 参数:选中的 SessionId 字符串
DECLARE_MULTICAST_DELEGATE_OneParam(FOnSessionEntrySelected, const FString& /*SelectedSessionIdStr*/)

/**
 * 会话列表中的一个条目(用于显示会话名称和点击按钮)
 */
UCLASS()
class CRUNCH_API USessionEntryWidget : public UUserWidget
{
	GENERATED_BODY()
public:	
	virtual void NativeConstruct() override;

	// 点击时对外广播的事件
	FOnSessionEntrySelected OnSessionEntrySelected;

	// 用于初始化显示的内容(会话名称和 ID)
	void InitializeEntry(const FString& Name, const FString& SessionIdStr);

	// 获取缓存的 SessionId
	FORCEINLINE FString GetCachedSessionIdStr() const { return CachedSessionIdStr; }

private:
	// 会话按钮
	UPROPERTY(meta = (BindWidget))
	TObjectPtr<UButton> SessionButton;

	// 会话名称
	UPROPERTY(meta = (BindWidget))
	TObjectPtr<UTextBlock> SessionNameText;

	// 缓存的 SessionId
	FString CachedSessionIdStr;

	// 会话按钮点击时调用
	UFUNCTION()
	void SessionEntrySelected();
};
#include "SessionEntryWidget.h"

void USessionEntryWidget::NativeConstruct()
{
	Super::NativeConstruct();
	if (SessionButton)
	{
		SessionButton->OnClicked.AddDynamic(this, &USessionEntryWidget::SessionEntrySelected);
	}
}

void USessionEntryWidget::InitializeEntry(const FString& Name, const FString& SessionIdStr)
{
	// 设置会话名称
	SessionNameText->SetText(FText::FromString(Name));
	// 缓存 SessionId
	CachedSessionIdStr = SessionIdStr;
}

void USessionEntryWidget::SessionEntrySelected()
{
	OnSessionEntrySelected.Broadcast(CachedSessionIdStr);
}

在这里插入图片描述
主界面中添加房间小部件以及加入房间等逻辑

	// 加入房间(会话)失败时调用
	void JoinSessionFailed();

	// 更新房间列表
	void UpdateLobbyList(const TArray<FOnlineSessionSearchResult>& SearchResults);

	// 房间列表容器
	UPROPERTY(meta=(BindWidget))
	TObjectPtr<UScrollBox> SessionScrollBox;

	// 加入房间(会话)按钮
	UPROPERTY(meta=(BindWidget))
	TObjectPtr<UButton> JoinSessionBtn;

	// 会话条目小部件类
	UPROPERTY(EditDefaultsOnly, Category = "Session")
	TSubclassOf<class USessionEntryWidget> SessionEntryWidgetClass;

	// 当前选中的会话条目ID
	FString CurrentSelectedSessionId = "";

	// 点击“加入房间(会话)”按钮时调用
	UFUNCTION()
	void JoinSessionBtnClicked();

	// 会话条目被选中时调用
	void SessionEntrySelected(const FString& SelectedEntryIdStr);
void UMainMenuWidget::NativeConstruct()
{
	Super::NativeConstruct();

	// 获取游戏实例
	MGameInstance = GetGameInstance<UMGameInstance>();
	if (MGameInstance)
	{
		MGameInstance->OnLoginCompleted.AddUObject(this, &UMainMenuWidget::LoginCompleted);
		if (MGameInstance->IsLoggedIn())
		{
			SwitchToMainWidget();
		}
		// 绑定加入会话失败事件
		MGameInstance->OnJoinSessionFailed.AddUObject(this, &UMainMenuWidget::JoinSessionFailed);
		// 绑定会话搜索完成事件, 会话列表更新
		MGameInstance->OnGlobalSessionSearchCompleted.AddUObject(this, &UMainMenuWidget::UpdateLobbyList);
		// 开始全局会话搜索
		MGameInstance->StartGlobalSessionSearch();
	}

	// 绑定登录按钮点击事件
	LoginButton->OnClicked.AddDynamic(this, &UMainMenuWidget::OnLoginButtonClicked);
	// 绑定创建会话按钮点击事件
	CreateSessionButton->OnClicked.AddDynamic(this, &UMainMenuWidget::CreateSessionBtnClicked);
	// 绑定新会话名称输入框内容改变事件
	NewSessionNameText->OnTextChanged.AddDynamic(this, &UMainMenuWidget::NewSessionNameTextChanged);
	// 绑定加入会话按钮点击事件
	JoinSessionBtn->OnClicked.AddDynamic(this, &UMainMenuWidget::JoinSessionBtnClicked);
	// 设置加入按钮为不可用
	JoinSessionBtn->SetIsEnabled(false);
}

void UMainMenuWidget::JoinSessionFailed()
{
	// 加入会话失败,返回到主界面
	SwitchToMainWidget();
}

void UMainMenuWidget::UpdateLobbyList(const TArray<FOnlineSessionSearchResult>& SearchResults)
{
	UE_LOG(LogTemp, Warning, TEXT("更新会话列表"))

	// 清理列表,重新加载
	SessionScrollBox->ClearChildren();

	bool bCurrentSelectedSessionValid = false;

	for (const FOnlineSessionSearchResult& SearchResult : SearchResults)
	{
		// 创建会话条目
		USessionEntryWidget* NewSessionWidget = CreateWidget<USessionEntryWidget>(GetOwningPlayer(), SessionEntryWidgetClass);
		if (NewSessionWidget)
		{
			// 获取会话名称
			FString SessionName = "Name_None";
			SearchResult.Session.SessionSettings.Get<FString>(UTNetStatics::GetSessionNameKey(), SessionName);

			// 获取会话ID
			FString SessionIdStr = SearchResult.Session.GetSessionIdStr();
			// 初始化会话条目,绑定按钮点击事件
			NewSessionWidget->InitializeEntry(SessionName, SessionIdStr);
			NewSessionWidget->OnSessionEntrySelected.AddUObject(this, &UMainMenuWidget::SessionEntrySelected);
			SessionScrollBox->AddChild(NewSessionWidget);

			// 检查之前选中的会话 ID 是否仍然存在
			if (CurrentSelectedSessionId == SessionIdStr)
			{
				bCurrentSelectedSessionValid = true;
			}
		}
	}
	// 如果之前的选择的会话无效了,则清空
	CurrentSelectedSessionId = bCurrentSelectedSessionValid ? CurrentSelectedSessionId : "";

	// 更新“加入会话”按钮是否可用
	JoinSessionBtn->SetIsEnabled(bCurrentSelectedSessionValid);	
}

void UMainMenuWidget::JoinSessionBtnClicked()
{
	// 检查是否有选中的会话
	if (MGameInstance && !CurrentSelectedSessionId.IsEmpty())
	{
		UE_LOG(LogTemp, Warning, TEXT("[尝试加入会话] -> ID: %s"), *CurrentSelectedSessionId)
		// 尝试加入会话
		if (MGameInstance->JoinSessionWithId(CurrentSelectedSessionId))
		{
			SwitchToWaitingWidget(FText::FromString(FString(TEXT("正在加入房间"))));
		}
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("[无法加入会话] -> 原因: 没有选中会话"))
	}
}

void UMainMenuWidget::SessionEntrySelected(const FString& SelectedEntryIdStr)
{
	CurrentSelectedSessionId = SelectedEntryIdStr;
}

添加滚动框以及按钮
在这里插入图片描述
设置一下房间号的类以及修改一下名称
在这里插入图片描述
添加一个垂直框包裹住按钮和滚动框,再用尺寸框包裹滚动框
在这里插入图片描述

让两个账号加入游戏

来到组织这里,然后点击邀请新的
在这里插入图片描述

在这里插入图片描述

然后去邮箱中加入组织
然后脚本运行游戏
创建房间加入房间就好了
在这里插入图片描述

在这里插入图片描述

添加项目图标和启动画面

项目设置中,在电脑找一个.ico的图标文件,点我在线转换ico图片。上面两个是启动画面
在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到