# 46 · 平台发布集成指南 > **命名空间** `BaseGames.Platform` > **所属文档集** [← 返回索引](./README.md) · [总览](./00_Overview.md) > **依赖** `BaseGames.Core.Events` · `BaseGames.Persistence`(云存档)· `BaseGames.Audio`(振动) > **外部 SDK** Steamworks.NET · Unity GameKit(可选 Switch) > **关联** 31_SaveDataSchema(云存档)· 32_AchievementSystem(成就桥接)· 38_SettingsSystem(平台特定设置) --- ## 目录 1. [平台目标矩阵](#1-平台目标矩阵) 2. [IPlatformService 抽象接口](#2-iplatformservice-抽象接口) 3. [Steam PC / Steam Deck 集成](#3-steam-pc--steam-deck-集成) 4. [Steam Cloud 存档同步](#4-steam-cloud-存档同步) 5. [Steam Rich Presence](#5-steam-rich-presence) 6. [手柄振动平台适配](#6-手柄振动平台适配) 7. [Nintendo Switch 适配(预留)](#7-nintendo-switch-适配预留) 8. [平台特定构建配置](#8-平台特定构建配置) 9. [发布前验收清单](#9-发布前验收清单) --- ## 1. 平台目标矩阵 | 功能 | Steam PC | Steam Deck | Nintendo Switch | Xbox Series | PS5 | |------|---------|-----------|----------------|-------------|-----| | 成就系统 | ✅ Steamworks | ✅ Steamworks | ✅ Switch SDK | ✅ Xbox SDK | ✅ PSN | | 云存档 | ✅ Steam Cloud | ✅ Steam Cloud | ✅ Switch 云 | ✅ Xbox Cloud | ✅ PS Plus | | Rich Presence | ✅ | ✅ | ❌(无 RP)| ✅ Xbox 活动 | ❌ | | 手柄振动 | ✅ XInput 基础 | ✅ SD 振动 | ✅ Joy-Con HD | ✅ Xbox 精确 | ✅ DualSense 自适应 | | 覆盖层(Overlay)| ✅ Steam | ✅ Steam | ❌ | ✅ Xbox | ❌ | | 截图功能 | ✅ Steam 截图 | ✅ | ✅ Switch | ✅ | ✅ | | 输入图标 | 键盘/XInput | XInput | Joy-Con | Xbox | DualSense | | 目标帧率 | 165+ | 60(锁定)| 60 | 60/120 | 60/120 | | 分辨率 | 动态 | 1280×800 | 720p/1080p | 4K | 4K | **当前优先开发**:Steam PC + Steam Deck(同 Build,Steamworks.NET 统一覆盖) **次优先**:Nintendo Switch(需单独 Unity 构建 + Switch SDK 订阅) **预留架构**:Xbox / PS5 SDK 留位,实际集成视版权洽谈决定 --- ## 2. IPlatformService 抽象接口 **核心设计**:所有游戏逻辑通过 `IPlatformService` 接口调用,不直接引用 `Steamworks` 类。 当目标平台变化时,只需替换实现类,游戏逻辑零改动。 ```csharp namespace BaseGames.Platform { /// /// 平台服务抽象接口,具体实现由各平台 ServiceLocator 注册。 /// 默认实现为 NullPlatformService(编辑器/不支持平台时使用)。 /// public interface IPlatformService { // ── 成就 ────────────────────────────────────────────────── void UnlockAchievement(string achievementId); bool IsAchievementUnlocked(string achievementId); // ── 统计数据(用于成就进度)──────────────────────────────── void SetStat(string statId, int value); void IncrementStat(string statId, int increment = 1); int GetStat(string statId); // ── 云存档 ───────────────────────────────────────────────── Task CloudSaveAsync(string fileName, byte[] data); Task CloudLoadAsync(string fileName); bool IsCloudAvailable { get; } // ── Rich Presence ──────────────────────────────────────── void SetRichPresence(string key, string value); void ClearRichPresence(); // ── 振动 ────────────────────────────────────────────────── void Rumble(float lowFreq, float highFreq, float duration); void StopRumble(); // ── 生命周期 ─────────────────────────────────────────────── void Initialize(); void RunCallbacks(); // 在 Update 中调用,处理 SDK 回调 void Shutdown(); } /// /// 空实现(编辑器 / 不支持平台时的默认值)。 /// public class NullPlatformService : IPlatformService { public void UnlockAchievement(string id) { } public bool IsAchievementUnlocked(string id) => false; public void SetStat(string id, int v) { } public void IncrementStat(string id, int inc = 1) { } public int GetStat(string id) => 0; public Task CloudSaveAsync(string f, byte[] d) => Task.FromResult(false); public Task CloudLoadAsync(string f) => Task.FromResult(null); public bool IsCloudAvailable => false; public void SetRichPresence(string k, string v) { } public void ClearRichPresence() { } public void Rumble(float l, float h, float dur) { } public void StopRumble() { } public void Initialize() { } public void RunCallbacks() { } public void Shutdown() { } } } ``` ### 2.1 ServiceLocator 注册 ```csharp // PlatformBootstrap.cs(挂载在 Persistent 场景的 Bootstrap GameObject) public class PlatformBootstrap : MonoBehaviour { void Awake() { IPlatformService service; #if UNITY_STANDALONE && STEAMWORKS_NET service = new SteamPlatformService(); #elif UNITY_SWITCH service = new SwitchPlatformService(); #else service = new NullPlatformService(); #endif service.Initialize(); ServiceLocator.Register(service); } void Update() => ServiceLocator.Get()?.RunCallbacks(); void OnApplicationQuit() => ServiceLocator.Get()?.Shutdown(); } ``` --- ## 3. Steam PC / Steam Deck 集成 ### 3.1 Steamworks.NET 安装 ``` 安装步骤: 1. 从 GitHub 下载 Steamworks.NET(https://github.com/rlabrecque/Steamworks.NET/releases) 2. 解压到 Assets/Plugins/Steamworks.NET/ 3. 在 Project Root(Assets/.. 同级)创建 steam_appid.txt,内容为 AppID(开发期使用 480) 4. 在 Player Settings → Scripting Define Symbols 添加 STEAMWORKS_NET 5. 运行时必须有 Steam 客户端进程(否则 SteamAPI.Init() 返回 false) ``` ### 3.2 SteamPlatformService 实现 ```csharp #if STEAMWORKS_NET using Steamworks; namespace BaseGames.Platform { public class SteamPlatformService : IPlatformService { bool _initialized; // ── 生命周期 ──────────────────────────────────────────── public void Initialize() { if (!SteamAPI.Init()) { Debug.LogError("[Platform] Steam 初始化失败,Steam 客户端未运行?"); return; } _initialized = true; Debug.Log($"[Platform] Steam 初始化成功 | AppID: {SteamUtils.GetAppID()}"); } public void RunCallbacks() { if (_initialized) SteamAPI.RunCallbacks(); } public void Shutdown() { if (_initialized) SteamAPI.Shutdown(); } // ── 成就 ──────────────────────────────────────────────── public void UnlockAchievement(string achievementId) { if (!_initialized) return; SteamUserStats.SetAchievement(achievementId); SteamUserStats.StoreStats(); } public bool IsAchievementUnlocked(string id) { if (!_initialized) return false; SteamUserStats.GetAchievement(id, out bool achieved); return achieved; } // ── 统计数据 ───────────────────────────────────────────── public void SetStat(string id, int value) { if (!_initialized) return; SteamUserStats.SetStat(id, value); SteamUserStats.StoreStats(); } public void IncrementStat(string id, int increment = 1) { if (!_initialized) return; SteamUserStats.GetStat(id, out int current); SetStat(id, current + increment); } public int GetStat(string id) { if (!_initialized) return 0; SteamUserStats.GetStat(id, out int value); return value; } // ── 振动 ───────────────────────────────────────────────── public void Rumble(float lowFreq, float highFreq, float duration) { // XInput: Intensity 0–65535 ushort lo = (ushort)(Mathf.Clamp01(lowFreq) * 65535); ushort hi = (ushort)(Mathf.Clamp01(highFreq) * 65535); SteamInput.TriggerVibration(SteamInput.GetConnectedControllers()[0], lo, hi); } public void StopRumble() => Rumble(0, 0, 0); } } #endif ``` ### 3.3 AchievementSystem 桥接 `32_AchievementSystem` 中的 `AchievementManager` 内部通过 `IPlatformService` 派发,无 Steam 直接依赖: ```csharp // 32_AchievementSystem.md 中已有 AchievementManager,此处仅展示桥接点 public void OnAchievementConditionMet(string achievementId) { // 本地记录(SaveData) SaveManager.Instance.CurrentSave.unlockedAchievements.Add(achievementId); // 平台成就同步(零耦合) ServiceLocator.Get()?.UnlockAchievement(achievementId); // UI 通知 _achievementUnlockedChannel.Raise(achievementId); } ``` --- ## 4. Steam Cloud 存档同步 ### 4.1 同步策略 ``` 同步策略(时序): 本地文件(PlayerPrefs路径) ←──→ Steam Cloud(ISteamRemoteStorage) 游戏启动时: 1. 检查 Steam Cloud 是否可用(_platform.IsCloudAvailable) 2. 比较本地存档和云端存档的 timestamp 3. 以较新的版本覆盖较旧的版本(以 lastSaveTime 为准) 4. 冲突时(timestamp 相差 < 5s 且内容不同)→ 弹出对话框让玩家选择 保存时: 1. 写入本地文件(SaveManager 原有逻辑) 2. 异步上传到 Steam Cloud(不阻塞游戏线程) 3. 上传失败不影响本地游戏(失败日志写入 Logs/cloud_sync.log) ``` ### 4.2 CloudSyncManager ```csharp public class CloudSyncManager : MonoBehaviour { [SerializeField] SaveManager _saveManager; [SerializeField] VoidEventChannelSO _onSyncComplete; const string CLOUD_SAVE_FILENAME = "zeling_save.json"; public async UniTask SyncOnStartupAsync() { var platform = ServiceLocator.Get(); if (!platform.IsCloudAvailable) return; // 下载云端存档 byte[] cloudData = await platform.CloudLoadAsync(CLOUD_SAVE_FILENAME); if (cloudData == null) return; // 云端无存档,首次游玩 string cloudJson = System.Text.Encoding.UTF8.GetString(cloudData); var cloudSave = JsonConvert.DeserializeObject(cloudJson); var localSave = _saveManager.CurrentSave; // 比较时间戳 if (cloudSave.lastSaveTime > localSave.lastSaveTime) { // 云端更新,用云端覆盖本地 _saveManager.OverwriteWithCloudData(cloudSave); Debug.Log("[Cloud] 已从云端恢复更新的存档"); } // 本地更新,不做任何操作(保存时会自动上传) } public async UniTask UploadToCloudAsync() { var platform = ServiceLocator.Get(); if (!platform.IsCloudAvailable) return; string json = _saveManager.SerializeCurrentSave(); byte[] data = System.Text.Encoding.UTF8.GetBytes(json); bool success = await platform.CloudSaveAsync(CLOUD_SAVE_FILENAME, data); if (!success) Debug.LogWarning("[Cloud] 云存档上传失败,本地存档已保存"); } } ``` ### 4.3 SteamPlatformService 中的云存档实现 ```csharp // 在 SteamPlatformService 中补全 public async Task CloudSaveAsync(string fileName, byte[] data) { if (!_initialized) return false; bool ok = SteamRemoteStorage.FileWrite(fileName, data, data.Length); return ok; } public async Task CloudLoadAsync(string fileName) { if (!_initialized) return null; if (!SteamRemoteStorage.FileExists(fileName)) return null; int fileSize = SteamRemoteStorage.GetFileSize(fileName); if (fileSize <= 0) return null; byte[] buffer = new byte[fileSize]; int read = SteamRemoteStorage.FileRead(fileName, buffer, fileSize); return read > 0 ? buffer : null; } ``` --- ## 5. Steam Rich Presence ### 5.1 Rich Presence 状态设计 | 游戏状态 | Key-Value | Steam 显示文字 | |---------|---------|--------------| | 主菜单 | `steam_display: #MainMenu` | "在主菜单" | | 探索区域 | `steam_display: #Exploring` `region: Forest` | "正在探索:扎根森林" | | 挑战 Boss | `steam_display: #FightingBoss` `boss: ForestGuardian` | "正在挑战:森林守卫者" | | Boss 已击败 | `steam_display: #BossDefeated` | "击败了 Boss!" | | 游戏通关 | `steam_display: #GameComplete` | "完成了泽灵!" | ### 5.2 RichPresenceManager ```csharp public class RichPresenceManager : MonoBehaviour { // 订阅游戏事件,自动更新 RP 状态(零耦合) [SerializeField] StringEventChannelSO _onRegionEntered; // 进入区域 [SerializeField] StringEventChannelSO _onBossEncounter; // 遭遇 Boss [SerializeField] VoidEventChannelSO _onBossDefeated; [SerializeField] VoidEventChannelSO _onGameComplete; IPlatformService _platform; void Start() { _platform = ServiceLocator.Get(); _onRegionEntered.OnEventRaised += SetExploring; _onBossEncounter.OnEventRaised += SetFightingBoss; _onBossDefeated.OnEventRaised += SetBossDefeated; _onGameComplete.OnEventRaised += SetGameComplete; } void SetExploring(string regionId) { _platform.SetRichPresence("steam_display", "#Exploring"); _platform.SetRichPresence("region", GetLocalizedRegionName(regionId)); } void SetFightingBoss(string bossId) { _platform.SetRichPresence("steam_display", "#FightingBoss"); _platform.SetRichPresence("boss", GetLocalizedBossName(bossId)); } void SetBossDefeated() => _platform.SetRichPresence("steam_display", "#BossDefeated"); void SetGameComplete() => _platform.SetRichPresence("steam_display", "#GameComplete"); // 查找本地化名称(内部) string GetLocalizedRegionName(string id) => LanguageManagerSO.Instance.GetString($"REGION_{id.ToUpper()}"); string GetLocalizedBossName(string id) => LanguageManagerSO.Instance.GetString($"BOSS_{id.ToUpper()}_NAME"); } ``` ### 5.3 Steam Rich Presence 本地化文件 在 Steamworks 后台 Partner 页面配置本地化字符串(不在游戏内): ```ini ; steam_localization.vdf(提交到 Steam 后台) "lang" { "Language" "schinese" "Tokens" { "MainMenu" "在主菜单" "Exploring" "正在探索:%region%" "FightingBoss" "正在挑战:%boss%" "BossDefeated" "击败了 Boss!" "GameComplete" "完成了泽灵!" } } ``` --- ## 6. 手柄振动平台适配 ### 6.1 IVibrationService 接口 振动与 `IPlatformService` 解耦,单独一个接口,因为振动需要更细粒度的控制(左/右马达分离、触发马达): ```csharp public interface IVibrationService { // 基础振动(左马达=低频,右马达=高频,持续 duration 秒) void Rumble(float leftMotor, float rightMotor, float duration); // 平台扩展(非所有平台支持) void RumbleTriggers(float leftTrigger, float rightTrigger, float duration); // DualSense / Xbox Elite void StopAll(); } ``` ### 6.2 振动平台实现对比 | 平台 | 实现类 | 特性 | |------|--------|------| | PC / Steam Deck(XInput)| `XInputVibrationService` | 左右马达基础振动 | | PS5 DualSense | `DualSenseVibrationService` | 自适应扳机电阻 + 高精度马达 | | Switch Joy-Con | `SwitchHDRumbleService` | HD 振动(模拟材质感)| | Xbox Series | `XboxVibrationService` | 4 马达(左/右手柄 + 左/右扳机)| ### 6.3 Feel MMF_Player 对接 `NiceVibrations`(LofeltSDK,项目已集成)通过 Feel 的 `MMF_HapticFeedback` 调用振动。 对于 Steam 平台特定 XInput 振动,用 `MMF_Custom_SteamRumble` 包装: ```csharp [AddComponentMenu("More Mountains/Feedbacks/Steam Rumble")] public class MMF_SteamRumble : MMF_Player { [Header("Steam Rumble")] public float LeftMotor = 0.5f; public float RightMotor = 0.5f; public float Duration = 0.2f; protected override IEnumerator PlayFeedbacksCoroutine(Vector3 position, float attenuation) { ServiceLocator.Get()?.Rumble(LeftMotor * attenuation, RightMotor * attenuation, Duration); yield break; } } ``` ### 6.4 振动强度规范 | 游戏事件 | 左马达(低频)| 右马达(高频)| 持续时间 | |---------|-------------|-------------|---------| | 普通攻击命中 | 0.0 | 0.4 | 0.08s | | 重攻击命中 | 0.5 | 0.6 | 0.15s | | 玩家受伤 | 0.6 | 0.3 | 0.25s | | 弹反成功 | 0.0 | 0.8 | 0.12s | | 落地冲击 | 0.7 | 0.1 | 0.10s | | Boss 登场 | 0.8 | 0.5 | 0.5s | | 死亡 | 1.0 | 0.8 | 0.6s | | 玩家死亡后渐停 | 渐弱至 0 | 渐弱至 0 | 0.8s | --- ## 7. Nintendo Switch 适配(预留) ### 7.1 架构预留 Switch SDK(`nn::hid`)使用条件编译宏 `UNITY_SWITCH` 隔离: ```csharp #if UNITY_SWITCH using nn.hid; public class SwitchPlatformService : IPlatformService { public void Initialize() { Nn.Initialize(); NpadJoy.SetHoldType(NpadJoyHoldType.Horizontal); } public void Rumble(float leftMotor, float rightMotor, float duration) { // Switch HD Rumble: 振幅 0–1,频率 40–1250 Hz // 低频手感 = 小振幅低频率(手感"沉") // 高频手感 = 小振幅高频率(手感"脆") var vibrationValue = new VibrationValue { amplitudeLow = leftMotor * 0.5f, frequencyLow = 160f, amplitudeHigh = rightMotor * 0.3f, frequencyHigh = 320f }; // ... 发送到 Joy-Con } } #endif ``` ### 7.2 Switch 特定分辨率适配 ``` Switch 显示模式: 掌机模式: 720p(1280×720)→ UI ScaleMode: Scale With Screen Size(参考 1280×720) 底座模式: 1080p(1920×1080)→ 自动适配(同 UI ScaleMode) QA 要求: 两种模式均需跑通全流程,无 UI 溢出 ``` --- ## 8. 平台特定构建配置 ### 8.1 Steam PC / Steam Deck 构建设置 ``` Player Settings → PC, Mac & Linux Standalone: Company Name: BaseGames Studio Product Name: 泽灵 (Zeling) Version: 1.0.0(遵循 SemVer) Build Number: CI 自动递增 Graphics API: Windows: DirectX11(主), DirectX12(可选,开启后 DX12 即时 Shader 编译) Mac: Metal Linux: Vulkan(Steam Deck 推荐) Scripting Backend: IL2CPP(发布版本),Mono(开发调试) API Compatibility: .NET Standard 2.1 Quality Settings(PC): Ultra: URP 2D Renderer, All Post-Processing ON High: URP, Most Post-Processing ON Medium: URP, 基础 Post-Processing(Bloom + Color Grading) Low: 禁用所有 Post-Processing,禁用粒子 LOD Steam Deck 额外配置: Target Frame Rate: 60(强制锁帧,见 27_PerformanceBudgetGuide §3) Resolution: 1280×800 优先(宽屏适配 Aspect Ratio Guard) ``` ### 8.2 构建流水线(CI/CD) ``` 推荐工具: GitHub Actions + GameCI 构建触发: 推送到 main 分支,或手动触发 Pipeline 步骤: 1. 激活 Unity 许可证(无头模式) 2. 运行单元测试(EditMode + PlayMode) 3. 构建 PC 版本(IL2CPP, Release) 4. 上传到 Steam(steamcmd + build_script.vdf) 5. 通知 Discord Webhook(构建成功/失败) ``` ### 8.3 版本号规范 ``` 格式: Major.Minor.Patch-Build Major: 重大版本(完整游戏 = 1.x.x) Minor: 较大功能更新或 DLC Patch: Bug 修复、平衡性调整 Build: CI 构建号(自动) 示例: 1.0.3-247 ``` --- ## 9. 发布前验收清单 ### 9.1 Steam 页面资产 ``` [ ] 游戏主图(460×215 Header Capsule) [ ] 竖版小图(231×87 Small Capsule) [ ] 主页横图(616×353 Main Capsule) [ ] 截图(最少 5 张,建议 10 张,1920×1080 或 2560×1440) [ ] 宣传视频(480p + 1080p 版本,15–90秒) [ ] 商店描述(中文 + 英文,含 Short 版本 ≤ 300 字) [ ] 系统需求(最低/推荐) [ ] 支持的控制器列表(Steam 官方控制器标签) [ ] 语言支持列表(界面语言 + 音频语言) [ ] 内容评级申请(PEGI / ESRB / USK) ``` ### 9.2 功能测试(发布前必须通过) ``` [ ] Steam 成就全部解锁测试(逐一验证,非全成就,抽样 + 全类型覆盖) [ ] Steam 云存档同步测试(两台设备互通,冲突解决流程测试) [ ] Steam Overlay 兼容性(截图/好友/成就面板打开不崩溃) [ ] Rich Presence 正确显示(所有游戏状态) [ ] Steam Deck 官方验证测试(通过 Deck Verified 四项标准) [ ] 手柄全功能测试(Xbox / PlayStation / 键鼠 三种输入方式) [ ] 无障碍设置测试(色盲模式 / 大字体 / 降低闪烁,见 38_SettingsSystem §6) ``` ### 9.3 Steam Deck Verified 四项标准 ``` [ ] 1. 输入兼容性:支持 Steam 输入,可在无键盘情况下完成所有核心操作 [ ] 2. 显示兼容性:1280×800 分辨率正确显示,无文字裁切/UI 溢出 [ ] 3. 无障碍字体:最小 UI 字体 ≥ 9pt(在 800p 分辨率下可读) [ ] 4. 系统可用性:无需外部工具即可安装/启动/退出游戏 ``` ### 9.4 性能目标 | 平台 | 目标帧率 | VRAM 上限 | 加载时间上限 | |------|---------|----------|------------| | PC(推荐配置)| 稳定 120fps | 2GB | 5秒/场景 | | PC(最低配置)| 稳定 60fps | 1GB | 10秒/场景 | | Steam Deck | 稳定 60fps | 1GB(显存共享)| 8秒/场景 | 详细性能预算见 `27_PerformanceBudgetGuide`。 --- ## 附录 A:Steam API Key 安全说明 ``` ⚠️ 安全注意: - Steam AppID (steam_appid.txt) 不提交到版本控制(已加入 .gitignore) - 开发期间使用 480(Spacewar)测试 AppID - 发布 AppID 仅在构建服务器的 CI 环境变量中设置 - 不在代码中硬编码任何 Steam Web API Key - Steam Web API Key 仅在服务器端后台使用(若有排行榜等服务器功能) ``` ## 附录 B:快速入门(新开发者) ``` 1. 安装 Steam 客户端并登录 2. 从 Releases 下载 Steamworks.NET 并放入 Assets/Plugins/ 3. 创建 Assets/../steam_appid.txt,内容填 480 4. 在 Build Settings → Scripting Define Symbols 加入 STEAMWORKS_NET 5. 运行游戏,Console 中出现 "[Platform] Steam 初始化成功" 即可 6. 测试成就:调用 _platform.UnlockAchievement("ACH_TEST_001") → 游戏内弹出 Steam 成就通知即成功 ``` --- *文档版本 1.0 · 2026*