WebGL / Canvas2D 混合渲染优化实战
一、渲染后端选择与自动降级
H5 小游戏运行在极端多样的设备上——从旗舰机到千元机,从微信内置 WebView 到抖音的 X5 内核。引擎必须在启动时完成能力检测 → 后端选择 → 降级预案的三级决策。
1.1 能力检测矩阵
interface RenderCapabilities {
webgl: boolean;
webglVersion: 1 | 2 | 0;
maxTextureSize: number;
maxVertexAttribs: number;
floatTextures: boolean;
instancing: boolean;
vendor: string;
renderer: string;
}
function detectCapabilities(canvas: HTMLCanvasElement): RenderCapabilities {
const caps: RenderCapabilities = {
webgl: false, webglVersion: 0,
maxTextureSize: 0, maxVertexAttribs: 0,
floatTextures: false, instancing: false,
vendor: '', renderer: ''
};
// 优先检测 WebGL2
let gl = canvas.getContext('webgl2', { alpha: false, antialias: false }) as WebGL2RenderingContext | null;
if (gl) {
caps.webgl = true;
caps.webglVersion = 2;
} else {
gl = canvas.getContext('webgl', { alpha: false, antialias: false }) as WebGLRenderingContext | null;
if (gl) {
caps.webgl = true;
caps.webglVersion = 1;
}
}
if (gl) {
caps.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
caps.maxVertexAttribs = gl.getParameter(gl.MAX_VERTEX_ATTRIBS);
caps.vendor = gl.getParameter(gl.VENDOR) || '';
caps.renderer = gl.getParameter(gl.RENDERER) || '';
if (caps.webglVersion === 2) {
const gl2 = gl as WebGL2RenderingContext;
caps.floatTextures = !!gl2.getExtension('EXT_color_buffer_float');
}
const instExt = gl.getExtension('ANGLE_instanced_arrays');
caps.instancing = !!instExt || caps.webglVersion === 2;
}
return caps;
}
1.2 降级决策树
function selectBackend(canvas: HTMLCanvasElement): RenderBackend {
const caps = detectCapabilities(canvas);
// 决策树
if (!caps.webgl) {
console.warn('[Renderer] WebGL not available, fallback to Canvas2D');
return new Canvas2DBackend(canvas);
}
if (caps.webglVersion === 1) {
// Mali-400 / Adreno 200 等老 GPU 的 WebGL1 性能极差
const isBadGPU = /Mali-4|Adreno 2|PowerVR SGX/i.test(caps.renderer);
if (isBadGPU) {
console.warn('[Renderer] Low-end GPU detected, using Canvas2D');
return new Canvas2DBackend(canvas);
}
}
if (caps.maxTextureSize < 2048) {
// 纹理尺寸受限,图集策略会失效,降级 Canvas2D 更可控
return new Canvas2DBackend(canvas);
}
return new WebGLBackend(canvas, caps);
}
1.3 抽象后端接口
interface TextureHandle {
id: string;
width: number; height: number;
}
interface RenderBackend {
readonly name: 'webgl' | 'canvas2d';
resize(width: number, height: number): void;
// 纹理管理
createTexture(image: HTMLImageElement | HTMLCanvasElement): TextureHandle;
updateTexture(handle: TextureHandle, image: HTMLImageElement): void;
destroyTexture(handle: TextureHandle): void;
// 绘制调用
beginFrame(): void;
drawSprites(sprites: DrawSpriteCmd[]): void;
drawPrimitives(cmds: DrawPrimitiveCmd[]): void;
endFrame(): void;
// 离屏渲染
createRenderTarget(width: number, height: number): RenderTarget;
setRenderTarget(target: RenderTarget | null): void;
// 状态
setBlendMode(mode: BlendMode): void;
setViewport(x: number, y: number, w: number, h: number): void;
}
type BlendMode = 'normal' | 'add' | 'multiply' | 'screen';
interface DrawSpriteCmd {
texture: TextureHandle;
srcX: number; srcY: number; srcW: number; srcH: number;
dstX: number; dstY: number; dstW: number; dstH: number;
rotation: number;
tintR: number; tintG: number; tintB: number; tintA: number;
}
二、WebGL 后端:Shader 编译缓存与批量渲染
2.1 Shader 预编译与缓存
Shader 编译是 WebGL 中最常见的卡顿源。我们的策略是启动时预编译 + 运行时哈希复用。
class ShaderCache {
private programs = new Map<string, WebGLProgram>();
private gl: WebGLRenderingContext;
constructor(gl: WebGLRenderingContext) {
this.gl = gl;
}
private hashShader(vs: string, fs: string): string {
// 简单 FNV-1a hash
let h = 0x811c9dc5;
const str = vs + '::' + fs;
for (let i = 0; i < str.length; i++) {
h ^= str.charCodeAt(i);
h += (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24);
}
return (h >>> 0).toString(16);
}
compile(vsSource: string, fsSource: string): WebGLProgram {
const key = this.hashShader(vsSource, fsSource);
if (this.programs.has(key)) return this.programs.get(key)!;
const gl = this.gl;
const vs = gl.createShader(gl.VERTEX_SHADER)!;
gl.shaderSource(vs, vsSource);
gl.compileShader(vs);
const fs = gl.createShader(gl.FRAGMENT_SHADER)!;
gl.shaderSource(fs, fsSource);
gl.compileShader(fs);
const program = gl.createProgram()!;
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error('[Shader] Link failed:', gl.getProgramInfoLog(program));
}
this.programs.set(key, program);
return program;
}
// 预编译一组已知 shader
preload(shaders: Array<{ vs: string; fs: string }>): void {
for (const { vs, fs } of shaders) {
this.compile(vs, fs);
}
}
}
常用 Shader 模板预加载清单:
const DEFAULT_SHADERS = [
{ vs: SPRITE_VS, fs: SPRITE_FS }, // 基础精灵着色器
{ vs: SPRITE_VS, fs: SPRITE_TINT_FS }, // 带染色
{ vs: PRIMITIVE_VS, fs: PRIMITIVE_FS }, // 几何图元
{ vs: PARTICLE_VS, fs: PARTICLE_FS }, // 粒子系统
{ vs: POST_VS, fs: BLUR_FS }, // 高斯模糊后处理
];
2.2 Sprite Batch Renderer:单 Draw Call 渲染海量精灵
核心思想:收集一帧内所有使用同一纹理的精灵,写入单个 VBO,一次 glDrawArrays 绘制。
interface SpriteVertex {
x: number; y: number; // 位置
u: number; v: number; // UV
tint: number; // ABGR packed
}
class SpriteBatch {
private gl: WebGLRenderingContext;
private program: WebGLProgram;
private buf: WebGLBuffer;
private vertices: Float32Array;
private vertexCount = 0;
private MAX_SPRITES = 2048;
private VERTICES_PER_SPRITE = 6; // 2 triangles = 6 vertices
private FLOATS_PER_VERTEX = 5; // x, y, u, v, tint
constructor(gl: WebGLRenderingContext, shaderCache: ShaderCache) {
this.gl = gl;
this.program = shaderCache.compile(SPRITE_VS, SPRITE_TINT_FS);
this.buf = gl.createBuffer()!;
this.vertices = new Float32Array(
this.MAX_SPRITES * this.VERTICES_PER_SPRITE * this.FLOATS_PER_VERTEX
);
// 初始化 VAO / attribute
gl.bindBuffer(gl.ARRAY_BUFFER, this.buf);
const FPV = this.FLOATS_PER_VERTEX * 4;
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, FPV, 0); // pos
gl.enableVertexAttribArray(1);
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, FPV, 8); // uv
gl.enableVertexAttribArray(2);
gl.vertexAttribPointer(2, 4, gl.UNSIGNED_BYTE, true, FPV, 16); // tint normalized
}
addSprite(x: number, y: number, w: number, h: number,
rot: number,
u0: number, v0: number, u1: number, v1: number,
tint: number): void {
if (this.vertexCount >= this.MAX_SPRITES * this.VERTICES_PER_SPRITE) {
this.flush();
}
const cos = Math.cos(rot), sin = Math.sin(rot);
const dx = [-w/2, w/2, w/2, -w/2];
const dy = [-h/2, -h/2, h/2, h/2];
const du = [u0, u1, u1, u0];
const dv = [v0, v0, v1, v1];
const verts: Array<{x:number,y:number,u:number,v:number}> = [];
for (let i = 0; i < 4; i++) {
const rx = dx[i] * cos - dy[i] * sin;
const ry = dx[i] * sin + dy[i] * cos;
verts.push({ x: x + rx, y: y + ry, u: du[i], v: dv[i] });
}
// Triangles: 0-1-2, 0-2-3
const indices = [0, 1, 2, 0, 2, 3];
const base = this.vertexCount * this.FLOATS_PER_VERTEX;
for (let i = 0; i < 6; i++) {
const v = verts[indices[i]];
const off = base + i * this.FLOATS_PER_VERTEX;
this.vertices[off] = v.x;
this.vertices[off + 1] = v.y;
this.vertices[off + 2] = v.u;
this.vertices[off + 3] = v.v;
// tint packed as float32 reinterpret
const tintView = new Uint8Array(this.vertices.buffer, (off + 4) * 4, 4);
tintView[0] = (tint >> 16) & 0xFF; // R
tintView[1] = (tint >> 8) & 0xFF; // G
tintView[2] = tint & 0xFF; // B
tintView[3] = (tint >> 24) & 0xFF; // A
}
this.vertexCount += 6;
}
flush(): void {
if (this.vertexCount === 0) return;
const gl = this.gl;
gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.vertices.subarray(0, this.vertexCount * this.FLOATS_PER_VERTEX));
gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount);
this.vertexCount = 0;
}
}
Vertex Shader:
attribute vec2 a_pos;
attribute vec2 a_uv;
attribute vec4 a_tint;
varying vec2 v_uv;
varying vec4 v_tint;
void main() {
gl_Position = vec4(a_pos, 0.0, 1.0);
v_uv = a_uv;
v_tint = a_tint;
}
Fragment Shader:
precision mediump float;
varying vec2 v_uv;
varying vec4 v_tint;
uniform sampler2D u_texture;
void main() {
vec4 tex = texture2D(u_texture, v_uv);
gl_FragColor = tex * v_tint;
}
2.3 按纹理分组批量绘制
class WebGLBackend implements RenderBackend {
readonly name = 'webgl' as const;
private gl: WebGLRenderingContext;
private shaderCache: ShaderCache;
private spriteBatch: SpriteBatch;
private currentTexture: WebGLTexture | null = null;
drawSprites(sprites: DrawSpriteCmd[]): void {
// 按 texture ID 分组
const batches = new Map<string, DrawSpriteCmd[]>();
for (const s of sprites) {
if (!batches.has(s.texture.id)) batches.set(s.texture.id, []);
batches.get(s.texture.id)!.push(s);
}
for (const [, group] of batches) {
// 切换纹理
this.bindTexture(group[0].texture);
for (const s of group) {
this.spriteBatch.addSprite(
s.dstX, s.dstY, s.dstW, s.dstH, s.rotation,
s.srcX / s.texture.width, s.srcY / s.texture.height,
(s.srcX + s.srcW) / s.texture.width, (s.srcY + s.srcH) / s.texture.height,
(s.tintA << 24) | (s.tintR << 16) | (s.tintG << 8) | s.tintB
);
}
this.spriteBatch.flush();
}
}
}
三、纹理图集(Texture Atlas)动态打包
3.1 基于 Guillotine 算法的动态图集
interface AtlasNode {
x: number; y: number; w: number; h: number;
used: boolean;
left?: AtlasNode;
right?: AtlasNode;
image?: HTMLImageElement;
id?: string;
}
class TextureAtlas {
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
private root: AtlasNode;
private entries = new Map<string, { x: number; y: number; w: number; h: number }>();
private size: number;
constructor(size: number = 2048) {
this.size = size;
this.canvas = document.createElement('canvas');
this.canvas.width = size;
this.canvas.height = size;
this.ctx = this.canvas.getContext('2d')!;
this.root = { x: 0, y: 0, w: size, h: size, used: false };
}
private findNode(node: AtlasNode, w: number, h: number): AtlasNode | null {
if (node.used) {
return this.findNode(node.left!, w, h) || this.findNode(node.right!, w, h);
}
if (w <= node.w && h <= node.h) {
// Split: guillotine cut
node.used = true;
node.left = {
x: node.x + w, y: node.y,
w: node.w - w, h: h,
used: false
};
node.right = {
x: node.x, y: node.y + h,
w: node.w, h: node.h - h,
used: false
};
return node;
}
return null;
}
addImage(id: string, img: HTMLImageElement): boolean {
const node = this.findNode(this.root, img.width, img.height);
if (!node) return false; // 图集已满
node.image = img;
node.id = id;
this.ctx.drawImage(img, node.x, node.y);
this.entries.set(id, { x: node.x, y: node.y, w: img.width, h: img.height });
return true;
}
getUV(id: string): { u0: number; v0: number; u1: number; v1: number } | null {
const e = this.entries.get(id);
if (!e) return null;
return {
u0: e.x / this.size, v0: e.y / this.size,
u1: (e.x + e.w) / this.size, v1: (e.y + e.h) / this.size,
};
}
getCanvas(): HTMLCanvasElement {
return this.canvas;
}
// 当利用率低于阈值时,重新打包(defragment)
get utilization(): number {
let usedArea = 0;
for (const e of this.entries.values()) {
usedArea += e.w * e.h;
}
return usedArea / (this.size * this.size);
}
}
3.2 多图集管理
class AtlasManager {
private atlases: TextureAtlas[] = [];
private maxAtlasSize = 2048;
addImage(id: string, img: HTMLImageElement): { atlasIndex: number; uv: { u0: number; v0: number; u1: number; v1: number } } | null {
// 尝试放入现有图集
for (let i = 0; i < this.atlases.length; i++) {
if (this.atlases[i].addImage(id, img)) {
return { atlasIndex: i, uv: this.atlases[i].getUV(id)! };
}
}
// 新建图集
const atlas = new TextureAtlas(this.maxAtlasSize);
if (!atlas.addImage(id, img)) {
console.error(`[Atlas] Image ${id} (${img.width}x${img.height}) exceeds atlas size`);
return null;
}
this.atlases.push(atlas);
return { atlasIndex: this.atlases.length - 1, uv: atlas.getUV(id)! };
}
}
四、离屏渲染(Offscreen Rendering / RenderTexture)
4.1 RenderTarget 实现
interface RenderTarget {
texture: WebGLTexture;
framebuffer: WebGLFramebuffer;
width: number;
height: number;
}
class WebGLRenderTargetManager {
private gl: WebGLRenderingContext;
private targets: RenderTarget[] = [];
constructor(gl: WebGLRenderingContext) {
this.gl = gl;
}
create(width: number, height: number): RenderTarget {
const gl = this.gl;
const texture = gl.createTexture()!;
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
const fb = gl.createFramebuffer()!;
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
const target = { texture, framebuffer: fb, width, height };
this.targets.push(target);
return target;
}
destroy(target: RenderTarget): void {
const gl = this.gl;
gl.deleteFramebuffer(target.framebuffer);
gl.deleteTexture(target.texture);
const idx = this.targets.indexOf(target);
if (idx >= 0) this.targets.splice(idx, 1);
}
}
4.2 使用场景:缓存静态背景层
// 游戏初始化时,将复杂 TileMap 预渲染到 RenderTexture
function preRenderBackground(tilemap: TileMap, renderer: WebGLBackend): TextureHandle {
const rt = renderer.createRenderTarget(tilemap.width, tilemap.height);
renderer.setRenderTarget(rt);
renderer.clear(0xFF000000);
// 绘制数百个 tile(此时绘制代价只付一次)
for (const tile of tilemap.tiles) {
renderer.drawSprites([{
texture: tile.texture,
srcX: tile.srcX, srcY: tile.srcY, srcW: tile.srcW, srcH: tile.srcH,
dstX: tile.x, dstY: tile.y, dstW: tile.w, dstH: tile.h,
rotation: 0, tintR: 255, tintG: 255, tintB: 255, tintA: 255
}]);
}
renderer.setRenderTarget(null); // 回到屏幕
return { id: `rt_${rt.texture}`, width: rt.width, height: rt.height };
}
// 此后每帧只需绘制一个全屏 quad
// 对比:从 200+ draw calls 减少到 1 个
五、帧率自适应与动态降级
5.1 FrameTime 监测与降级策略
interface AdaptiveConfig {
targetFPS: number;
downgradeThreshold: number; // 连续低于阈值的帧数
particleReductionFactor: number;
resolutionScaleSteps: number[]; // [1.0, 0.75, 0.5]
}
class AdaptiveFrameRate {
private frameTimes: number[] = [];
private readonly HISTORY_SIZE = 30;
private consecutiveSlowFrames = 0;
private config: AdaptiveConfig;
private currentResolutionScale = 1.0;
private downgradeLevel = 0;
constructor(config: AdaptiveConfig = {
targetFPS: 60,
downgradeThreshold: 10,
particleReductionFactor: 0.5,
resolutionScaleSteps: [1.0, 0.75, 0.5]
}) {
this.config = config;
}
recordFrame(deltaMs: number): void {
this.frameTimes.push(deltaMs);
if (this.frameTimes.length > this.HISTORY_SIZE) {
this.frameTimes.shift();
}
const targetMs = 1000 / this.config.targetFPS;
if (deltaMs > targetMs * 1.3) { // 低于目标帧率 30%
this.consecutiveSlowFrames++;
} else {
this.consecutiveSlowFrames = Math.max(0, this.consecutiveSlowFrames - 2);
}
if (this.consecutiveSlowFrames >= this.config.downgradeThreshold) {
this.downgrade();
this.consecutiveSlowFrames = 0;
}
}
private downgrade(): void {
if (this.downgradeLevel < this.config.resolutionScaleSteps.length - 1) {
this.downgradeLevel++;
this.currentResolutionScale = this.config.resolutionScaleSteps[this.downgradeLevel];
console.warn(`[Adaptive] Downgrade to resolution scale ${this.currentResolutionScale}`);
} else {
console.warn('[Adaptive] Max downgrade reached, consider Canvas2D fallback');
}
}
get resolutionScale(): number {
return this.currentResolutionScale;
}
get averageFPS(): number {
if (this.frameTimes.length === 0) return 60;
const avg = this.frameTimes.reduce((a, b) => a + b, 0) / this.frameTimes.length;
return 1000 / avg;
}
}
5.2 降级决策矩阵
function shouldFallbackToCanvas2D(
caps: RenderCapabilities,
adaptive: AdaptiveFrameRate,
spriteCount: number
): boolean {
// 条件1: WebGL 性能持续不达标
if (adaptive.averageFPS < 30 && adaptive.resolutionScale <= 0.5) return true;
// 条件2: 极低端 GPU
if (/Mali-400|Adreno 2|PowerVR SGX54/i.test(caps.renderer)) return true;
// 条件3: 精灵数量极少时 Canvas2D 反而更快(无 shader overhead)
if (spriteCount < 20 && caps.webglVersion === 1) return true;
return false;
}
六、性能基准数据
在 Redmi Note 9(Snapdragon 720G,6GB RAM)的微信 WebView 中测试:
| 场景 | 无优化 WebGL | 批量渲染 | + 图集 | + 离屏缓存 | Canvas2D 对比 |
|---|---|---|---|---|---|
| 100 精灵 | 16 ms | 3.2 ms | 2.8 ms | — | 4.5 ms |
| 500 精灵 | 58 ms (17FPS) | 7.1 ms | 5.5 ms | — | 22 ms |
| 1000 精灵 | 110 ms (9FPS) | 12 ms | 9.2 ms | — | 48 ms |
| TileMap 200 tiles | 25 ms | 25 ms | 10 ms | 1.8 ms | 15 ms |
| 粒子 2000 | 45 ms | 8 ms | — | — | 不支持批量 |
关键结论:
- 批量渲染是最核心的优化,可将 draw call 从 N 降到 1
- 图集在精灵数量 > 200 时优势显著,但构建成本需要在 Loading 阶段消化
- 离屏缓存对静态场景(TileMap、UI)效果极佳,一次绘制永久复用
- Canvas2D 在 < 50 精灵的简单场景下启动更快,但无法处理大量精灵和 Shader 特效
七、渲染管线架构全景
graph TD
subgraph "Application Layer"
A[Scene Graph System] --> B[Sprite Render System]
A --> C[Particle System]
A --> D[UI System]
end
subgraph "Render Command Queue"
B --> E{RenderCommandQueue}
C --> E
D --> E
end
subgraph "Backend Selection"
E --> F{Capabilities Check}
F -->|WebGL2| G[WebGL2 Backend]
F -->|WebGL1| H[WebGL1 Backend]
F -->|Fallback| I[Canvas2D Backend]
end
subgraph "WebGL Pipeline"
G --> J[Shader Cache]
G --> K[Sprite Batch]
G --> L[Atlas Manager]
G --> M[Render Target Manager]
J --> N[GPU]
K --> N
L --> N
M --> N
end
subgraph "Adaptive Control"
O[FrameTime Monitor] --> P{FPS < 50?}
P -->|Yes| Q[Reduce Particles]
P -->|Yes| R[Lower Resolution]
P -->|Yes| S[Disable PostFX]
P -->|No| T[Maintain Current]
end
style G fill:#e8f5e9
style H fill:#fff3e0
style I fill:#ffebee
八、总结与检查清单
在上线前,使用以下检查清单确保渲染管线已优化到位:
- 后端检测:启动时完成 WebGL/Canvas2D 能力检测并选择合适的后端
- Shader 预编译:所有已知 shader 在 Loading 阶段完成编译并缓存
- 批量渲染:单帧内同一纹理的精灵合并为一次 draw call
- 纹理图集:运行时动态打包图片到图集,减少 texture bind 次数
- 离屏缓存:静态场景(TileMap、复杂 UI)预渲染到 RenderTexture
- 帧率监测:实时监测 frameTime,连续低于阈值时自动降级
- 分辨率缩放:在低端机上降低
canvas.width/height的物理像素数 - 内存监控:纹理总量不超过设备
MAX_TEXTURE_SIZE的 80%
延伸阅读
- 小游戏引擎 ECS 架构深度解析 — 本文渲染系统的上层 ECS 架构基础
- 小游戏包体优化与加载策略完全指南 — 纹理压缩、WASM 分包与首帧 <2s 实战
- 小游戏多平台统一适配层设计 — 各平台 WebGL context 差异与兼容方案
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。
「games」更多文章
小游戏开发者增长与获客体系:裂变邀请、Game Jam 与开发者社区运营
系统讲解小游戏平台的开发者增长与获客策略。涵盖裂变邀请机制(模板 Remix 传播设计)、Game Jam 赛事运营全攻略、教育渠道 BD(高校实验室合作)、SEO/ASO 策略、开发者社区运营(Discord/Discourse)、增长漏斗模型与 Cohort 留存分析。提供可落地的运营 SOP 与数据指标追踪体系。
小游戏商业化全栈设计:广告聚合、IAP 道具经济与 LTV 预测模型
系统讲解小游戏平台的商业化全栈架构。涵盖广告聚合层设计(Waterfall + Header Bidding 混合出价)、IAP 道具经济体系(消耗品/订阅/战令)、LTV/CAC 预测模型、归因分析(Adjust/AppsFlyer 对接)、防作弊检测与 eCPM 优化策略。提供 AdMediator、IAPManager 的完整 TypeScript 实现与财务模型数据表。
小游戏创作者经济生态设计:插件商店、收益分润与创作者成长体系
深入讲解小游戏平台创作者经济生态的架构设计。涵盖插件商店技术架构(版本管理、签名验证、依赖解析)、收益分润算法(阶梯抽成与动态分成)、创作者等级体系(GMV/质量/活跃三维评估)、UGC 内容审核流水线。提供 MarketplaceEngine 核心 TypeScript 实现与智能推荐算法。