路线阶段:Unity WebGL 小游戏实战第 8 章。
本章目标:把“感觉上玩家不留存”变成可量化、可定位、可迭代优化的数据闭环。
学习目标
完成本章后,你应该能做到:
- 定义统一埋点模型与命名规范,避免数据口径混乱。
- 建立关键漏斗(启动->首战->首通->二次回流->付费/广告转化)。
- 追踪经济与平衡指标,定位版本问题来源。
- 为 A/B 实验和活动效果评估提供稳定数据基础。
为什么需要统一埋点
常见问题:
- 同一事件被多个名字上报,无法对齐。
- 只记“发生了什么”,不记“在什么上下文发生”。
- 版本切换后口径变化,趋势不可比。
核心原则:事件结构稳定 > 事件数量堆叠。
事件模型
[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:
session_startstage_enterstage_completestage_failupgrade_pickad_reward_completeevent_claim_success
避免:
- 拼音+中文混用。
- 同义不同名(
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;
}
核心漏斗定义
新手漏斗
session_startstage_enter(stage=1)stage_complete(stage=1)session_start(day=1)
商业化漏斗
ad_reward_showad_reward_completereward_granted
运营漏斗
event_panel_openevent_task_progressevent_claim_success
平衡指标
每局至少计算:
- 局时长(秒)
- 击杀数
- 受击数
- 局内金币总获取/总消耗
- 升级选择分布(每个升级被选次数)
- 失败原因分布(超时/死亡/退出)
事件接线示例
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"}
});
}
}
数据看板最小集
首版看板建议包含:
- DAU / Session
- D1/D3/D7 留存
- 首关通过率
- 平均局时长
- 广告完播率
- 活动参与率与领奖率
A/B 实验准备
埋点需要带上实验分组:
_ctx.ExperimentGroup = "director_v2_B";
事件中统一加 exp_group 属性,后续统计不需要二次拼表。
WebGL 注意点
- 上报不能阻塞主线程。
- 失败重试需节流,避免网络异常导致循环上报。
- 注意隐私合规,只采集必要字段。
与前面系统联动
- 导演系统:记录决策强度与结果。
- 经济系统:记录货币流入流出与升级选择。
- 广告系统:记录展示、完播与奖励发放链路。
- 活动系统:记录参与与领奖转化。
验收清单
- 关键事件命名统一且可持续扩展。
- 本地可导出完整事件序列并校验字段。
- 网络异常时事件不丢失或可重试。
- 看板可回答“玩家在哪一步流失”。
常见坑
坑 1:事件字段频繁变更
会导致历史不可比。字段变更需版本化管理。
坑 2:在高频路径上报大量细粒度事件
会拖慢性能。高频事件应采样或聚合。
坑 3:只看均值不看分布
容易误判。至少区分新手、活跃、回流群体。
本月作业
搭建“首日体验漏斗看板”:
session_start -> stage_enter_1 -> stage_complete_1 -> return_day1全链路。- 加入失败原因与设备分层分析。
- 基于结果给出 3 条可执行优化项。
下一章进入 Unity WebGL 小游戏实战 09:性能分层优化(CPU/GPU/内存/加载全链路)。