小游戏多平台统一适配层设计:微信、抖音、快手、QQ、百度 SDK 差异抽象

深入讲解小游戏多平台适配层的架构设计。涵盖微信/抖音/快手/QQ/百度/4399 六大平台 API 差异矩阵、统一广告接口(激励视频/插屏/Banner/全屏视频)、支付通道桥接、开放数据域(子域)渲染、平台能力检测与运行时自动降级。提供完整的 TypeScript 适配器工厂模式实现。

小游戏多平台统一适配层设计

一、中国小游戏平台生态全景

截至 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 / vivo0.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% 逻辑可在浏览器验证

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「games」更多文章