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 {}
}
动画
做常用的动画为 Tween
和 movieclip
,骨骼动画过于复杂不在这里介绍。可以查看官方“龙骨”动画的教程。
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);
}
}