pixi.js7.1.4开发微信小游戏环境搭建详细教程

836 阅读7分钟

大家好!我是程序员小菜。最近突然想尝试做微信小游戏。考虑到自己只是一个普通程序员,要学游戏开发代价实在是太大了,而且游戏开发工具中创建复杂的游戏场景咱也用不上,小游戏开发本身就是靠创意和玩法简单。作为一名程序员,小游戏应该尽量能依赖代码实现的最好,这样遇到技术难题也好自己解决。因为自己工作也做前端,最终考虑使用pixi.js来做图像渲染代码实现游戏逻辑。

但是微信小游戏是一个不同于浏览器的 JavaScript 运行环境,没有 BOM 和 DOM API。然而pixi.js是用JavaScript 结合其他 HTML5 技术来显示媒体,创建动画或管理交互式图像。是依赖浏览器提供的 BOM 和 DOM API 的。所以不能直接微信小游戏中使用pixi.js。好在小游戏提供了对大部分 Canvas 2d 和 WebGL 1.0 特性的支持,支持情况参见 RenderingContext,pixi.js它能自动侦测使用WebGL还是Canvas来创建图形

小游戏的运行环境在 iOS 上是 JavaScriptCore,在 Android 上是 V8,都是没有 BOM 和 DOM 的运行环境,没有全局的 document 和 window 对象。因此当你希望使用 DOM API 来创建 Canvas 和 Image 等元素的时候,会引发错误。所以要想使用pixi.js我们就需要在微信小游戏宿主环境模拟实现浏览器环境,对基于浏览器环境的游戏引擎在小游戏运行环境下的一层适配层Adapte,使游戏引擎在调用 DOM API 和访问 DOM 属性时不会产生错误。Adapter 是一个抽象的代码层,并不特指某一个适配小游戏的第三方库,每位开发者都可以根据自己的项目需要实现相应的 Adapter。好在官方实现了一个 Adapter 名为 weapp-adapter, 并提供了完整的源码,供开发者使用和参考。

**Adapter 下载地址 [weapp-adapter.zip](https://res.wx.qq.com/wxdoc/dist/assets/media/weapp-adapter.9568fddf.zip)**

但是官方提供的weapp-adapter 并不能完全兼容pixi.js。在实际使用过程中我遇到了很多问题,有些问题通过百度和Google的没有找到很好就解决办法,通过自己的几个晚上的努力终于解决了。所以我整理最新pixie.js7.1.4开发微信小游戏遇到的问题和解决方案方便大家。

## 搭建pixi.js微信小游戏过程中我主要遇到三个问题: ###

pixi/unsafe-eval

###

纹理加载问题

###

TouchEvent

我先从创建微信小游戏开始,使用微信开发者工具创建小游戏不要创建小程序。开始我考虑我是使用pixi.js代码实现小游戏用不到微信小游戏提供的游戏引擎和模板代码,所以就直接创建了小程序。但是创建完小程序之后就发现对canvas支持有问题就直接果断放弃了改创建小游戏。在创建微信小游戏时没有找到创建空白项目就直接选用了飞机大战。然后删掉多余文件,不删也可以我们测试就直接修改game.js

引入小游戏如适配文件weapp-adapter.js和symbol.js。 weapp-adapter.js直接使用上面Adapter.zip包下dist目录打包好的文件。

引入[pixi.js](https://github.com/pixijs/pixijs/releases),我们选择稳定版的最新版v7.1.4,不要选新版本的v7.2.0。因为微信小游戏规定单个js文件不能超过500k,所以我们只能选用压缩过的开发者版本。但是之后讲解问题的原因时会使用测试版本讲解方便大家理解。

1678883662(1).jpg

pixi.js每个版本的都不太一样,所以看别人示例或者官方示例一定要注意看版本。如果出现ReferenceError: xxx is not defined 时候不要惊慌,可能是这个版本本来就没有这个属性或方法。可以断点看一下它有那些属性和方法。

目录结构和game.js代码如下图:

1678884163(1).jpg

我们遇到了第一个问题pixi/unsafe-eval报错:

Error: Current environment does not allow unsafe-eval, please use @pixi/unsafe-eval module to enable support.

直接在开发者版js搜索报错:

systemCheck() {
    if (!unsafeEvalSupported()) {
      throw new Error("Current environment does not allow unsafe-eval, please use @pixi/unsafe-eval module to enable support.");
    }
  }
function unsafeEvalSupported() {
  if (typeof unsafeEval === "boolean") {
    return unsafeEval;
  }
  try {
    const func = new Function("param1", "param2", "param3", "return param1[param2] === param3;");
    unsafeEval = func({ a: "b" }, "a", "b") === true;
  } catch (e) {
    unsafeEval = false;
  }
  return unsafeEval;
}

再找到unsafeEvalSupported方法,我们发现了new Function("param1", "param2", "param3", "return param1[param2] === param3;")。这种将字符串编译成代码方式存在安全问题导致了这个报错,pixi官方也提供解决方法[@pixi/unsafe-eval](https://api.pixijs.io/@pixi/unsafe-eval.html)。我们选择第二种。下载unsafe-eval.min.js

``` https://cdn.jsdelivr.net/npm/@pixi/unsafe-eval@7.x/dist/unsafe-eval.min.js ```

``` https://unpkg.com/@pixi/unsafe-eval@7.x/dist/unsafe-eval.min.js ```

1678885434(1).jpg

因为this对象是undefined,所以休要把的unsafe-eval.min.js里的三个this.PIXI换成GameGlobal.PIXI。

解决了第一个问题我们就开心的开始撸代码了,直接上game.js代码。

require('./js/libs/weapp-adapter')
import './js/libs/symbol'
import * as PIXI from "./js/libs/pixi/pixi.min";
GameGlobal.PIXI = PIXI
require("./js/libs/pixi/unsafe-eval.min")

// 获取适配屏幕大小
const {pixelRatio, windowWidth, windowHeight} = wx.getSystemInfoSync();
 
let app = new PIXI.Application({
    width: windowWidth * pixelRatio,
    height: windowHeight * pixelRatio,
    background: '#1099bb',
    view: canvas
});
const graphics = new PIXI.Graphics();
graphics.beginFill(0xDE3249);
graphics.drawRect(50, 50, 100, 100);
graphics.endFill();

app.stage.addChild(graphics)

代码在执行到new PIXI.Graphics()报错了:Error: Unrecognized source type to auto-detect Resource,直接找到对应代码。

function autoDetectResource(source, options) {
  // ...
  for (let i = INSTALLED.length - 1; i >= 0; --i) {
    const ResourcePlugin = INSTALLED[i];
    // ResourcePlugin.test判断是不是支持的类型
    if (ResourcePlugin.test && ResourcePlugin.test(source, extension)) {
      return new ResourcePlugin(source, options);
    }
  }
  throw new Error("Unrecognized source type to auto-detect Resource");
}
// 支持的所以类型
INSTALLED.push(ImageBitmapResource, ImageResource, CanvasResource, VideoResource, SVGResource, BufferResource, CubeResource, ArrayResource);
const _BaseTexture = class extends eventemitter3 {
    // 忽略其他代码代码
    if (resource && !(resource instanceof Resource)) {
      resource = autoDetectResource(resource, resourceOptions);
      resource.internal = true;
    }
}    

现在我们来结合代码分析报错的原因,pixie.js画图对象Graphics会继承纹理基类_BaseTexture,_BaseTexture会调用autoDetectResource来确定类型。从INSTALLED中可以看出一共支持8种,分别是ImageBitmapResource, ImageResource, CanvasResource, VideoResource, SVGResource, BufferResource, CubeResource, ArrayResource。autoDetectResource方法通过for循环依次去调用对应类型的ResourcePlugin.test来适配。所有类型的都没有匹配到所以抛出了throw new Error("Unrecognized source type to auto-detect Resource")错误。

同样的代码我们放在浏览器环境执行发现,适配到的类型是CanvasResource。代码如下:

class CanvasResource extends BaseImageResource {
  static test(source) {
    // ...
    return globalThis.HTMLCanvasElement && source instanceof HTMLCanvasElement;
  }
}

判断是否是HTMLCanvasElement类型source instanceof HTMLCanvasElement;。浏览器环境是true。小程序环境则不对,那我们一起来看一下小程序下source是什么HTMLElement类型。

1678954803(1).jpg

我们继续看一下pixi.js的Texture的官方源码。

  static get WHITE() {
    if (!Texture._WHITE) {
      const canvas = settings.ADAPTER.createCanvas(16, 16);
      const context = canvas.getContext("2d");
      canvas.width = 16;
      canvas.height = 16;
      context.fillStyle = "white";
      context.fillRect(0, 0, 16, 16);
      Texture._WHITE = new Texture(BaseTexture.from(canvas));
      removeAllHandlers(Texture._WHITE);
      removeAllHandlers(Texture._WHITE.baseTexture);
    }
    return Texture._WHITE;
  }

这段代码第一个重要的地方就是const canvas = settings.ADAPTER.createCanvas(16, 16), 这里生成cancas就是前面用来做类型推断的(source)。再来一起看一下settings.ADAPTER.createCanvas方法。控制台中打印的如下,确实就是调用document.createElement("canvas")创建了一个canvas实例并且这个canvas的constructor也是HTMLCanvasElement。

1678960191(1).jpg

现在问题已经很明确了,我们看一下微信的adapter中document.createElement方法在创建canvas和canvas的实现。

  // 微信adapter中document.createElement方法的实现
  createElement(tagName) {
    if (tagName === 'canvas') {
      return new Canvas()
    } else if (tagName === 'audio') {
      return new Audio()
    } else if (tagName === 'img') {
      return new Image()
    }

    return new HTMLElement(tagName)
  }
// 微信adapter中Canvas的实现
export default function Canvas() {
  const canvas = wx.createCanvas()

  canvas.type = 'canvas'

  canvas.__proto__.__proto__ = new HTMLElement('canvas')

  const _getContext = canvas.getContext

  canvas.getBoundingClientRect = () => {
    const ret = {
      top: 0,
      left: 0,
      width: window.innerWidth,
      height: window.innerHeight
    }
    return ret
  }

  return canvas
}

微信weapp-adapter原代码中canvas.__proto__.__proto__ = new HTMLElement('canvas')这句代码可以看出来继承错了,canvas原型的原型应该继承HTMLCanvasElement。把代码修改为canvas.__proto__.__proto__ = new HTMLCanvasElement('canvas')。再重新打包weapp-adapter,把新生成weapp-adapter.js替换微信小游戏项目中的文件就可以了。

终于可以正常的显示图像了,我们点击图像的时候突然又报了一个错。TouchEvent is not defined.这个问题相对简单,TouchEvent变量没有赋值给全局对象。修改一下weapp-adapter的TouchEvent.js ,再要把window.TouchEvent暴露出来即可。

// TouchEvent.js 
export class TouchEvent  {}
// 修改改为
export default class TouchEvent {}

// 再window.js文件加上这句
export TouchEvent from './EventIniter/TouchEvent'

这样就解决了pixi.js在微信小游戏环境下的运行问题。我来一个拖动小飞机的例子结束本教程。下一篇文章我会展示一些复杂的例子.

require('./js/libs/weapp-adapter')
import './js/libs/symbol'
import * as PIXI from "./js/libs/pixi/pixi.min";
GameGlobal.PIXI = PIXI
require("./js/libs/pixi/unsafe-eval.min")

// 获取适配屏幕大小
const {pixelRatio, windowWidth, windowHeight} = wx.getSystemInfoSync();
 
let app = new PIXI.Application({
    width: windowWidth * pixelRatio,
    height: windowHeight * pixelRatio,
    background: '#1099bb',
    view: canvas
});

const texture = PIXI.Texture.from('./images/hero.png')
texture.baseTexture.scaleMode = PIXI.SCALE_MODES.NEAREST
const bunny = new PIXI.Sprite(texture)
bunny.anchor.set(0.5)
bunny.x = app.screen.width / 2 
bunny.y = app.screen.height / 2
bunny.on('pointerdown', onDragStart, bunny)

bunny.interactive = true;

let dragTarget = null;
app.stage.interactive = true
app.stage.hitArea = app.screen
app.stage.on('pointerup', onDragEnd)
app.stage.on('pointerupoutside', onDragEnd)
app.stage.addChild(bunny)
function onDragMove(event) {
    if (dragTarget) {
        dragTarget.parent.toLocal(event.global, null, dragTarget.position)
    }
}
function onDragStart() {
    this.alpha = 0.5
    dragTarget = this
    app.stage.on('pointermove', onDragMove)
}
function onDragEnd () {
    if(dragTarget) {
        app.stage.off('pointermove', onDragMove)
        dragTarget.alpha = 1
        dragTarget = null
    }
}