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

24 KiB
Raw Permalink Blame History

46 · 平台发布集成指南

命名空间 BaseGames.Platform
所属文档集 ← 返回索引 · 总览
依赖 BaseGames.Core.Events · BaseGames.Persistence(云存档)· BaseGames.Audio(振动)
外部 SDK Steamworks.NET · Unity GameKit可选 Switch
关联 31_SaveDataSchema云存档· 32_AchievementSystem成就桥接· 38_SettingsSystem平台特定设置


目录

  1. 平台目标矩阵
  2. IPlatformService 抽象接口
  3. Steam PC / Steam Deck 集成
  4. Steam Cloud 存档同步
  5. Steam Rich Presence
  6. 手柄振动平台适配
  7. Nintendo Switch 适配(预留)
  8. 平台特定构建配置
  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 类。 当目标平台变化时,只需替换实现类,游戏逻辑零改动。

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 注册

// 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 实现

#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 直接依赖:

// 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

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 中的云存档实现

// 在 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

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 页面配置本地化字符串(不在游戏内):

; steam_localization.vdf提交到 Steam 后台)
"lang"
{
    "Language" "schinese"
    "Tokens"
    {
        "MainMenu"     "在主菜单"
        "Exploring"    "正在探索:%region%"
        "FightingBoss" "正在挑战:%boss%"
        "BossDefeated" "击败了 Boss"
        "GameComplete" "完成了泽灵!"
    }
}

6. 手柄振动平台适配

6.1 IVibrationService 接口

振动与 IPlatformService 解耦,单独一个接口,因为振动需要更细粒度的控制(左/右马达分离、触发马达):

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 对接

NiceVibrationsLofeltSDK项目已集成通过 Feel 的 MMF_HapticFeedback 调用振动。
对于 Steam 平台特定 XInput 振动,用 MMF_Custom_SteamRumble 包装:

[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 SDKnn::hid)使用条件编译宏 UNITY_SWITCH 隔离:

#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