Article

Unity WebGL 小游戏实战 08:埋点体系与KPI漏斗(留存、转化、平衡闭环)

路线阶段:Unity WebGL 小游戏实战第 8 章。
本章目标:把“感觉上玩家不留存”变成可量化、可定位、可迭代优化的数据闭环。

学习目标

完成本章后,你应该能做到:

  1. 定义统一埋点模型与命名规范,避免数据口径混乱。
  2. 建立关键漏斗(启动->首战->首通->二次回流->付费/广告转化)。
  3. 追踪经济与平衡指标,定位版本问题来源。
  4. 为 A/B 实验和活动效果评估提供稳定数据基础。

为什么需要统一埋点

常见问题:

  1. 同一事件被多个名字上报,无法对齐。
  2. 只记“发生了什么”,不记“在什么上下文发生”。
  3. 版本切换后口径变化,趋势不可比。

核心原则:事件结构稳定 > 事件数量堆叠

事件模型

[Serializable]
public sealed class TelemetryEvent
{
    public string EventName;
    public long TsMs;

    public string SessionId;
    public string PlayerId;
    public string ClientVersion;
    public int StageId;

    public Dictionary<string, string> Attr;
}

命名规范

建议使用 domain_action_result

  1. session_start
  2. stage_enter
  3. stage_complete
  4. stage_fail
  5. upgrade_pick
  6. ad_reward_complete
  7. event_claim_success

避免:

  1. 拼音+中文混用。
  2. 同义不同名(stage_win / level_complete)。

上报网关

public interface ITelemetrySink
{
    void Enqueue(TelemetryEvent e);
    void Flush();
}
public sealed class TelemetryService
{
    private readonly ITelemetrySink _sink;
    private readonly TelemetryContext _ctx;

    public TelemetryService(ITelemetrySink sink, TelemetryContext ctx)
    {
        _sink = sink;
        _ctx = ctx;
    }

    public void Track(string eventName, Dictionary<string, string> attr)
    {
        var e = new TelemetryEvent
        {
            EventName = eventName,
            TsMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
            SessionId = _ctx.SessionId,
            PlayerId = _ctx.PlayerId,
            ClientVersion = _ctx.ClientVersion,
            StageId = _ctx.StageId,
            Attr = attr ?? new Dictionary<string, string>()
        };

        _sink.Enqueue(e);
    }
}

public sealed class TelemetryContext
{
    public string SessionId;
    public string PlayerId;
    public string ClientVersion;
    public int StageId;
}

批量发送与失败重试

public sealed class BufferedHttpTelemetrySink : ITelemetrySink
{
    private readonly List<TelemetryEvent> _buffer = new List<TelemetryEvent>(128);
    private readonly string _endpoint;

    public BufferedHttpTelemetrySink(string endpoint)
    {
        _endpoint = endpoint;
    }

    public void Enqueue(TelemetryEvent e)
    {
        _buffer.Add(e);
        if (_buffer.Count >= 32)
        {
            Flush();
        }
    }

    public void Flush()
    {
        if (_buffer.Count == 0)
        {
            return;
        }

        var payload = JsonUtility.ToJson(new TelemetryBatch { Events = _buffer });
        // WebGL 中通过 JS bridge 或 UnityWebRequest 上报
        // 失败时可写本地缓存并下次重试

        _buffer.Clear();
    }
}

[Serializable]
public sealed class TelemetryBatch
{
    public List<TelemetryEvent> Events;
}

核心漏斗定义

新手漏斗

  1. session_start
  2. stage_enter(stage=1)
  3. stage_complete(stage=1)
  4. session_start(day=1)

商业化漏斗

  1. ad_reward_show
  2. ad_reward_complete
  3. reward_granted

运营漏斗

  1. event_panel_open
  2. event_task_progress
  3. event_claim_success

平衡指标

每局至少计算:

  1. 局时长(秒)
  2. 击杀数
  3. 受击数
  4. 局内金币总获取/总消耗
  5. 升级选择分布(每个升级被选次数)
  6. 失败原因分布(超时/死亡/退出)

事件接线示例

public sealed class TelemetryBinder : IDisposable
{
    private readonly EventBus _bus;
    private readonly TelemetryService _telemetry;

    public TelemetryBinder(EventBus bus, TelemetryService telemetry)
    {
        _bus = bus;
        _telemetry = telemetry;

        _bus.Subscribe("StageStarted", OnStageStarted);
        _bus.Subscribe("StageCompleted", OnStageCompleted);
        _bus.Subscribe("StageFailed", OnStageFailed);
        _bus.Subscribe("RunUpgradeBought", OnUpgradeBought);
        _bus.Subscribe("AdFinished", OnAdFinished);
    }

    public void Dispose()
    {
        _bus.Unsubscribe("StageStarted", OnStageStarted);
        _bus.Unsubscribe("StageCompleted", OnStageCompleted);
        _bus.Unsubscribe("StageFailed", OnStageFailed);
        _bus.Unsubscribe("RunUpgradeBought", OnUpgradeBought);
        _bus.Unsubscribe("AdFinished", OnAdFinished);
    }

    private void OnStageStarted(object payload)
    {
        _telemetry.Track("stage_enter", new Dictionary<string, string>
        {
            {"stage_id", payload.ToString()}
        });
    }

    private void OnStageCompleted(object payload)
    {
        _telemetry.Track("stage_complete", null);
    }

    private void OnStageFailed(object payload)
    {
        _telemetry.Track("stage_fail", new Dictionary<string, string>
        {
            {"reason", payload.ToString()}
        });
    }

    private void OnUpgradeBought(object payload)
    {
        _telemetry.Track("upgrade_pick", new Dictionary<string, string>
        {
            {"upgrade_id", payload.ToString()}
        });
    }

    private void OnAdFinished(object payload)
    {
        var ad = (AdResult)payload;
        _telemetry.Track("ad_reward_complete", new Dictionary<string, string>
        {
            {"placement", ad.Placement},
            {"success", ad.Success ? "1" : "0"},
            {"reward", ad.RewardGranted ? "1" : "0"}
        });
    }
}

数据看板最小集

首版看板建议包含:

  1. DAU / Session
  2. D1/D3/D7 留存
  3. 首关通过率
  4. 平均局时长
  5. 广告完播率
  6. 活动参与率与领奖率

A/B 实验准备

埋点需要带上实验分组:

_ctx.ExperimentGroup = "director_v2_B";

事件中统一加 exp_group 属性,后续统计不需要二次拼表。

WebGL 注意点

  1. 上报不能阻塞主线程。
  2. 失败重试需节流,避免网络异常导致循环上报。
  3. 注意隐私合规,只采集必要字段。

与前面系统联动

  1. 导演系统:记录决策强度与结果。
  2. 经济系统:记录货币流入流出与升级选择。
  3. 广告系统:记录展示、完播与奖励发放链路。
  4. 活动系统:记录参与与领奖转化。

验收清单

  1. 关键事件命名统一且可持续扩展。
  2. 本地可导出完整事件序列并校验字段。
  3. 网络异常时事件不丢失或可重试。
  4. 看板可回答“玩家在哪一步流失”。

常见坑

坑 1:事件字段频繁变更

会导致历史不可比。字段变更需版本化管理。

坑 2:在高频路径上报大量细粒度事件

会拖慢性能。高频事件应采样或聚合。

坑 3:只看均值不看分布

容易误判。至少区分新手、活跃、回流群体。

本月作业

搭建“首日体验漏斗看板”:

  1. session_start -> stage_enter_1 -> stage_complete_1 -> return_day1 全链路。
  2. 加入失败原因与设备分层分析。
  3. 基于结果给出 3 条可执行优化项。

下一章进入 Unity WebGL 小游戏实战 09:性能分层优化(CPU/GPU/内存/加载全链路)。