Files
zeling_v2/Docs/Design/46_PlatformIntegration.md
2026-05-08 11:04:00 +08:00

680 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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同 BuildSteamworks.NET 统一覆盖)
**次优先**Nintendo Switch需单独 Unity 构建 + Switch SDK 订阅)
**预留架构**Xbox / PS5 SDK 留位,实际集成视版权洽谈决定
---
## 2. IPlatformService 抽象接口
**核心设计**:所有游戏逻辑通过 `IPlatformService` 接口调用,不直接引用 `Steamworks` 类。
当目标平台变化时,只需替换实现类,游戏逻辑零改动。
```csharp
namespace BaseGames.Platform
{
/// <summary>
/// 平台服务抽象接口,具体实现由各平台 ServiceLocator 注册。
/// 默认实现为 NullPlatformService编辑器/不支持平台时使用)。
/// </summary>
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<bool> CloudSaveAsync(string fileName, byte[] data);
Task<byte[]> 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();
}
/// <summary>
/// 空实现(编辑器 / 不支持平台时的默认值)。
/// </summary>
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<bool> CloudSaveAsync(string f, byte[] d) => Task.FromResult(false);
public Task<byte[]> CloudLoadAsync(string f) => Task.FromResult<byte[]>(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<IPlatformService>(service);
}
void Update() => ServiceLocator.Get<IPlatformService>()?.RunCallbacks();
void OnApplicationQuit() => ServiceLocator.Get<IPlatformService>()?.Shutdown();
}
```
---
## 3. Steam PC / Steam Deck 集成
### 3.1 Steamworks.NET 安装
```
安装步骤:
1. 从 GitHub 下载 Steamworks.NEThttps://github.com/rlabrecque/Steamworks.NET/releases
2. 解压到 Assets/Plugins/Steamworks.NET/
3. 在 Project RootAssets/.. 同级)创建 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 065535
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<IPlatformService>()?.UnlockAchievement(achievementId);
// UI 通知
_achievementUnlockedChannel.Raise(achievementId);
}
```
---
## 4. Steam Cloud 存档同步
### 4.1 同步策略
```
同步策略(时序):
本地文件PlayerPrefs路径 ←──→ Steam CloudISteamRemoteStorage
游戏启动时:
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<IPlatformService>();
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<SaveData>(cloudJson);
var localSave = _saveManager.CurrentSave;
// 比较时间戳
if (cloudSave.lastSaveTime > localSave.lastSaveTime)
{
// 云端更新,用云端覆盖本地
_saveManager.OverwriteWithCloudData(cloudSave);
Debug.Log("[Cloud] 已从云端恢复更新的存档");
}
// 本地更新,不做任何操作(保存时会自动上传)
}
public async UniTask UploadToCloudAsync()
{
var platform = ServiceLocator.Get<IPlatformService>();
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<bool> CloudSaveAsync(string fileName, byte[] data)
{
if (!_initialized) return false;
bool ok = SteamRemoteStorage.FileWrite(fileName, data, data.Length);
return ok;
}
public async Task<byte[]> 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<IPlatformService>();
_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 DeckXInput| `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<IVibrationService>()?.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: 振幅 01频率 401250 Hz
// 低频手感 = 小振幅低频率(手感"沉"
// 高频手感 = 小振幅高频率(手感"脆"
var vibrationValue = new VibrationValue
{
amplitudeLow = leftMotor * 0.5f,
frequencyLow = 160f,
amplitudeHigh = rightMotor * 0.3f,
frequencyHigh = 320f
};
// ... 发送到 Joy-Con
}
}
#endif
```
### 7.2 Switch 特定分辨率适配
```
Switch 显示模式:
掌机模式: 720p1280×720→ UI ScaleMode: Scale With Screen Size参考 1280×720
底座模式: 1080p1920×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: VulkanSteam Deck 推荐)
Scripting Backend: IL2CPP发布版本Mono开发调试
API Compatibility: .NET Standard 2.1
Quality SettingsPC:
Ultra: URP 2D Renderer, All Post-Processing ON
High: URP, Most Post-Processing ON
Medium: URP, 基础 Post-ProcessingBloom + 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. 上传到 Steamsteamcmd + 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 版本1590秒
[ ] 商店描述(中文 + 英文,含 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`
---
## 附录 ASteam API Key 安全说明
```
⚠️ 安全注意:
- Steam AppID (steam_appid.txt) 不提交到版本控制(已加入 .gitignore
- 开发期间使用 480Spacewar测试 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*