Skip to content

Phaser 4 微信小游戏实战(Phaser 3.90+ 迁移参考)

预计阅读 35 分钟
版本信息
最后更新:2026-07-09 · Phaser 4.2.0(2026-06-19,代号 Giedi) · Phaser 3.90+ 项目可参照迁移指南升级,但存在破坏性变更 · 适用基础库:≥ 3.9.0(最低兼容 ≥ 2.30.0) · 微信开发者工具:稳定版 1.06+

Phaser 是 Web 2D 游戏开发领域最成熟的开源框架之一,拥有活跃的社区、丰富的插件生态和极佳的 TypeScript 支持。2026 年 4 月 Phaser 4 已正式发布,最新稳定版为 4.2.0(2026-06-19,代号 Giedi),带来了全新的 WebGL 渲染架构、Mesh2D、Stencil、Cone Lights 等高级特性。本章以 Phaser 4 为主角讲解微信小游戏适配(Phaser 3.90+ 项目可参照官方迁移指南升级,但渲染管线、FX、Geometry 等存在破坏性变更),并以一款极简的 2D 纵向射击游戏 为例,覆盖从环境搭建到上线的完整链路。


1. Phaser 4 适合微信小游戏吗?

1.1 核心难点

浏览器环境微信小游戏环境适配方案
document.createElement('canvas')wx.createCanvas()注入自定义 Canvas 工厂
window.addEventListenerwx.onTouchStart适配输入事件
XMLHttpRequest / fetchwx.request替换资源加载器
localStoragewx.setStorageSync替换存储实现
Imagewx.createImage()替换图片加载
音频 APIwx.createInnerAudioContext()替换音频系统

1.2 可选适配方案

  1. 社区适配包:搜索 phaser-minigamephaser-wechat 等社区项目,它们通常提供了基础的 DOM polyfill 和 Canvas 注入。
  2. 自定义裁剪:只使用 Phaser 的 Game Loop、Scene、Input、Physics 逻辑层,自己用微信 Canvas API 实现渲染层。
  3. Web 优先 + 小游戏分包:先发布 Web 版本验证玩法,再为小游戏单独做一个精简适配版。

本章以方案 1(社区适配包思路)为主,演示最小可运行项目。


2. 环境准备

2.1 项目初始化

bash
# 创建项目
mkdir phaser-shooter-wechat
cd phaser-shooter-wechat
npm init -y

# 安装 Phaser 4 和 TypeScript
pnpm add phaser
pnpm add -D typescript vite @types/node

2.2 目录结构

phaser-shooter-wechat/
├── src/
│   ├── main.ts              # 游戏入口
│   ├── scenes/
│   │   ├── BootScene.ts     # 资源预加载
│   │   ├── GameScene.ts     # 主游戏场景
│   │   └── GameOverScene.ts # 结束场景
│   ├── objects/
│   │   ├── Player.ts        # 玩家飞机
│   │   ├── Bullet.ts        # 子弹
│   │   └── Enemy.ts         # 敌人
│   └── utils/
│       └── wx-adapter.ts    # 微信环境适配
├── raw-assets/              # 原始美术资源
├── dist/                    # 构建输出
├── game.json                # 小游戏配置
├── project.config.json      # 开发者工具配置
└── index.html               # Web 调试入口

2.3 Vite 配置(同时支持 Web 和小游戏)

typescript
// vite.config.ts
import { defineConfig } from 'vite'
import path from 'path'

export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
  build: {
    outDir: 'dist',
    lib: {
      entry: path.resolve(__dirname, 'src/main.ts'),
      formats: ['iife'],
      name: 'PhaserShooter',
      fileName: () => 'game.js',
    },
    rollupOptions: {
      // Phaser 较大,建议 external 后通过分包加载
      external: ['phaser'],
      output: {
        globals: {
          phaser: 'Phaser',
        },
      },
    },
  },
})

3. 微信环境适配层

src/utils/wx-adapter.ts 中注入浏览器缺失的全局对象:

typescript
// src/utils/wx-adapter.ts
export function initWxAdapter() {
  if (typeof wx === 'undefined') return

  // 注入 Canvas
  const canvas = wx.createCanvas()
  ;(globalThis as any).canvas = canvas
  ;(globalThis as any).document = {
    createElement: (tag: string) => {
      if (tag === 'canvas') return wx.createCanvas()
      return {}
    },
    body: {},
  }

  // 注入 Image
  // 注意:微信小游戏的 wx.createImage 不遵循浏览器 CORS,
  // 因此 crossOrigin 属性仅作兼容占位,实际跨域由小游戏运行环境处理
  ;(globalThis as any).Image = class WxImage {
    private _src = ''
    private _crossOrigin: string | null = null
    onload: (() => void) | null = null
    onerror: (() => void) | null = null
    width = 0
    height = 0

    get src() {
      return this._src
    }
    set src(value: string) {
      this._src = value
      const img = wx.createImage()
      img.src = value
      img.onload = () => {
        this.width = img.width ?? 0
        this.height = img.height ?? 0
        this.onload?.()
      }
      img.onerror = () => this.onerror?.()
    }

    get crossOrigin() {
      return this._crossOrigin ?? ''
    }
    set crossOrigin(value: string) {
      this._crossOrigin = value
    }
  }

  // 注入 requestAnimationFrame
  // 微信小游戏没有原生 RAF,使用递归 setTimeout 模拟,并尽量与目标帧率对齐
  let rafId = 0
  const callbacks = new Map<number, FrameRequestCallback>()
  let running = false
  const targetFps = 60
  const frameInterval = 1000 / targetFps

  const tick = () => {
    if (!running) return
    const now = Date.now()
    // 复制当前帧的回调,避免回调中注册新回调导致本帧执行
    const currentCallbacks = new Map(callbacks)
    callbacks.clear()
    currentCallbacks.forEach((fn, id) => fn(now))

    // 计算下一帧延迟,尽量贴近 targetFps
    const elapsed = Date.now() - now
    const delay = Math.max(0, frameInterval - elapsed)
    setTimeout(tick, delay)
  }

  ;(globalThis as any).requestAnimationFrame = (cb: FrameRequestCallback) => {
    const id = ++rafId
    callbacks.set(id, cb)
    if (!running) {
      running = true
      tick()
    }
    return id
  }

  ;(globalThis as any).cancelAnimationFrame = (id: number) => {
    callbacks.delete(id)
    if (callbacks.size === 0) {
      running = false
    }
  }
}

/** 在场景/游戏销毁时清理适配层注入的全局对象 */
export function destroyWxAdapter() {
  ;(globalThis as any).canvas = undefined
  ;(globalThis as any).document = undefined
  ;(globalThis as any).Image = undefined
  ;(globalThis as any).requestAnimationFrame = undefined
  ;(globalThis as any).cancelAnimationFrame = undefined
}

4. 最小可运行项目

4.1 游戏入口

typescript
// src/main.ts
import { initWxAdapter } from './utils/wx-adapter'
import { GameScene } from './scenes/GameScene'

initWxAdapter()

const canvas = (globalThis as any).canvas
const { windowWidth, windowHeight, pixelRatio } = wx.getSystemInfoSync()

const config: Phaser.Types.Core.GameConfig = {
  type: Phaser.CANVAS,
  canvas,
  width: windowWidth,
  height: windowHeight,
  pixelArt: true,
  physics: {
    default: 'arcade',
    arcade: {
      gravity: { x: 0, y: 0 },
      debug: false,
    },
  },
  scene: [GameScene],
}

new Phaser.Game(config)

4.2 游戏场景

typescript
// src/scenes/GameScene.ts
import { Scene } from 'phaser'

export class GameScene extends Scene {
  private player!: Phaser.GameObjects.Rectangle
  private bullets!: Phaser.GameObjects.Group
  private enemies!: Phaser.GameObjects.Group
  private lastFired = 0
  private enemySpawnTimer = 0
  private score = 0
  private scoreText!: Phaser.GameObjects.Text
  // 存储微信触摸事件回调引用,用于场景销毁时正确移除
  private _onTouchStart: ((e: any) => void) | null = null
  private _onTouchMove: ((e: any) => void) | null = null

  constructor() {
    super({ key: 'GameScene' })
  }

  create() {
    // 玩家飞机(用矩形代替美术资源)
    this.player = this.add.rectangle(
      this.scale.width / 2,
      this.scale.height - 100,
      40,
      40,
      0x00e5ff
    )

    this.physics.add.existing(this.player)

    // 子弹和敌人组
    this.bullets = this.add.group()
    this.enemies = this.add.group()

    // 分数
    this.scoreText = this.add.text(20, 20, 'Score: 0', {
      fontSize: '24px',
      color: '#ffffff',
    })

    // 触摸控制 — 存储回调引用以便场景销毁时移除
    this._onTouchStart = (e: any) => {
      const touch = e.touches[0]
      this.player.x = touch.clientX
    }
    this._onTouchMove = (e: any) => {
      const touch = e.touches[0]
      this.player.x = touch.clientX
    }
    wx.onTouchStart(this._onTouchStart)
    wx.onTouchMove(this._onTouchMove)
  }

  // ⚠️ 场景关闭时必须移除事件监听器,否则会导致重复注册和内存泄漏
  shutdown() {
    if (this._onTouchStart) wx.offTouchStart(this._onTouchStart)
    if (this._onTouchMove) wx.offTouchMove(this._onTouchMove)
  }

  update(time: number, delta: number) {
    // 自动发射子弹
    if (time > this.lastFired + 200) {
      this.fireBullet()
      this.lastFired = time
    }

    // 生成敌人
    this.enemySpawnTimer += delta
    if (this.enemySpawnTimer > 1000) {
      this.spawnEnemy()
      this.enemySpawnTimer = 0
    }

    // 子弹上移
    this.bullets.children.iterate((bullet: any) => {
      bullet.y -= 500 * (delta / 1000)
      if (bullet.y < -20) bullet.destroy()
      return true
    })

    // 敌人下移
    this.enemies.children.iterate((enemy: any) => {
      enemy.y += 150 * (delta / 1000)
      if (enemy.y > this.scale.height + 20) enemy.destroy()
      return true
    })

    // 碰撞检测
    this.physics.overlap(this.bullets, this.enemies, (bullet, enemy) => {
      bullet.destroy()
      enemy.destroy()
      this.score += 10
      this.scoreText.setText(`Score: ${this.score}`)
    })

    this.physics.overlap(this.player, this.enemies, () => {
      this.scene.restart()
    })
  }

  private fireBullet() {
    const bullet = this.add.rectangle(this.player.x, this.player.y - 30, 6, 16, 0xff6ec7)
    this.physics.add.existing(bullet)
    this.bullets.add(bullet)
  }

  private spawnEnemy() {
    const x = Phaser.Math.Between(30, this.scale.width - 30)
    const enemy = this.add.rectangle(x, -20, 36, 36, 0xff4444)
    this.physics.add.existing(enemy)
    this.enemies.add(enemy)
  }
}

5. 微信 API 集成

5.1 登录与用户信息

typescript
// src/utils/wx-api.ts
export function wxLogin(): Promise<WechatMinigame.LoginSuccessCallbackResult> {
  return new Promise((resolve, reject) => {
    wx.login({ success: resolve, fail: reject })
  })
}

export function uploadScore(score: number): void {
  wx.setUserCloudStorage({
    KVDataList: [{ key: 'shooterScore', value: String(score) }],
    success: () => console.log('分数上传成功'),
    fail: (err) => console.error('分数上传失败', err),
  })
}

5.2 激励视频广告

typescript
// src/ads/RewardedAd.ts
export class RewardedAdManager {
  private videoAd: WechatMinigame.RewardedVideoAd | null = null

  init(adUnitId: string) {
    this.videoAd = wx.createRewardedVideoAd({ adUnitId })
    this.videoAd.load()
  }

  async show(): Promise<boolean> {
    if (!this.videoAd) return false
    try {
      await this.videoAd.show()
      return new Promise((resolve) => {
        this.videoAd?.onClose((res) => {
          resolve(res.isEnded)
        })
      })
    } catch {
      // 首次展示失败,尝试重新加载
      await this.videoAd.load()
      try {
        await this.videoAd.show()
      } catch {
        return false // 重试后仍失败
      }
      // 重试成功后,仍需监听关闭回调判断用户是否完整观看
      return new Promise((resolve) => {
        this.videoAd?.onClose((res) => {
          resolve(res.isEnded)
        })
      })
    }
  }
}

5.3 分享功能

typescript
export function shareGame(score: number) {
  wx.shareAppMessage({
    title: `我在 Phaser 射击游戏中获得了 ${score} 分,来挑战我吧!`,
    imageUrl: 'https://your-cdn.com/share.png',
  })
}

6. 分包与性能

6.1 主包控制

Phaser 3 引擎本身体积较大(压缩后约 800KB+),建议:

  1. 将 Phaser 放入分包:主包只保留启动逻辑,Phaser 引擎作为分包加载。
  2. 按需加载场景:非首屏场景放入分包。
json
{
  "subpackages": [
    {
      "name": "phaser-engine",
      "root": "subpackages/phaser/"
    }
  ]
}

6.2 加载分包

typescript
async function loadPhaserEngine(): Promise<void> {
  return new Promise((resolve, reject) => {
    wx.loadSubpackage({
      name: 'phaser-engine',
      success: resolve,
      fail: reject,
    })
  })
}

// 在启动画面显示后加载
loadPhaserEngine().then(() => {
  // 初始化 Phaser 游戏
})

6.3 性能建议

优化项说明
使用 Arcade PhysicsPhaser 的 Arcade 物理性能最好,适合 2D 射击游戏
对象池复用子弹、敌人使用对象池,避免频繁创建销毁
限制粒子数量微信小游戏粒子渲染开销较大
避免复杂 ShaderWebGL 特性在部分机型上支持不稳定
真机测试开发者工具帧率不能代表真机表现

7. 构建与发布

7.1 构建命令

bash
# 构建 IIFE 格式的 game.js
pnpm build

# 复制小游戏配置文件到 dist
cp game.json dist/
cp project.config.json dist/

7.2 小游戏配置

json
{
  "deviceOrientation": "portrait",
  "showStatusBar": false,
  "networkTimeout": {
    "request": 10000,
    "downloadFile": 10000
  }
}

7.3 发布流程

  1. 用微信开发者工具打开 dist/ 目录
  2. 点击「预览」测试基础功能
  3. 真机调试验证性能
  4. 上传代码并提交审核

8. 与 Cocos / LayaAir 的对比

维度Phaser 4Cocos CreatorLayaAir
学习曲线低(纯代码)中(编辑器 + 代码)
微信适配需社区适配/自行裁剪官方成熟适配官方成熟适配
2D 渲染性能优秀优秀优秀
3D 支持内置 3D内置 3D
编辑器无官方可视化编辑器Cocos EditorLayaAir IDE
插件生态丰富(Web 生态)中等较少
最适合Web 优先 + 小游戏复用微信小游戏原生 2D/3D2D 重度游戏

本章小结

通过本章,你了解了:

  1. Phaser 4 的微信小游戏适配原理和难点
  2. 如何搭建最小可运行的 Phaser 4 微信小游戏项目
  3. 微信 API(登录、广告、分享)与 Phaser 4 的集成方式
  4. 分包加载和性能优化策略
Phaser 4 实战建议
  • 先做 Web 版本验证玩法,再投入时间做小游戏适配。
  • 保持项目简单,避免使用 Phaser 的高级 WebGL 特性。
  • 真机测试优先,开发者工具只能验证逻辑,不能验证性能。
  • 考虑引擎锁定风险,如果你的目标平台只有微信小游戏,Cocos Creator 通常是更稳妥的选择。

WebGL vs Canvas 渲染器选择

Phaser 3 支持两种渲染模式:Phaser.CANVAS(Canvas 2D)和 Phaser.WEBGL(WebGL)。在微信小游戏环境中,选择正确的渲染器至关重要。

对比分析

维度Canvas 2DWebGL
兼容性✅ 所有设备⚠️ 部分低端安卓不支持 WebGL
批量渲染❌ 逐个 Draw Call✅ GPU 批量处理
同屏精灵数100-300 个1,000-5,000+ 个
Shader 特效❌ 不支持✅ 自定义着色器
内存占用低(CPU 内存)中(显存 + CPU 内存)
微信小游戏环境默认可用wx.getSystemInfoSync() 查询

如何选择

typescript
// main.ts — 启动时检测并选择渲染模式
function detectRenderMode(): Phaser.Types.Core.RenderType {
  // 1. 检测微信小游戏环境的 WebGL 能力
  try {
    const sysInfo = wx.getSystemInfoSync()

    // 部分设备信息中包含 GPU 信息,可辅助决策
    const isLowEndDevice = sysInfo.benchmarkLevel !== undefined && sysInfo.benchmarkLevel < 20

    // 2. 低端设备强制 Canvas
    if (isLowEndDevice) {
      console.log('低端设备,使用 Canvas 渲染')
      return Phaser.CANVAS
    }

    // 3. 创建测试 Canvas 检测 WebGL 支持
    const testCanvas = wx.createCanvas()
    const gl = testCanvas.getContext('webgl') || testCanvas.getContext('experimental-webgl')

    if (gl) {
      console.log('WebGL 可用,使用 WebGL 渲染')
      return Phaser.WEBGL
    }
  } catch (err) {
    console.warn('WebGL 检测失败,降级为 Canvas', err)
  }

  console.log('使用 Canvas 渲染')
  return Phaser.CANVAS
}

// Phaser 配置
const config: Phaser.Types.Core.GameConfig = {
  type: detectRenderMode(),
  width: 750,
  height: 1334,
  canvas: wx.createCanvas(),
  // Canvas 渲染专用优化
  render: {
    antialias: false, // 关闭抗锯齿提升性能
    pixelArt: true, // 像素风格避免模糊
    roundPixels: true, // 像素对齐避免抖动
  },
  // 物理引擎选择
  physics: {
    default: 'arcade',
    arcade: {
      gravity: { x: 0, y: 300 },
      // debug: process.env.NODE_ENV !== 'production',
    },
  },
}
渲染器选择建议
  • 休闲 2D 游戏Phaser.CANVAS(兼容性最好,性能足够)
  • 精灵数量 > 500Phaser.WEBGL(批量渲染优势明显)
  • 需要 Shader 效果Phaser.WEBGL(唯一选择)
  • 不确定 → 先默认 Canvas,有性能问题再评估 WebGL

纹理加载系统适配

Phaser 3 的 Loader 底层使用浏览器的 XMLHttpRequestfetch 来加载资源。在微信小游戏中,需要用 wx.request 替代。

自定义 File Loader

typescript
// adapter/WxImageLoader.ts
/**
 * 微信小游戏图片加载器 — 替代 Phaser 默认的 ImageFile
 * Phaser 3.90+ 支持通过 LoaderPlugin 注册自定义文件类型
 * (Phaser 4 也支持类似机制,API 路径可能略有不同)
 */
class WxImageFile extends Phaser.Loader.File {
  constructor(loader: Phaser.Loader.LoaderPlugin, key: string, url: string) {
    super(loader, {
      type: 'image',
      key,
      url,
    })
  }

  /** 重写加载逻辑,使用 wx.request 代替 XMLHttpRequest */
  load(): void {
    const key = this.key
    const url = this.url

    // 对于本地分包内的资源,使用 wx.getFileSystemManager
    if (url.startsWith('subpackages/') || url.startsWith('assets/')) {
      this.loadLocal(key, url)
    } else {
      // CDN 远程资源使用 wx.request
      this.loadRemote(key, url)
    }
  }

  private loadLocal(key: string, url: string): void {
    try {
      // 微信小游戏不支持 Blob/URL.createObjectURL,使用 wx.createImage() 加载本地文件
      const img = wx.createImage()
      img.onload = () => {
        this.loader.textureManager.addImage(key, img as unknown as HTMLImageElement)
        this.onProcessComplete()
      }
      img.onerror = () => {
        console.error(`本地纹理加载失败: ${url}`)
        this.onProcessError()
      }
      img.src = url // 微信小游戏中 wx.createImage() 可直接加载本地文件路径
    } catch (err) {
      console.error(`本地纹理加载失败: ${url}`, err)
      this.onProcessError()
    }
  }

  private loadRemote(key: string, url: string): void {
    wx.request({
      url,
      responseType: 'arraybuffer',
      success: (res) => {
        if (res.statusCode === 200) {
          const uint8Array = new Uint8Array(res.data as ArrayBuffer)
          const blob = new Blob([uint8Array])
          const img = new Image()
          img.onload = () => {
            this.loader.textureManager.addImage(key, img)
            this.onProcessComplete()
          }
          img.onerror = () => this.onProcessError()
          img.src = URL.createObjectURL(blob)
        } else {
          this.onProcessError()
        }
      },
      fail: () => {
        this.onProcessError()
      },
    })
  }

  private onProcessComplete(): void {
    this.pendingDestroy()
    this.loader.nextFile(this, true)
  }

  private onProcessError(): void {
    this.pendingDestroy()
    this.loader.nextFile(this, false)
  }
}

注册自定义加载器

typescript
// main.ts — 在创建 Game 之前注册自定义文件类型
function registerCustomLoader(): void {
  // 注意:完整替换 Phaser 的默认图片加载器需要深入 LoaderPlugin
  // 以下为简化示例——实际使用时建议使用社区适配包

  // 替代方案:使用 Phaser 的 Loader 事件钩子
  const game = new Phaser.Game(config)

  // 监听加载失败,尝试用 wx.request 重试
  game.load.on('loaderror', (file: Phaser.Loader.File) => {
    console.warn(`Phaser 默认加载失败: ${file.key}, 尝试 wx.request 重试`)
    // 使用 WxImageFile 重试加载
    const wxFile = new WxImageFile(game.load, file.key, file.url as string)
    wxFile.load()
  })
}

Phaser 3 → Phaser 4 迁移注意点

Phaser 4 在 API 层面与 Phaser 3 保持高度兼容,但渲染架构和新增特性需要关注:

维度Phaser 3Phaser 4
渲染后端WebGL 1/2 + Canvas 2D 回退全新 WebGL 2/3 渲染器,仍保留 Canvas 回退(Phaser.CANVAS 已弃用但可用)
粒子系统CPU 粒子 ParticleEmitter新增 GPU 粒子,支持更多粒子数
新特性Mesh2D、Stencil 蒙版、Cone Lights、AlphaStrategy 透明模式
包体相对较大模块化更好,可按需裁剪
微信小游戏社区适配包以 Phaser 3 为主Phaser 4 适配层仍在演进,建议先验证核心 Scene/Loader/Input

迁移建议

  1. 先用 Phaser 4 跑通一个最小场景(Preloader → GameScene → GameOverScene)。
  2. 重点验证资源加载器在微信小游戏中的行为;Phaser 4 的 Loader 事件与 Phaser 3 基本一致,但文件类型注册有调整。
  3. 如果使用 Spine 或 DragonBones 插件,确认插件已提供 Phaser 4 版本。
  4. 小游戏环境优先关闭 Phaser 4 的高级光照/后效,以降低 GPU 压力。

📝 课后练习

练习 1:对象池优化

题目: 本章示例中子弹和敌人都是每帧创建/销毁的。请用对象池改写 fireBulletspawnEnemy

参考答案要点:

  • 预创建 50 个子弹对象和 20 个敌人对象
  • fireBullet 从池中取出一个 inactive 对象,重置位置并激活
  • 对象飞出屏幕或碰撞后不销毁,改为 inactive 状态放回池中
练习 2:适配层完善

题目: 除了 Canvas 和 Image,Phaser 还可能用到哪些浏览器 API?列出至少 3 个,并说明在微信小游戏中的替代方案。

参考答案:

  1. localStoragewx.setStorageSync
  2. Audiowx.createInnerAudioContext
  3. fetch / XMLHttpRequestwx.request
  4. navigator.userAgentwx.getSystemInfoSync

📚 相关阅读

用心学习,持续实践