17.3 活动配置与数据看板
17.1 让发布可控,17.2 让线上可修,这一节让运营可自助。运营的需求其实很朴素:今天开个双倍金币,明天关掉;看看昨天有多少人玩到第 10 关。如果每次都要程序员改代码发版,运营就永远在排队。
本节把《方块冲刺》推进到「运营自助」:用
configs/activities.json描述活动,纯逻辑校验时间窗与权重,运行时下发并本地兜底,再定义埋点事件模型和看板四项核心指标。收口时工程里会多出assets/scripts/platform/liveops.ts、assets/configs/activities.json和一份埋点事件表。
与站内 game/client/ 下 LiveOps 相关文章的分工:那些文章讲运营策略与活动设计,本节讲工程侧落点——配置长什么样、怎么校验、埋点打在哪、看板看哪几个数。
17.3.1 运营要的三件事
把运营的诉求拆到工程层,其实只有三件事:
- 上活动 / 关活动:不改代码、不发版,改一份配置就生效。
- 分人群:新用户和老用户看到的可能不一样,要能按条件命中。
- 看数据:活动开了以后,得知道有没有用。
对应到工程就是:配置表 + 下发通道 + 埋点。三件事缺一不可——只做配置表没有下发,运营还得找你;只做埋点没有配置,运营看着数据却没法调。
17.3.2 活动配置表的结构
先定义结构。活动配置是纯数据,用 L1 的类型 + 校验,可以脱离引擎直接单测:
// core/activity.ts —— 与引擎无关,可 vitest 直接测
export type ActivityKind = 'double_coin' | 'discount' | 'login_bonus';
export interface ActivityConfig {
id: string;
kind: ActivityKind;
startAt: number;
endAt: number;
weight: number;
enabled: boolean;
}
export interface ActivityIssue {
id: string;
reason: string;
}
export function validateActivities(
list: ActivityConfig[],
now: number,
): ActivityIssue[] {
const issues: ActivityIssue[] = [];
const ids = new Set<string>();
for (const a of list) {
if (ids.has(a.id)) issues.push({ id: a.id, reason: 'id 重复' });
ids.add(a.id);
if (a.startAt >= a.endAt) {
issues.push({ id: a.id, reason: '时间窗非法:startAt >= endAt' });
}
if (a.weight < 0 || a.weight > 100) {
issues.push({ id: a.id, reason: `weight 越界:${a.weight}` });
}
if (a.endAt < now && a.enabled) {
issues.push({ id: a.id, reason: '已过期但仍 enabled' });
}
}
return issues;
}
校验规则里最容易被忽略的是最后一条:过期但没关的活动。运营常常只配 endAt,忘了改 enabled,结果活动窗口过了还在跑。把这类问题在校验期就报出来,比线上发现强。
weight 的含义是「同类型活动的相对权重」。比如同时开两个 double_coin,权重 70 和 30 就是 70% 的用户进第一个。权重不是百分比,只是相对值——所以校验只保证非负,不要求求和为 100。
17.3.3 下发与本地兜底
配置的下发通道复用的就是 17.1 那套远端拉取,只是内容换成活动表。运行时组件读取并缓存到本地:
import { _decorator, Component, JsonAsset, assetManager, sys } from 'cc';
import { ActivityConfig, validateActivities } from '../core/activity';
const { ccclass, property } = _decorator;
@ccclass('LiveOps')
export class LiveOps extends Component {
@property
configUrl = '';
private _activities: ActivityConfig[] = [];
private _version = 0;
async refresh(): Promise<void> {
const url = `${this.configUrl}?t=${Date.now()}`;
await new Promise<void>((resolve) => {
assetManager.loadRemote<JsonAsset>(url, (err, asset) => {
if (!err && asset) {
const data = asset.json as Record<string, unknown> | null;
const list = (data?.activities ?? []) as ActivityConfig[];
const now = Date.now();
const issues = validateActivities(list, now);
if (issues.length === 0) {
this._activities = list;
this._version = (data?.version as number) ?? 0;
sys.localStorage.setItem('liveops:activities', JSON.stringify(list));
} else {
console.warn('活动配置校验未通过', issues);
}
}
resolve();
});
});
}
activeAt(now: number): ActivityConfig | null {
const hit = this._activities.filter(
(a) => a.enabled && a.startAt <= now && now < a.endAt,
);
return hit.length > 0 ? hit[0] : null;
}
}
三处设计:
- 校验不通过就不落地,保留上一次的配置。宁可跑旧活动,也不要跑一份结构错的活动。
- 写
sys.localStorage做本地缓存。键名liveops:activities与 §C 的存档约定一致——H5 端sys.localStorage等价于window.localStorage,微信端由引擎适配,三端统一。 _version用于「配置版本随每次加载校验」,这是 17.2 末尾提到的、规避「用户不重启导致新旧配置混用」的手段。
17.3.4 互斥与优先级:同时开多个活动怎么办
运营常常同时开两三个活动,问题立刻来了:双倍金币和登录奖励同时命中,玩家一次拿到两份奖励,数值崩了。解决方式有两层——互斥与优先级。
- 互斥:同一
kind的活动在同一时刻只能有一个生效。上面activeAt里hit[0]取第一个,就是最简单的互斥实现。 - 优先级:不同
kind之间若会叠加,用weight或显式的priority决定谁盖过谁。
把这条写进校验:同 kind 且时间窗重叠的活动,必须由权重分出主次,否则报错。这样运营配错时,是本地校验先报,而不是线上数值爆炸:
// core/activity.ts(续)—— 同 kind 时间窗重叠检测
export function findOverlaps(list: ActivityConfig[]): ActivityIssue[] {
const issues: ActivityIssue[] = [];
for (let i = 0; i < list.length; i += 1) {
for (let j = i + 1; j < list.length; j += 1) {
const a = list[i];
const b = list[j];
if (a.kind !== b.kind) continue;
const overlap = a.startAt < b.endAt && b.startAt < a.endAt;
if (overlap && a.weight === b.weight) {
issues.push({ id: a.id, reason: `与 ${b.id} 重叠且权重相同` });
}
}
}
return issues;
}
这段是 O(n²),但活动数量通常在 10 个以内,完全够用。不要为运营配置做过度优化——真正的问题是配置写错,不是比对慢。
17.3.5 埋点事件模型
看板的数据来自埋点。埋点最容易失控的地方是事件名和参数随手写,三个月后没人看得懂。所以先定一份事件表,代码里只允许用表里的常量:
| 事件名 | 触发时机 | 关键参数 |
|---|---|---|
game_start | 进入首局 | channel、version、device |
level_start | 开始某关 | level_id、activity_id |
level_end | 结算某关 | level_id、result、duration_ms、coins |
activity_enter | 活动生效被读取 | activity_id、kind |
purchase | 内购成功 | sku、amount、currency |
事件名的命名约定:全小写 + 下划线,动词在后。level_end 而不是 endLevel 或 LevelEnd——跨端跨语言时,小写下划线最不容易出错。
参数里的 duration_ms 显式带单位后缀。这一条看着啰嗦,但没有单位的数字是埋点里最大的歧义源——曾经有个项目 duration 一半是毫秒一半是秒,看板上的曲线根本没法读。
17.3.6 看板的四项核心指标
埋点数据上来之后,看板只需要盯四组数,多了就是噪音:
| 指标 | 定义 | 看什么 |
|---|---|---|
| DAU | 当日去重 game_start 用户数 | 大盘趋势 |
| 关卡漏斗 | 第 N 关 level_start / 第 N-1 关 level_end | 卡点在哪一关 |
| 活动参与率 | activity_enter / game_start | 活动有没有被感知到 |
| ARPU | purchase 金额 / DAU | 变现效率 |
其中关卡漏斗是《方块冲刺》最有用的一张图:它直接告诉你玩家卡在第几关,运营据此调 configs/levels.json 里那一关的难度参数,改完热更生效。运营发现卡点 → 改配置 → 热更 → 再看漏斗,这条闭环不需要程序员介入,这正是本节的目标。
活动参与率低通常不是活动本身的问题,而是入口没被看到。如果 activity_enter / game_start 低于 5%,先查活动入口的 UI 层级,而不是怀疑活动设计。
17.3.7 在编辑器里看什么
本节涉及一份配置资源,需 Cocos Creator 3.8 编辑器打开,本节不可命令行复现。
打开 assets/configs/activities.json,属性面板会显示它是一个 JsonAsset,json 字段内容形如:
{
"version": 12,
"activities": [
{
"id": "double_coin_weekend",
"kind": "double_coin",
"startAt": 1790000000000,
"endAt": 1790200000000,
"weight": 100,
"enabled": true
}
]
}
在层级管理器里,常驻节点上的 LiveOps 组件会显示 configUrl 字段,指向 https://cdn.example.com/blockdash/config/activities.json。
这里和 17.1 的 remote.json 有一个共同点:assets/configs/ 下的这份是本地兜底,远端拉不到时用。所以它的 version 必须永远小于等于线上版本,否则用户会被旧活动覆盖新活动。把「本地配置保守」写进团队约定,能省掉一大类线上怪问题。
17.3.8 运营配置上线自检
- 活动
id全局唯一,startAt < endAt,weight在[0, 100] - 已过期活动的
enabled已置false,没有「僵尸活动」 - 同
kind时间窗重叠的活动,权重已分出主次 - 本地
activities.json的version不大于线上版本 - 五类埋点事件名与参数名取自事件表,没有现场命名
- 看板已能按
activity_id分组,活动开没开一测便知
第 6 条是闭环的关键:活动上线后第一件事不是看流水,而是确认埋点能按 activity_id 拆开。拆不开,活动效果就永远是个黑盒。
17.3.9 工程收口
这一节做完,工程里多了这些东西:
assets/scripts/core/activity.ts:活动类型与校验,纯逻辑可单测。assets/scripts/platform/liveops.ts:活动配置下发与本地兜底。assets/configs/activities.json:本地兜底配置。- 埋点事件表:五类事件、固定参数名与单位后缀。
到这里,第 17 章「上线与运营」就闭环了:17.1 控制发布、17.2 修复线上、17.3 支撑运营。三节共用同一套「远端配置 + 本地兜底 + 纯逻辑校验」的骨架——这不是巧合,而是卷二想传达的一个工程直觉:凡是运营会频繁改的东西,都该是数据、都该可校验、都该有兜底。
下一章是收尾。第 18 章把整卷的检查清单、踩过的坑、以及通向高级卷的路线一次性交代清楚。下一节先把发布检查清单补齐——它要能和第 1.1 节定的验收标准逐条对上。
阅读导航:上一节:17.2 热更与线上修复 · 下一节:18.1 发布检查清单 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。