UE5多人MOBA+GAS 55、基于 Python 协调器与 EOS 的会话编排
文章目录
- 创建和测试协调器 HTTP 服务器
- 根据请求使用 HTTP 服务器启动和实例化服务器
- 设置查找会话定时器,并加入会话
- 实施全局会话搜索
- 添加选房功能
- 让两个账号加入游戏
- 添加项目图标和启动画面
创建和测试协调器 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
vsrequests==2.32
) - 开发 UE 插件、工具或服务时,不会和系统里的其他 Python 项目冲突
创建完成后,你会在 D:\ue_texiao\Crunch\Coordinator
目录下看到一个 .venv
文件夹,里面有独立的 python.exe
、pip.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):# 使用全局变量 nextAvailablePortglobal 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 = nextAvailablePortnextAvailablePort += 1# 返回当前使用的端口号return usedPort# 定义路由,当客户端以 POST 请求访问 /Sessions 时触发该函数
@app.route('/Sessions', methods=['POST'])
def CreateServer():# 打印请求头信息(调试用,可以看到客户端传过来的数据)print(dict(request.headers))# 获取请求体中的会话名称和搜索 IDsessionName = 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)
{// 暂时放着还没写,不让他报错丢一个falsereturn 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; // 只搜索一个结果// 设置搜索条件: 匹配 SessionSearchIdSessionSearch->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// 实际的 URLUTNetStatics::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 的会话,则返回 falsereturn 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);// 获取缓存的 SessionIdFORCEINLINE FString GetCachedSessionIdStr() const { return CachedSessionIdStr; }private:// 会话按钮UPROPERTY(meta = (BindWidget))TObjectPtr<UButton> SessionButton;// 会话名称UPROPERTY(meta = (BindWidget))TObjectPtr<UTextBlock> SessionNameText;// 缓存的 SessionIdFString 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));// 缓存 SessionIdCachedSessionIdStr = 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;// 当前选中的会话条目IDFString 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);// 获取会话IDFString 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图片。上面两个是启动画面