Skip to content

Egret 游戏引擎学习笔记

INFO

Egret 游戏引擎学习,以下是对用到的知识点和常用代码片段进行归纳。

Main 最简结构

最简结构:包含资源加载和展示过渡动画

typescript
class Main extends egret.DisplayObjectContainer {
  private logo: egret.Bitmap;

  public constructor() {
    super();
    this.addEventListener(egret.Event.ADDED_TO_STAGE, this.onAddedToStage, this);
  }

  private onAddedToStage(): void {
    this.loadResource();
  }

  private async loadResource(): Promise<void> {
    RES.addEventListener(RES.ResourceEvent.GROUP_COMPLETE, this.onLoadResourceComplete, this);
    RES.addEventListener(RES.ResourceEvent.GROUP_PROGRESS, this.onLoadResource, this);
    await RES.loadConfig("resource/default.res.json", "resource/");
    await RES.loadGroup("preload");
  }

  private onLoadResourceComplete(ev: RES.ResourceEvent): void {}
  private onLoadResource(ev: RES.ResourceEvent): void {}
}

动画

做常用的动画为 Tweenmovieclip,骨骼动画过于复杂不在这里介绍。可以查看官方“龙骨”动画的教程。

Tween 的使用示例

实现一个平移

typescript
this.logo = new egret.Bitmap();
this.logo.texture = RES.getRes("egret_icon_png");
this.addChild(this.logo);
// 过渡动画 tween
const tw = egret.Tween.get(this.logo);
tw.to({ x: 100, y: 100 }, 500).to({ x: 200, y: 400 }, 500);

movieclip 的使用示例

typescript
const faceBitmap = RES.getRes("face_png");
const faceBitmapJSON = RES.getRes("face_json");
const faceFactory: egret.MovieClipDataFactory = new egret.MovieClipDataFactory(faceBitmapJSON, faceBitmap);
const movie: egret.MovieClip = new egret.MovieClip(faceFactory.generateMovieClipData("眼睛"));
movie.gotoAndPlay("转动", -1);

egret 中的事件分发于订阅

typescript
// 分发
this.dispatchEvent(new egret.Event(CustomEventConst.CHANGE_STATE, true, false, "stateName"));
typescript
// 订阅
this.addEventListener(CustomEventConst.CHANGE_STATE, this.changeState, this);
// ...
class Main extends egret.DisplayObjectContainer {
  // ...
  private changeState(ev: egret.Event): void {
    const stateName: string = ev.data;
  }
}

当然也可以继承和实现自己的 Event 对象,来进行传值,这种方式比较麻烦,但是比较严谨,可以查看官方文档。

获取舞台的大小

typescript
const stageWidth: number = egret.MainContext.instance.stage.stageWidth;
const stageHeight: number = egret.MainContext.instance.stage.stageHeight;

测试点是否在对象内

typescript
displayObject.hitTestPoint(x, y);

交互事件

egret 是为移动端设计的,所以基本是 TouchEvent。

typescript
displayObject.touchEnabled = true;
displayObject.addEventListener(egret.TouchEvent.TOUCH_BEGIN, (ev: egret.TouchEvent) => {}, this);
displayObject.addEventListener(egret.TouchEvent.TOUCH_MOVE, (ev: egret.TouchEvent) => {}, this);
displayObject.addEventListener(egret.TouchEvent.TOUCH_END, (ev: egret.TouchEvent) => {}, this);
displayObject.addEventListener(egret.TouchEvent.TOUCH_RELEASE_OUTSIDE, (ev: egret.TouchEvent) => {}, this);

如果物体用了 P2 演算,需要在拖动时禁用 boxBody.sleep();,释放时唤醒 boxBody.wakeUp();

物理向量演算和实体世界的映射

P2 在 egret 中是作为一个独立的第三方模块存在的。

创建向量世界

typescript
// factor 是单位转换常量
const positionX: number = Math.floor(x / P2util.factor);
const positionY: number = Math.floor(this.stageHeight / P2util.factor);
// 添加方形刚体
const boxShape: p2.Box = new p2.Box({
  width: trash.width / P2util.factor,
  height: trash.height / P2util.factor,
});
const boxBody: p2.Body = new p2.Body({
  mass: 1,
  position: [positionX, positionY],
  // angularVelocity: 1,
  fixedRotation: true,
});

// const debugBox = P2util.createDebugBox(trash);

boxBody.addShape(boxShape);
boxBody.displays = [trash]; // 绑定视图对象
boxBody.allowSleep = false; // 禁止休眠,防止拖动时候导致的异常

this.world.addBody(boxBody); // 向量世界

同步向量和实体

这个方法会经常用到,所以我在项目里抽离到 P2util 中。

typescript
const stageHeight: number = egret.MainContext.instance.stage.stageHeight;
const bodyCount = world.bodies.length;
for (let i: number = 0; i < bodyCount; i++) {
  const body: p2.Body = world.bodies[i];
  if (body.displays && body.displays.length > 0) {
    body.displays.forEach((displayObject: egret.DisplayObject) => {
      // 位置同步
      displayObject.x = body.position[0] * P2util.factor;
      displayObject.y = stageHeight - body.position[1] * P2util.factor;
      // 角度同步
      displayObject.rotation = 360 - ((body.angle + body.shapes[0].angle) * 180) / Math.PI;
      // if (body.sleepState == p2.Body.SLEEPING) {
      //   displayObject.alpha = 0.5;
      // } else {
      //   displayObject.alpha = 1;
      // }
    });
  }
}

设置静止的刚体(如地面)

typescript
class PlayState extends egret.DisplayObjectContainer {
  private createGround(): void {
    //创建地面
    const groundHeight = 122;
    // 创建可视图形
    const ground: egret.Bitmap = new egret.Bitmap();
    ground.texture = RES.getRes("ground_png");
    ground.anchorOffsetX = ground.width / 2;
    ground.anchorOffsetY = ground.height / 2;

    const groundBody: p2.Body = new p2.Body({
      mass: 1,
      fixedRotation: true,
      position: [750 / 2 / P2util.factor, groundHeight / 2 / P2util.factor], // p2使用中心点定位
      type: p2.Body.STATIC, // 不动的刚体
      velocity: [0, 0],
    });
    groundBody.displays = [ground];
    // 具体物理形状
    groundBody.addShape(
      new p2.Box({
        width: this.stageWidth / P2util.factor,
        height: groundHeight / P2util.factor,
      })
    );

    this.world.addBody(groundBody);
    this.addChild(ground);
  }
}

egret 定时器 Timer 示例

typescript
this.trashGenTimer = new egret.Timer(2000, 0);
this.trashGenTimer.addEventListener(
  egret.TimerEvent.TIMER,
  () => {
    if (this.trashLayer.$children.length < this.limit) {
      this.addOneTrash();
    }
  },
  this
);
this.trashGenTimer.start();

资源远程加载的 CORS 问题

远程加载的方式有 2 种,

方式一:

typescript
RES.getResByUrl("http://xxx/cdn/xxx.m4a");

方式二:将资源清单放在远程

typescript
// resourceDir 是远程地址
RES.loadConfig(this.resourceDir + "/default.res.json", this.resourceDir);

注意,需要同时在 server 端和 egret 端设置 CORS,光设置一方是无效的,被这个问题坑了很久。

server 端 nginx 设置 CORS 这里不再阐述。

egret 设置 CORS 的方式如下:

typescript
class Main extends egret.DisplayObjectContainer {
  public constructor() {
    super();
    //...
    egret.ImageLoader.crossOrigin = "anonymous";
  }
}

三个重要的生命周期事件

egret.Event.ADDED_TO_STAGE 视图对象进入舞台时

egret.Event.ENTER_FRAME 当每帧进入时(相当于帧定时器)

egret.Event.REMOVED_FROM_STAGE 视图对象移出舞台时

文字对象使用示例

typescript
class EndState extends egret.DisplayObjectContainer {
  private textTitle: egret.TextField;
  private textScore: egret.TextField;
  private textTime: egret.TextField;

  // ...

  private addText() {
    this.textTitle = new egret.TextField();
    this.textTitle.text = "最终得分";
    this.textTitle.textAlign = egret.HorizontalAlign.CENTER;
    this.textTitle.width = egret.MainContext.instance.stage.stageWidth;
    this.textTitle.textColor = 0x089240;
    this.textTitle.y = 100;

    this.textScore = new egret.TextField();
    this.textScore.textFlow = <egret.ITextElement[]>[
      { text: Store.score.toString(), style: { size: 80, fontFamily: "楷体" } },
      { text: "分", style: { size: 30, fontFamily: "楷体" } },
    ];
    this.textScore.textAlign = egret.HorizontalAlign.CENTER;
    this.textScore.width = egret.MainContext.instance.stage.stageWidth;
    this.textScore.textColor = 0x089240;
    this.textScore.verticalAlign = egret.VerticalAlign.MIDDLE;
    this.textScore.y = this.textTitle.y + 80;

    this.textTime = new egret.TextField();
    this.textTime.text = "用时:120秒";
    this.textTime.textAlign = egret.HorizontalAlign.CENTER;
    this.textTime.width = egret.MainContext.instance.stage.stageWidth;
    this.textTime.textColor = 0x089240;
    this.textTime.y = this.textScore.y + 120;

    this.addChild(this.textTitle);
    this.addChild(this.textScore);
    this.addChild(this.textTime);
  }
}

优化的 LoadingUI

typescript
class LoadingUI extends egret.Sprite implements RES.PromiseTaskReporter {
  public constructor() {
    super();
    this.createView();
  }

  private textField: egret.TextField;

  private createView(): void {
    this.textField = new egret.TextField();
    this.addChild(this.textField);
    this.textField.width = 480;
    this.textField.height = 100;
    this.textField.textAlign = "center";
  }

  public onProgress(current: number, total: number): void {
    // console.log(`Loading...${current}/${total}`);
    if (current === total) {
      this.textField.text = `正在载入音频资源...`;
    } else {
      this.textField.text = `正在加载图像资源...${Math.floor((current / total) * 100)}%`;
    }
  }
}

工具类

随机数

typescript
class Random {
  public static randomColor(): number {
    return parseInt("0x" + ("000000" + ((Math.random() * 16777215 + 0.5) >> 0).toString(16)).slice(-6));
  }

  public static randomNumber(minNum: number, maxNum?: number) {
    switch (arguments.length) {
      case 1:
        return Math.floor(Math.random() * minNum + 1);
      case 2:
        return Math.floor(Math.random() * (maxNum - minNum + 1) + minNum);
      default:
        return 0;
    }
  }
}

P2 相关

typescript
class P2util {
  /**
   * 同步视图对象和物理对象
   * 视图对象锚点必须设置为中心点
   * @param displayObject
   * @param body
   * @param factor
   */
  public static factor: number = 50;
  public static syncDisplayAndBody(world): void {
    const stageHeight: number = egret.MainContext.instance.stage.stageHeight;
    const bodyCount = world.bodies.length;
    for (let i: number = 0; i < bodyCount; i++) {
      const body: p2.Body = world.bodies[i];
      if (body.displays && body.displays.length > 0) {
        body.displays.forEach((displayObject: egret.DisplayObject) => {
          // 位置同步
          displayObject.x = body.position[0] * P2util.factor;
          displayObject.y = stageHeight - body.position[1] * P2util.factor;
          // 角度同步
          displayObject.rotation = 360 - ((body.angle + body.shapes[0].angle) * 180) / Math.PI;
          // if (body.sleepState == p2.Body.SLEEPING) {
          //   displayObject.alpha = 0.5;
          // } else {
          //   displayObject.alpha = 1;
          // }
        });
      }
    }
  }
  public static createDebugBox(display: egret.DisplayObject) {
    const debugBox: egret.Shape = new egret.Shape();
    debugBox.graphics.beginFill(0xff0000, 0.5);
    debugBox.graphics.drawRect(0, 0, display.width, display.height);
    debugBox.graphics.endFill();
    debugBox.anchorOffsetX = debugBox.width / 2;
    debugBox.anchorOffsetY = debugBox.height / 2;
    debugBox.x = display.x;
    debugBox.y = display.y;
    return debugBox;
  }
  public static clearBoxAndDisplays(world: p2.World, boxBody: p2.Body) {
    if (boxBody.displays && boxBody.displays.length > 0) {
      boxBody.displays.forEach((display: egret.DisplayObject) => {
        display.parent.removeChild(display);
      });
    }
    world.removeBody(boxBody);
  }
}

移植到微信小游戏

P2 物理引擎无法正常工作的问题

需要修改构建脚本 scripts/wxgame/wxgame.ts

加入以下文件处理脚本:

typescript
// p2 physics
if (filename == "libs/modules/physics/physics.js" || filename == "libs/modules/physics/physics.min.js") {
  content = content.replace("module.exports = a();", "{window.p2=a();module.exports=window.p2;}");
  content = content.replace("module.exports=t();", "{window.p2=t();module.exports=window.p2;}");
}

新的微信小游戏授权兼容

由于腾讯官方修改了小程序的授权策略,需要修改部分源码才能移植

修改 platform.js

js
/**
 * 请在白鹭引擎的Main.ts中调用 platform.login() 方法调用至此处。
 */

class WxgamePlatform {
  name = "wxgame";

  login() {
    return new Promise((resolve, reject) => {
      wx.login({
        success: (res) => {
          resolve(res);
        },
      });
    });
  }

  getUserInfo() {
    return new Promise((resolve, reject) => {
      let sysInfo = wx.getSystemInfoSync();
      let sdkVersion = sysInfo.SDKVersion;
      // 新的方式
      if (sdkVersion >= "2.0.1") {
        const button = wx.createUserInfoButton({
          type: "image",
          text: "",
          image: "openDataContext/assets/box.png",
          style: {
            left: 0,
            top: 0,
            width: 100,
            height: 100,
            backgroundColor: "#ff0000",
            color: "#ffffff",
          },
        });
        button.onTap((res) => {
          if (res.userInfo) {
            console.log("用户授权:", res);
            const userInfo = res.userInfo;
            button.destroy();
            resolve(userInfo);
          } else {
            console.log("拒绝授权");
          }
        });
      } else {
        // 原来的方法
        wx.getUserInfo({
          withCredentials: true,
          success: (res) => {
            const userInfo = res.userInfo;
            resolve(userInfo);
          },
          fail: (res) => {
            wx.showModal({
              title: "友情提醒",
              content: "请允许微信获得授权!",
              confirmText: "授权",
              showCancel: false,
              success: (res) => {
                resolve(null);
              },
            });
          },
        });
      }
    });
  }
}

window.platform = new WxgamePlatform();

EUI 示例

定义一个皮肤 resource/eui/RedBox.exml

xml
<?xml version="1.0" encoding="utf-8"?>
<e:Skin class="SkinRedBox" width="640" height="1136" xmlns:e="http://ns.egret.com/eui"
        xmlns:w="http://ns.egret.com/wing">
    <e:Rect width="640" height="1136" x="0" y="0" anchorOffsetX="0" anchorOffsetY="0" />
    <e:Rect id="redbox" width="157" height="93" anchorOffsetX="0" anchorOffsetY="0" fillColor="0xff0000" enabled="true"
            horizontalCenter="0" verticalCenter="0" />
</e:Skin>

定义配置 resource/default.thm.json

json
{
  "skins": {},
  "autoGenerateExmlsList": true,
  "path": "resource/default.thm.json",
  "exmls": ["resource/eui/RedBox.exml"]
}

定义 EUI 对应的 Class src/RedBox.ts

typescript
class RedBox extends eui.Component implements eui.UIComponent {
  constructor() {
    super();
    this.addEventListener(eui.UIEvent.COMPLETE, this.onComplete, this);
    this.skinName = "SkinRedBox";
  }

  private redbox: eui.Rect; // 变量与exml中定义的id同名即可自动映射

  protected createChildren() {
    super.createChildren();
    console.log("createChildren");
  }
  private onComplete(): void {
    console.log("onComplete");
    const tw = egret.Tween.get(this.redbox, { loop: true });
    tw.to({ rotation: 360 }, 4000, egret.Ease.circIn).to({ rotation: 0 }, 0).wait(500);
  }
}

使用 Class src/Main.ts

typescript
class Main extends egret.DisplayObjectContainer {
  public constructor() {
    super();
    this.addEventListener(egret.Event.ADDED_TO_STAGE, this.onAddToStage, this);
  }

  private onAddToStage(event: egret.Event) {
    egret.lifecycle.addLifecycleListener((context) => {
      // custom lifecycle plugin

      context.onUpdate = () => {};
    });

    egret.lifecycle.onPause = () => {
      egret.ticker.pause();
    };

    egret.lifecycle.onResume = () => {
      egret.ticker.resume();
    };

    this.runGame().catch((e) => {
      console.log(e);
    });
  }

  private async runGame() {
    await this.loadResource();
    this.createGameScene();
  }

  private async loadResource() {
    try {
      const loadingView = new LoadingUI();
      this.stage.addChild(loadingView);
      await RES.loadConfig("resource/default.res.json", "resource/");
      await RES.loadGroup("preload", 0, loadingView);
      await this.loadTheme();
      this.stage.removeChild(loadingView);
    } catch (e) {
      console.error(e);
    }
  }

  private loadTheme() {
    return new Promise((resolve, reject) => {
      // load skin theme configuration file, you can manually modify the file. And replace the default skin.
      //加载皮肤主题配置文件,可以手动修改这个文件。替换默认皮肤。
      console.log("eui.UIEvent.START");
      let theme = new eui.Theme("resource/default.thm.json", this.stage);
      theme.addEventListener(
        eui.UIEvent.COMPLETE,
        () => {
          console.log("eui.UIEvent.COMPLETE");
          resolve();
        },
        this
      );
    });
  }

  /**
   * 创建游戏场景
   * Create a game scene
   */
  private createGameScene() {
    const box: RedBox = new RedBox();
    this.addChild(box);
  }
}

最后编辑时间:

Version 4.2 (core-1.3.4)