前 7 集,游戏本身已经成型:玩法、数据、经济、商城、留存、手感。但商业游戏和 demo 的真正分水岭,在上线之后——你能不能看见玩家在哪流失、能不能在不发新版本的情况下调一个数值、能不能不更新整包就换掉一批资源。这一集(EP8)做的就是这层"上线后还能调"的能力:埋点、远程配置、热更接缝。

先看这层能力最直观的样子——不发版开一个"限时双倍金币"活动。平日商城(远程配置没有活动)是这样:

平日商城(无活动)

然后我用 MCP 模拟服务端下发一份配置 {"event.coinMultiplier":"2"}RemoteConfig.LoadJson),点开商城——价格栏一个字没变,整城金币全部翻倍(一袋金币 2,200 → 4,400,金币宝库 28,000 → 56,000):

真机:开双倍金币活动,整城金币翻倍

游戏包没动一行、没重新编译,改的只是服务端那一份 JSON。这就是"上线后还能调"。下面把这套(埋点 + 远程配置 + 热更接缝)怎么实现、怎么用 MCP 验的拆开讲。

埋点:攒批上报,但把出口抽出来

埋点的难点不是"记事件",而是"怎么验证它记对了"——真上报是发网络,没法在断言里测。解法和 EP7 的音频一个套路:把出口(sink)抽成接口注入。线上接真 SDK,测试接捕获实现。这样"记了什么、何时 flush、批次里是什么"全是可测的纯逻辑:

public interface IAnalyticsSink { void Send(List<AnalyticsEvent> batch); }

public void Track(string name, string payload = "")
{
    _queue.Add(new AnalyticsEvent { name = name, ts = _clock, payload = payload });
    TotalTracked++;
    if (_queue.Count >= FlushThreshold) Flush();   // 攒够 20 条自动上报
}

而且埋点对游戏逻辑只读——它订阅 EP4/5/6 那些已有的事件(CoinChangedMsg/PurchaseGrantedMsg/DailyRewardClaimedMsg),转成埋点。这就是从 EP1 就坚持事件总线的回报:加遥测不用动任何业务代码,只在边上挂订阅。

public void Bind()
{
    MsgController.Register<CoinChangedMsg>(m => Track("coin_change", $"delta={m.delta};reason={m.reason}"));
    MsgController.Register<PurchaseGrantedMsg>(m => Track("purchase", $"sku={m.skuId};coins={m.coins}"));
    MsgController.Register<DailyRewardClaimedMsg>(m => Track("daily_reward", $"streak={m.streak};coins={m.coins}"));
}

左图是队列长度随事件累积的轨迹——爬到 20 立刻清零上报,循环往复:

埋点攒批flush + 不发版开活动

远程配置:不发版调数值,但绝不让它搞崩游戏

远程配置就是服务端下发一组 key→value,盖在本地默认值之上。它的威力是"不发版改经济、调难度、开关功能、开活动"。但它也是个危险品——一条脏配置可能让线上炸掉。所以这套实现有一条铁律:取值一律带默认值兜底,缺失或解析失败就回落默认

public float GetFloat(string key, float def)
    => _kv.TryGetValue(key, out var v)
       && float.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out var r)
       ? r : def;     // 解析不出来?用默认,绝不抛、绝不崩

下发格式是 version + entries[] 的扁平键值(JsonUtility 直接能解析)。LoadJson 对空串/坏 JSON 返回 false 而不是抛异常——线上拿到一坨乱码,最坏结果是"配置没更新",而不是"游戏崩了"。

把价值钉死:不发版开一个双倍金币活动

光说"能调"太虚,用一个真实运营场景落地——双倍金币活动。LiveOps 读远程配置里的 event.coinMultiplier,发金币的地方乘上它:

public static class LiveOps
{
    public static float CoinMultiplier => RemoteConfig.Instance.GetFloat("event.coinMultiplier", 1f);
    public static int ApplyCoinMultiplier(int baseCoins)
        => (int)Math.Round(baseCoins * CoinMultiplier, MidpointRounding.AwayFromZero);
}

右图就是它的效果:平日一笔奖励 1000 金币,活动期间服务端把 coinMultiplier 下发成 2,立刻变 2000——改的是服务端配置,不是游戏包。活动结束下发个空配置,系数回落 1,又无需发版。这就是远程配置对运营的全部意义。

热更:现在用不上,但接缝先留好

真正的热更(不更新整包就换资源/逻辑)要靠 YooAsset 或 Addressables,那是重外部依赖,我在这台机器上接不了完整链路。但我能做的是把接缝留好——抽一个 IAssetSource

public interface IAssetSource { T Load<T>(string path) where T : Object; }

public class ResourcesAssetSource : IAssetSource          // 现在:走 Resources
{ public T Load<T>(string path) where T : Object => Resources.Load<T>(path); }

#if YOOASSET
public class YooAssetSource : IAssetSource { /* 走热更包 */ }   // 装好 YooAsset 即切
#endif

上层全程"按 path 加载数据资产"(EconomyConfig/DifficultyConfig/ShopConfigDatabase/ThemeConfig…),所以切到热更时业务代码一行不改。这正是前面每一集都坚持数据资产化、按路径加载的深层原因——它天然为热更预留了接缝。

验证:23 条断言

A 埋点:累积5条不flush · 手动flush清空且批次==5 · 事件带时钟戳与名
  · 到阈值(20)自动flush · 游戏事件CoinChanged→被埋点 · 队列出现coin_change
B 远程配置:解析JSON · version==7 · Float/Int/Bool/String 覆盖生效
  · 缺失键回落默认 · 解析失败(abc→99)回落 · 空/坏JSON返回false
C 不发版活动:系数==2 · 双倍激活 · 1000→2000 · 下线回落1 · 1000→1000
D 热更抽象:按path加载EconomyConfig成功(与YooAsset同接口)
==== 23/23 PASS ====

钉死的核心:埋点到阈值必须自动上报(不然事件堆内存里丢)、游戏事件确实被遥测捕获(埋点漏了等于瞎运营)、远程配置坏数据必须回落默认(这是线上不崩的生命线)、活动系数能开能关都无需发版、热更抽象和最终接口同形(切换零改动)。

这一集的产物与诚实的话

  • AnalyticsManager(攒批埋点,出口可注入)+ RemoteConfig(下发覆盖 + 兜底)+ LiveOps(不发版活动)+ AssetService/IAssetSource(热更接缝)。
  • 23 条断言全绿。

诚实地讲:这一集做的是运营能力的客户端骨架。真正跑起来还差后端——埋点要有数据仓库和看板(这是另一整套服务)、远程配置要有下发后台和灰度系统、热更要有 CDN 和 YooAsset 完整接入。这些都需要服务端工程和基础设施,不在"纯客户端 + AI"的射程内。但客户端这侧的接口、攒批、兜底、接缝——决定了"后端接上来时客户端是否就绪"——已经建好并验证了。

下一篇 EP9(系列收尾):打包与复盘——把 9 集的系统串成完整启动链路、跑一次端到端、并诚实总结"全程 Claude + MCP 做商业级游戏"这件事到底做到了哪、没做到哪、边界在哪。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐