小游戏多平台统一适配层设计
一、中国小游戏平台生态全景
截至 2026 年,中国小游戏市场已形成**“两超多强”**格局:
| 平台 | DAU 估算 | 主要变现 | 技术特征 | 审核周期 |
|---|---|---|---|---|
| 微信小游戏 | 3.5 亿+ | 广告 + IAP | 完整 WebGL2 支持、开放域、云开发 | 1–3 天 |
| 抖音小游戏 | 1.2 亿+ | 广告 + 电商 | 字节系流量互通、直播挂载 | 即时上线(一审制) |
| 快手小游戏 | 0.4 亿+ | 广告 | 老铁社区、私域流量 | 1–2 天 |
| QQ 小游戏 | 0.3 亿+ | 广告 | 年轻化用户、厘米秀联动 | 3–5 天 |
| 百度小游戏 | 0.1 亿+ | 广告 | 搜索流量入口 | 1–3 天 |
| 4399 / OPPO / vivo | 0.2 亿+ | 广告 | 快应用框架、渠道联运 | 渠道各异 |
关键洞察:不同平台的 SDK 设计哲学差异巨大——微信追求"能力丰富但限制严格",抖音追求"快速迭代、流量打通",百度追求"搜索即分发"。适配层必须足够灵活才能覆盖这些差异。
二、适配层核心架构:工厂模式 + 策略模式
2.1 架构总览
┌─────────────────────────────────────────┐
│ 上层业务代码 │
│ showRewardAd() share() pay() │
└──────────────┬──────────────────────────┘
│
┌──────────────▼──────────────────────────┐
│ PlatformAdapter (统一接口) │
│ abstract showRewardAd(): Promise<AdResult>
│ abstract share(options: ShareOptions): Promise<void>
│ abstract pay(order: Order): Promise<Receipt>
└──────────────┬──────────────────────────┘
│
┌──────────┼──────────┬──────────┐
▼ ▼ ▼ ▼
┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐
│ WeChat│ │TikTok │ │Kuaishou│ │ QQ │
│Adapter│ │Adapter│ │Adapter│ │Adapter│
└───────┘ └───────┘ └───────┘ └───────┘
2.2 平台检测与工厂
type PlatformName = 'wechat' | 'tiktok' | 'kuaishou' | 'qq' | 'baidu' | 'web';
interface PlatformProfile {
name: PlatformName;
version: string;
capabilities: PlatformCapabilities;
screen: ScreenInfo;
isDebug: boolean;
}
interface PlatformCapabilities {
rewardAd: boolean;
interstitialAd: boolean;
bannerAd: boolean;
fullscreenAd: boolean;
iap: boolean;
share: boolean;
login: boolean;
openDataContext: boolean;
cloudFunctions: boolean;
fileSystem: boolean;
vibrate: boolean;
accelerometer: boolean;
}
interface ScreenInfo {
width: number;
height: number;
pixelRatio: number;
safeArea: { top: number; bottom: number; left: number; right: number };
}
class PlatformDetector {
static detect(): PlatformName {
// 检测顺序很重要:微信和抖音都可能在某些环境下同时存在
if (typeof wx !== 'undefined' && wx.getSystemInfoSync) return 'wechat';
if (typeof tt !== 'undefined' && tt.getSystemInfoSync) return 'tiktok';
if (typeof ks !== 'undefined' && ks.getSystemInfoSync) return 'kuaishou';
if (typeof qq !== 'undefined' && qq.getSystemInfoSync) return 'qq';
if (typeof swan !== 'undefined' && swan.getSystemInfoSync) return 'baidu';
return 'web';
}
static getProfile(): PlatformProfile {
const name = this.detect();
const sysInfo = this.getSystemInfo(name);
return {
name,
version: sysInfo.version || 'unknown',
capabilities: this.detectCapabilities(name),
screen: {
width: sysInfo.windowWidth || sysInfo.screenWidth,
height: sysInfo.windowHeight || sysInfo.screenHeight,
pixelRatio: sysInfo.pixelRatio || 1,
safeArea: sysInfo.safeArea || { top: 0, bottom: sysInfo.screenHeight, left: 0, right: sysInfo.screenWidth },
},
isDebug: sysInfo.enableDebug || false,
};
}
private static getSystemInfo(name: PlatformName): any {
try {
switch (name) {
case 'wechat': return wx.getSystemInfoSync();
case 'tiktok': return tt.getSystemInfoSync();
case 'kuaishou': return ks.getSystemInfoSync();
case 'qq': return qq.getSystemInfoSync();
case 'baidu': return swan.getSystemInfoSync();
default: return {};
}
} catch {
return {};
}
}
private static detectCapabilities(name: PlatformName): PlatformCapabilities {
const caps: PlatformCapabilities = {
rewardAd: false, interstitialAd: false, bannerAd: false,
fullscreenAd: false, iap: false, share: false,
login: false, openDataContext: false, cloudFunctions: false,
fileSystem: false, vibrate: false, accelerometer: false,
};
switch (name) {
case 'wechat':
caps.rewardAd = !!wx.createRewardedVideoAd;
caps.interstitialAd = !!wx.createInterstitialAd;
caps.bannerAd = !!wx.createBannerAd;
caps.fullscreenAd = !!wx.createFullScreenAd;
caps.iap = !!wx.requestMidasPayment;
caps.share = !!wx.shareAppMessage;
caps.login = !!wx.login;
caps.openDataContext = !!wx.getOpenDataContext;
caps.cloudFunctions = !!wx.cloud;
caps.fileSystem = !!wx.getFileSystemManager;
caps.vibrate = !!wx.vibrateShort;
caps.accelerometer = !!wx.startAccelerometer;
break;
case 'tiktok':
caps.rewardAd = !!tt.createRewardedVideoAd;
caps.interstitialAd = !!tt.createInterstitialAd;
caps.bannerAd = !!tt.createBannerAd;
caps.fullscreenAd = !!tt.createFullScreenAd;
caps.iap = !!tt.requestGamePayment;
caps.share = !!tt.shareAppMessage;
caps.login = !!tt.login;
caps.openDataContext = !!tt.getOpenDataContext;
caps.fileSystem = !!tt.getFileSystemManager;
caps.vibrate = !!tt.vibrateShort;
caps.accelerometer = !!tt.startAccelerometer;
break;
// ... 其他平台类似
}
return caps;
}
}
// 工厂
class PlatformAdapterFactory {
static create(): PlatformAdapter {
const name = PlatformDetector.detect();
switch (name) {
case 'wechat': return new WeChatAdapter();
case 'tiktok': return new TikTokAdapter();
case 'kuaishou': return new KuaishouAdapter();
case 'qq': return new QQAdapter();
case 'baidu': return new BaiduAdapter();
default: return new WebAdapter();
}
}
}
三、统一广告接口
广告是小游戏最核心的变现方式,但各平台的广告 API 设计差异极大。
3.1 标准化广告接口
interface AdConfig {
adUnitId: string;
placement?: string; // 广告位场景描述
}
interface AdResult {
success: boolean;
isCompleted?: boolean; // 激励视频是否看完
error?: { code: number; message: string };
rewarded?: boolean; // 是否获得奖励
}
interface BannerConfig extends AdConfig {
style: {
left: number; top: number;
width: number; height?: number;
};
}
abstract class PlatformAdapter {
abstract readonly profile: PlatformProfile;
// 激励视频
abstract preloadRewardAd(config: AdConfig): Promise<void>;
abstract showRewardAd(config: AdConfig): Promise<AdResult>;
// 插屏
abstract preloadInterstitialAd(config: AdConfig): Promise<void>;
abstract showInterstitialAd(config: AdConfig): Promise<AdResult>;
// Banner
abstract createBannerAd(config: BannerConfig): BannerAdHandle;
abstract destroyBannerAd(handle: BannerAdHandle): void;
// 全屏视频
abstract showFullscreenAd?(config: AdConfig): Promise<AdResult>;
}
interface BannerAdHandle {
show(): Promise<void>;
hide(): Promise<void>;
destroy(): void;
resize(style: { width: number }): void;
}
3.2 微信适配器实现
class WeChatAdapter extends PlatformAdapter {
readonly profile: PlatformProfile;
private rewardAdCache = new Map<string, WechatMiniprogram.RewardedVideoAd>();
constructor() {
super();
this.profile = PlatformDetector.getProfile();
}
async preloadRewardAd(config: AdConfig): Promise<void> {
if (!this.profile.capabilities.rewardAd) throw new Error('RewardAd not supported');
const ad = wx.createRewardedVideoAd({ adUnitId: config.adUnitId });
this.rewardAdCache.set(config.adUnitId, ad);
return new Promise((resolve, reject) => {
ad.load().then(resolve).catch(reject);
});
}
async showRewardAd(config: AdConfig): Promise<AdResult> {
let ad = this.rewardAdCache.get(config.adUnitId);
if (!ad) {
ad = wx.createRewardedVideoAd({ adUnitId: config.adUnitId });
this.rewardAdCache.set(config.adUnitId, ad);
}
return new Promise((resolve) => {
const onClose = (res: any) => {
ad!.offClose(onClose);
resolve({
success: true,
isCompleted: res && res.isEnded,
rewarded: res && res.isEnded,
});
};
const onError = (err: any) => {
ad!.offError(onError);
resolve({
success: false,
error: { code: err.errCode || -1, message: err.errMsg || 'unknown' },
});
};
ad.onClose(onClose);
ad.onError(onError);
ad.show().catch(onError);
});
}
createBannerAd(config: BannerConfig): BannerAdHandle {
const ad = wx.createBannerAd({
adUnitId: config.adUnitId,
style: config.style,
});
return {
show: () => ad.show(),
hide: () => ad.hide(),
destroy: () => ad.destroy(),
resize: (s) => ad.style.width = s.width,
};
}
// 插屏类似,省略...
}
3.3 抖音适配器实现(关键差异点)
class TikTokAdapter extends PlatformAdapter {
readonly profile: PlatformProfile;
private rewardAdCache = new Map<string, any>();
constructor() {
super();
this.profile = PlatformDetector.getProfile();
}
async showRewardAd(config: AdConfig): Promise<AdResult> {
// 抖音的 createRewardedVideoAd 签名不同:需要 gameId + extra
const ad = tt.createRewardedVideoAd({
adUnitId: config.adUnitId,
// 抖音特有参数
multiton: true, // 支持多实例
});
this.rewardAdCache.set(config.adUnitId, ad);
return new Promise((resolve) => {
// 抖音 onClose 回调直接传参,不是事件对象
ad.show().then(() => {
ad.onClose((isEnded: boolean) => {
resolve({
success: true,
isCompleted: isEnded,
rewarded: isEnded,
});
});
}).catch((err: any) => {
resolve({
success: false,
error: { code: err.code || -1, message: err.message || 'unknown' },
});
});
});
}
// Banner 抖音支持更灵活的 style 表达式
createBannerAd(config: BannerConfig): BannerAdHandle {
const ad = tt.createBannerAd({
adUnitId: config.adUnitId,
style: {
...config.style,
// 抖音支持居中对齐快捷写法
center: config.style.left === undefined,
},
});
return {
show: () => ad.show(),
hide: () => ad.hide(),
destroy: () => ad.destroy(),
resize: (s) => {
ad.style.width = s.width;
// 抖音会自适应高度,不需要手动设置
},
};
}
}
3.4 广告聚合层(Waterfall Mediation)
class AdMediator {
constructor(
private adapters: PlatformAdapter[],
private config: { fallbackOrder: PlatformName[] }
) {}
async showRewardAd(request: AdRequest): Promise<AdResult> {
for (const platformName of this.config.fallbackOrder) {
const adapter = this.adapters.find(a => a.profile.name === platformName);
if (!adapter || !adapter.profile.capabilities.rewardAd) continue;
try {
const result = await adapter.showRewardAd({
adUnitId: request.adUnitIds[platformName]!,
});
if (result.success) return result;
} catch (e) {
console.warn(`[AdMediator] ${platformName} failed, trying next...`);
}
}
return { success: false, error: { code: -999, message: 'All platforms failed' } };
}
}
四、统一支付接口
4.1 标准化支付模型
interface IAPProduct {
productId: string;
title: string;
price: number; // 人民币分
currency: 'CNY' | 'USD';
type: 'consumable' | 'non-consumable' | 'subscription';
}
interface IAPOrder {
orderId: string;
productId: string;
quantity: number;
extra?: Record<string, any>;
}
interface IAPReceipt {
success: boolean;
transactionId?: string;
orderId: string;
timestamp: number;
platformReceipt?: any; // 平台原始凭证
error?: { code: number; message: string };
}
abstract class PlatformAdapter {
// ... 广告接口省略
abstract getProducts(): Promise<IAPProduct[]>;
abstract purchase(order: IAPOrder): Promise<IAPReceipt>;
abstract consume?(receipt: IAPReceipt): Promise<boolean>; // 消耗型道具需要确认消耗
}
4.2 微信支付的米大师接入
class WeChatAdapter extends PlatformAdapter {
async purchase(order: IAPOrder): Promise<IAPReceipt> {
return new Promise((resolve) => {
wx.requestMidasPayment({
mode: 'game',
env: 0, // 0=正式 1=沙箱
offerId: 'your_offer_id',
currencyType: 'CNY',
platform: 'android', // 或 ios
buyQuantity: order.quantity,
zoneId: '1',
success: (res: any) => {
resolve({
success: true,
transactionId: res.transactionId,
orderId: order.orderId,
timestamp: Date.now(),
platformReceipt: res,
});
},
fail: (err: any) => {
resolve({
success: false,
orderId: order.orderId,
timestamp: Date.now(),
error: { code: err.errCode, message: err.errMsg },
});
},
});
});
}
}
4.3 抖音虚拟支付接入
class TikTokAdapter extends PlatformAdapter {
async purchase(order: IAPOrder): Promise<IAPReceipt> {
return new Promise((resolve) => {
tt.requestGamePayment({
offerId: 'your_offer_id',
buyQuantity: order.quantity,
zoneId: '1',
success: (res: any) => {
resolve({
success: true,
transactionId: res.transactionId,
orderId: order.orderId,
timestamp: Date.now(),
platformReceipt: res,
});
},
fail: (err: any) => {
resolve({
success: false,
orderId: order.orderId,
timestamp: Date.now(),
error: { code: err.code, message: err.message },
});
},
});
});
}
}
五、开放数据域(子域)渲染架构
5.1 子域架构原理
┌─────────────────────────────────────────┐
│ 主域 (Main Context) │
│ ┌─────────┐ ┌──────────────────┐ │
│ │ Game │ │ sharedCanvas │ │
│ │ Render │◄─────│ (from 子域) │ │
│ └─────────┘ └──────────────────┘ │
└─────────────────────────────────────────┘
↑
┌─────────────────────────────────────────┐
│ 子域 (Open Context) │
│ ┌─────────┐ ┌──────────────────┐ │
│ │ Rank │ │ OpenDataCanvas │ │
│ │ Render │─────►│ (绘制好友排行) │ │
│ └─────────┘ └──────────────────┘ │
└─────────────────────────────────────────┘
5.2 子域渲染器实现
// 主域:子域启动与 sharedCanvas 获取
class OpenDataManager {
private sharedCanvas: HTMLCanvasElement | null = null;
private context: any = null;
init(): void {
const profile = PlatformDetector.getProfile();
if (!profile.capabilities.openDataContext) return;
switch (profile.name) {
case 'wechat':
this.context = wx.getOpenDataContext();
this.sharedCanvas = this.context.canvas;
break;
case 'tiktok':
this.context = tt.getOpenDataContext();
this.sharedCanvas = this.context.canvas;
break;
default:
// Web 环境:模拟子域
this.sharedCanvas = document.createElement('canvas');
}
}
postMessage(message: { action: string; data?: any }): void {
if (this.context) {
this.context.postMessage(message);
}
}
getSharedCanvas(): HTMLCanvasElement | null {
return this.sharedCanvas;
}
}
// 子域代码(独立 JS 文件 openData.js)
// 微信/抖音子域只允许使用 canvas 2D API
if (typeof wx !== 'undefined') {
wx.onMessage((msg: any) => {
if (msg.action === 'renderRank') {
renderRankList(msg.data);
}
});
}
function renderRankList(data: { self: any; friends: any[] }) {
const canvas = wx.getSharedCanvas();
const ctx = canvas.getContext('2d')!;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 绘制标题
ctx.fillStyle = '#FFF';
ctx.font = 'bold 24px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('好友排行榜', canvas.width / 2, 40);
// 绘制列表
data.friends.forEach((friend, i) => {
const y = 80 + i * 50;
ctx.fillStyle = i < 3 ? '#FFD700' : '#FFF';
ctx.font = '18px sans-serif';
ctx.textAlign = 'left';
ctx.fillText(`${i + 1}. ${friend.nickname}`, 40, y);
ctx.textAlign = 'right';
ctx.fillText(`${friend.score}`, canvas.width - 40, y);
});
}
5.3 主域渲染子域 Canvas
class UIRenderSystem {
private openDataManager: OpenDataManager;
renderRankPanel(ctx: CanvasRenderingContext2D | WebGLRenderingContext): void {
const shared = this.openDataManager.getSharedCanvas();
if (!shared) return;
// 在主域中将 sharedCanvas 作为普通纹理绘制
if (ctx instanceof CanvasRenderingContext2D) {
ctx.drawImage(shared, 50, 100, 300, 400);
} else {
// WebGL:创建 texture from canvas
const gl = ctx;
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, shared);
// ... 绘制 quad
}
}
}
六、平台差异矩阵速查表
const PLATFORM_DIFF_MATRIX: Record<string, Record<string, string>> = {
'广告创建': {
wechat: 'wx.createRewardedVideoAd({ adUnitId })',
tiktok: 'tt.createRewardedVideoAd({ adUnitId, multiton })',
kuaishou: 'ks.createRewardedVideoAd({ adUnitId })',
qq: 'qq.createRewardedVideoAd({ adUnitId })',
web: 'mock / Google AdSense',
},
'激励回调': {
wechat: 'onClose(res => res.isEnded)',
tiktok: 'onClose(isEnded => isEnded)',
kuaishou: 'onClose(res => res.isEnded)',
qq: 'onClose(res => res.isEnded)',
web: 'Promise.resolve(true)',
},
'支付接口': {
wechat: 'wx.requestMidasPayment({ mode: "game" })',
tiktok: 'tt.requestGamePayment({ offerId })',
kuaishou: 'ks.requestPayment({})',
qq: 'qq.requestPayment({})',
web: 'Stripe / PayPal',
},
'分享': {
wechat: 'wx.shareAppMessage({ title, imageUrl })',
tiktok: 'tt.shareAppMessage({ title, imageUrl, query })',
kuaishou: 'ks.share({})',
qq: 'qq.shareAppMessage({})',
web: 'navigator.share()',
},
'登录': {
wechat: 'wx.login() -> code -> openid',
tiktok: 'tt.login() -> code -> anonymous_openid',
kuaishou: 'ks.login() -> code',
qq: 'qq.login() -> code',
web: 'OAuth2 / Guest ID',
},
'开放数据': {
wechat: 'wx.getOpenDataContext() + sharedCanvas',
tiktok: 'tt.getOpenDataContext() + sharedCanvas',
kuaishou: '❌ 不支持',
qq: 'qq.getOpenDataContext()',
web: '本地模拟',
},
'文件系统': {
wechat: 'wx.getFileSystemManager()',
tiktok: 'tt.getFileSystemManager()',
kuaishou: 'ks.getFileSystemManager()',
qq: 'qq.getFileSystemManager()',
web: 'IndexedDB / localStorage',
},
};
七、Web 降级适配器
开发调试和海外 Web 版本需要 Web 适配器:
class WebAdapter extends PlatformAdapter {
readonly profile: PlatformProfile = {
name: 'web',
version: '1.0',
capabilities: {
rewardAd: false, // Web 用 Google AdSense 或其他
interstitialAd: false,
bannerAd: false,
fullscreenAd: false,
iap: false,
share: !!navigator.share,
login: false,
openDataContext: false,
cloudFunctions: false,
fileSystem: false,
vibrate: !!navigator.vibrate,
accelerometer: !!window.DeviceMotionEvent,
},
screen: {
width: window.innerWidth,
height: window.innerHeight,
pixelRatio: window.devicePixelRatio,
safeArea: { top: 0, bottom: window.innerHeight, left: 0, right: window.innerWidth },
},
isDebug: location.hostname === 'localhost',
};
async showRewardAd(config: AdConfig): Promise<AdResult> {
// Web 环境:模拟激励视频(或接入 Google AdSense Rewarded Ads)
console.log('[WebAdapter] Mock reward ad:', config.adUnitId);
return new Promise(resolve => {
setTimeout(() => resolve({ success: true, isCompleted: true, rewarded: true }), 1000);
});
}
async purchase(order: IAPOrder): Promise<IAPReceipt> {
// Web 环境:Stripe / PayPal 集成
throw new Error('Web IAP not implemented. Use Stripe/PayPal integration.');
}
// ... 其他接口 mock 实现
}
八、初始化流程全景
sequenceDiagram
participant Game as 游戏主循环
participant Adapter as PlatformAdapter
participant Factory as AdapterFactory
participant Detector as PlatformDetector
participant SDK as 平台 SDK
Game->>Detector: detect()
Detector->>SDK: getSystemInfoSync()
SDK-->>Detector: sysInfo
Detector-->>Game: PlatformProfile
Game->>Factory: create()
Factory->>Adapter: new WeChatAdapter()
Adapter->>SDK: 初始化广告/支付
SDK-->>Adapter: ready
Adapter-->>Game: PlatformAdapter instance
Game->>Adapter: showRewardAd()
Adapter->>SDK: createRewardedVideoAd().show()
SDK-->>Adapter: onClose(isEnded)
Adapter-->>Game: AdResult
九、总结与最佳实践
| 实践 | 说明 |
|---|---|
| 能力检测优先 | 任何平台 API 调用前先检查 profile.capabilities,避免运行时崩溃 |
| Promise 化所有异步 | 平台 SDK 的回调风格各异,统一包装为 Promise |
| 错误兜底 | 每个平台 API 调用都要有 catch/fail 处理,返回标准错误结构 |
| 广告预加载 | 激励视频必须在展示前 preload,否则用户等待超 3s 会流失 |
| 子域资源最小化 | 子域 JS 文件必须极小(只包含渲染代码),否则会影响主域加载 |
| Web 优先可调试 | 始终以 WebAdapter 作为默认开发环境,确保 80% 逻辑可在浏览器验证 |
延伸阅读
- 小游戏引擎 ECS 架构深度解析 — 本文适配层的上层 ECS 架构
- WebGL / Canvas2D 混合渲染优化实战 — 各平台 WebGL Context 差异与兼容
- 小游戏包体优化与加载策略完全指南 — 各平台分包策略与缓存 API 差异
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。
「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 实现与智能推荐算法。