文章目录
创建和测试协调器 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):
# 使用全局变量 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图片。上面两个是启动画面