[TOC]

pixi.js 是一个2D渲染引擎。优先使用WebGL渲染,如果不支持就回退到canvas。现在的版本是最新的支持版本是:5.2.4(2020.6.9),4.x.x的版本是稳定的版本。可以渲染图片和文字形状等,使用音频需要加载插件库或者使用第三方库sound.js来实现。
其他可以使用库: CreateJS中文网Phaser-全家桶式的游戏游戏引擎Hilo-阿里开发的一个游戏引擎
具体的H5引擎的推荐:游戏引擎入门推荐

开始引入pixi.js

推荐使用vscode + Typescript来编写, 可以进行检查和提示。打包使用webpack就可以了。
chrome开发插件:https://github.com/bfanger/pixi-inspector 可以方便调试,webpack打包后不能正常显示需要在window上挂载PIXI window.PIXI = PIXI;

  1. demo开发时期,直接使用vscode的插件Live Server 来直接本地起一个服务来测试使用 ```html <!doctype html>

正式开发使用webpack 打包,pixi.js(渲染库)、bump(碰撞检测)、pixi-sound(声音播放,支持性有点问题建议使用howler)、gsap(补间动画库)来开发试玩游戏,使用typescirpt+babel来做es5支持 ,如需使用Promise等并支持es5则需要自己进行添加对应的polyfill,或使用babel来配置自动添加。<br />typescript类型别名: [https://juejin.im/post/5c2f87ce5188252593122c98#heading-1](https://juejin.im/post/5c2f87ce5188252593122c98#heading-1)<br />巧用ts: [https://zhuanlan.zhihu.com/p/39620591](https://zhuanlan.zhihu.com/p/39620591)<br />深入理解ts:[https://jkchao.github.io/typescript-book-chinese/](https://jkchao.github.io/typescript-book-chinese/)<br />ts中文网:[https://www.tslang.cn/](https://www.tslang.cn/)<br />ts升入研究-en:[https://basarat.gitbook.io/typescript/recap](https://basarat.gitbook.io/typescript/recap)<br />typescipt仅仅是js的超集,支持一些新的语法转换新语法到老的语法,js的基础一定要好,如果出问题可以快速回转。用ts主要是vscode这个免费的IDE的功能越来越强大,提示越来越好,可以显著的提升工作效率。
```typescript
//types/global.d.ts
//全局声明需要导入的图片字体,css等,在Webpack的rules中进行配置
declare module "*.png" {
  const value: string;
  export default value;
}
declare module '*.jpg' {
  const value: string;
  export default value;
}
declare module '*.css' {
  const value: string;
  export default value;
}
declare module '*.mp3' {
  const value: string;
  export default value;
}

interface Window{
  mraid
  dapi
}
declare const FbPlayableAd: {
  onCTAClick: Function;
};
declare const ExitApi: {
  exit:Function
};
declare const mraid: {
  addEventListener: Function;
  getState(): string;
  isViewable(): boolean;
  open(url:string):void
  openStoreUrl(url:string):void
};
declare const dapi: {
  isReady():boolean
  isViewable(): boolean;
  getScreenSize():{width:number,height:number}
  openStoreUrl(callback?: Function, option?: any): void;
  open(url: string): void;
  getAudioVolume(): number;
  addEventListener:Function
  removeEventListener:Function
};
declare const $: any;
declare let screenSize: any;

declare const type: string;
//用到的一些工具函数
/** 贴图在整个精灵图中的位置和大小 */
type TextureData = { x: number; y: number; w: number; h: number };
/**
 * @description 从精灵图集中拿到对应的一部分贴图
 * @param {string} name 精灵图的文件名,例如:xx.png
 * @param {TextureData} data 图片的位置和大小
 */
function getTexture(name: string, data: TextureData) {
  const baseTexture = loader.resources[name].texture.baseTexture;
  let rectangle = new Rectangle(data.x, data.y, data.w, data.h);
  return new Texture(baseTexture, rectangle);
}
// 泛型类型推导
/**
 * @description 从精灵图集中拿到所有的贴图
 * @param {string} name 精灵图的文件名,例如:xx.png
 * @param {Object} datas 图片的位置和大小的数据对象
 * @returns {Object} 按照datas中的key来对应贴图的一个对象
 */
function getTextures<T>(name: string, datas: { [key: string]: TextureData }): { [K in keyof T]: Texture } {
  let textures: { [K in keyof T]: Texture } = {} as { [K in keyof T]: Texture };
  for (const key in datas) {
    if (datas.hasOwnProperty(key)) {
      textures[key] = getTexture(name, datas[key]);
    }
  }
  return textures;
}
/**
 * @description 一个贴图数组来创建一个动画对象
 * @param {Array} textureArr 贴图的数组
 * @param {number} speed 动画速度 0-1
 * @param {number} x 横坐标
 * @param {number} y 纵坐标
 * @returns {AnimatedSprite} 动画对象
 */
function createSpritesAnimation(textureArr: Texture[], speed: number, x: number, y: number): AnimatedSprite {
  // 创建动画精灵
  let result = new AnimatedSprite(textureArr);
  // 设置动画精灵的速度
  result.animationSpeed = speed;
  // 设置动画精灵的位置
  result.position.set(x, y);
  return result;
}
// 所有的资源数据,使用了函数来写,类型会去掉,节约空间
interface TextureData {
  x: number;
  y: number;
  w: number;
  h: number;
}
/**
 * cannon1.png 中图片的位置和大小
 */
export const cannonData = (function () {
  const names = ["cannon1-0.png", "cannon1-1.png", "cannon1-2.png", "cannon1-3.png", "cannon1-4.png"];
  //方便ts自动的类型推导可以有很好的提示
  const result = {} as {
    "cannon1-0.png": TextureData;
    "cannon1-1.png": TextureData;
    "cannon1-2.png": TextureData;
    "cannon1-3.png": TextureData;
    "cannon1-4.png": TextureData;
  };
  names.forEach((e, index) => {
    result[e] = { x: 0, y: 74 * index, w: 74, h: 74 };
  });
  return result;
})();
/**
 * @description 取范围内的随机整数,[min,max]
 * @param {number} min 下限
 * @param {number} max 上限
 * @returns {number} 随机数
 */
export function randomInt(min: number, max: number): number {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

/**
 * @description 把一个对象中自己的值导入一个数组中
 * @param {object} datas 要转换的对象
 * @returns {Array}  转换后的数组
 */
export function objToArr<T>(datas: { [key: string]: T }): T[] {
  let arr: T[] = [];
  for (const key in datas) {
    if (datas.hasOwnProperty(key)) {
      arr.push(datas[key]);
    }
  }
  return arr;
}
/**
 * @description 已知 tan 值 求角度,四舍五入
 * @param {number} tan tan 值
 * @return {number} 求出的角度值,整数
 */
export function getTanDeg(tan: number): number {
  return Math.round(Math.atan(tan) / (Math.PI / 180));
}
/**
 * @description 已知两个点坐标求 tan 值
 * @param {number} a 所求角度的顶点
 * @param {number} b 所求角度边上的点
 * @return {number} 求出的tan值
 */
export type MyPoint = { x: number; y: number };
export function getTan(a: MyPoint, b: MyPoint): number {
  // 转换坐标系,以a点位原点
  let x = b.x - a.x;
  let y = a.y - b.y;
  return x / y;
}
/**
 * @description 已知两个点坐标,求a点为顶点的角度值
 * @param {number} a 所求角度的顶点
 * @param {number} b 所求角度边上的点
 * @return {number} 求出的求出的角度值,整数(四舍五入)
 */
export function getDeg(a: MyPoint, b: MyPoint): number {
  let result: number = getTanDeg(getTan(a, b));
  if (a.y < b.y && a.x > b.x) {
    // 第三象限
    result -= 180;
  } else if (a.y < b.y && a.x < b.x) {
    // 第四象限
    result += 180;
  }
  return result;
}
/**
 * @description 调整页面的canvas 的父元素的大小来适应屏幕
 * @params {HTMLElement} parent 父元素
 * @params {number} w canvas 宽度
 * @params {number} h canvas 高度
 */
export function resize(parent: HTMLElement, w: number, h: number) {
  //其他使用了css来适配,使用@media@media only screen and (orientation: landscape){
  //@media only screen and (min-aspect-ratio: 18/9) {}}等来适配
  //  设置宽高
  let width: number,
    height: number,
    aspectRatio = w / h;
  if (window.innerWidth > window.innerHeight) {
    // 横屏
    height = window.innerHeight;
    width = height * aspectRatio;
  } else {
    // 竖屏
    width = window.innerWidth;
    height = width / aspectRatio;
  }
  parent.style.width = width + "px";
  parent.style.height = height + "px";
}

记录

主要使用的数学知识

主要的动画和功能实现使用的数学知识有三角函数和平面直角坐标系的变换(主要用做,碰撞检测,边界判断和子物体初始坐标计算,移动的计算等)。注意角度的初始0度的位置以便于做坐标转化
sin_cos.gif
可以点击以上图片查看详细知识

主要使用的设计模式

游戏中状态较多,且需求修改比较频繁,设计模式使用较多。单例模式主要用在游戏对象的管理类上,建造者主要用在创建不同的游戏对象,可以使用对象池来复用一些比较消耗资源的对象。

pixi.js的官网和文档、参考教程

官网: https://www.pixijs.com/
文档: http://pixijs.download/release/docs/index.html
教程: https://github.com/Zainking/learningPixi
参考博客:
深红结界:http://anata.me/2019/12/04/PixiJS%E5%9F%BA%E7%A1%80%E6%95%99%E7%A8%8B/
FEWY : https://blog.csdn.net/fe_dev?t=1
插件:
pixi-sound: https://github.com/pixijs/pixi-sound —facebook试玩广告使用后不支持WebAudio Api,改用 howler
howler.js: https://goldfirestudios.com/howler-js-modern-web-audio-javascript-library
gsap: https://www.tweenmax.com.cn/api/tweenmax/

具体的游戏流程

具体业务代码不再展示

//导入引用库
import * as PIXI from "pixi.js";
import gsap from "gsap";
import PixiPlugin from "gsap/PixiPlugin";
import { Application, Container, Loader, Texture, Sprite, AnimatedSprite, Rectangle, Text, TextStyle } from "pixi.js";
//...省略一些管理类,管理类使用单例模式,用init来初始化
// 导入资源
import "../css/index.css";
import { cannonData } from "./assets"; //图片对应的数据
import cannon1 from "../img/cannon1.png";
// 导入音频
import bgm from "../audio/bgm.mp3";
//...省略资源
// 为了使用chrome devtools pixi的插件,webpack打包后不能正常显示需要在window上挂载PIXI来解决问题
window.PIXI = PIXI;
// 当宽高改变时,对应的游戏界面改变,设置canvas父元素的大小,有很多方法,适配需要根据情景来做
/** canvas 的父元素 */
const gameWrap = document.getElementById("game");
export const width = 800; //导出全局宽度和高度,保证所有文件中应用的都是同一个值,方便修改
export const height = 600;
resize(gameWrap, width, height);
window.addEventListener("resize", function () {
  resize(gameWrap, width, height);
});
/** 适配移动端的点击的事件名 */
const clickEventName = "ontouchstart" in window ? "tap" : "click";
/** pixi 插件 */
gsap.registerPlugin(PixiPlugin);
PixiPlugin.registerPIXI(PIXI);

/** pixi 的应用 */
const app = new Application({
  width: width,
  height: height,
  antialias: true,
  transparent: false
  // resizeTo: gameWrap,
  // backgroundColor: 0x237804
});
// 将 canvas 标签插入到页面中
gameWrap.appendChild(app.view);
/** 开始场景 */
const startContainer = new Container();
/** 加载器 */
const loader = Loader.shared;
//初始化声音,必须用户第一次点击后才会开始播放,使用第一次全屏点击时播放一个空白的声音来让bgm自动播放
new Howl({
  src: bgm,
  autoplay: true,
  loop: true
});
loader
  .add("cannon1.png", cannon1)
//.on("progress", loadProgressHandler)
  .load(setup);
// 监听加载事件,进度条,根据需求来做
// loader.onProgress.add((loader) => {
//   console.log(loader.progress);
// });
//function loadProgressHandler(loader, resource) {

  //Display the file `url` currently being loaded
  //console.log("loading: " + resource.url);

  //Display the percentage of files currently loaded
 // console.log("progress: " + loader.progress + "%");

  //If you gave your files names as the first argument
  //of the `add` method, you can access them like this
  //console.log("loading: " + resource.name);
//}
// 游戏的状态
let state: (delta: number) => void;
function setup() {

  // 添加炮台动画精灵
  const cannonAnimal = createSpritesAnimation(
    objToArr<Texture>(getTextures<typeof cannonData>("cannon1.png", cannonData)),
    2,
    app.view.width / 2,
    app.view.height - cannonData["cannon1-0.png"].h / 2
  );

  // 初始化炮台控制类
  CannonControl.ins.init(cannonAnimal); //炮台的控制类
  // 添加炮台
  startContainer.addChild(cannonAnimal);
  // 添加图片,并开启交互,确定点击坐标
  xxx.interactive = true;
  xxx.addListener(clickEventName, function (event) {
    // event.data.global 点击的全局点
    CannonControl.ins.shootBullet(event.data.global);
  });
  app.stage.addChild(startContainer);
  // 添加文本
  let coinStyle = new TextStyle({
    fontSize: 36,
    fill: "white"
  });
  let coinText = new Text("", coinStyle);
  coinText.anchor.set(0, 0);
  coinText.position.set(500, 540);
  startContainer.addChild(coinText);
  //设置游戏状态,开始引导
  state = start;
  //开始游戏,开启循环
  app.ticker.add((delta) => gameLoop(delta));
}
function gameLoop(delta: number) {
  //循环运行当前游戏`state`并渲染精灵
  state(delta);
}
//在这些函数中设置条件做场景的跳转,state = play;
// 开始引导
function start(delta: number) {
  BulletManagement.ins.fishing(fishContainer);
  allFishMove();
}
// 游戏中
function play(delta: number) {
  allFishMove();//自动位移
}
// 游戏结束,弹出弹窗
function end(delta: number) {
  allFishMove();
}
//其他的都可以通过看文档来解决

试玩广告最好是在30s之内结束,如果有按钮就控制点击次数来结束,不好控制的就使用时间来控制,一般时间都不会超过30s;

主要的使用的库和一些工程化代码

//package.json
{
  "name": "buyu",
  "private": true,
  "version": "1.0.0",
  "description": "H5 game pixi.js",
  "main": "index.js",
  "scripts": {
    "dev": "webpack-dev-server --open  --config webpack.dev.js --type",
    "clean": "rimraf dist",
    "prebuild": "npm run clean",
    "build": "webpack --config webpack.prod.js --type",
    "build-fb": "webpack --config webpack.prod.js --type fb",
    "build-google": "webpack --config webpack.prod.js --type google",
    "build-unity": "webpack --config webpack.prod.js --type unity",
    "postbuild-unity":"rimraf dist/unity/*.js & rimraf dist/unity/*.css",
    "build-vungle": "webpack --config webpack.prod.js --type vungle",
    "build-applovin": "webpack --config webpack.prod.js --type applovin",
    "postbuild-applovin":"rimraf dist/applovin/*.js & rimraf dist/applovin/*.css",
    "build-ironsource": "webpack --config webpack.prod.js --type ironsource",
    "postbuild-ironsource":"rimraf dist/ironsource/*.js & rimraf dist/ironsource/*.css",
    "build-all": "npm run build-fb & npm run build-google & npm run build-unity & npm run build-vungle & npm run build-applovin & npm run build-ironsource"
  },
  "author": "",
  "dependencies": {
    "@babel/polyfill": "^7.10.1",
    "@babel/runtime": "^7.10.1",
    "core-js": "^3.6.5",
    "gsap": "^3.3.0",
    "howler": "^2.2.0",
    "pixi-sound": "^3.0.4",
    "pixi.js": "^5.2.4"
  },
  "devDependencies": {
    "@babel/core": "^7.10.1",
    "@babel/plugin-transform-runtime": "^7.10.1",
    "@babel/preset-env": "^7.10.1",
    "babel-loader": "^8.1.0",
    "chalk": "^4.1.0",
    "clean-webpack-plugin": "^3.0.0",
    "copy-webpack-plugin": "^4.6.0",
    "css-loader": "^1.0.1",
    "file-loader": "^6.0.0",
    "html-webpack-inline-source-plugin": "^1.0.0-beta.2",
    "html-webpack-plugin": "^4.3.0",
    "mini-css-extract-plugin": "^0.9.0",
    "optimize-css-assets-webpack-plugin": "^5.0.3",
    "raw-loader": "^4.0.1",
    "rimraf": "^3.0.2",
    "style-loader": "^0.23.1",
    "ts-loader": "^7.0.5",
    "typescript": "^3.9.3",
    "url-loader": "^4.1.0",
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.11",
    "webpack-dev-server": "^3.11.0",
    "webpack-merge": "^4.2.2",
    "yargs": "^15.3.1"
  }
}
//tsconfig.json
{
  "compilerOptions": {
    "allowJs": true,
    "module": "esnext",
    "moduleResolution": "node",
    "target": "es5",
    "baseUrl": ".",
    "lib": ["es6", "es7", "dom"],
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "outDir": "dist"
  },
  "include": [
    "./global.d.ts",
    "./src/**/*.ts"
  ],
  "exclude": []
}
//webpack.common.js
const path = require("path");
const HtmlPlugin = require("html-webpack-plugin");
const Webpack = require("webpack");
// const CopyWebpackPlugin = require("copy-webpack-plugin");
// const ImageminPlugin = require("imagemin-webpack-plugin").default;
const Chalk = require("chalk");
const Yargs = require("yargs");
var {
  argv: { type }
} = Yargs;
const definePlugin = {
  type: JSON.stringify(type)
};
// console.log(type);
if (!type) {
  console.log(Chalk.red.bold("miss action"));
  process.exit();
}
module.exports = {
  //游戏入口文件
  context: path.join(__dirname, "src"),
  entry: ["./js/index.ts"],
  output: {
    //js文件最终发布到哪个路径
    path: path.resolve(__dirname, `dist/${type}`),

    //开发调试阶段webpack会自动处理这个文件让html引用到,虽然磁盘上不会有这个文件。
    //但是最终发布项目的时候会生成这个文件,并会插入到index.html中。
    //[hash:8]的意思是生成随机的八位hash值,为了缓存更新问题。
    // filename: "game.min.[hash:6].js"
    filename: "game.min.js"
  },
  target: "web",
  module: {
    rules: [
      {
        // Match woff2 in addition to patterns like .woff?v=1.1.1.
        test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/,
        loader: "url-loader",
        options: {
          // Limit at 50k. Above that it emits separate files
          limit: 50 * 1024 * 8,
          esModule: false,
          // url-loader sets mimetype if it's passed.
          // Without this it derives it from the file extension
          mimetype: "application/font-woff",

          // Output below fonts directory
          name: "./fonts/[name].[ext]"
        }
      }
    ]
  },
  resolve: {
    extensions: [".tsx", ".ts", ".js"]
  },
  plugins: [
    //拷贝src/assets目录下的所有资源到dist/assets下
    // new CopyWebpackPlugin([{ from: "assets/", to: "assets/" }], {
    //   ignore: [],
    //   debug: "debug",
    //   copyUnmodified: true,
    // }),

    //优化图片资源,压缩png。
    // new ImageminPlugin({
    //   test: /\.(jpe?g|png|gif|svg)$/i,

    //   //这种方式压缩在mac上效果不太好
    //   // optipng: {
    //   //   optimizationLevel: 4
    //   // },

    //   //这个方式在mac上压缩效果更好,windows上尚未测试有待验证。
    //   pngquant: {
    //     verbose: true,
    //     quality: "80-90",
    //   },
    // }),
    new Webpack.DefinePlugin(definePlugin),
    //拷贝html,插入js。
    new HtmlPlugin({
      file: path.join(__dirname, "dist/" + type, type === "vungle" ? "ad.html" : "index.html"),
      template: getTemplate(type),
      inject: true,
      inlineSource: ".(js|css)"
    })
  ]
  // externals: {
  //   PIXI: "window.PIXI"
  // }
};

function getTemplate(type) {
  switch (type) {
    case "fb":
      return "./ejs/fb.html";
    case "google":
      return "./ejs/fb.html";
    case "ironsource":
      return "./ejs/ironSource.html";
    default:
      return "./ejs/unity.html";
  }
}

//webpack.dev.js
const path = require("path");
const merge = require("webpack-merge");
const common = require("./webpack.common.js");

//和common配置文件合并
module.exports = merge(common, {
  devtool: "inline-source-map",
  mode: "none",

  //调试用http服务器配置
  devServer: {
    //调试时候源代码的位置
    contentBase: path.join(__dirname, "src"),

    //服务器端口
    port: 8080,

    //服务器地址,注意这里如果配置为`127.0.0.1`的话局域网内其他机器无法访问此服务器。
    host: "172.16.10.123",

    //监听本地js文件,有修改的话自动重新刷新页面。
    hot: true,
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [{ loader: "style-loader", options: { singleton: true } }, "css-loader"],
      },
      {
        test: /\.(ts|tsx)$/,
        use: ['ts-loader']
      },
      {
        test: /\.(png|svg|jpg|gif)$/,
        use: [
          {
            loader: "url-loader",
            options: {
              // limit: 200 * 1024 * 8, //默认单位为bytes
              esModule: false
            }
          }
        ]
      },
      {
        test: /\.(mp3)$/,
        use: [
          {
            loader: "url-loader"
          }
        ]
      }
    ],
  },
});


//webpack.prod.js
const path = require("path");
const merge = require("webpack-merge");
const common = require("./webpack.common.js");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
// const UglifyJsPlugin = require('uglifyjs-webpack-plugin');//压缩js
const TerserJSPlugin = require("terser-webpack-plugin");
const HTMLInlineCSSWebpackPlugin = require("html-webpack-inline-source-plugin");
const HtmlPlugin = require("html-webpack-plugin");
const Chalk = require("chalk");
const Yargs = require("yargs");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

var {
  argv: { type }
} = Yargs;
const definePlugin = {
  type: JSON.stringify(type)
};
if (!type) {
  console.log(Chalk.red.bold("miss action"));
  process.exit();
}
let plugins = [
  new MiniCssExtractPlugin({
    filename: "index.css"
  })
];
// 需要打包成一个html文件的渠道
if (["unity", "applovin", "ironsource"].indexOf(type) !== -1) {
  plugins.push(new HTMLInlineCSSWebpackPlugin(HtmlPlugin));
  plugins.push(
    new CleanWebpackPlugin({
      // dry: true,//测试
      verbose: true
    })
  );
}
module.exports = merge(common, {
  mode: "production",
  // devtool: 'source-map',
  optimization: {
    minimizer: [
      new TerserJSPlugin({
        terserOptions: {
          output: {
            comments: false
          },
          compress: {
            // warnings: false,
            // drop_console: true,
            // drop_debugger: true,
            // pure_funcs: ["console.log"],
          }
        }
      }),
      new OptimizeCSSAssetsPlugin({})
    ]
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, "css-loader"]
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: [
              [
                "@babel/preset-env",
                {
                  corejs: "3",
                  useBuiltIns: "usage"
                }
              ]
            ],
            plugins: ["@babel/plugin-transform-runtime"]
          }
        }
      },
      {
        test: /\.(ts|tsx)$/,
        use: [
          {
            loader: "babel-loader",
            options: {
              presets: [
                [
                  "@babel/preset-env",
                  {
                    corejs: "3",
                    useBuiltIns: "usage"
                  }
                ]
              ],
              plugins: ["@babel/plugin-transform-runtime"]
            }
          },
          "ts-loader"
        ]
      },
      {
        test: /\.(png|svg|jpg|gif)$/,
        use: [
          {
            loader: "url-loader",
            options: {
              limit: 200 * 1024 * 8, //默认单位为bytes
              esModule: false
            }
          }
        ]
      },
      {
        test: /\.(mp3)$/,
        use: [
          {
            loader: "url-loader",
            options: {
              limit: 200 * 1024 * 8, //默认单位为bytes
              esModule: false
              // mimetype: 'x-wav'
            }
          }
        ]
      }
    ]
  },
  plugins: plugins
});

最终出现的问题

  1. facebook 试玩广告上传后没有声音,不支持webAudio Api 控制音频播放,改用howler.js库后修复此问题。
  2. 全部打包所有的渠道 Yargs 库拿参数,用参数来判断具体使用的库 yargs,clean-webpack-plugin,Webpack.DefinePlugin,内联删除多余文件使用rimraf 来删除,使用npm 的postxxx 来做打包完成后删除的工作。prexxx xxx命令的前置命令,postxxx xxx命令的后置命令,具体查看npm的说明或者阮一峰的博客:https://www.ruanyifeng.com/blog/2016/10/npm_scripts.html

    对应的渠道

    facebook

    文档: https://www.facebook.com/business/help/412951382532338?ref=FBB_PlayableAds ``` 试玩 HTML5 素材 试玩游戏应为 HTML5 格式(扩展名为“html”或“htm”)。 文件大小应小于 2 MB。 试玩广告素材不应使用 mraid.js 格式。 试玩广告素材不应发出任何 HTTP 请求。 试玩广告素材应为纵向模式。 试玩广告素材应采用响应式设计,因为它需要支持多种设备类型,而设备的分辨率各不相同。 包含所有素材的试玩广告的单个文件都可作为单个 HTML 文件和素材的一部分,而且应采用 URI 压缩数据格式(Javascript、CSS、图片、声音)。 试玩广告代码应在用户与广告中的行动号召互动时使用 Javascript 函数 FbPlayableAd.onCTAClick()。Facebook 使用该代码以导航至应用正确的应用商店页面。 试玩 HTML5 素材中不应包含任何 JavaScript 重定向。 不允许通过外部网络加载动态素材。 如要合并为一个文件:将图像编码为 base64 字符串,将 js 集成到 index.html 中。

压缩文件 当前,所有 iOS 版和 Android 版广告版位均支持压缩文件格式的试玩广告。 压缩归档文件应包含位于根目录的 index.html,例如:

./index.html 注意:只要是相对于 index.html 来引用资源,资源可位于目录结构中的任意位置。例如,以下文件: ./assets/splash.png 必须引用为:

Pixi.js 开发试玩广告 - 图2 压缩归档文件中的所有 html 文件和 JavaScript 文件应存在相同的限制,但只需在一个位置调用 FbPlayableAd.onCTAClick()。 上传文件的总大小不得超过 5MB。 捆绑包内的文件数量不得超过 100 个。 index.html 文件的大小不得超过 2MB。

<a name="7WtZg"></a>
### google试玩广告
文档: [https://support.google.com/google-ads/answer/7584219](https://support.google.com/google-ads/answer/7584219)<br />测试: [https://h5validator.appspot.com/adwords/asset](https://h5validator.appspot.com/adwords/asset) 点击左下角单选框后,上传自动检测
```html
<!-- google时添加 -->
 <meta name="ad.size" content="width=320,height=480">
<script type="text/javascript" src="https://tpc.googlesyndication.com/pagead/gadgets/html5/api/exitapi.js"> </script>
//点击下载的回调
<script>ExitApi.exit();</script>
HTML 要求    
HTML5 素材资源必须包含:

<!DOCTYPE html> 声明
<html> 标记
<body> 标记
<head> 标记中的广告格式尺寸元标记。例如:<meta name="ad.size" content="width=300,height=250">
明确的结束标记(不接受内嵌的结束标记):

有效的结束标记示例:<path></path>
无效的结束标记示例:<path> 或 <path />
或者,您可以将内嵌的 svg 提取到单独的文件中。

上传要求    
所有代码和素材资源都必须使用指向 .ZIP 文件中所含资源的相对路径进行引用。

除对以下对象的引用外,不得使用任何外部引用:

Google Fonts
Google 托管的 jQuery
Google 托管的 Greensock(文件应可正常使用,无论版本如何)
tweenlite
tweenmax
cssplugin
easepack
timelinelite
timelinemax
Google 托管的 CreateJS
tweenjs
easeljs
createjs
可点击的按钮    
如果您想确保用户只能点击您的号召性用语按钮(例如“下载”),请按照以下说明添加 exitapi.js 脚本。如果您不添加 exitapi.js 脚本,Google Ads 会将您的整个素材资源都设为可点击。

说明

    1. 请在 HTML <head> 标记中添加以下脚本:<scripttype="text/javascript" src="https://tpc.googlesyndication.com/pagead/gadgets/html5/api/exitapi.js"> </script>
         然后执行以下 JavaScript 调用以启用最终到达网址:ExitApi.exit()
         例如,要将用户引导至最终到达网址,请使用标准锚标记:
       <a onclick="ExitApi.exit()">了解详情</a>

Unity

文档:https://static-test.zplay.cn/wap/ad_canPlay/api/unity/Unity%E5%8F%AF%E8%AF%95%E7%8E%A9%E5%B9%BF%E5%91%8A%E5%AE%9E%E8%B7%B5%E6%8C%87%E5%8D%97.pdf
测试android: https://play.google.com/store/apps/details?id=com.unity3d.auicreativetestapp&hl=en_SG
ios: 商城搜 unity ads testing

其他的一般大小都是5M 个别是2M 其他的还有必须支持横竖屏,支持 MRAID v2.0 的单一 HTML 文件
APPLOVIN第三方试玩广告指_南+常见问题.pdf 测试网址:https://p.applov.in/playablePreview?create=1
ironSource可玩广告开发指南 V1.3.pdf
Vungle
文档: https://static-test.zplay.cn/wap/ad_canPlay/api/Vungle/Vungle%20-%20%E5%8F%AF%E7%8E%A9%E8%A7%84%E6%A0%BC%20Chinese%20CIEC%20Spec.pdf

mraid: 广告接口

mraid中文文档: https://github.com/graypants/mraid_cn/blob/master/mraid_cn.md
https://blog.csdn.net/weixin_34242819/article/details/93344730
https://www.jianshu.com/p/6bbda3d5b5f1