【unity游戏开发——网络】unity对接steam,并上传发布游戏版本——Steamworks.NET
Steamworks.NET是Valve提供的Steamworks API的C#封装,支持Windows、OSX和Linux平台。本文介绍了该插件的安装使用指南:首先需要启动Steam客户端,然后在Unity项目中添加SteamManager脚本作为基础组件。文章展示了获取Steam用户名的基本功能实现,并详细讲解了Steam回调机制和调用结果的使用方法,包括GameOverlayActivate
文章目录
一、前言
Steamworks.NET 是 Valve 的 Steamworks API 的 C# 封装,可用于 Unity 或基于 C# 的应用程序。
Steamworks.NET 的设计尽可能接近原始 C++ API,因此 Valve 提供的文档主要涵盖了 Steamworks.NET 的使用。可以在 Steamworks.NET 之上轻松实现一些便捷功能和 C# 特有的用法。
Steamworks.NET 完全支持 Windows(32 位和 64 位)、OSX 和 Linux。目前基于 Steamworks SDK 1.63 进行构建。
开发文档:https://steamworks.github.io/
二、插件下载安装
https://github.com/rlabrecque/Steamworks.NET/releases/tag/2025.162.1
示例:https://github.com/rlabrecque/Steamworks.NET-Example
三、快速入门
1、启动steam
注意:后续测试都要记得要先启动steam再运行,不然可能会报错:[Steamworks.NET] SteamAPI_Init() failed. Refer to Valve's documentation or the comment above this line for more information.UnityEngine.Debug:LogError (object,UnityEngine.Object) SteamManager:Awake () (at Assets/Scripts/Steamworks.NET/SteamManager.cs:124)
安装steam地址:https://store.steampowered.com/about/
2、添加SteamManager 脚本
SteamManager 脚本为您的项目提供了理想的起点,我强烈建议使用它,这将大大减少上手所需的时间。
只需在第一个场景中创建一个新的空 GameObject 并将 SteamManager 脚本附加到它上面。
现在,当您启动游戏时,Steam 应该会显示您正处于游戏中。
3、获取 Steam 用户的显示名称
接下来,在确认基本功能工作后,我建议尝试一个简单的 SteamAPI 方法调用。
创建一个名为 SteamScript.cs 的新脚本:
using Steamworks;
using UnityEngine;
public class SteamScript : MonoBehaviour
{
void Start() {
if(SteamManager.Initialized) {
// 获取 Steam 用户的显示名称
string name = SteamFriends.GetPersonaName();
Debug.Log(name);
}
}
}
请注意:在调用任何 Steamworks 函数之前,我们必须始终通过检查
SteamManager.Initialized来确保
Steam 已初始化。
现在只需将此脚本添加到一个 GameObject 上并尝试运行!应该会打印出你的steam名字

如果遇到任何问题,请查看 常见问题解答 ,看看是否已有解决方案!
4、Steam 回调
回调是 Steamworks 最重要的方面,它们允许您从 Steam 异步获取数据,而不会锁定您的游戏。
您很可能希望使用的一个回调是 GameOverlayActivated_t。顾名思义,每当 Steam 覆盖层被激活或停用时,它都会向您发送一个回调。
我们将继续使用之前的脚本来演示如何使用它。
要在 Steamworks.NET 中使用回调,首先必须在类作用域内声明一个受保护的 Callback<> 作为成员变量。
public class SteamScript : MonoBehaviour {
protected Callback<GameOverlayActivated_t> m_GameOverlayActivated;
}
然后,我们通过调用 Callback<>.Create() 来创建回调,并将其分配给 m_GameOverlayActivated。这可以防止回调被垃圾回收。
我们通常在 OnEnable 中执行此操作,因为这允许我们在 Unity 重新加载程序集后重新创建回调。
public class SteamScript : MonoBehaviour {
protected Callback<GameOverlayActivated_t> m_GameOverlayActivated;
private void OnEnable() {
if (SteamManager.Initialized) {
m_GameOverlayActivated = Callback<GameOverlayActivated_t>.Create(OnGameOverlayActivated);
}
}
}
最后一块拼图是 OnGameOverlayActivated 函数。
using Steamworks;
using UnityEngine;
public class SteamScript : MonoBehaviour {
protected Callback<GameOverlayActivated_t> m_GameOverlayActivated;
private void OnEnable() {
if (SteamManager.Initialized) {
m_GameOverlayActivated = Callback<GameOverlayActivated_t>.Create(OnGameOverlayActivated);
}
}
private void OnGameOverlayActivated(GameOverlayActivated_t pCallback) {
if(pCallback.m_bActive != 0) {
Debug.Log("Steam 覆盖层已被激活");
}
else {
Debug.Log("Steam 覆盖层已被关闭");
}
}
}
搞定!
GameOverlayActivated 回调一个流行且推荐的用例是:当覆盖层打开时暂停游戏。比如打开背包,打开设置面板等等。
5、Steam 调用结果
调用结果与回调非常相似,但它们是特定函数调用的异步结果,而不是像回调那样的全局事件接收器。
您可以通过检查函数的返回值来识别提供调用结果的函数。如果它返回 SteamAPICall_t,那么您必须设置一个调用结果。
在 Steamworks.NET 中,设置调用结果与回调几乎相同!
比如我们获取正在玩您游戏的玩家数量
using Steamworks;
using UnityEngine;
public class SteamScript : MonoBehaviour {
private CallResult<NumberOfCurrentPlayers_t> m_NumberOfCurrentPlayers;
//在 OnEnable 中执行此操作,以便每次 Unity 重新加载程序集时都会重新创建它。
private void OnEnable() {
if (SteamManager.Initialized) {
//创建调用结果
m_NumberOfCurrentPlayers = CallResult<NumberOfCurrentPlayers_t>.Create(OnNumberOfCurrentPlayers);
}
}
private void Update() {
// 我们需要调用一个返回调用结果的函数,然后将其返回的 SteamAPICall_t 句柄与我们的调用结果关联起来。
if(Input.GetKeyDown(KeyCode.Space)) {
SteamAPICall_t handle = SteamUserStats.GetNumberOfCurrentPlayers();
m_NumberOfCurrentPlayers.Set(handle);
Debug.Log("已调用 GetNumberOfCurrentPlayers()");
}
}
//创建异步调用的函数,调用结果的函数签名与回调略有不同,增加了 bool bIOFailure 参数。
private void OnNumberOfCurrentPlayers(NumberOfCurrentPlayers_t pCallback, bool bIOFailure) {
if (pCallback.m_bSuccess != 1 || bIOFailure) {
Debug.Log("检索玩家数量时出错。");
}
else {
Debug.Log("正在玩您游戏的玩家数量: " + pCallback.m_cPlayers);
}
}
}
运行,按空格,查看结果
对于回调和调用结果,您都必须定期调用 SteamAPI.RunCallbacks。当然这在 SteamManager 中已经帮你完成。
现在您已经熟悉了 Steamworks 的基础构建模块,请查看 Steamworks.NET 示例应用程序,了解 Steamworks 统计和成就的工业级实现!
Steamworks.NET-Test 项目对于了解如何在 Steamworks.NET 中完成各种任务非常有价值。
还有一个 Steamworks.NET-GameServerTest 项目可用,它实现了 Steamworks 游戏服务器接口的一些基本功能。
四、SteamManager
1、SteamManager 脚本的工作原理
SteamManager 脚本是我们认为的 Steamworks "用户模式"端。它提供了一些基本逻辑来设置和维护与 Steam 的连接,并为您提供了一个良好的起点以供构建。
您很可能需要自行修改 SteamManager 脚本,了解其工作原理是全面掌握 Steamworks 的重要一步。
或者,如果您已经拥有平台抽象层,可以编写自己的实现。
您可以在 GitHub 上找到最新版本的 SteamManager 脚本:https://github.com/rlabrecque/Steamworks.NET-SteamManager/blob/master/SteamManager.cs
请注意:如果不使用类似 SteamManager 的脚本,Steamworks.NET 将完全无法工作。
2、样板代码
以下所有代码都包装在一个 MonoBehavior 类中,以便可以添加到 GameObject 上。
using UnityEngine;
using System.Collections;
using Steamworks;
class SteamManager : MonoBehaviour {
}
3、持久化 GameObject/单例逻辑
SteamManager 脚本依赖于创建一次并在整个游戏过程中持续存在。这涉及到一些相当复杂的逻辑来与 Unity 的 GameObject 系统集成。
我们使用"自创建持久单例"模式来实现这一点。
通过此模式,您可以从游戏中的任何场景使用 SteamManager,而无需在每个场景中手动放置 SteamManager GameObject。与往常一样,请避免在其他脚本的 Awake() 或 OnDestroy() 中与 SteamManager 交互,因为执行顺序无法保证。
如果您的游戏中已经有维护全局状态的方法,您可能希望用您自己的方法替换此逻辑,以确保以正确的顺序设置。
private static SteamManager s_instance;
private static SteamManager Instance {
get {
return s_instance ?? new GameObject("SteamManager").AddComponent<SteamManager>();
}
}
private void Awake() {
if (s_instance != null) {
Destroy(gameObject);
return;
}
s_instance = this;
DontDestroyOnLoad(gameObject);
}
private void OnEnable() {
if (s_instance == null) {
s_instance = this;
}
}
private void OnDestroy() {
if (s_instance != this) {
return;
}
s_instance = null;
}
4、完整性检查
Steamworks.NET 提供了几个非必需的完整性检查,以确保正确使用 Steamworks.NET。
Packsize.Test() 确保 Steamworks.NET 在正确的平台下运行。在 Unity 正常操作下,这永远不会返回 false。
DllCheck.Test() 检查确保 Steamworks 可再发行二进制文件版本正确。这在您升级 Steamworks.NET 时特别有用,尤其是在不使用 Steamworks.NET 编辑器脚本的情况下。使用错误的 steam_api.dll 运行 Steamworks.NET 可能会导致问题。(当前仅检查 steam_api[64].dll)
if (!Packsize.Test()) {
Debug.LogError("[Steamworks.NET] Packsize 测试返回 false,此平台上正在运行错误版本的 Steamworks.NET。", this);
}
if (!DllCheck.Test()) {
Debug.LogError("[Steamworks.NET] DllCheck 测试返回 false,一个或多个 Steamworks 二进制文件版本错误。", this);
}
5、SteamAPI.RestartAppIfNecessary
脚本调用的第一个 Steamworks 函数是 SteamAPI.RestartAppIfNecessary((AppId)480)。请将 480 替换为您自己的 AppId。
SteamAPI.RestartAppIfNecessary() 检查 Steam 客户端是否正在运行,如果没有则启动它。
如果返回 true,则会在需要时启动 Steam 客户端并通过它重新启动您的游戏,然后您应尽快手动关闭应用程序。这实际上是运行 steam://run/[AppId],因此可能不会重新启动调用它的确切可执行文件。
如果返回 false,则表示您的游戏是由 Steam 客户端启动的,正常继续执行。
如果当前工作目录中存在 steam_appid.txt 文件,则 SteamAPI_RestartAppIfNecessary() 将返回 false。这允许您进行开发而无需每次都通过 Steam 重新启动。
由于这是调用的第一个 Steamworks 函数,因此是确保 steam_api.dll 确实可以加载的理想位置。这是通过将此函数调用包装在 try..catch 块中以捕获 DllNotFoundException 来实现的。
private void Awake() {
try {
if (SteamAPI.RestartAppIfNecessary((AppId)480)) {
Application.Quit();
return;
}
}
catch (System.DllNotFoundException e) {
Debug.LogError("[Steamworks.NET] 无法加载 [lib]steam_api.dll/so/dylib。它可能不在正确的位置。有关更多详细信息,请参阅 README。\n" + e, this);
Application.Quit();
return;
}
}
6、SteamAPI.Init
应该调用的第二个 Steamworks 函数是 SteamAPI.Init(),这会启动 SteamAPI,并且必须在调用任何其他 Steamworks 函数之前调用。
如果 SteamAPI.Init() 返回 true,则表示已设置好一切以继续使用 Steamworks.NET。
否则,返回值为 false 由以下三个问题之一引起:
- Steam 客户端未运行。需要运行的 Steam 客户端来提供各种 Steamworks 接口的实现。
- Steam 客户端无法确定游戏的 AppID。确保游戏目录中有
steam_appid.txt。当通过 Steam 下载启动游戏时,这永远不会发生,因为SteamAPI.RestartAppIfNecessary()将通过 Steam 重新启动它。 - 确保您的应用程序在与 Steam 客户端相同的用户上下文(包括管理员权限)下运行。
如果遇到 Init 问题,请尝试在启动前运行 Microsoft 的 DbgView 以获取 Steam 的内部输出。
SteamManager 公开了 Initialized 属性,您可以从其他脚本中使用它来确保在调用任何 Steamworks 函数之前 SteamAPI 已初始化。
private bool m_bInitialized;
public static bool Initialized {
get {
return Instance.m_bInitialized;
}
}
private void Awake() {
m_bInitialized = SteamAPI.Init();
if (!m_bInitialized) {
Debug.LogError("[Steamworks.NET] SteamAPI_Init() 失败。有关更多信息,请参阅 Valve 的文档或此行上方的注释。", this);
return;
}
}
7、SteamAPIWarningMessageHook
通过使用函数委托调用 SteamClient.SetWarningMessageHook(),我们可以在某些情况下拦截来自 Steam 的警告消息。
请注意,在调用任何 Steamworks 函数之前,我们确保 Steam API 已初始化。
我们在 OnEnable 中调用此函数,以便在 Unity 执行程序集重新加载(例如重新编译脚本时)后重新创建它。
要从 Steam 接收警告消息,您必须在启动参数中使用 -debug_steamapi 启动游戏。
private SteamAPIWarningMessageHook_t m_SteamAPIWarningMessageHook;
private static void SteamAPIDebugTextHook(int nSeverity, System.Text.StringBuilder pchDebugText) {
Debug.LogWarning(pchDebugText);
}
private void OnEnable() {
if (!m_bInitialized) {
return;
}
if (m_SteamAPIWarningMessageHook == null) {
m_SteamAPIWarningMessageHook = new SteamAPIWarningMessageHook_t(SteamAPIDebugTextHook);
SteamClient.SetWarningMessageHook(m_SteamAPIWarningMessageHook);
}
}
8、SteamAPI.RunCallbacks
要使回调和调用结果系统能够分发事件,必须频繁调用 SteamAPI.RunCallbacks()。调用之间的时间越长,从 Steam API 接收事件或结果的潜在延迟就越大。
如果通过将 Time.timeScale 设置为 0 来暂停游戏,则 Update() 函数将不再运行。您需要考虑替代方案,以确保即使游戏暂停时 SteamAPI.RunCallbacks() 也在运行。协程可能是一个不错的选择。
请注意,在调用任何 Steamworks 函数之前,我们确保 Steam API 已初始化。
private void Update() {
if (!m_bInitialized) {
return;
}
// 运行 Steam 客户端回调
SteamAPI.RunCallbacks();
}
9、SteamAPI.Shutdown
SteamManager 将进行的最终调用是 SteamAPI.Shutdown(),它会清理 SteamAPI 并让 Steam 知道您正在准备关闭。
使用 OnDestroy,因为这是关闭时最后调用的内容。
由于 SteamManager 应该是持久化的且永远不会被禁用或销毁,我们可以使用 OnDestroy 来关闭 SteamAPI。
private void OnDestroy() {
if (!m_bInitialized) {
return;
}
SteamAPI.Shutdown();
}
五、对接自己的应用
前面测试你会发现我们都没有设置自己的游戏信息,默认使用的都是480测试appid
要修改成自己的,我们需要
-
找到工程目录中的
steam_appid.txt文件,默认是480,是UnitySpaceWar的id,把480改成你自己的appid(appid在Steamworks 创建应用的时候会分配)
-
找到SteamManager.cs 中的SteamAPI.RestartAppIfNecessary并修改为SteamAPI.RestartAppIfNecessary(new AppId_t(你的appid))

-
保存,
重启unity -
我们可以打印AppID测试一下,看看对不对
using Steamworks; using UnityEngine; public class SteamScript : MonoBehaviour { void Start() { if (SteamManager.Initialized) { // 获取当前运行的AppID AppId_t currentAppId = SteamUtils.GetAppID(); uint appIdValue = currentAppId.m_AppId; Debug.Log($"当前AppID: {appIdValue}"); } } }
六、设置Steamwork商店Depot
生成分支和Depot到底是啥?咋上传不同语言?可以参考:https://www.bilibili.com/video/BV1xamnYbED6/
简单来说,Depot可以理解为你的游戏不同版本
1、找到所有应用程序

2、找到你在Steam上花100美元申请的AppID的应用程序,点击Steamworks管理员

3、找到SteamPipe中的Depot

4、添加新的depot,因为我这里已经添加过了因此有一个depot

5、添加depot的名称和选中depotID,这个depotid默认是比appid+1的,比如你的appid是480,depotid就应该是481,然后点击保存 ,这里的depot就设置完成了。

七、上传游戏版本
1、下载SteamworkSDK
Steamwork的网址将SDK下载下来,解压到一个最好是英文的目录中
2、\sdk\tools\ContentBuilder\scripts里面默认有4个文件,我们首先打开第一个app_build_1000这个文件
注意文件名要跟着改
3、将自己的游戏打包出来,将整个打包的游戏文件拷贝到sdk/tools/ContentBuilder/content下面
注意查看steam_appid.txt文件,是否填好正确的appid
4、编辑depot_build_1001文件

5、然后就通过SteamPipeGUI工具将自己的exe包进行上传了

出现success,则成功了,如果你是第一次上传会需要邮箱进行验证,等待验证成功即可
如果上传失败,检测刚刚那两个文件的参数是否正确,可以在output文件夹中看到错误输出日志
这里一个坑,Steam有一个命令行工具,run_build这个工具也能成功上传,但是虽然显示成功,但提示是preview,要修改
run_build里面的账号密码为自己的,再双击运行,可以把末尾的quit删掉,命令行界面就不会自动关闭了
八、Steamwork商店配置发布
1、找到Steamworks管理员,然后找到SteamPile,点击生成版本,我们可以看到刚刚上传然后生成的版本,这里已经上传过很多次了,因此你第一次上传的话应该是只有一个的,点击default分支,点击预览更改

2、立即将生成版本设置上线

3、设置通用安装文件夹以及启动项

4、点击发布

5、之前没有上线过的等待审核,审核通过了的界面就是这样的,可以安装自己的游戏测试

九、实战:实现一个p2p steam大厅功能
1、参考代码
大厅项UI组件代码
//-----------------------------------------------------------------------------
// 大厅项UI组件
//-----------------------------------------------------------------------------
using Steamworks;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class LobbyItemUI : MonoBehaviour {
public TextMeshProUGUI lobbyNameText;
public TextMeshProUGUI playerCountText;
public TextMeshProUGUI hostNameText;
public Button joinButton;
private CSteamID lobbyID;
private P2PGameLobbyManager lobbyManager;
void Start() {
if (joinButton != null) {
joinButton.onClick.AddListener(OnJoinButtonClick);
}
}
public void Setup(CSteamID id, string name, int currentPlayers, int maxPlayers, string hostName, P2PGameLobbyManager manager) {
lobbyID = id;
lobbyManager = manager;
if (lobbyNameText != null) {
lobbyNameText.text = name;
}
if (playerCountText != null) {
playerCountText.text = $"{currentPlayers}/{maxPlayers}";
// 如果大厅已满,禁用加入按钮
if (joinButton != null) {
joinButton.interactable = currentPlayers < maxPlayers;
}
}
if (hostNameText != null) {
hostNameText.text = $"房主: {hostName}";
}
}
public void SetupEmpty() {
if (lobbyNameText != null) {
lobbyNameText.text = "没有找到可用大厅";
}
if (playerCountText != null) {
playerCountText.text = "";
}
if (hostNameText != null) {
hostNameText.text = "点击刷新或创建新大厅";
}
if (joinButton != null) {
joinButton.gameObject.SetActive(false);
}
}
private void OnJoinButtonClick() {
if (lobbyManager != null) {
lobbyManager.JoinLobby(lobbyID);
}
}
}
p2p大厅管理脚本
using UnityEngine;
using Steamworks;
using System.Collections.Generic;
using UnityEngine.UI;
using System;
public class P2PGameLobbyManager : MonoBehaviour
{
// 当前游戏版本
const string SPACEWAR_VERSION = "1.0.0.0";
// Steam API初始化
protected Callback<SteamServersConnected_t> m_CallbackSteamServersConnected;
protected Callback<SteamServerConnectFailure_t> m_CallbackSteamServerConnectFailure;
protected Callback<SteamServersDisconnected_t> m_CallbackSteamServersDisconnected;
// P2P相关回调
protected Callback<P2PSessionRequest_t> m_CallbackP2PSessionRequest;
protected Callback<P2PSessionConnectFail_t> m_CallbackP2PSessionConnectFail;
// 认证回调
protected Callback<ValidateAuthTicketResponse_t> m_CallbackValidateAuthTicketResponse;
// 大厅相关回调
protected Callback<LobbyCreated_t> m_CallbackLobbyCreated;
protected Callback<LobbyEnter_t> m_CallbackLobbyEnter;
protected Callback<LobbyChatUpdate_t> m_CallbackLobbyChatUpdate;
protected Callback<LobbyDataUpdate_t> m_CallbackLobbyDataUpdate;
protected Callback<LobbyMatchList_t> m_CallbackLobbyMatchList;
// P2P相关变量
private CSteamID m_CurrentLobby = CSteamID.Nil; // 当前加入的大厅
private bool m_bIsLobbyOwner = false; // 是否为大厅创建者(主机)
private Dictionary<CSteamID, string> m_ConnectedPeers = new Dictionary<CSteamID, string>(); // 连接的P2P对等体
// 大厅列表相关
private List<CSteamID> m_LobbyList = new List<CSteamID>(); // 搜索到的大厅列表
private bool m_bIsSearchingLobbies = false; // 是否正在搜索大厅
private float m_LastLobbyRefreshTime = 0f; // 上次刷新大厅列表的时间
// UI引用(需要在Unity编辑器中赋值)
public Transform lobbyListContent; // 大厅列表的父物体
public GameObject lobbyItemPrefab; // 大厅项预制体
public Button refreshButton; // 刷新按钮
public Button createLobbyButton; // 创建大厅按钮
public Button leaveCurrentLobbyButton; // 离开大厅按钮
public Text statusText; // 状态文本
public GameObject lobbyPanel; // 大厅面板(创建/加入后显示)
public GameObject searchPanel; // 搜索面板(初始显示)
public string m_strServerName = "P2P测试服务器";
public string m_strMapName = "银河系";
public int m_nMaxPlayers = 4;
bool m_bInitialized; // Steam是否已初始化
bool m_bConnectedToSteam; // 是否已连接到Steam
void Start()
{
InitializeUI();
InitializeSteam();
}
// private void OnEnable()
// {
// InitializeUI();
// InitializeSteam();
// }
private void InitializeUI()
{
// 绑定按钮事件
if (refreshButton != null)
{
refreshButton.onClick.AddListener(RefreshLobbyList);
}
if (createLobbyButton != null)
{
createLobbyButton.onClick.AddListener(CreateP2PLobby);
}
if (leaveCurrentLobbyButton != null)
{
leaveCurrentLobbyButton.onClick.AddListener(LeaveCurrentLobby);
}
// 初始状态
if (searchPanel != null) searchPanel.SetActive(true);
if (lobbyPanel != null) lobbyPanel.SetActive(false);
UpdateStatus("正在初始化Steam...");
}
private void InitializeSteam()
{
if (!SteamManager.Initialized)
{
UpdateStatus("SteamAPI初始化失败!");
return;
}
// 初始化Steam客户端API
// m_bInitialized = SteamAPI.Init();
// if (!m_bInitialized) {
// Debug.LogError("SteamAPI初始化失败!请确保Steam客户端正在运行且用户已登录。");
// UpdateStatus("SteamAPI初始化失败!");
// return;
// }
Debug.Log("SteamAPI初始化成功,用户SteamID: " + SteamUser.GetSteamID());
UpdateStatus("已连接到Steam");
// 直接设置为已连接,因为 SteamManager 已经处理了连接
m_bInitialized = true;
m_bConnectedToSteam = true;
Debug.Log("SteamAPI初始化");
// 创建回调
m_CallbackSteamServersConnected = Callback<SteamServersConnected_t>.Create(OnSteamServersConnected);//用于客户端
// m_CallbackSteamServersConnected = Callback<SteamServersConnected_t>.CreateGameServer(OnSteamServersConnected);//用于游戏服务器
m_CallbackSteamServerConnectFailure = Callback<SteamServerConnectFailure_t>.Create(OnSteamServerConnectFailure);
m_CallbackSteamServersDisconnected = Callback<SteamServersDisconnected_t>.Create(OnSteamServersDisconnected);
m_CallbackP2PSessionRequest = Callback<P2PSessionRequest_t>.Create(OnP2PSessionRequest);
m_CallbackP2PSessionConnectFail = Callback<P2PSessionConnectFail_t>.Create(OnP2PSessionConnectFail);
m_CallbackValidateAuthTicketResponse = Callback<ValidateAuthTicketResponse_t>.Create(OnValidateAuthTicketResponse);
// 大厅回调
m_CallbackLobbyCreated = Callback<LobbyCreated_t>.Create(OnLobbyCreated);
m_CallbackLobbyEnter = Callback<LobbyEnter_t>.Create(OnLobbyEntered);
m_CallbackLobbyChatUpdate = Callback<LobbyChatUpdate_t>.Create(OnLobbyChatUpdate);
m_CallbackLobbyDataUpdate = Callback<LobbyDataUpdate_t>.Create(OnLobbyDataUpdate);
m_CallbackLobbyMatchList = Callback<LobbyMatchList_t>.Create(OnLobbyMatchList);
// m_bConnectedToSteam = false;
// 自动搜索大厅
RefreshLobbyList();
}
private void OnDisable()
{
if (!m_bInitialized)
{
return;
}
// 离开大厅
if (m_CurrentLobby.IsValid())
{
SteamMatchmaking.LeaveLobby(m_CurrentLobby);
m_CurrentLobby = CSteamID.Nil;
}
// 关闭所有P2P连接
foreach (var peer in m_ConnectedPeers.Keys)
{
CloseP2PSession(peer);
}
m_ConnectedPeers.Clear();
// 释放回调
m_CallbackSteamServersConnected.Dispose();
m_CallbackSteamServerConnectFailure.Dispose();
m_CallbackSteamServersDisconnected.Dispose();
m_CallbackP2PSessionRequest.Dispose();
m_CallbackP2PSessionConnectFail.Dispose();
m_CallbackValidateAuthTicketResponse.Dispose();
m_CallbackLobbyCreated.Dispose();
m_CallbackLobbyEnter.Dispose();
m_CallbackLobbyChatUpdate.Dispose();
m_CallbackLobbyDataUpdate.Dispose();
m_CallbackLobbyMatchList.Dispose();
// 关闭SteamAPI
SteamAPI.Shutdown();
// m_bInitialized = false;
Debug.Log("P2P模式已关闭。");
}
private void Update()
{
// if (!m_bInitialized) {
// return;
// }
// // 运行Steam回调
// SteamAPI.RunCallbacks();
// 接收P2P消息
ReceiveP2PMessages();
// 自动刷新大厅列表(每30秒)
if (Time.time - m_LastLobbyRefreshTime > 30f && !m_bIsSearchingLobbies)
{
RefreshLobbyList();
}
}
//-----------------------------------------------------------------------------
// 大厅列表功能
//-----------------------------------------------------------------------------
// 刷新大厅列表
public void RefreshLobbyList()
{
if (!m_bConnectedToSteam || m_bIsSearchingLobbies)
{
return;
}
m_bIsSearchingLobbies = true;
m_LobbyList.Clear();
ClearLobbyListUI();
UpdateStatus("正在搜索大厅...");
// 创建搜索过滤器
// List<MatchMakingKeyValuePair_t> filters = new List<MatchMakingKeyValuePair_t>();
// 示例过滤器:只搜索非空大厅
// filters.Add(new MatchMakingKeyValuePair_t() {
// m_szKey = "and",
// m_szValue = "1"
// });
// 过滤器:搜索特定版本的游戏
// filters.Add(new MatchMakingKeyValuePair_t() {
// m_szKey = "version",
// m_szValue = SPACEWAR_VERSION
// });
// 开始搜索大厅
SteamMatchmaking.AddRequestLobbyListResultCountFilter(50); // 最多返回50个大厅
// SteamMatchmaking.AddRequestLobbyListFilterSlotsAvailable(1); // 至少有一个空位
// SteamMatchmaking.AddRequestLobbyListDistanceFilter(ELobbyDistanceFilter.k_ELobbyDistanceFilterWorldwide); // 全球范围
// 添加字符串过滤器示例(搜索特定游戏模式)
// SteamMatchmaking.AddRequestLobbyListStringFilter("gamemode", "deathmatch", ELobbyComparison.k_ELobbyComparisonEqual);
// 请求大厅列表 - 这会触发 OnLobbyMatchList 回调
SteamAPICall_t hSteamAPICall = SteamMatchmaking.RequestLobbyList();
m_LastLobbyRefreshTime = Time.time;
}
// 清空大厅列表UI
private void ClearLobbyListUI()
{
if (lobbyListContent != null)
{
foreach (Transform child in lobbyListContent)
{
Destroy(child.gameObject);
}
}
}
// 更新大厅列表UI
private void UpdateLobbyListUI()
{
ClearLobbyListUI();
if (m_LobbyList.Count == 0)
{
// 显示"无大厅"消息
GameObject noLobbyItem = Instantiate(lobbyItemPrefab, lobbyListContent);
LobbyItemUI itemUI = noLobbyItem.GetComponent<LobbyItemUI>();
if (itemUI != null)
{
itemUI.SetupEmpty();
}
return;
}
foreach (CSteamID lobbyID in m_LobbyList)
{
GameObject lobbyItem = Instantiate(lobbyItemPrefab, lobbyListContent);
LobbyItemUI itemUI = lobbyItem.GetComponent<LobbyItemUI>();
if (itemUI != null)
{
// 获取大厅信息
string lobbyName = SteamMatchmaking.GetLobbyData(lobbyID, "name");
int memberCount = SteamMatchmaking.GetNumLobbyMembers(lobbyID);
int maxPlayers = SteamMatchmaking.GetLobbyMemberLimit(lobbyID);
string ownerName = SteamFriends.GetFriendPersonaName(SteamMatchmaking.GetLobbyOwner(lobbyID));
// 如果大厅没有名称,使用创建者的名称
if (string.IsNullOrEmpty(lobbyName))
{
lobbyName = $"{ownerName}的大厅";
}
itemUI.Setup(lobbyID, lobbyName, memberCount, maxPlayers, ownerName, this);
}
}
UpdateStatus($"找到 {m_LobbyList.Count} 个大厅");
}
// 加入指定大厅
public void JoinLobby(CSteamID lobbyID)
{
if (!m_bConnectedToSteam)
{
UpdateStatus("未连接到Steam,无法加入大厅");
return;
}
if (m_CurrentLobby.IsValid())
{
SteamMatchmaking.LeaveLobby(m_CurrentLobby);
}
SteamMatchmaking.JoinLobby(lobbyID);
UpdateStatus("正在加入大厅...");
}
// 创建P2P大厅(作为主机)
public void CreateP2PLobby()
{
if (!m_bConnectedToSteam)
{
UpdateStatus("未连接到Steam,无法创建大厅");
return;
}
// 公开的大厅
SteamMatchmaking.CreateLobby(ELobbyType.k_ELobbyTypePublic, m_nMaxPlayers);
// 大厅类型:
// k_ELobbyTypePrivate:私有,只能通过邀请加入
// k_ELobbyTypeFriendsOnly:仅好友可见
// k_ELobbyTypePublic:公开,所有人都能看到
// k_ELobbyTypeInvisible:隐形,只能通过直接邀请加入
UpdateStatus("正在创建大厅...");
}
// 离开当前大厅
public void LeaveCurrentLobby()
{
if (m_CurrentLobby.IsValid())
{
SteamMatchmaking.LeaveLobby(m_CurrentLobby);
m_CurrentLobby = CSteamID.Nil;
m_bIsLobbyOwner = false;
// 关闭所有P2P连接
foreach (var peer in m_ConnectedPeers.Keys)
{
CloseP2PSession(peer);
}
m_ConnectedPeers.Clear();
// 切换回搜索界面
if (searchPanel != null) searchPanel.SetActive(true);
if (lobbyPanel != null) lobbyPanel.SetActive(false);
UpdateStatus("已离开大厅");
RefreshLobbyList();
}
}
// 设置大厅数据(主机调用)
private void SetLobbyData()
{
if (!m_CurrentLobby.IsValid() || !m_bIsLobbyOwner)
{
return;
}
// 设置大厅基本信息
SteamMatchmaking.SetLobbyData(m_CurrentLobby, "name", m_strServerName);
SteamMatchmaking.SetLobbyData(m_CurrentLobby, "map", m_strMapName);
SteamMatchmaking.SetLobbyData(m_CurrentLobby, "version", SPACEWAR_VERSION);
SteamMatchmaking.SetLobbyData(m_CurrentLobby, "game", "SpaceWar");
// 设置大厅类型
SteamMatchmaking.SetLobbyType(m_CurrentLobby, ELobbyType.k_ELobbyTypePublic);
// 设置加入规则(可选)
SteamMatchmaking.SetLobbyJoinable(m_CurrentLobby, true);
}
//-----------------------------------------------------------------------------
// 大厅回调处理
//-----------------------------------------------------------------------------
void OnLobbyCreated(LobbyCreated_t callback)
{
if (callback.m_eResult != EResult.k_EResultOK)
{
UpdateStatus($"创建大厅失败: {callback.m_eResult}");
return;
}
m_CurrentLobby = new CSteamID(callback.m_ulSteamIDLobby);
m_bIsLobbyOwner = true;
// 设置大厅数据
SetLobbyData();
UpdateStatus("大厅创建成功,等待玩家加入...");
}
void OnLobbyEntered(LobbyEnter_t callback)
{
m_CurrentLobby = new CSteamID(callback.m_ulSteamIDLobby);
m_bIsLobbyOwner = SteamMatchmaking.GetLobbyOwner(m_CurrentLobby) == SteamUser.GetSteamID();
// 获取大厅成员并建立P2P连接
int memberCount = SteamMatchmaking.GetNumLobbyMembers(m_CurrentLobby);
for (int i = 0; i < memberCount; i++)
{
CSteamID memberID = SteamMatchmaking.GetLobbyMemberByIndex(m_CurrentLobby, i);
// 不与自己建立P2P连接
if (memberID != SteamUser.GetSteamID())
{
// 发送P2P连接请求给对方
SteamNetworking.SendP2PPacket(memberID, new byte[0], 0, EP2PSend.k_EP2PSendReliable);
}
}
// 更新UI
if (searchPanel != null) searchPanel.SetActive(false);
if (lobbyPanel != null) lobbyPanel.SetActive(true);
UpdateStatus($"已加入大厅,当前玩家: {memberCount}/{m_nMaxPlayers}");
// 如果是主机,设置游戏状态为等待中
if (m_bIsLobbyOwner)
{
SteamMatchmaking.SetLobbyData(m_CurrentLobby, "status", "waiting");
}
}
void OnLobbyMatchList(LobbyMatchList_t callback)
{
m_bIsSearchingLobbies = false;
Debug.Log($"找到 {callback.m_nLobbiesMatching} 个大厅");
UpdateStatus($"找到 {callback.m_nLobbiesMatching} 个大厅");
m_LobbyList.Clear();
// 获取所有大厅ID
for (int i = 0; i < callback.m_nLobbiesMatching; i++)
{
CSteamID lobbyID = SteamMatchmaking.GetLobbyByIndex(i);
m_LobbyList.Add(lobbyID);
// 请求大厅数据(如果需要更多信息)
// SteamMatchmaking.RequestLobbyData(lobbyID);
}
// 更新UI
UpdateLobbyListUI();
}
void OnLobbyDataUpdate(LobbyDataUpdate_t callback)
{
// 大厅数据更新(当其他人设置了大厅数据时)
// 可以在这里更新大厅列表UI中的特定大厅项
// 强制刷新UI
UpdateLobbyListUI();
}
void OnLobbyChatUpdate(LobbyChatUpdate_t callback)
{
CSteamID lobbyID = new CSteamID(callback.m_ulSteamIDLobby);
CSteamID changedUserID = new CSteamID(callback.m_ulSteamIDUserChanged);
CSteamID makingChangeUserID = new CSteamID(callback.m_ulSteamIDMakingChange);
EChatMemberStateChange change = (EChatMemberStateChange)callback.m_rgfChatMemberStateChange;
switch (change)
{
case EChatMemberStateChange.k_EChatMemberStateChangeEntered:
Debug.Log($"玩家 {changedUserID} 加入了大厅");
// 新玩家加入,建立P2P连接
if (changedUserID != SteamUser.GetSteamID())
{
SteamNetworking.SendP2PPacket(changedUserID, new byte[0], 0, EP2PSend.k_EP2PSendReliable);
}
break;
case EChatMemberStateChange.k_EChatMemberStateChangeLeft:
case EChatMemberStateChange.k_EChatMemberStateChangeDisconnected:
case EChatMemberStateChange.k_EChatMemberStateChangeKicked:
case EChatMemberStateChange.k_EChatMemberStateChangeBanned:
Debug.Log($"玩家 {changedUserID} 离开了大厅");
// 玩家离开,关闭P2P连接
if (m_ConnectedPeers.ContainsKey(changedUserID))
{
CloseP2PSession(changedUserID);
}
break;
}
// 更新大厅内玩家数量显示
if (m_CurrentLobby.IsValid())
{
int memberCount = SteamMatchmaking.GetNumLobbyMembers(m_CurrentLobby);
UpdateStatus($"大厅内玩家: {memberCount}/{m_nMaxPlayers}");
}
}
//-----------------------------------------------------------------------------
// P2P网络功能
//-----------------------------------------------------------------------------
void OnP2PSessionRequest(P2PSessionRequest_t pCallback)
{
CSteamID remoteUser = pCallback.m_steamIDRemote;
Debug.Log("收到P2P连接请求来自: " + remoteUser);
// 检查是否在同一大厅中
if (m_CurrentLobby.IsValid() && IsUserInLobby(remoteUser))
{
// 接受来自大厅成员的连接
SteamNetworking.AcceptP2PSessionWithUser(remoteUser);
if (!m_ConnectedPeers.ContainsKey(remoteUser))
{
m_ConnectedPeers[remoteUser] = "已连接";
Debug.Log("已接受P2P连接: " + remoteUser);
}
}
else
{
Debug.LogWarning("拒绝P2P连接:不在同一大厅中");
}
}
void OnP2PSessionConnectFail(P2PSessionConnectFail_t pCallback)
{
CSteamID remoteUser = pCallback.m_steamIDRemote;
Debug.LogError("P2P连接失败: " + remoteUser + ", 错误: " + pCallback.m_eP2PSessionError);
if (m_ConnectedPeers.ContainsKey(remoteUser))
{
m_ConnectedPeers.Remove(remoteUser);
}
}
bool IsUserInLobby(CSteamID userID)
{
if (!m_CurrentLobby.IsValid())
{
return false;
}
int memberCount = SteamMatchmaking.GetNumLobbyMembers(m_CurrentLobby);
for (int i = 0; i < memberCount; i++)
{
if (SteamMatchmaking.GetLobbyMemberByIndex(m_CurrentLobby, i) == userID)
{
return true;
}
}
return false;
}
// 发送P2P消息
public void SendP2PMessage(CSteamID targetUser, byte[] data, EP2PSend sendType = EP2PSend.k_EP2PSendReliable)
{
if (!m_ConnectedPeers.ContainsKey(targetUser))
{
Debug.LogWarning("尝试向未连接的玩家发送消息: " + targetUser);
return;
}
SteamNetworking.SendP2PPacket(targetUser, data, (uint)data.Length, sendType);
}
// 向所有连接的玩家广播消息
public void BroadcastP2PMessage(byte[] data, EP2PSend sendType = EP2PSend.k_EP2PSendReliable)
{
foreach (var peer in m_ConnectedPeers.Keys)
{
SteamNetworking.SendP2PPacket(peer, data, (uint)data.Length, sendType);
}
}
// 接收P2P消息
private void ReceiveP2PMessages()
{
uint msgSize;
CSteamID remoteUser;
while (SteamNetworking.IsP2PPacketAvailable(out msgSize))
{
byte[] buffer = new byte[msgSize];
if (SteamNetworking.ReadP2PPacket(buffer, msgSize, out msgSize, out remoteUser))
{
OnP2PMessageReceived(remoteUser, buffer);
}
}
}
private void OnP2PMessageReceived(CSteamID sender, byte[] data)
{
Debug.Log($"收到来自 {sender} 的P2P消息,大小: {data.Length} 字节");
// 处理游戏消息...
}
// 关闭P2P会话
private void CloseP2PSession(CSteamID user)
{
SteamNetworking.CloseP2PSessionWithUser(user);
m_ConnectedPeers.Remove(user);
Debug.Log("已关闭与 " + user + " 的P2P会话");
}
//-----------------------------------------------------------------------------
// 其他回调
//-----------------------------------------------------------------------------
void OnSteamServersConnected(SteamServersConnected_t pLogonSuccess)
{
Debug.Log("成功连接到Steam");
m_bConnectedToSteam = true;
UpdateStatus("已连接到Steam");
// 连接后自动搜索大厅
RefreshLobbyList();
}
void OnSteamServerConnectFailure(SteamServerConnectFailure_t pConnectFailure)
{
m_bConnectedToSteam = false;
Debug.LogError("连接到Steam失败: " + pConnectFailure.m_eResult);
UpdateStatus("连接到Steam失败");
}
void OnSteamServersDisconnected(SteamServersDisconnected_t pLoggedOff)
{
m_bConnectedToSteam = false;
Debug.Log("从Steam断开连接");
UpdateStatus("从Steam断开连接");
}
void OnValidateAuthTicketResponse(ValidateAuthTicketResponse_t pResponse)
{
Debug.Log("用户验证响应: " + pResponse.m_SteamID + ", 结果: " + pResponse.m_eAuthSessionResponse);
// 验证处理...
}
//-----------------------------------------------------------------------------
// UI辅助函数
//-----------------------------------------------------------------------------
private void UpdateStatus(string message)
{
if (statusText != null)
{
statusText.text = message;
}
Debug.Log("状态: " + message);
}
}
2.、创建UI
在Unity中创建以下UI元素:
-
一个ScrollView作为大厅列表容器
-
大厅项预制体(包含:大厅名称、玩家数量、房主名称、加入按钮)
-
刷新按钮
-
创建大厅按钮
-
状态文本
-
大厅面板(加入大厅后显示)
-
搜索面板(初始显示)
3、设置过滤器:
在 RefreshLobbyList() 方法中,可以使用以下过滤器:
// 按距离过滤
SteamMatchmaking.AddRequestLobbyListDistanceFilter(ELobbyDistanceFilter.k_ELobbyDistanceFilterWorldwide);
// 按空位过滤
SteamMatchmaking.AddRequestLobbyListFilterSlotsAvailable(1);
// 按字符串值过滤
SteamMatchmaking.AddRequestLobbyListStringFilter("gamemode", "team_deathmatch", ELobbyComparison.k_ELobbyComparisonEqual);
SteamMatchmaking.AddRequestLobbyListStringFilter("map", "dust2", ELobbyComparison.k_ELobbyComparisonEqual);
// 按数值过滤
SteamMatchmaking.AddRequestLobbyListNumericalFilter("min_level", 10, ELobbyComparison.k_ELobbyComparisonGreaterThanOrEqualTo);
4、设置大厅数据:
主机可以在 SetLobbyData() 中设置自定义数据:
// 设置游戏模式
SteamMatchmaking.SetLobbyData(m_CurrentLobby, "gamemode", "deathmatch");
// 设置地图名称
SteamMatchmaking.SetLobbyData(m_CurrentLobby, "map", "space_station");
// 设置游戏难度
SteamMatchmaking.SetLobbyData(m_CurrentLobby, "difficulty", "hard");
// 设置密码(如果有)
SteamMatchmaking.SetLobbyData(m_CurrentLobby, "has_password", "true");
5、大厅类型:
-
k_ELobbyTypePrivate:私有,只能通过邀请加入
-
k_ELobbyTypeFriendsOnly:仅好友可见
-
k_ELobbyTypePublic:公开,所有人都能看到
-
k_ELobbyTypeInvisible:隐形,只能通过直接邀请加入
6、大厅排序:
public void SortLobbiesByPing() {
// Steam会自动按ping排序
SteamMatchmaking.AddRequestLobbyListDistanceFilter(ELobbyDistanceFilter.k_ELobbyDistanceFilterClose);
}
参考
https://blog.csdn.net/qq_41884036/article/details/134667607
专栏推荐
完结
好了,我是向宇,博客地址:https://xiangyu.blog.csdn.net,如果学习过程中遇到任何问题,也欢迎你评论私信找我。
赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注,你的每一次支持都是我不断创作的最大动力。当然如果你发现了文章中存在错误或者有更好的解决方法,也欢迎评论私信告诉我哦!
这里是一个专注于游戏开发的社区,我们致力于为广大游戏爱好者提供一个良好的学习和交流平台。我们的专区包含了各大流行引擎的技术博文,涵盖了从入门到进阶的各个阶段,无论你是初学者还是资深开发者,都能在这里找到适合自己的内容。除此之外,我们还会不定期举办游戏开发相关的活动,让大家更好地交流互动。加入我们,一起探索游戏开发的奥秘吧!
更多推荐



所有评论(0)