文章目录

一、前言

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 由以下三个问题之一引起:

  1. Steam 客户端未运行。需要运行的 Steam 客户端来提供各种 Steamworks 接口的实现。
  2. Steam 客户端无法确定游戏的 AppID。确保游戏目录中有 steam_appid.txt。当通过 Steam 下载启动游戏时,这永远不会发生,因为 SteamAPI.RestartAppIfNecessary() 将通过 Steam 重新启动它。
  3. 确保您的应用程序在与 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


专栏推荐

地址
【unity游戏开发入门到精通——C#篇】
【unity游戏开发入门到精通——unity通用篇】
【unity游戏开发入门到精通——unity3D篇】
【unity游戏开发入门到精通——unity2D篇】
【unity实战】
【制作100个Unity游戏】
【推荐100个unity插件】
【实现100个unity特效】
【unity框架/工具集开发】
【unity游戏开发——模型篇】
【unity游戏开发——InputSystem】
【unity游戏开发——Animator动画】
【unity游戏开发——UGUI】
【unity游戏开发——联网篇】
【unity游戏开发——优化篇】
【unity游戏开发——shader篇】
【unity游戏开发——编辑器扩展】
【unity游戏开发——热更新】
【unity游戏开发——网络】

完结

好了,我是向宇,博客地址:https://xiangyu.blog.csdn.net,如果学习过程中遇到任何问题,也欢迎你评论私信找我。

赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注,你的每一次支持都是我不断创作的最大动力。当然如果你发现了文章中存在错误或者有更好的解决方法,也欢迎评论私信告诉我哦!
在这里插入图片描述

Logo

这里是一个专注于游戏开发的社区,我们致力于为广大游戏爱好者提供一个良好的学习和交流平台。我们的专区包含了各大流行引擎的技术博文,涵盖了从入门到进阶的各个阶段,无论你是初学者还是资深开发者,都能在这里找到适合自己的内容。除此之外,我们还会不定期举办游戏开发相关的活动,让大家更好地交流互动。加入我们,一起探索游戏开发的奥秘吧!

更多推荐