# 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*