《Cocos 游戏开发实战》10.2 多端音频限制

10.1 的 AudioManager 在桌面浏览器里跑得好好的,一上手机就哑了——浏览器的自动播放策略、微信小游戏的音频初始化、iOS 的静音开关,三端各有各的脾气。本节讲清这些限制,给出用 sys.platform 判定环境、首次交互解锁音频、以及切后台中断恢复的可编译代码。

10.2 多端音频限制

10.1 的 AudioManager 在桌面浏览器里调试时一切正常:main.scene 一加载,BGM 就响起来。但把同样的包发到手机上,第一件事往往是——没有声音。

这不是代码写错了,而是三端各有各的限制:浏览器有自动播放策略、微信小游戏有独立的音频初始化、iOS 有物理静音开关。这一节把这些限制逐条讲清,并给出应对代码。判错运行平台是卷二最常见的一类 bug,所以每个平台判定都落到具体的 sys.Platform 成员上。

10.2.1 浏览器的自动播放策略

Chrome、Safari、Firefox 都有一条共同规则:页面在没有收到任何用户交互之前,不允许播放音频。AudioSource.play() 在这个阶段调用不会报错,只是被静默忽略——所以你看到的是「代码跑了、日志也打了、就是没声音」。

规则的两面:

  • 首次交互之前:音频被拒。这里的「交互」指真实的用户手势——点击、触摸、按键。程序触发的 input 事件不算。
  • 首次交互之后:音频解锁,后续所有播放正常。

所以《方块冲刺》的正确做法是:不要求 BGM 在场景加载时就响,而是等到玩家第一次点击(开始按钮、屏幕任意处)再播。这在体验上也说得通——玩家还没准备好玩,先别放音乐。

10.2.2 微信小游戏的音频差异

微信小游戏有自己的音频体系。Cocos 3.8 会把它适配到 AudioSource 上,但底层行为仍有差异:

项Web 浏览器微信小游戏
自动播放限制有,需用户交互有,且更严格
音频通道上限约 32约 10~16,更易撞墙
volume 生效是部分机型不生效
后台播放随页面挂起需显式配置
静音跟随系统否是(跟系统静音开关)

两个要点:

  1. 通道上限更低。10.1 里那个音效池的 maxChannels 默认值 16,在微信端要调低到 8~10,否则「有的音效不响」会更频繁。
  2. volume 可能不生效。某些 Android 机型的微信里,AudioSource.volume 设了没反应。对策是用音效资源本身的音量(在编辑器里对 AudioClip 预处理),而不是运行时靠 volume 调。

微信端还可以调 wx.setInnerAudioOption 控制是否与系统音量同步,但这是平台 API,需要先判定环境。

10.2.3 用 sys.platform 判定运行环境

sys 模块暴露了当前运行平台。判定必须精确到具体成员,不能用「有没有 wx 对象」这种土办法——微信小程序里也有 wx,但音频能力不同。

import { sys } from 'cc';

export function platformName(): string {
  switch (sys.platform) {
    case sys.Platform.WECHAT_GAME:
      return 'wechat-game';
    case sys.Platform.MOBILE_BROWSER:
      return 'mobile-browser';
    case sys.Platform.DESKTOP_BROWSER:
      return 'desktop-browser';
    case sys.Platform.ANDROID:
      return 'android-native';
    case sys.Platform.IOS:
      return 'ios-native';
    default:
      return 'unknown';
  }
}

sys.platform 返回的是 sys.Platform 枚举值,常用成员如下:

成员含义
sys.Platform.WECHAT_GAME微信小游戏
sys.Platform.BYTEDANCE_MINI_GAME抖音小游戏
sys.Platform.MOBILE_BROWSER手机浏览器
sys.Platform.DESKTOP_BROWSER桌面浏览器
sys.Platform.ANDROIDAndroid 原生
sys.Platform.IOSiOS 原生

注意:枚举里没有 NATIVE 这个成员。要判断「是不是原生平台」,用布尔属性 sys.isNative,而不是拿 sys.platform 去比某个值。要判断「具体是哪个平台」才用 sys.platform。这个区别看着小,但写错了在原生端会永远判 false。

10.2.4 首次交互解锁音频

把「等第一次交互再播」写成组件。下面这段可以过 tsc --noEmit:

import { _decorator, Component, AudioSource, input, Input } from 'cc';

const { ccclass, property } = _decorator;

@ccclass('AudioUnlocker')
export class AudioUnlocker extends Component {
  @property(AudioSource)
  private bgm: AudioSource | null = null;

  private unlocked = false;

  protected onLoad(): void {
    // 触摸或按键都算用户交互,哪个先来用哪个
    input.once(Input.EventType.TOUCH_END, this.unlock, this);
    input.once(Input.EventType.KEY_DOWN, this.unlock, this);
  }

  private unlock(): void {
    if (this.unlocked) {
      return;
    }
    this.unlocked = true;
    if (this.bgm && !this.bgm.playing) {
      this.bgm.play();
    }
  }
}

要点:

  • input.once 而不是 input.on——交互解锁只需要一次,用 once 免去手动 off。
  • 同时监听 TOUCH_END 和 KEY_DOWN,覆盖手机(触摸)和桌面(键盘)。
  • unlock 里有 this.unlocked 双保险:两个事件可能几乎同时触发,once 各自注册,所以要再挡一次。

把这个组件挂在 AudioManager 节点上,bgm 指向 BGM 的 AudioSource。这样无论玩家是先点开始按钮还是先按空格,BGM 都会在第一次交互后正确响起。

10.2.5 iOS 的静音开关与音频会话

iPhone 侧边有一个物理静音拨片。Web 端(含微信小游戏)的音频默认跟随这个开关——拨到静音,游戏就没声。这是设计如此,不是 bug,开发者无法绕过。

原生 iOS 构建则可以通过音频会话类别控制。Cocos 原生构建里这个配置在 settings/v2/packages/*.json 的音频相关项里(需 Cocos Creator 3.8 编辑器打开,本节不可命令行复现)。但多数休闲游戏不需要动它——跟随系统静音反而是用户期望的行为。

给策划的结论:不要把关键信息只放在声音里。如果「金币到手」只靠音效提示,玩家手机静音时就收不到反馈。视觉(9.3 的粒子)必须能独立完成信息传达。

10.2.6 音频格式与体积

三端对音频格式的支持不同,选错会导致「某端放不出来」:

格式体积Web微信小游戏原生建议用途
.mp3中支持支持支持通用兜底
.ogg小支持(Safari 旧版弱)部分支持音效
.m4a / .aac小支持支持支持移动端首选
.wav大支持支持支持极短音效(<0.5s)

《方块冲刺》的取舍:

  • BGM:用 .m4a,单曲控制在 1 MB 以内。BGM 是首包体的大头,微信小游戏首包有 4 MB 硬限制,一段 2 分钟的 mp3 就能吃掉一半。
  • 音效:用 .ogg 或 .m4a,单条 20 KB 以内。跳跃、拾取这种短音效压缩后很小。
  • 不要用 .wav 做长音频——它是未压缩的,一段 3 秒的 44.1kHz 立体声 wav 就有约 1 MB。

10.2.7 中断与恢复:切后台、来电

手机来电、玩家切到别的 App,游戏会进后台。此时音频必须暂停,回到前台再恢复——否则会出现「切走了音乐还在响」或者「回来音乐没了」两种情况。

Cocos 用 game 上的生命周期事件通知前后台切换:

import { _decorator, Component, AudioSource, Game, game, sys } from 'cc';

const { ccclass, property } = _decorator;

@ccclass('AudioInterruptHandler')
export class AudioInterruptHandler extends Component {
  @property(AudioSource)
  private bgm: AudioSource | null = null;

  private wasPlaying = false;

  protected onEnable(): void {
    game.on(Game.EVENT_HIDE, this.onHide, this);
    game.on(Game.EVENT_SHOW, this.onShow, this);
  }

  protected onDisable(): void {
    game.off(Game.EVENT_HIDE, this.onHide, this);
    game.off(Game.EVENT_SHOW, this.onShow, this);
  }

  private onHide(): void {
    this.wasPlaying = this.bgm?.playing ?? false;
    this.bgm?.pause();
  }

  private onShow(): void {
    // 微信端恢复前台后重新播放偶发失效,交给下一次用户交互解锁
    if (this.wasPlaying && sys.platform !== sys.Platform.WECHAT_GAME) {
      this.bgm?.play();
    }
  }
}

两个细节:

  • Game.EVENT_HIDE 是切后台,Game.EVENT_SHOW 是回前台,都是字符串常量,注册在 game 对象上。
  • wasPlaying 记录切走前是否在播。如果玩家本来就把 BGM 关了,回前台就不该自动打开。
  • 微信端有个已知的偶发问题:回前台后 play() 可能不生效。最稳的做法是回前台时把音频重新标记为「未解锁」,等下一次用户交互再解锁。

10.2.8 音频资源的加载时机

音频文件不是代码,不会随脚本一起进包。放哪里决定了它在什么时候被加载:

  • 进首包:assets/audio/ 下的资源会被打进主包。BGM 和几个高频音效适合进首包,随游戏一起下载。
  • 进分包 / 远程:低频音效(如某些关卡专属的)可以放进分包,用到时再拉。分包策略在第 15 章细讲。
  • 运行时加载:如果音频放在 assets/resources/ 下,可以用 resources.load 按需拉取:
import { AudioClip, resources } from 'cc';

// 前提:音频放在 assets/resources/audio/ 下(resources 是内置 Bundle)
resources.load('audio/sfx-pickup', AudioClip, (err, clip) => {
  if (err) {
    console.error(err.message);
    return;
  }
  // 拿到 clip 后交给 AudioManager 播放
  console.log(clip.getDuration()); // 时长要走方法,AudioClip 上没有 duration 属性
});

一个容易忽略的点:BGM 别等第一次交互才加载。如果 playBGM 里现拉资源,玩家点开始按钮后会先卡一下(等下载)再出声。正确做法是在启动场景 main.scene 的加载阶段就把 BGM 预加载好,交互解锁时只是 play(),零延迟。

10.2.9 三端对照表

把这一节的所有差异收进一张表,发布前逐条核对:

限制Web微信小游戏原生
需用户交互解锁是是否
音频通道上限~321016较高
volume 生效是部分机型否是
跟随系统静音否是可配
后台自动暂停随页面需配置需配置
推荐音效格式.ogg/.m4a.m4a.m4a

10.2.10 工程视角的收口

这一节做完,工程里多了:

  • assets/scripts/platform/audio-unlocker.ts:首次交互解锁;
  • assets/scripts/platform/audio-interrupt.ts:前后台中断恢复;
  • assets/scripts/platform/platform-detect.ts:platformName() 等环境判定;
  • AudioManager 的 maxChannels 在微信端下调到 10。

到这里,音频的「响」和「不响」都管住了。但反馈还差最后一层——触觉与节奏。手机能震动,动作能有「顿帧」,这些是打击感的来源。10.3 会讲怎么用平台震动 API、怎么用 timeScale 做顿帧、怎么用节点位移做屏幕震动,把《方块冲刺》的反馈闭环补完。

阅读导航:上一节:10.1 音频管理器与音效池 · 下一节:10.3 震动、顿帧与打击感 。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「cocos」更多文章

  1. 《Cocos 游戏联机实战》9.3 延迟补偿与体验权衡
  2. 《Cocos 游戏联机实战》9.2 网络状态 UI 与提示
  3. 《Cocos 游戏联机实战》9.1 同步表现与插值动画